Fix NPCs walking backwards
Avatars called Godot's look_at(), which points the node's -Z axis at the target, but the rig's face (eyes/brows/mouth/focus disc, and the Blender successor) is on +Z. Every walking NPC therefore faced away from its movement direction. - add the pure yaw_towards(direction) helper (+Z forward) and use it in both movement paths and the reaction face-turn - fix the head-turn offset to the same +Z convention (basis.z) - test +Z/+X/-Z/arbitrary directions; recapture targeting screenshot - TODO bug item moved to DONE, MEMORY records the convention Headless 209/209.
This commit is contained in:
1 parent
a7daae8a14
commit
0eb17e3bb4
6 files changed
+53
-9
No files matched your search
@@ -814,3 +814,19 @@ lookup, constants, gamepad bindings, and settings persistence.
|
||||
deadzone, remap descriptor/apply/labels, and settings persistence for camera and bindings.
|
||||
- [x] Added `smoke_movement.gd` (windowed) and `11-precision-walk.png` with a movement catalogue
|
||||
entry, AI mockup, and compare coverage.
|
||||
|
||||
## NPC Facing During Movement
|
||||
|
||||
Completed 2026-11-01 (post-v0.5.0, pre-v0.6.0). Headless suite **209/209**.
|
||||
|
||||
- [x] Root cause: `NpcAvatar` used Godot's `look_at()`, which aims the node's **-Z** axis at
|
||||
the target, but the procedural rig's face (eyes, brows, mouth, focus disc) and every
|
||||
Blender-authored successor face **+Z**. Every walking NPC therefore showed its face
|
||||
toward the side it was leaving.
|
||||
- [x] Added the pure `NpcAvatar.yaw_towards(direction)` helper (+Z forward, tested at +Z/+X/-Z
|
||||
and an arbitrary direction), and replaced `look_at()` in the navigation and direct-move
|
||||
paths plus the reaction-face turn.
|
||||
- [x] Corrected the head-turn offsets to use the same +Z convention (`basis.z` instead of
|
||||
`-basis.z`), so noticed NPCs still look at the guard with the fixed facing.
|
||||
- [x] Added `test_yaw_faces_the_movement_direction`; recaptured the targeting screenshot to
|
||||
confirm NPCs now show their back only when walking away.
|
||||
@@ -20,6 +20,12 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
- Shift length is the scenario's `shift_duration_s` (900 s in all three venues). Tests and the screenshot tool override elapsed time; they do not change the default.
|
||||
- RNG: NPC reactions and event scheduling are seedable for deterministic tests.
|
||||
|
||||
- **NPC facing (fixed after v0.5.0):** the avatar's visual front is **+Z** (procedural eyes
|
||||
and the Blender model's face). Godot `look_at()` aims **-Z**, which made every NPC walk
|
||||
backwards. Use `NpcAvatar.yaw_towards(dir)` and set `rotation.y`; do not call `look_at()` on
|
||||
avatars. Head-turn offsets use `+basis.z` for the same reason. Covered by
|
||||
`test_yaw_faces_the_movement_direction`.
|
||||
|
||||
## Handoff (read this first)
|
||||
|
||||
- **Phase 2 — AI texturing (this agent, DONE 2026-11-01).** Phase 1 assets were
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
# TODO
|
||||
|
||||
Godot 4.3 prototype. Latest release **v0.5.0** (NPC characters and player movement). Headless
|
||||
suite **208/208** plus **56** Python tooling tests; the session, PLAN-verb, crowd, emergency,
|
||||
suite **209/209** plus **56** Python tooling tests; the session, PLAN-verb, crowd, emergency,
|
||||
timeline, audio, scenario-content, props, NPC-budget, movement, and windowed focus/radial smokes
|
||||
pass. Anonymous downloads are enabled on the host.
|
||||
|
||||
The scheduled presentation pass is complete (see `DONE.md`). The next scheduled pass is the
|
||||
graphics-quality work in [`docs/plan-graphics-v0.6.md`](docs/plan-graphics-v0.6.md).
|
||||
|
||||
## Bug Fix NPCs walking backwards
|
||||
|
||||
- [ ] Investigate and fix the issue causing NPCs to walk backwards during movement.
|
||||
|
||||
## Graphics quality — Blender-authored NPC characters (v0.6.0)
|
||||
|
||||
Detailed plan: [`docs/plan-graphics-v0.6.md`](docs/plan-graphics-v0.6.md) · [ADR-0013](docs/adr/0013-blender-authored-npc-characters.md)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
+13
-4
@@ -252,6 +252,15 @@ static func queue_slot_for(index: int) -> Vector3:
|
||||
]
|
||||
return slots[clampi(index, 0, slots.size() - 1)]
|
||||
|
||||
## Yaw that makes the avatar's face (+Z, the procedural and Blender-front axis)
|
||||
## point along `direction`. Godot's `look_at` aims -Z, which used to make every
|
||||
## NPC walk backwards; keep the +Z convention in one tested helper.
|
||||
static func yaw_towards(direction: Vector3) -> float:
|
||||
var flat := Vector2(direction.x, direction.z)
|
||||
if flat.length() < 0.001:
|
||||
return 0.0
|
||||
return atan2(flat.x, flat.y)
|
||||
|
||||
static func lod_tier_for_distance(distance: float) -> int:
|
||||
if distance < LOD_TIERS[0]:
|
||||
return 0
|
||||
@@ -656,8 +665,8 @@ func _face_reaction_point(delta: float) -> void:
|
||||
to_point.y = 0.0
|
||||
if to_point.length() < 0.1:
|
||||
return
|
||||
var target_yaw := atan2(-to_point.x, -to_point.z)
|
||||
rotation.y = lerp_angle(rotation.y, target_yaw, clampf(delta * 4.0, 0.0, 1.0))
|
||||
rotation.y = lerp_angle(rotation.y, yaw_towards(to_point),
|
||||
clampf(delta * 4.0, 0.0, 1.0))
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if npc == null or npc.fsm == null:
|
||||
@@ -1021,7 +1030,7 @@ func _move_toward_target(delta: float) -> void:
|
||||
return
|
||||
dir = dir.normalized()
|
||||
var speed := FLEE_SPEED if reaction_mode == "flee" else 1.3
|
||||
look_at(global_position + Vector3(dir.x, 0, dir.z), Vector3.UP)
|
||||
rotation.y = yaw_towards(dir)
|
||||
if agent.avoidance_enabled:
|
||||
# The navigation server returns the obstacle-aware velocity through
|
||||
# velocity_computed; never dash straight at the point in the meantime.
|
||||
@@ -1059,7 +1068,7 @@ func _move_direct(delta: float) -> void:
|
||||
velocity.x = dir.x * speed
|
||||
velocity.z = dir.z * speed
|
||||
velocity.y -= 9.8 * delta
|
||||
look_at(global_position + Vector3(dir.x, 0, dir.z), Vector3.UP)
|
||||
rotation.y = yaw_towards(dir)
|
||||
move_and_slide()
|
||||
|
||||
func _arrive() -> void:
|
||||
|
||||
@@ -55,6 +55,23 @@ func test_idle_styles_are_known() -> bool:
|
||||
ok = ok and style in allowed
|
||||
return TestUtils.check(ok, "idle styles stay in the catalogue")
|
||||
|
||||
func test_yaw_faces_the_movement_direction() -> bool:
|
||||
var a := TestUtils.check(is_zero_approx(NpcAvatar.yaw_towards(Vector3(0, 0, 1))),
|
||||
"+Z is the neutral facing")
|
||||
var x_dir := NpcAvatar.yaw_towards(Vector3(1, 0, 0))
|
||||
var b := TestUtils.check(is_equal_approx(x_dir, PI / 2.0),
|
||||
"+X turns a quarter turn")
|
||||
var back := NpcAvatar.yaw_towards(Vector3(0, 0, -1))
|
||||
var c := TestUtils.check(absf(absf(back) - PI) < 0.001, "-Z is a half turn")
|
||||
var direction := Vector3(0.4, 0, -0.9).normalized()
|
||||
var yaw := NpcAvatar.yaw_towards(direction)
|
||||
var facing := Basis(Vector3.UP, yaw) * Vector3(0, 0, 1)
|
||||
var d := TestUtils.check(facing.dot(direction) > 0.999,
|
||||
"the avatar's +Z face points along the walk direction")
|
||||
var e := TestUtils.check(is_zero_approx(NpcAvatar.yaw_towards(Vector3.ZERO)),
|
||||
"zero direction falls back to neutral")
|
||||
return a and b and c and d and e
|
||||
|
||||
func test_queue_slots_are_spaced_and_unique() -> bool:
|
||||
var seen := {}
|
||||
var ok := true
|
||||
|
||||
Reference in new issue
Block a user