public partial class AnimationController : Node3D { [Export] public Skeleton3D Skeleton { get; set; } [Export] public BoneAttachment3D RootBone { get; set; } // Contains all the bones in the skeleton, keyed by name (e.g. "LowerArm_L"). Dictionary _bones = []; // Whether the player's body is currently turning to match up with the camera rotation. bool _isTurning = false; // Current amount the body is turned due to walking sideways. float _bodyYaw = 0.0f; Player _player; Transform3D _cameraDefaultTransform; AnimationTree _animTree; Animation _walkForwardAnim; Animation _walkBackwardAnim; public override void _Ready() { _player = GetParent(); _animTree = GetNode("AnimationTree"); _walkForwardAnim = _animTree.GetAnimation("walk_forward"); _walkBackwardAnim = _animTree.GetAnimation("walk_backward"); void AddBone(BoneAttachment3D bone) { bone.OverridePose = true; _bones[bone.Name] = bone; } AddBone(RootBone); foreach (var child in RootBone.FindChildren("*", "BoneAttachment3D")) AddBone((BoneAttachment3D)child); } public override void _Process(double delta) { ResetTransforms(); HandleTurning(delta); HandleLookingAnimation(delta); HandleWalkingAnimation(delta); } void ResetTransforms() { foreach (var bone in _bones.Values) bone.Transform = Skeleton.GetBonePose(bone.BoneIdx); 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; } } 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; _animTree.Set("parameters/walk_state/transition_request", walkState); _animTree.Set("parameters/walk_direction/transition_request", walkDirection); var prevWalkSpeed = (float)_animTree.Get("parameters/walk_speed/blend_amount"); _animTree.Set("parameters/walk_speed/blend_amount", Lerp(prevWalkSpeed, walkSpeed, 10 * (float)delta)); const float YawFactorLowerBody = 0.15f; const float YawFactorUpperBody = 0.20f; const float YawFactorNeck = 0.45f; 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); } }