Карта 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:
@@ -1,5 +1,7 @@
|
||||
extends Node3D
|
||||
# Камень преследует ближайшего живого игрока, обходя препятствия по навмешу, и ловит его.
|
||||
# С sight_limited он замечает игрока лишь вблизи или в поле зрения без стен между ними;
|
||||
# потеряв из виду, идёт к последнему месту, где видел, и дальше бродит по карте.
|
||||
# В сети камень считает один клиент (владелец, его назначает сервер), остальные
|
||||
# плавно повторяют его позицию — поэтому у всех игроков камень один и тот же.
|
||||
|
||||
@@ -12,15 +14,37 @@ extends Node3D
|
||||
@export var sync_smoothing := 12.0 # Плавность повтора чужого камня
|
||||
@export var snap_distance := 20.0 # Дальше этого чужой камень не едет, а прыгает (сброс на старт)
|
||||
|
||||
@export_group("Зрение")
|
||||
@export var sight_limited := false # false — знает, где игрок, из любой точки карты
|
||||
@export var sight_range := 30.0 # Дальность зрения, м
|
||||
@export var sight_fov := 120.0 # Угол обзора, градусы
|
||||
@export var hearing_radius := 6.0 # Ближе этого замечает игрока, даже за стеной
|
||||
@export var lose_time := 4.0 # Сколько ещё идёт за игроком после потери из виду, сек
|
||||
@export var patrol_speed := 0.4 # Доля speed, когда бродит
|
||||
@export var wander_radius := 30.0 # Куда выбирает точки для прогулки, м
|
||||
@export var eye_height := 0.9 # Высота «глаз» над полом, м
|
||||
|
||||
@onready var agent: NavigationAgent3D = $NavigationAgent3D
|
||||
@onready var start_transform := global_transform
|
||||
|
||||
enum State { WANDER, CHASE }
|
||||
|
||||
var player: Node3D = null
|
||||
var _state := State.WANDER
|
||||
var _last_seen := Vector3.ZERO
|
||||
var _since_seen := 0.0
|
||||
var _wander_target := Vector3.ZERO
|
||||
var _wander_active := false
|
||||
var _wander_wait := 0.0
|
||||
var _wander_time := 0.0
|
||||
var _own_rids: Array[RID] = []
|
||||
|
||||
func _ready() -> void:
|
||||
agent.target_desired_distance = 1.0
|
||||
agent.path_desired_distance = 1.5
|
||||
agent.path_height_offset = 0.25 # Навмеш лежит чуть выше пола
|
||||
for body in find_children("*", "CollisionObject3D", true, false):
|
||||
_own_rids.append(body.get_rid()) # Луч зрения не должен упираться в сам камень
|
||||
Net.rock = self
|
||||
Net.peer_respawned.connect(_on_respawned)
|
||||
|
||||
@@ -51,52 +75,151 @@ func _try_catch() -> void:
|
||||
if flat.length() <= catch_distance and absf(dy) <= catch_height:
|
||||
player.die()
|
||||
|
||||
# Ближайший по горизонтали живой игрок: свой или чужой
|
||||
func _pick_target() -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_d := INF
|
||||
var candidates: Array[Node3D] = []
|
||||
# Живые игроки: свой и чужие
|
||||
func _candidates() -> Array[Node3D]:
|
||||
var out: Array[Node3D] = []
|
||||
if not player.get("is_dead"):
|
||||
candidates.append(player)
|
||||
out.append(player)
|
||||
for id in Net.ghosts:
|
||||
var g: Node3D = Net.ghosts[id]
|
||||
if is_instance_valid(g) and not Net.dead_peers.has(id):
|
||||
candidates.append(g)
|
||||
for c in candidates:
|
||||
var d := Vector2(c.global_position.x - global_position.x, c.global_position.z - global_position.z).length_squared()
|
||||
out.append(g)
|
||||
return out
|
||||
|
||||
func _horizontal_dist_sq(c: Node3D) -> float:
|
||||
return Vector2(c.global_position.x - global_position.x, c.global_position.z - global_position.z).length_squared()
|
||||
|
||||
# Ближайший по горизонтали живой игрок
|
||||
func _pick_target() -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_d := INF
|
||||
for c in _candidates():
|
||||
var d := _horizontal_dist_sq(c)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = c
|
||||
return best
|
||||
|
||||
func _chase(delta: float) -> void:
|
||||
var target := _pick_target()
|
||||
if target == null:
|
||||
return # Все лежат — стоим
|
||||
# Ближайший из тех, кого камень сейчас замечает
|
||||
func _pick_visible_target() -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_d := INF
|
||||
for c in _candidates():
|
||||
var d := _horizontal_dist_sq(c)
|
||||
if d < best_d and _can_see(c):
|
||||
best_d = d
|
||||
best = c
|
||||
return best
|
||||
|
||||
var flat := target.global_position - global_position
|
||||
flat.y = 0.0
|
||||
if flat.length() <= catch_distance:
|
||||
_face(flat, delta) # Стоим над жертвой, пока её клиент не засчитает поимку
|
||||
# Рядом — слышит (даже за стеной); дальше — только в конусе обзора и если стены не мешают
|
||||
func _can_see(c: Node3D) -> bool:
|
||||
var to := c.global_position - global_position
|
||||
var flat := Vector3(to.x, 0.0, to.z)
|
||||
var dist := flat.length()
|
||||
if dist <= hearing_radius:
|
||||
return true
|
||||
if dist > sight_range:
|
||||
return false
|
||||
var fwd := -global_transform.basis.z
|
||||
fwd.y = 0.0
|
||||
if fwd.normalized().dot(flat / dist) < cos(deg_to_rad(sight_fov * 0.5)):
|
||||
return false
|
||||
var query := PhysicsRayQueryParameters3D.create(global_position + Vector3(0, eye_height, 0), c.global_position, 1)
|
||||
var exclude := _own_rids.duplicate()
|
||||
if c is CollisionObject3D:
|
||||
exclude.append(c.get_rid())
|
||||
query.exclude = exclude
|
||||
return get_world_3d().direct_space_state.intersect_ray(query).is_empty()
|
||||
|
||||
func _chase(delta: float) -> void:
|
||||
if not sight_limited:
|
||||
var target := _pick_target()
|
||||
if target != null: # Все лежат — стоим
|
||||
_move_to(target.global_position, speed, catch_distance, delta)
|
||||
return
|
||||
|
||||
var seen := _pick_visible_target()
|
||||
if seen != null:
|
||||
_state = State.CHASE
|
||||
_last_seen = seen.global_position
|
||||
_since_seen = 0.0
|
||||
elif _state == State.CHASE:
|
||||
_since_seen += delta
|
||||
if _since_seen > lose_time:
|
||||
_state = State.WANDER
|
||||
_wander_active = false
|
||||
_wander_wait = 1.0
|
||||
|
||||
if _state == State.CHASE:
|
||||
# Видим — стоим над жертвой у catch_distance; потеряли — идём к последнему месту
|
||||
_move_to(_last_seen, speed, catch_distance if seen != null else 1.5, delta)
|
||||
else:
|
||||
_wander(delta)
|
||||
|
||||
func _wander(delta: float) -> void:
|
||||
if _wander_wait > 0.0:
|
||||
_wander_wait -= delta
|
||||
return
|
||||
if not _wander_active:
|
||||
if not _pick_wander_target():
|
||||
_wander_wait = 1.0
|
||||
return
|
||||
_wander_active = true
|
||||
_wander_time = 0.0
|
||||
_wander_time += delta
|
||||
if _move_to(_wander_target, speed * patrol_speed, 1.5, delta) or _wander_time > 20.0:
|
||||
_wander_active = false
|
||||
_wander_wait = randf_range(1.0, 3.0)
|
||||
|
||||
# Случайная точка навмеша на расстоянии 8..wander_radius м; нет такой — ближайшая из дальних
|
||||
func _pick_wander_target() -> bool:
|
||||
var map := agent.get_navigation_map()
|
||||
if NavigationServer3D.map_get_iteration_id(map) == 0:
|
||||
return false
|
||||
var best := Vector3.INF
|
||||
var best_d := INF
|
||||
for i in 8:
|
||||
var p := NavigationServer3D.map_get_random_point(map, 1, true)
|
||||
var d := p.distance_to(global_position)
|
||||
if d < 8.0:
|
||||
continue
|
||||
if d <= wander_radius:
|
||||
best = p
|
||||
break
|
||||
if d < best_d:
|
||||
best = p
|
||||
best_d = d
|
||||
if best == Vector3.INF:
|
||||
return false
|
||||
_wander_target = best
|
||||
return true
|
||||
|
||||
# Шаг к точке по навмешу. true — дошли (ближе stop_dist по горизонтали)
|
||||
func _move_to(dest: Vector3, spd: float, stop_dist: float, delta: float) -> bool:
|
||||
var flat := dest - global_position
|
||||
flat.y = 0.0
|
||||
if flat.length() <= stop_dist:
|
||||
_face(flat, delta) # Стоим над жертвой, пока её клиент не засчитает поимку
|
||||
return true
|
||||
|
||||
var step: Vector3
|
||||
var map := agent.get_navigation_map()
|
||||
if NavigationServer3D.map_get_iteration_id(map) > 0:
|
||||
if agent.target_position.distance_to(target.global_position) > repath_distance:
|
||||
agent.target_position = target.global_position
|
||||
if agent.target_position.distance_to(dest) > repath_distance:
|
||||
agent.target_position = dest
|
||||
if not agent.is_navigation_finished():
|
||||
step = agent.get_next_path_position() - global_position
|
||||
|
||||
# Навмеш ещё не готов или игрок вне его — идём напрямую
|
||||
# Навмеш ещё не готов или точка вне его — идём напрямую
|
||||
if step == Vector3.ZERO:
|
||||
step = flat
|
||||
|
||||
var d := step.length()
|
||||
if d < 0.001:
|
||||
return
|
||||
global_position += step / d * min(speed * delta, d)
|
||||
return false
|
||||
global_position += step / d * min(spd * delta, d)
|
||||
_face(Vector3(step.x, 0.0, step.z), delta)
|
||||
return false
|
||||
|
||||
# Камень считает другой клиент — плавно идём к тому, что он прислал
|
||||
func _follow_owner(delta: float) -> void:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://models/Bochi-Rock/rock-follow.gd" id="1_script"]
|
||||
[ext_resource type="PackedScene" uid="uid://cowp33ejxcfcl" path="res://models/Bochi-Rock/Rock.glb" id="2_model"]
|
||||
[ext_resource type="AudioStream" uid="uid://280pvcvxt2n2" path="res://models/Bochi-Rock/Guitar.ogg" id="3_audio"]
|
||||
|
||||
[node name="Rock" type="Node3D"]
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Model" parent="." instance=ExtResource("2_model")]
|
||||
|
||||
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
|
||||
height = 5.0
|
||||
radius = 2.0
|
||||
|
||||
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Model"]
|
||||
stream = ExtResource("3_audio")
|
||||
autoplay = true
|
||||
max_distance = 30.0
|
||||
Reference in New Issue
Block a user