diff --git a/src/Features/Players/Domain/PlayerLook.cs b/src/Features/Players/Domain/PlayerLook.cs new file mode 100644 index 0000000..6d2000e --- /dev/null +++ b/src/Features/Players/Domain/PlayerLook.cs @@ -0,0 +1,24 @@ +using Godot; + +namespace Game.Features.Players.Domain; + +public sealed class PlayerLook(float mouseSensitivity, float maxLookAngleDegrees) +{ + public float MouseSensitivity { get; } = mouseSensitivity; + public float MaxLookAngleDegrees { get; } = maxLookAngleDegrees; + public float Pitch { get; private set; } + + /// + /// Yaw rotation (radians) to apply for this mouse movement. + /// + public float CalculateYawDelta(float mouseDeltaX) => -mouseDeltaX * MouseSensitivity; + + /// + /// Updates for this mouse movement. + /// + public void UpdatePitch(float mouseDeltaY) + { + float pitch = Pitch - mouseDeltaY * MouseSensitivity; + Pitch = Mathf.Clamp(pitch, Mathf.DegToRad(-MaxLookAngleDegrees), Mathf.DegToRad(MaxLookAngleDegrees)); + } +} diff --git a/src/Features/Players/Domain/PlayerLook.cs.uid b/src/Features/Players/Domain/PlayerLook.cs.uid new file mode 100644 index 0000000..a2561bb --- /dev/null +++ b/src/Features/Players/Domain/PlayerLook.cs.uid @@ -0,0 +1 @@ +uid://dke8w2hp71p1g diff --git a/src/Features/Players/Domain/PlayerMovement.cs b/src/Features/Players/Domain/PlayerMovement.cs new file mode 100644 index 0000000..e2f6099 --- /dev/null +++ b/src/Features/Players/Domain/PlayerMovement.cs @@ -0,0 +1,54 @@ +using Godot; + +namespace Game.Features.Players.Domain; + +/// +/// Per-frame input for movement calculation. +/// +/// World-space right direction of the body. +/// World-space back direction of the body. +/// Movement input, X = left/right, Y = forward/back. +/// Whether the body is currently grounded. +/// Whether jump was just pressed this frame. +/// Physics frame delta time in seconds. +public readonly record struct MovementInput( + Vector3 Right, + Vector3 Back, + Vector2 Direction, + bool IsOnFloor, + bool JumpPressed, + float Delta +); + +public sealed class PlayerMovement(float moveSpeed, float jumpVelocity, float gravity) +{ + public float MoveSpeed { get; } = moveSpeed; + public float JumpVelocity { get; } = jumpVelocity; + public float Gravity { get; } = gravity; + + /// + /// Calculates next-frame velocity from input and ground state. + /// + /// Body's velocity before this frame. + /// Per-frame movement input. + public Vector3 CalculateVelocity(Vector3 currentVelocity, MovementInput input) + { + Vector3 velocity = currentVelocity; + + if (!input.IsOnFloor) + { + velocity.Y -= Gravity * input.Delta; + } + else if (input.JumpPressed) + { + velocity.Y = JumpVelocity; + } + + Vector3 moveDirection = input.Right * input.Direction.X + input.Back * input.Direction.Y; + Vector3 moveDirectionNormalized = moveDirection.Normalized(); + velocity.X = moveDirectionNormalized.X * MoveSpeed; + velocity.Z = moveDirectionNormalized.Z * MoveSpeed; + + return velocity; + } +} diff --git a/src/Features/Players/Domain/PlayerMovement.cs.uid b/src/Features/Players/Domain/PlayerMovement.cs.uid new file mode 100644 index 0000000..ca981bb --- /dev/null +++ b/src/Features/Players/Domain/PlayerMovement.cs.uid @@ -0,0 +1 @@ +uid://chnyf8efw3xw2 diff --git a/src/Features/Players/Godot/PlayerController.cs b/src/Features/Players/Godot/PlayerController.cs index adc18c3..21d9dda 100644 --- a/src/Features/Players/Godot/PlayerController.cs +++ b/src/Features/Players/Godot/PlayerController.cs @@ -1,10 +1,63 @@ -using Game.Features.Player.Domain; +using System.Linq; +using Game.Features.Players.Domain; +using Game.Godot.Extensions; +using Game.Godot.Mappings; using Godot; -using System; -namespace Game.Features.Player.Godot; +namespace Game.Features.Players.Godot; public partial class PlayerController : Node3D { [Export] public CharacterBody3D Body; + [Export] public Camera3D Camera; + + [Export] public float MoveSpeed = 4.0f; + [Export] public float JumpVelocity = 4.5f; + [Export] public float MouseSensitivity = 0.003f; + [Export] public float MaxLookAngleDegrees = 89.0f; + + private PlayerMovement _movement; + private PlayerLook _look; + + private static bool IsMouseCaptured() => Input.MouseMode == Input.MouseModeEnum.Captured; + + public override void _Ready() + { + this.AssertRequiredExports(); + + float gravity = (float)ProjectSettings.GetSetting(ProjectSetting.Gravity.SettingPath()); + _movement = new PlayerMovement(MoveSpeed, JumpVelocity, gravity); + _look = new PlayerLook(MouseSensitivity, MaxLookAngleDegrees); + } + + public override void _UnhandledInput(InputEvent @event) + { + if (@event is not InputEventMouseMotion mouseMotion || !IsMouseCaptured()) return; + + Body.RotateY(_look.CalculateYawDelta(mouseMotion.Relative.X)); + _look.UpdatePitch(mouseMotion.Relative.Y); + Camera.Rotation = new Vector3(_look.Pitch, 0f, 0f); + } + + public override void _PhysicsProcess(double delta) + { + Vector2 inputDirection = Input.GetVector( + InputAction.MoveLeft.ActionName(), + InputAction.MoveRight.ActionName(), + InputAction.MoveForward.ActionName(), + InputAction.MoveBack.ActionName() + ); + + MovementInput input = new( + Right: Body.Transform.Basis.X, + Back: Body.Transform.Basis.Z, + Direction: inputDirection, + IsOnFloor: Body.IsOnFloor(), + JumpPressed: Input.IsActionJustPressed(InputAction.Jump.ActionName()), + Delta: (float)delta + ); + + Body.Velocity = _movement.CalculateVelocity(Body.Velocity, input); + Body.MoveAndSlide(); + } } diff --git a/src/Features/Players/Godot/PlayerNode.tscn b/src/Features/Players/Godot/PlayerNode.tscn index 1d0315e..69e6d5f 100644 --- a/src/Features/Players/Godot/PlayerNode.tscn +++ b/src/Features/Players/Godot/PlayerNode.tscn @@ -1,8 +1,11 @@ [gd_scene format=4 uid="uid://bka1dmgdi5btd"] -[ext_resource type="Script" uid="uid://mjm7ghkibnxa" path="res://src/Features/Player/Godot/PlayerController.cs" id="1_gkqfc"] +[ext_resource type="Script" uid="uid://mjm7ghkibnxa" path="res://src/Features/Players/Godot/PlayerController.cs" id="1_gkqfc"] [ext_resource type="Texture2D" uid="uid://bymgknsy145ap" path="res://assets/models/simple-character-psx_character_256.png" id="2_tduai"] +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_130ks"] +radius = 0.25 + [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_sgsg4"] resource_name = "character_256" cull_mode = 2 @@ -287,16 +290,21 @@ _surfaces = [{ }] blend_shape_mode = 0 -[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_130ks"] -radius = 0.25 - -[node name="Player" type="Node3D" unique_id=26823676 node_paths=PackedStringArray("characterBody3D")] +[node name="Player" type="Node3D" unique_id=26823676 node_paths=PackedStringArray("Body", "Camera")] script = ExtResource("1_gkqfc") -characterBody3D = NodePath("Mesh/CharacterBody3D") +Body = NodePath("CharacterBody3D") +Camera = NodePath("CharacterBody3D/Camera3D") -[node name="Mesh" type="Node3D" parent="." unique_id=388629767] +[node name="CharacterBody3D" type="CharacterBody3D" parent="." unique_id=1327672780] -[node name="Skeleton3D" type="Skeleton3D" parent="Mesh" unique_id=183161254] +[node name="CollisionShape3D" type="CollisionShape3D" parent="CharacterBody3D" unique_id=1006347064] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("CapsuleShape3D_130ks") + +[node name="Mesh" type="Node3D" parent="CharacterBody3D" unique_id=388629767] +transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0, 0) + +[node name="Skeleton3D" type="Skeleton3D" parent="CharacterBody3D/Mesh" unique_id=183161254] transform = Transform3D(0.1225099, 0, 0, 0, 0.1225099, 0, 0, 0, 0.1225099, 0, 0, 0) bones/0/name = "spine" bones/0/parent = -1 @@ -747,23 +755,22 @@ bones/63/position = Vector3(0.24846391, -0.054724697, 0.53722215) bones/63/rotation = Quaternion(-0.689515, 0.6895178, -0.15673877, -0.15673946) bones/63/scale = Vector3(1, 1, 1) -[node name="arms" type="MeshInstance3D" parent="Mesh/Skeleton3D" unique_id=1995581269] +[node name="arms" type="MeshInstance3D" parent="CharacterBody3D/Mesh/Skeleton3D" unique_id=1995581269] mesh = SubResource("ArrayMesh_wr7hk") skin = SubResource("Skin_dmu40") skeleton = NodePath("..") -[node name="body" type="MeshInstance3D" parent="Mesh/Skeleton3D" unique_id=2033799835] +[node name="body" type="MeshInstance3D" parent="CharacterBody3D/Mesh/Skeleton3D" unique_id=2033799835] mesh = SubResource("ArrayMesh_5ti08") skin = SubResource("Skin_dmu40") skeleton = NodePath("..") -[node name="head" type="MeshInstance3D" parent="Mesh/Skeleton3D" unique_id=1273139092] +[node name="head" type="MeshInstance3D" parent="CharacterBody3D/Mesh/Skeleton3D" unique_id=1273139092] +layers = 2 mesh = SubResource("ArrayMesh_31tkm") skin = SubResource("Skin_dmu40") skeleton = NodePath("..") -[node name="CharacterBody3D" type="CharacterBody3D" parent="Mesh" unique_id=1327672780] - -[node name="CollisionShape3D" type="CollisionShape3D" parent="Mesh/CharacterBody3D" unique_id=1006347064] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) -shape = SubResource("CapsuleShape3D_130ks") +[node name="Camera3D" type="Camera3D" parent="CharacterBody3D" unique_id=718409984] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.817, 0) +cull_mask = 1048573