public partial class AnimationController : Node3D { [Export] public Skeleton3D Skeleton { get; set; } // Contains all the bones in the skeleton, keyed by name (e.g. "LowerArm_L"). Dictionary _bones = []; bool _isTurning = false; // Whether the player's body is currently turning to match up with the camera rotation. float _bodyYaw = 0.0f; // Current amount the body is turned due to walking sideways. Player _player; Transform3D _cameraDefaultTransform; AnimationTree _animTree; Animation _walkForwardAnim; Animation _walkBackwardAnim; BoneAttachment3D _rootBone; public override void _Ready() { _player = GetParent(); _animTree = GetNode("AnimationTree"); _walkForwardAnim = _animTree.GetAnimation("walk_forward"); _walkBackwardAnim = _animTree.GetAnimation("walk_backward"); _rootBone = GetNode("Root"); foreach (var bone in FindChildren("*").OfType()) { bone.OverridePose = true; _bones[bone.Name] = bone; } // We disable the AnimationTree while in the editor so our // BoneAttackment3D nodes don't get updated, resulting in // those changes being picked up by version control. _animTree.Active = true; } public override void _Process(double delta) { ResetTransforms(); HandleTurning(delta); HandleLookingAnimation(delta); HandleWalkingAnimation(delta); HandleHoldingAnimation(delta); } void ResetTransforms() { foreach (var bone in _bones.Values) { bone.Transform = Skeleton.GetBonePose(bone.BoneIdx); if (bone == _rootBone) { // This is a dirty hack that makes sure the `Root` bone has the same // Transform as it would if it was parented to `Player/Model/Skeleton`. var skel = Skeleton.GetParent(); var model = skel.GetParent(); bone.Transform = model.Transform * skel.Transform * bone.Transform; } } if (_player.Camera.Camera is Camera3D camera) camera.Transform = _player.Camera.DefaultTransform; } void HandleTurning(double delta) { const float TurnBegin = 60.0f; // Start turning when camera is rotated this much. const float TurnEnd = 5.0f; // Stop turning when body is this close to camera rotation. const float TurnSpeed = 6.0f; var yaw = _player.Camera.CurrentYaw; // Camera yaw relative to player yaw. var movement = _player.Movement; var isWalking = movement.LocalMoveVector.Length() > movement.MaxSpeed / 4; _isTurning = isWalking || (Abs(yaw) > DegToRad(TurnBegin)); if (_isTurning) { var yawDelta = Sign(yaw) * Min(Abs(yaw), Abs(yaw) * TurnSpeed * (float)delta); _player.Camera.CurrentYaw -= (float)yawDelta; _player.RotateY(yawDelta); if (Abs(_player.Camera.CurrentYaw) < DegToRad(TurnEnd)) _isTurning = false; } } void HandleLookingAnimation(double delta) { var camera = _player.Camera.Camera; var pitch = _player.Camera.CurrentPitch; var yaw = _player.Camera.CurrentYaw; const float PitchFactorLowerBody = 0.05f; const float PitchFactorUpperBody = 0.20f; const float PitchFactorNeck = 0.25f; const float PitchFactorHead = 0.35f; _bones["LowerBody" ].RotateX( pitch * PitchFactorLowerBody); _bones["UpperBody" ].RotateX(-pitch * PitchFactorUpperBody); _bones["Neck" ].RotateX(-pitch * PitchFactorNeck); _bones["Head" ].RotateX(-pitch * PitchFactorHead); _bones["UpperArm_L"].RotateX( pitch * (PitchFactorLowerBody + PitchFactorUpperBody) / 2); _bones["UpperArm_R"].RotateX( pitch * (PitchFactorLowerBody + PitchFactorUpperBody) / 2); camera?.RotateX(pitch * (1 - PitchFactorLowerBody - PitchFactorUpperBody - PitchFactorNeck - PitchFactorHead)); const float YawFactorLowerBody = 0.06f; const float YawFactorUpperBody = 0.18f; const float YawFactorNeck = 0.2f; const float YawFactorHead = 0.3f; _bones["LowerBody"].GlobalRotate(Vector3.Up, yaw * YawFactorLowerBody); _bones["UpperBody"].GlobalRotate(Vector3.Up, yaw * YawFactorUpperBody); _bones["Neck" ].GlobalRotate(Vector3.Up, yaw * YawFactorNeck); _bones["Head" ].GlobalRotate(Vector3.Up, yaw * YawFactorHead); camera?.GlobalRotate(Vector3.Up, yaw * (1 - YawFactorLowerBody - YawFactorUpperBody - YawFactorNeck - YawFactorHead)); if (camera != null) { // How much of the "ideal" camera rotation (rather than animation rotation) should be applied. const float CameraFactorIdealPitch = 0.7f; const float CameraFactorIdealYaw = 0.8f; const float CameraFactorIdealRoll = 0.9f; var globalYaw = yaw + _player.Rotation.Y; var rot = camera.GlobalRotation; // FIXME: This doesn't apply correctly when looking up or down. rot.X = LerpAngle(rot.X, pitch, CameraFactorIdealPitch); rot.Y = LerpAngle(rot.Y, globalYaw, CameraFactorIdealYaw); rot.Z = LerpAngle(rot.Z, 0, CameraFactorIdealRoll); camera.GlobalRotation = rot; } } float _yawFactorLowerBody = 0.15f; float _yawFactorUpperBody = 0.20f; float _yawFactorNeck = 0.45f; void HandleWalkingAnimation(double delta) { const float ForwardAngle = 95.0f; var movement = _player.Movement; var localAngle = movement.LocalMoveAngle; var isOnFloor = movement.TimeSinceOnFloor < 0.25f; var isMovingForward = Abs(localAngle) <= DegToRad(ForwardAngle); var walkState = (isOnFloor && movement.IsMoving) ? "move" : "idle"; var walkDirection = isMovingForward ? "forward" : "backward"; var walkSpeed = movement.LocalMoveVector.Length() / movement.MaxSpeed; const string WalkStateParam = "parameters/walk_state/transition_request"; const string WalkDirectionParam = "parameters/walk_direction/transition_request"; const string WalkSpeedParam = "parameters/walk_speed/blend_amount"; _animTree.Set(WalkStateParam, walkState); _animTree.Set(WalkDirectionParam, walkDirection); var prevWalkSpeed = (float)_animTree.Get(WalkSpeedParam); _animTree.Set(WalkSpeedParam, Lerp(prevWalkSpeed, walkSpeed, 10 * (float)delta)); const float EmptyYawFactorLowerBody = 0.15f; const float EmptyYawFactorUpperBody = 0.20f; const float EmptyYawFactorNeck = 0.45f; const float HoldingYawFactorLowerBody = 0.35f; const float HoldingYawFactorUpperBody = 0.25f; const float HoldingYawFactorNeck = 0.10f; // TODO: This should probably be using delta somehow, but I'm not sure how. var held = _player.Pickup.HasItemsHeld; _yawFactorLowerBody = (_yawFactorLowerBody + (held ? HoldingYawFactorLowerBody : EmptyYawFactorLowerBody)) / 2; _yawFactorUpperBody = (_yawFactorLowerBody + (held ? HoldingYawFactorUpperBody : EmptyYawFactorUpperBody)) / 2; _yawFactorNeck = (_yawFactorLowerBody + (held ? HoldingYawFactorNeck : EmptyYawFactorNeck )) / 2; if (movement.IsMoving) { var targetBodyYaw = localAngle; if (!isMovingForward) targetBodyYaw -= Sign(localAngle) * Tau / 2; _bodyYaw += (targetBodyYaw - _bodyYaw) * (float)delta * 6; } else _bodyYaw -= _bodyYaw * (float)delta * 2; _bones["Root" ].GlobalRotate(Vector3.Up, _bodyYaw * (_yawFactorLowerBody + _yawFactorUpperBody + _yawFactorNeck)); _bones["LowerBody"].GlobalRotate(Vector3.Up, -_bodyYaw * _yawFactorLowerBody); _bones["UpperBody"].GlobalRotate(Vector3.Up, -_bodyYaw * _yawFactorUpperBody); _bones["Neck" ].GlobalRotate(Vector3.Up, -_bodyYaw * _yawFactorNeck); } Vector3? _defaultPickupPosition; void HandleHoldingAnimation(double delta) { var posWristR = _bones["Wrist_R"].GlobalPosition; var posWristL = _bones["Wrist_L"].GlobalPosition; var bodyYaw = _bones["UpperBody"].GlobalRotation.Y; var posPickup = _defaultPickupPosition ??= _player.Pickup.Position; posPickup = _player.GlobalPosition + posPickup.Rotated(Vector3.Up, Tau / 2 + bodyYaw); _player.Pickup.GlobalPosition = (posPickup + posWristR + posWristL) / 3; _player.Pickup.GlobalRotation = _player.Pickup.GlobalRotation with { Y = bodyYaw }; const string IsHoldingParam = "parameters/is_holding/blend_amount"; var isHolding = _player.Pickup.HasItemsHeld ? 0.9f : 0.0f; var prevIsHolding = (float)_animTree.Get(IsHoldingParam); _animTree.Set(IsHoldingParam, Lerp(prevIsHolding, isHolding, 8 * (float)delta)); } }