extends Node3D @export var camera : Camera @export var skeleton : Skeleton3D @export var root_bone : BoneAttachment3D @onready var player := find_parent("Player") as Player @onready var anim_tree := $AnimationTree as AnimationTree @onready var anim_player := anim_tree.get_node(anim_tree.anim_player) as AnimationPlayer @onready var camera_default_transform := camera.transform @onready var walk_forward_anim := anim_player.get_animation("walk_forward") @onready var walk_backward_anim := anim_player.get_animation("walk_backward") # TODO: @onready var walk_loop_length := walk_forward_anim.length # Contains all the bones in the skeleton, keyed by name (e.g. "LowerArm_L"). var bones := { } # Whether the player's body is currently turning to match up with the camera rotation. var is_turning := false # Current amount the body is turned due to walking sideways. var body_yaw := 0.0 # radians func _ready() -> void: var add_bone := func(bone: BoneAttachment3D) -> void: bone.override_pose = true bones[bone.name] = bone add_bone.call(root_bone) for child in root_bone.find_children("*", "BoneAttachment3D"): add_bone.call(child as BoneAttachment3D) func _process(delta: float) -> void: reset_transforms() handle_turning(delta) handle_looking_animation(delta) handle_walking_animation(delta) func reset_transforms() -> void: for bone_name in bones: var bone := bones[bone_name] as BoneAttachment3D bone.transform = skeleton.get_bone_pose(bone.bone_idx) camera.transform = camera_default_transform func handle_turning(delta: float) -> void: const TURN_BEGIN := 60.0 # Start turning when camera is rotated this much. const TURN_END := 5.0 # Stop turning when body is this close to camera rotation. const TURN_SPEED := 6.0 var yaw := camera.current_yaw # Camera yaw relative to player yaw. if player.is_moving || abs(yaw) > deg_to_rad(TURN_BEGIN): is_turning = true if is_turning: var yaw_delta = sign(yaw) * min(abs(yaw), (abs(yaw) * TURN_SPEED) * delta) player.rotate_y(yaw_delta) camera.current_yaw -= yaw_delta if abs(camera.current_yaw) < deg_to_rad(TURN_END): is_turning = false func handle_looking_animation(_delta: float) -> void: const PITCH_FACTOR_NECK := 0.25 const PITCH_FACTOR_HEAD := 0.35 var pitch := camera.current_pitch bones["Neck"].rotate_x(-pitch * PITCH_FACTOR_NECK) bones["Head"].rotate_x(-pitch * PITCH_FACTOR_HEAD) camera.rotate_x(pitch * (1 - PITCH_FACTOR_NECK - PITCH_FACTOR_HEAD)) const YAW_FACTOR_LOWER_BODY := 0.06 const YAW_FACTOR_UPPER_BODY := 0.18 const YAW_FACTOR_NECK := 0.2 const YAW_FACTOR_HEAD := 0.3 var yaw := camera.current_yaw bones["LowerBody"].global_rotate(Vector3.UP, yaw * YAW_FACTOR_LOWER_BODY) bones["UpperBody"].global_rotate(Vector3.UP, yaw * YAW_FACTOR_UPPER_BODY) bones["Neck"].global_rotate(Vector3.UP, yaw * YAW_FACTOR_NECK) bones["Head"].global_rotate(Vector3.UP, yaw * YAW_FACTOR_HEAD) camera.global_rotate(Vector3.UP, yaw * (1 - YAW_FACTOR_LOWER_BODY - YAW_FACTOR_UPPER_BODY - YAW_FACTOR_NECK - YAW_FACTOR_HEAD)) # How much of the "ideal" camera rotation (rather than animation rotation) should be applied. const CAMERA_FACTOR_IDEAL_PITCH := 1.0 # 0.7 const CAMERA_FACTOR_IDEAL_YAW := 1.0 # 0.8 const CAMERA_FACTOR_IDEAL_ROLL := 1.0 # 0.9 var global_yaw := player.rotation.y + yaw camera.global_rotation.x = lerp_angle(camera.global_rotation.x, pitch, CAMERA_FACTOR_IDEAL_PITCH) camera.global_rotation.y = lerp_angle(camera.global_rotation.y, global_yaw, CAMERA_FACTOR_IDEAL_YAW) camera.global_rotation.z = lerp_angle(camera.global_rotation.z, 0, CAMERA_FACTOR_IDEAL_ROLL) func handle_walking_animation(delta: float) -> void: var input := Input.get_vector("move_strafe_left", "move_strafe_right", "move_forward", "move_backward") var is_on_floor := player.time_since_on_floor < 0.25 var is_moving_forward := input.y <= 0 var walk_state : String var walk_direction : String var target_body_yaw : float if is_on_floor && player.is_moving: walk_state = "move" else: walk_state = "idle" if is_moving_forward: walk_direction = "forward" target_body_yaw = -Vector2.UP.angle_to(input) else: walk_direction = "backward" target_body_yaw = -Vector2.DOWN.angle_to(input) anim_tree["parameters/walk_state/transition_request"] = walk_state anim_tree["parameters/walk_direction/transition_request"] = walk_direction const YAW_FACTOR_LOWER_BODY := 0.25 const YAW_FACTOR_UPPER_BODY := 0.25 const YAW_FACTOR_NECK := 0.50 body_yaw += (target_body_yaw - body_yaw) * delta * 6 bones["Root"].global_rotate(Vector3.UP, body_yaw) bones["LowerBody"].global_rotate(Vector3.UP, -body_yaw * YAW_FACTOR_LOWER_BODY) bones["UpperBody"].global_rotate(Vector3.UP, -body_yaw * YAW_FACTOR_UPPER_BODY) bones["Neck"].global_rotate(Vector3.UP, -body_yaw * YAW_FACTOR_NECK) static func angle_difference(from: float, to: float) -> float: return fposmod(to - from + PI, TAU) - PI