# player_move_test —— §3.1 Src/Dst 移动模型(player_controller.gd)headless 自检。 # godot --headless --path project --script player_move_test.gd # 覆盖:NEW_Goto 设 Src/Dst/is_going、__IsMovableGroundDistance 太近不动、 # 三门(frozen/lock/moving_skill)、__ReserveClickGround 的 0.1s delay 重试。 extends SceneTree const PlayerCtl = preload("res://player_controller.gd") const GameCam = preload("res://game_camera.gd") var _fail := 0 func _ck(c: bool, m: String) -> void: if not c: _fail += 1 printerr("FAIL: " + m) func _init() -> void: await _run() if _fail == 0: print("PASS: player_move_test (Src/Dst move model / gates / reserved ground)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: # SceneTree._init() 早于 root 就绪;等一帧再建 Node3D,避免 !is_inside_tree()。 await process_frame var pc: Node = PlayerCtl.new() get_root().add_child(pc) pc.player = Node3D.new() get_root().add_child(pc.player) pc.player.position = Vector3.ZERO # 1) NEW_Goto:远点 -> 设 Dst + is_going + 面向 var ok: bool = pc._goto(Vector3(10, 0, 0)) _ck(ok, "_goto(far) returns true") _ck(pc.is_going(), "_goto -> is_going()") _ck(pc._dst_pos.is_equal_approx(Vector3(10, 0, 0)), "Dst set to click point") _ck(pc._src_pos.is_equal_approx(Vector3.ZERO), "Src set to current pos") _ck(is_equal_approx(pc._dst_rot, atan2(10.0, 0.0)), "_dst_rot faces the target") # 2) __IsMovableGroundDistance:太近(< 1 m)不动 pc._is_going = false var near_ok: bool = pc._goto(Vector3(0.5, 0, 0)) _ck(near_ok, "_goto(near) returns true (treated as done)") _ck(not pc.is_going(), "within MOVABLE_GROUND_DISTANCE -> no movement") # 3) 每帧推进:朝 Dst 走,抵达后 is_going 归零 pc._goto(Vector3(3, 0, 0)) for i in 240: pc._process(0.05) if not pc.is_going(): break _ck(pc.player.position.distance_to(Vector3(3, 0, 0)) < 0.5, "advances to Dst, got %s" % pc.player.position) _ck(not pc.is_going(), "arrival clears is_going") # 4) 三门:frozen / lock -> _goto 被挡 -> 预约 + 0.1s 后重试 pc.player.position = Vector3.ZERO pc._is_going = false pc.frozen = true var blocked: bool = pc._goto(Vector3(8, 0, 0)) _ck(not blocked, "_goto while frozen returns false (gated)") _ck(not pc.is_going(), "frozen -> not going yet") _ck(pc._reserved_ground != null, "frozen -> click ground reserved") # 冻结中推进:不平移、不消预约 pc._process(0.05) _ck(pc.player.position.is_equal_approx(Vector3.ZERO), "frozen -> no translation") # 解冻:delay 递减到 0 后自动重试预约点 pc.frozen = false for i in 10: pc._process(0.02) _ck(pc.is_going() or pc.player.position.x > 0.1, "reserved ground fires after delay once unfrozen (going=%s x=%.2f)" % [pc.is_going(), pc.player.position.x]) # 5) moving_skill:只转向不平移 pc.player.position = Vector3.ZERO pc._is_going = false pc.moving_skill = true pc._goto(Vector3(0, 0, 6)) _ck(not pc.is_going(), "_goto while moving_skill is gated (no translate)") pc.moving_skill = false # 清掉第 5 组留下的预约点,免得下面各组被它改向。 pc._reserved_ground = null pc._reserved_delay_time = 0.0 # 6) Actor-Actor 碰撞(TestActorCollision / BlockMovement 近似)——正前方有远端 Actor # 时走不过去,停在两 body 球相接前;is_going 归零(BlockMovement = 不滑移硬停)。 pc.player.position = Vector3.ZERO pc.player.rotation.y = 0.0 pc._is_going = false pc.skip_actor_collision = false var blocker := Node3D.new() get_root().add_child(blocker) blocker.global_position = Vector3(2, 0, 0) # 正前方 2 m pc.pickables.append(blocker) pc._goto(Vector3(10, 0, 0)) # 想穿过 blocker for i in 240: pc._process(0.05) if not pc.is_going(): break _ck(pc.player.position.x > 0.3 and pc.player.position.x < 1.5, "actor collision halts before overlap (x=%.2f)" % pc.player.position.x) _ck(not pc.is_going(), "blocked advance clears is_going (BlockMovement)") # 6b) 死亡的 Actor 不挡(rVictim.IsDead()) pc.player.position = Vector3.ZERO pc._is_going = false blocker.set_meta("dead", true) pc._goto(Vector3(10, 0, 0)) for i in 400: pc._process(0.05) if not pc.is_going(): break _ck(pc.player.position.distance_to(Vector3(10, 0, 0)) < 0.6, "dead actor does not block (x=%.2f)" % pc.player.position.x) blocker.set_meta("dead", false) # 6c) CanSkipCollision(skip_actor_collision)为真 → 整段跳过,可穿过 pc.player.position = Vector3.ZERO pc._is_going = false pc.skip_actor_collision = true pc._goto(Vector3(10, 0, 0)) for i in 400: pc._process(0.05) if not pc.is_going(): break _ck(pc.player.position.distance_to(Vector3(10, 0, 0)) < 0.6, "skip_actor_collision passes straight through (x=%.2f)" % pc.player.position.x) pc.skip_actor_collision = false pc.pickables.erase(blocker) blocker.free() # 7) 坐骑转向速度:MountHorse → c_fDefaultHorseRotationSpeed(300),逐帧收敛不瞬转 _ck(is_equal_approx(pc.rotation_speed_deg, pc.ROT_SPEED_DEFAULT_DEG), "default rotation speed is c_fDefaultRotationSpeed 1200") pc.rotation_speed_deg = pc.ROT_SPEED_HORSE_DEG pc.player.position = Vector3.ZERO pc.player.rotation.y = 0.0 pc._is_going = false pc._goto(Vector3(0, 0, -6)) # 需要转到 yaw ≈ ±PI pc._process(0.05) _ck(absf(pc.player.rotation.y) <= deg_to_rad(pc.ROT_SPEED_HORSE_DEG) * 0.05 + 1e-3, "mounted: turn clamped to 300 deg/s in one 50ms step (%.3f rad)" % pc.player.rotation.y) _ck(pc.player.position.z < -0.05, "mounted: still translates toward Dst while turning") pc.rotation_speed_deg = pc.ROT_SPEED_DEFAULT_DEG # 8) 相机相对移动:W / 摇杆上 = 远离相机(沿视线),D / 摇杆右 = 屏幕右。 # GameCamera 摆在 head + (sin yaw, ·, cos yaw)*dist 并看向 head,前方取真实相机几何。 var cam: Camera3D = GameCam.new() get_root().add_child(cam) cam.target = pc.player cam.yaw = deg_to_rad(30.0) pc.camera = cam pc.player.rotation.y = 0.0 var off: Vector3 = cam._desired_pos(true) - (pc.player.global_position + cam.head_offset) var away := -Vector3(off.x, 0, off.z).normalized() var screen_right := away.cross(Vector3.UP) for c in [["mobile axis up", Vector2(0, -1), away], ["mobile axis right", Vector2(1, 0), screen_right]]: pc.player.position = Vector3.ZERO pc._is_going = false pc.set_mobile_axis(c[1]) pc._process(0.1) pc.clear_mobile_input() var moved := Vector3(pc.player.position.x, 0, pc.player.position.z) _ck(moved.length() > 0.05 and moved.normalized().dot(c[2]) > 0.95, "%s moves camera-relative (want %s, moved %s)" % [c[0], c[2], moved]) for c in [[KEY_W, "W", away], [KEY_S, "S", -away], [KEY_D, "D", screen_right]]: pc.player.position = Vector3.ZERO pc._is_going = false var down := InputEventKey.new() down.keycode = c[0] down.physical_keycode = c[0] down.pressed = true Input.parse_input_event(down) Input.flush_buffered_events() _ck(Input.is_key_pressed(c[0]), "synthetic %s press reaches Input" % c[1]) pc._process(0.1) var up := down.duplicate() up.pressed = false Input.parse_input_event(up) Input.flush_buffered_events() var moved := Vector3(pc.player.position.x, 0, pc.player.position.z) _ck(moved.length() > 0.05 and moved.normalized().dot(c[2]) > 0.95, "key %s moves camera-relative (want %s, moved %s)" % [c[1], c[2], moved]) pc.camera = null cam.free()