Карта Backrooms, порталы, комнаты по картам и зрение камня
- Backrooms на готовой модели (CC-BY, VTX): отдельный пакет backrooms.pck, качается при первом входе в портал (World/map_loader.gd), навмеш запечён заранее. - Порталы между картами (World/portal.gd); выход из Backrooms возвращает в мир. - Мультиплеер: игроки и общий камень делятся на комнаты по картам (Net/net.gd). - Камень видит игрока только вблизи или в поле зрения, может потерять и бродит по навмешу (sight_limited); в мире и в Backrooms включено. - Экспорт: пресет Backrooms для пакета карты, основной пресет без неё. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
extends Node3D
|
||||
# Backrooms на готовой модели. Сцена живёт в отдельном пакете (backrooms.pck) и качается
|
||||
# только при первом входе в портал. Коллизии строим по мешам модели при загрузке,
|
||||
# навмеш для камня запечён заранее (backrooms_nav.res) — в вебе запекать на лету долго.
|
||||
|
||||
func _ready() -> void:
|
||||
Net.enter_map("backrooms")
|
||||
_build_colliders()
|
||||
_build_environment()
|
||||
_build_hum()
|
||||
|
||||
func _build_colliders() -> void:
|
||||
for mi in $Map.find_children("*", "MeshInstance3D", true, false):
|
||||
mi.create_trimesh_collision()
|
||||
|
||||
func _build_environment() -> void:
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = Color(0.06, 0.05, 0.03)
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color(1.0, 0.95, 0.8)
|
||||
env.ambient_light_energy = 0.7
|
||||
env.fog_enabled = true
|
||||
env.fog_light_color = Color(0.25, 0.2, 0.09)
|
||||
env.fog_density = 0.012
|
||||
var we := WorldEnvironment.new()
|
||||
we.environment = env
|
||||
add_child(we)
|
||||
|
||||
# Гул ламп: 2 секунды, частоты кратны 0.5 Гц — петля без щелчка
|
||||
func _build_hum() -> void:
|
||||
const RATE := 11025
|
||||
var n := RATE * 2
|
||||
var data := PackedByteArray()
|
||||
data.resize(n * 2)
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 5
|
||||
for i in n:
|
||||
var t := float(i) / RATE
|
||||
var s := 0.50 * sin(TAU * 60.0 * t) + 0.25 * sin(TAU * 120.0 * t) \
|
||||
+ 0.12 * sin(TAU * 180.0 * t) + 0.06 * sin(TAU * 240.0 * t)
|
||||
s += rng.randf_range(-1.0, 1.0) * 0.03
|
||||
data.encode_s16(i * 2, int(clampf(s, -1.0, 1.0) * 12000.0))
|
||||
var wav := AudioStreamWAV.new()
|
||||
wav.format = AudioStreamWAV.FORMAT_16_BITS
|
||||
wav.mix_rate = RATE
|
||||
wav.stereo = false
|
||||
wav.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
wav.loop_begin = 0
|
||||
wav.loop_end = n
|
||||
wav.data = data
|
||||
var player := AudioStreamPlayer.new()
|
||||
player.stream = wav
|
||||
player.volume_db = -12.0
|
||||
player.autoplay = true
|
||||
add_child(player)
|
||||
@@ -0,0 +1 @@
|
||||
uid://gmpkjl6m3415
|
||||
@@ -0,0 +1,35 @@
|
||||
[gd_scene format=3 uid="uid://djgkggvwiaaoy"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://gmpkjl6m3415" path="res://World/Backrooms/backrooms.gd" id="1_level"]
|
||||
[ext_resource type="PackedScene" uid="uid://b8ef3wy1pthgm" path="res://models/backrooms/backrooms_vr.glb" id="2_map"]
|
||||
[ext_resource type="PackedScene" uid="uid://c3efk6k8xnf57" path="res://Player/Player.tscn" id="3_player"]
|
||||
[ext_resource type="PackedScene" path="res://models/Bochi-Rock/rock.tscn" id="4_rock"]
|
||||
[ext_resource type="Script" uid="uid://bu8v0ivhmnykt" path="res://World/portal.gd" id="5_portal"]
|
||||
[ext_resource type="NavigationMesh" path="res://World/Backrooms/backrooms_nav.res" id="6_nav"]
|
||||
|
||||
[node name="Backrooms" type="Node3D" unique_id=511186617]
|
||||
script = ExtResource("1_level")
|
||||
|
||||
[node name="Map" parent="." unique_id=82811432 instance=ExtResource("2_map")]
|
||||
transform = Transform3D(1.4, 0, 0, 0, 1.4, 0, 0, 0, 1.4, 0, 0, 0)
|
||||
|
||||
[node name="Player" parent="." unique_id=143512730 instance=ExtResource("3_player")]
|
||||
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, -0.4838078, 1.2, 2.0468445)
|
||||
|
||||
[node name="Rock" parent="." unique_id=311663607 instance=ExtResource("4_rock")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -48, 0, 3)
|
||||
speed = 10.0
|
||||
catch_distance = 2.0
|
||||
catch_height = 3.0
|
||||
sight_limited = true
|
||||
|
||||
[node name="Exit" type="Area3D" parent="." unique_id=1540431614]
|
||||
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, -72, -0.15, 9)
|
||||
script = ExtResource("5_portal")
|
||||
target_map = "world"
|
||||
target_scene = "res://main.tscn"
|
||||
title = "EXIT"
|
||||
color = Color(0.3, 1, 0.45, 1)
|
||||
|
||||
[node name="Navigation" type="NavigationRegion3D" parent="." unique_id=122126248]
|
||||
navigation_mesh = ExtResource("6_nav")
|
||||
Binary file not shown.
@@ -0,0 +1,123 @@
|
||||
extends Node
|
||||
# Подгружает карту отдельным пакетом при первом входе в портал: в вебе её не качают при старте игры.
|
||||
# Пакет (например backrooms.pck) лежит рядом с index.html. Если сцена уже есть в проекте
|
||||
# (редактор, десктоп-запуск) — качать нечего, переходим сразу.
|
||||
|
||||
signal enter_failed
|
||||
|
||||
const PACK_URL_ARG := "--pack-url=" # Для тестов на десктопе: --pack-url=http://127.0.0.1:8000/
|
||||
|
||||
var _loaded := {} # имя пакета -> true, чтобы не качать и не монтировать дважды
|
||||
var _busy := false
|
||||
var _layer: CanvasLayer
|
||||
var _label: Label
|
||||
var _bar: ProgressBar
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
||||
func enter(map: String, scene_path: String, pack_name: String, title: String) -> void:
|
||||
if _busy:
|
||||
return
|
||||
if pack_name != "" and not _loaded.has(pack_name) and not ResourceLoader.exists(scene_path):
|
||||
_busy = true
|
||||
var ok := await _download_and_mount(pack_name, title)
|
||||
_busy = false
|
||||
if not ok:
|
||||
enter_failed.emit()
|
||||
return
|
||||
Net.go_to_map(map, scene_path)
|
||||
|
||||
func _download_and_mount(pack_name: String, title: String) -> bool:
|
||||
_show("Загрузка: %s…" % title)
|
||||
get_tree().paused = true # Пока качаем, мир стоит (камень не должен убить игрока на экране загрузки)
|
||||
|
||||
var http := HTTPRequest.new()
|
||||
http.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
http.download_file = "user://" + pack_name
|
||||
add_child(http)
|
||||
var state := {"done": false, "result": -1, "code": 0}
|
||||
http.request_completed.connect(func(result: int, code: int, _h, _b):
|
||||
state.result = result
|
||||
state.code = code
|
||||
state.done = true)
|
||||
|
||||
var ok := false
|
||||
# no-cache: браузер сверяет файл с сервером (после обновления пакета не подсунет старый из кэша)
|
||||
if http.request(_pack_url(pack_name), ["Cache-Control: no-cache"]) == OK:
|
||||
while not state.done:
|
||||
_update_progress(http, title)
|
||||
await get_tree().process_frame
|
||||
if state.result == HTTPRequest.RESULT_SUCCESS and state.code == 200:
|
||||
ok = ProjectSettings.load_resource_pack("user://" + pack_name, false) # false: файлы основной игры в приоритете
|
||||
http.queue_free()
|
||||
|
||||
if ok:
|
||||
_loaded[pack_name] = true
|
||||
_hide()
|
||||
else:
|
||||
_label.text = "Не удалось загрузить карту"
|
||||
_bar.visible = false
|
||||
await get_tree().create_timer(2.5).timeout
|
||||
_hide()
|
||||
get_tree().paused = false
|
||||
return ok
|
||||
|
||||
func _pack_url(pack_name: String) -> String:
|
||||
for arg in OS.get_cmdline_user_args():
|
||||
if arg.begins_with(PACK_URL_ARG):
|
||||
return arg.substr(PACK_URL_ARG.length()) + pack_name
|
||||
if OS.has_feature("web"):
|
||||
return str(JavaScriptBridge.eval("new URL('%s', window.location.href).href" % pack_name))
|
||||
return "http://127.0.0.1:8000/" + pack_name
|
||||
|
||||
# ---------- Экран загрузки ----------
|
||||
|
||||
func _show(text: String) -> void:
|
||||
_layer = CanvasLayer.new()
|
||||
_layer.layer = 100
|
||||
add_child(_layer)
|
||||
|
||||
var bg := ColorRect.new()
|
||||
bg.color = Color(0, 0, 0, 0.88)
|
||||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_layer.add_child(bg)
|
||||
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
_label = Label.new()
|
||||
_label.text = text
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_label.add_theme_font_size_override("font_size", 40)
|
||||
bg.add_child(_label)
|
||||
_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
_bar = ProgressBar.new()
|
||||
_bar.custom_minimum_size = Vector2(420, 14)
|
||||
_bar.show_percentage = false
|
||||
_bar.indeterminate = true
|
||||
var track := StyleBoxFlat.new()
|
||||
track.bg_color = Color(1, 1, 1, 0.15)
|
||||
var fill := StyleBoxFlat.new()
|
||||
fill.bg_color = Color(1.0, 0.85, 0.25)
|
||||
_bar.add_theme_stylebox_override("background", track)
|
||||
_bar.add_theme_stylebox_override("fill", fill)
|
||||
bg.add_child(_bar)
|
||||
_bar.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM, Control.PRESET_MODE_KEEP_SIZE)
|
||||
_bar.position.y -= 120
|
||||
|
||||
func _update_progress(http: HTTPRequest, title: String) -> void:
|
||||
var got := http.get_downloaded_bytes()
|
||||
var total := http.get_body_size() # -1, если сервер не прислал Content-Length (например, gzip)
|
||||
if total > 0:
|
||||
_bar.indeterminate = false
|
||||
_bar.max_value = total
|
||||
_bar.value = got
|
||||
_label.text = "Загрузка: %s\n%.1f / %.1f МБ" % [title, got / 1048576.0, total / 1048576.0]
|
||||
else:
|
||||
_label.text = "Загрузка: %s\n%.1f МБ" % [title, got / 1048576.0]
|
||||
|
||||
func _hide() -> void:
|
||||
if _layer:
|
||||
_layer.queue_free()
|
||||
_layer = null
|
||||
@@ -0,0 +1 @@
|
||||
uid://bp4qld1nj4cla
|
||||
@@ -0,0 +1,90 @@
|
||||
extends Area3D
|
||||
# Портал на другую карту: шаг в рамку — переход. Для сети игрок попадает в «комнату» новой карты.
|
||||
|
||||
@export var target_map := "backrooms"
|
||||
@export var target_scene := "res://World/Backrooms/backrooms.tscn"
|
||||
@export var pack_name := "" # Пакет с картой (качается при первом входе); пусто — карта уже в игре
|
||||
@export var title := "BACKROOMS"
|
||||
@export var color := Color(1.0, 0.85, 0.25)
|
||||
@export var frame_size := Vector2(2.6, 3.6)
|
||||
@export var trigger_size := Vector3(3.2, 3.6, 3.0) # Зона входа шире рамки, чтобы не обойти сбоку
|
||||
|
||||
var _used := false
|
||||
var _glow: StandardMaterial3D
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 0
|
||||
collision_mask = 1
|
||||
var cs := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = trigger_size
|
||||
cs.shape = shape
|
||||
cs.position.y = trigger_size.y * 0.5
|
||||
add_child(cs)
|
||||
_build_visuals()
|
||||
body_entered.connect(_on_body_entered)
|
||||
MapLoader.enter_failed.connect(func(): _used = false) # Не скачалось — можно зайти ещё раз
|
||||
|
||||
func _build_visuals() -> void:
|
||||
var w := frame_size.x
|
||||
var h := frame_size.y
|
||||
|
||||
var frame_mat := StandardMaterial3D.new()
|
||||
frame_mat.albedo_color = color.darkened(0.5)
|
||||
frame_mat.emission_enabled = true
|
||||
frame_mat.emission = color
|
||||
frame_mat.emission_energy_multiplier = 1.5
|
||||
_add_box(Vector3(0.2, h, 0.2), Vector3(-w * 0.5 - 0.1, h * 0.5, 0), frame_mat)
|
||||
_add_box(Vector3(0.2, h, 0.2), Vector3(w * 0.5 + 0.1, h * 0.5, 0), frame_mat)
|
||||
_add_box(Vector3(w + 0.4, 0.2, 0.2), Vector3(0, h + 0.1, 0), frame_mat)
|
||||
|
||||
_glow = StandardMaterial3D.new()
|
||||
_glow.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_glow.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
_glow.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
_glow.albedo_color = Color(color, 0.35)
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = frame_size
|
||||
quad.material = _glow
|
||||
var plane := MeshInstance3D.new()
|
||||
plane.mesh = quad
|
||||
plane.position.y = h * 0.5
|
||||
plane.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(plane)
|
||||
|
||||
var label := Label3D.new()
|
||||
label.text = title
|
||||
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
label.pixel_size = 0.01
|
||||
label.font_size = 64
|
||||
label.outline_size = 12
|
||||
label.modulate = color
|
||||
label.position.y = h + 0.8
|
||||
add_child(label)
|
||||
|
||||
var light := OmniLight3D.new()
|
||||
light.light_color = color
|
||||
light.light_energy = 1.5
|
||||
light.omni_range = 9.0
|
||||
light.position.y = h * 0.5
|
||||
light.shadow_enabled = false
|
||||
add_child(light)
|
||||
|
||||
func _add_box(size: Vector3, pos: Vector3, mat: Material) -> void:
|
||||
var mesh := BoxMesh.new()
|
||||
mesh.size = size
|
||||
mesh.material = mat
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
mi.position = pos
|
||||
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(mi)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_glow.albedo_color.a = 0.3 + 0.12 * sin(Time.get_ticks_msec() * 0.004)
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if _used or not body.is_in_group("local_player") or body.get("is_dead"):
|
||||
return
|
||||
_used = true
|
||||
MapLoader.enter(target_map, target_scene, pack_name, title)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bu8v0ivhmnykt
|
||||
Reference in New Issue
Block a user