1b1260f371
checkpoint / checkpoint (push) Successful in 1m52s
The blocky body rendered fully black from every angle: _append_box wound its faces clockwise-from-outside, so every computed normal pointed inward -- an inside-out shell, lit from behind on all sides, and the level had ambient light disabled so nothing lifted it. Fixed the winding (normals now verify outward 234:36) and switched test_level's environment to a soft colour ambient so shaded sides read grey, not pure black. Limbs were static because the rig had no animation at all. Added scripts/avatar/limb_animator.gd: a clip-free walk cycle that swings the upper arm/leg bones with ground speed, folds the knees on the rear stride, and eases back to rest when idle. Wired into the local player (speed from velocity) and remote players in game.gd (speed from the per-snapshot authoritative position delta). Verified: avatar smoke 14/14 from source and exported pack; fresh-clone boot PASS; game + menu boot headless clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
71 lines
2.3 KiB
GDScript
71 lines
2.3 KiB
GDScript
extends Node
|
|
## Procedural locomotion for the placeholder blocky avatar.
|
|
##
|
|
## There are no animation clips yet -- this just swings the upper arm/leg bones
|
|
## around their local X by a sine wave whose phase advances with distance
|
|
## travelled, bends the knees on the rear half of the stride, and eases the
|
|
## whole thing back to the rest pose when the character stops.
|
|
##
|
|
## The owner sets `speed` (horizontal m/s) every frame; setup() is given the
|
|
## avatar's Skeleton3D. Bones are addressed by canonical name, so this works
|
|
## for the local player and for snapshot-driven remote players alike.
|
|
|
|
const STRIDE := 0.7 ## peak hip/shoulder swing, radians
|
|
const KNEE := 1.0 ## peak knee bend, radians
|
|
const CYCLE := 1.9 ## phase (radians) advanced per metre travelled
|
|
const BLEND := 12.0 ## ease rate toward the target pose
|
|
const MOVE_EPS := 0.35 ## m/s below which we treat the avatar as idle
|
|
|
|
var speed: float = 0.0
|
|
|
|
var _skel: Skeleton3D = null
|
|
var _idx := {} # bone name -> skeleton index
|
|
var _phase := 0.0
|
|
var _amount := 0.0 # 0 = idle, 1 = full walk
|
|
|
|
const _BONES := [
|
|
"LeftUpperLeg", "RightUpperLeg", "LeftLowerLeg", "RightLowerLeg",
|
|
"LeftUpperArm", "RightUpperArm",
|
|
]
|
|
|
|
|
|
func setup(skeleton: Skeleton3D) -> void:
|
|
_skel = skeleton
|
|
_idx.clear()
|
|
if skeleton == null:
|
|
return
|
|
for b in _BONES:
|
|
var i := skeleton.find_bone(b)
|
|
if i != -1:
|
|
_idx[b] = i
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if _skel == null or _idx.is_empty():
|
|
return
|
|
|
|
var moving := speed > MOVE_EPS
|
|
_amount = lerpf(_amount, 1.0 if moving else 0.0, clampf(BLEND * delta, 0.0, 1.0))
|
|
_phase += maxf(speed, 0.0) * delta * CYCLE
|
|
if _phase > TAU:
|
|
_phase -= TAU
|
|
|
|
var s := sin(_phase)
|
|
var swing := s * STRIDE * _amount
|
|
|
|
_pitch("LeftUpperLeg", swing)
|
|
_pitch("RightUpperLeg", -swing)
|
|
_pitch("LeftUpperArm", -swing)
|
|
_pitch("RightUpperArm", swing)
|
|
# Knee folds only while that leg is swinging back (negative pitch phase).
|
|
_pitch("LeftLowerLeg", maxf(0.0, -s) * KNEE * _amount)
|
|
_pitch("RightLowerLeg", maxf(0.0, s) * KNEE * _amount)
|
|
|
|
|
|
func _pitch(bone: String, angle: float) -> void:
|
|
if not _idx.has(bone):
|
|
return
|
|
var i: int = _idx[bone]
|
|
var rest_q := _skel.get_bone_rest(i).basis.get_rotation_quaternion()
|
|
_skel.set_bone_pose_rotation(i, rest_q * Quaternion(Vector3.RIGHT, angle))
|