You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
183 lines
7.2 KiB
183 lines
7.2 KiB
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<string, BoneAttachment3D> _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<Player>(); |
|
_animTree = GetNode<AnimationTree>("AnimationTree"); |
|
_walkForwardAnim = _animTree.GetAnimation("walk_forward"); |
|
_walkBackwardAnim = _animTree.GetAnimation("walk_backward"); |
|
|
|
_rootBone = GetNode<BoneAttachment3D>("Root"); |
|
foreach (var bone in FindChildren("*").OfType<BoneAttachment3D>()) |
|
{ 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<Node3D>(); |
|
var model = skel.GetParent<Node3D>(); |
|
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; |
|
} |
|
} |
|
|
|
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 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); |
|
} |
|
|
|
Vector3? _defaultPickupPosition; |
|
void HandleHoldingAnimation(double delta) |
|
{ |
|
var posPickup = _defaultPickupPosition ??= _player.Pickup.Position; |
|
posPickup = _player.GlobalPosition + posPickup.Rotated(Vector3.Up, Tau / 2 + _bones["UpperBody"].GlobalRotation.Y); |
|
|
|
var posWristR = _bones["Wrist_R"].GlobalPosition; |
|
var posWristL = _bones["Wrist_L"].GlobalPosition; |
|
_player.Pickup.GlobalPosition = (posPickup + posWristR + posWristL) / 3; |
|
_player.Pickup.Rotation = _player.Pickup.Rotation with { Y = _bones["UpperBody"].Rotation.Y }; |
|
|
|
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)); |
|
} |
|
}
|
|
|