diff --git a/project.godot b/project.godot
index 71e7e24..0586cf0 100644
--- a/project.godot
+++ b/project.godot
@@ -61,6 +61,11 @@ toggle_console={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194332,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
+move_down={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
[physics]
diff --git a/src/Features/Players/Contracts/IMovementModeSwitcher.cs b/src/Features/Players/Contracts/IMovementModeSwitcher.cs
new file mode 100644
index 0000000..f276c09
--- /dev/null
+++ b/src/Features/Players/Contracts/IMovementModeSwitcher.cs
@@ -0,0 +1,13 @@
+namespace Game.Features.Players.Contracts;
+
+public enum MovementMode
+{
+ Ground,
+ Noclip
+}
+
+public interface IMovementModeSwitcher
+{
+ MovementMode ActiveMovementMode { get; }
+ void SetMovementMode(MovementMode movementMode);
+}
diff --git a/src/Features/Players/Contracts/IMovementModeSwitcher.cs.uid b/src/Features/Players/Contracts/IMovementModeSwitcher.cs.uid
new file mode 100644
index 0000000..504863b
--- /dev/null
+++ b/src/Features/Players/Contracts/IMovementModeSwitcher.cs.uid
@@ -0,0 +1 @@
+uid://c5nmjyb5iw6rh
diff --git a/src/Features/Players/Domain/Movement/GroundMovement.cs b/src/Features/Players/Domain/Movement/GroundMovement.cs
new file mode 100644
index 0000000..022861a
--- /dev/null
+++ b/src/Features/Players/Domain/Movement/GroundMovement.cs
@@ -0,0 +1,27 @@
+using Godot;
+
+namespace Game.Features.Players.Domain.Movement;
+
+public sealed class GroundMovement(float moveSpeed, float jumpVelocity, float gravity) : IMovementStrategy
+{
+ 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/Movement/GroundMovement.cs.uid b/src/Features/Players/Domain/Movement/GroundMovement.cs.uid
new file mode 100644
index 0000000..96f7590
--- /dev/null
+++ b/src/Features/Players/Domain/Movement/GroundMovement.cs.uid
@@ -0,0 +1 @@
+uid://v4pc5k482usm
diff --git a/src/Features/Players/Domain/Movement/IMovementStrategy.cs b/src/Features/Players/Domain/Movement/IMovementStrategy.cs
new file mode 100644
index 0000000..d3e83ca
--- /dev/null
+++ b/src/Features/Players/Domain/Movement/IMovementStrategy.cs
@@ -0,0 +1,19 @@
+using Godot;
+
+namespace Game.Features.Players.Domain.Movement;
+
+public readonly record struct MovementInput(
+ Vector3 Right,
+ Vector3 Back,
+ Vector2 Direction,
+ bool IsOnFloor,
+ bool JumpPressed,
+ bool AscendPressed,
+ bool DescendPressed,
+ float Delta
+);
+
+public interface IMovementStrategy
+{
+ Vector3 CalculateVelocity(Vector3 currentVelocity, MovementInput input);
+}
diff --git a/src/Features/Players/Domain/Movement/IMovementStrategy.cs.uid b/src/Features/Players/Domain/Movement/IMovementStrategy.cs.uid
new file mode 100644
index 0000000..a61019b
--- /dev/null
+++ b/src/Features/Players/Domain/Movement/IMovementStrategy.cs.uid
@@ -0,0 +1 @@
+uid://b3bcwkq278ojo
diff --git a/src/Features/Players/Domain/Movement/NoclipMovement.cs b/src/Features/Players/Domain/Movement/NoclipMovement.cs
new file mode 100644
index 0000000..ae12204
--- /dev/null
+++ b/src/Features/Players/Domain/Movement/NoclipMovement.cs
@@ -0,0 +1,23 @@
+using Godot;
+
+namespace Game.Features.Players.Domain.Movement;
+
+public sealed class NoclipMovement(float flySpeed) : IMovementStrategy
+{
+ public Vector3 CalculateVelocity(Vector3 currentVelocity, MovementInput input)
+ {
+ Vector3 moveDirection = input.Right * input.Direction.X + input.Back * input.Direction.Y;
+
+ if (input.AscendPressed)
+ {
+ moveDirection += Vector3.Up;
+ }
+
+ if (input.DescendPressed)
+ {
+ moveDirection += Vector3.Down;
+ }
+
+ return moveDirection.Length() > 0f ? moveDirection.Normalized() * flySpeed : Vector3.Zero;
+ }
+}
diff --git a/src/Features/Players/Domain/Movement/NoclipMovement.cs.uid b/src/Features/Players/Domain/Movement/NoclipMovement.cs.uid
new file mode 100644
index 0000000..6064a3f
--- /dev/null
+++ b/src/Features/Players/Domain/Movement/NoclipMovement.cs.uid
@@ -0,0 +1 @@
+uid://c27csmmeunnld
diff --git a/src/Features/Players/Domain/PlayerMovement.cs b/src/Features/Players/Domain/PlayerMovement.cs
deleted file mode 100644
index e2f6099..0000000
--- a/src/Features/Players/Domain/PlayerMovement.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-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
deleted file mode 100644
index ca981bb..0000000
--- a/src/Features/Players/Domain/PlayerMovement.cs.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://chnyf8efw3xw2
diff --git a/src/Features/Players/Godot/PlayerController.cs b/src/Features/Players/Godot/PlayerController.cs
index ba1d9ba..a3a285e 100644
--- a/src/Features/Players/Godot/PlayerController.cs
+++ b/src/Features/Players/Godot/PlayerController.cs
@@ -1,5 +1,7 @@
-using System.Linq;
+using System.Collections.Generic;
+using Game.Features.Players.Contracts;
using Game.Features.Players.Domain;
+using Game.Features.Players.Domain.Movement;
using Game.Godot;
using Game.Godot.Extensions;
using Game.Godot.Mappings;
@@ -7,26 +9,44 @@ using Godot;
namespace Game.Features.Players.Godot;
-public partial class PlayerController : Node3D
+public partial class PlayerController : Node3D, IMovementModeSwitcher
{
[Export] public CharacterBody3D Body;
+ [Export] public CollisionShape3D Collision;
[Export] public Camera3D Camera;
[Export] public float MoveSpeed = 4.0f;
[Export] public float JumpVelocity = 4.5f;
+ [Export] public float NoclipFlySpeed = 8.0f;
[Export] public float MouseSensitivity = 0.003f;
[Export] public float MaxLookAngleDegrees = 89.0f;
- private PlayerMovement _movement;
+ private Dictionary _movementStrategies;
+ private IMovementStrategy _currentMovementStrategy;
private PlayerLook _look;
+ public MovementMode ActiveMovementMode { get; private set; } = MovementMode.Ground;
+
+ public void SetMovementMode(MovementMode movementMode)
+ {
+ ActiveMovementMode = movementMode;
+ _currentMovementStrategy = _movementStrategies[movementMode];
+ Collision.Disabled = movementMode == MovementMode.Noclip;
+ }
+
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);
+
+ float gravity = (float)ProjectSettings.GetSetting(ProjectSetting.Gravity.SettingPath());
+ _movementStrategies = new Dictionary
+ {
+ [MovementMode.Ground] = new GroundMovement(MoveSpeed, JumpVelocity, gravity),
+ [MovementMode.Noclip] = new NoclipMovement(NoclipFlySpeed)
+ };
+ _currentMovementStrategy = _movementStrategies[ActiveMovementMode];
}
public override void _UnhandledInput(InputEvent @event)
@@ -53,10 +73,12 @@ public partial class PlayerController : Node3D
Direction: inputDirection,
IsOnFloor: Body.IsOnFloor(),
JumpPressed: Input.IsActionJustPressed(InputAction.Jump.ActionName()),
+ AscendPressed: Input.IsActionPressed(InputAction.Jump.ActionName()),
+ DescendPressed: Input.IsActionPressed(InputAction.MoveDown.ActionName()),
Delta: (float)delta
);
- Body.Velocity = _movement.CalculateVelocity(Body.Velocity, input);
+ Body.Velocity = _currentMovementStrategy.CalculateVelocity(Body.Velocity, input);
Body.MoveAndSlide();
}
}
diff --git a/src/Features/Players/Godot/PlayerNode.tscn b/src/Features/Players/Godot/PlayerNode.tscn
index 69e6d5f..0690ea5 100644
--- a/src/Features/Players/Godot/PlayerNode.tscn
+++ b/src/Features/Players/Godot/PlayerNode.tscn
@@ -290,9 +290,10 @@ _surfaces = [{
}]
blend_shape_mode = 0
-[node name="Player" type="Node3D" unique_id=26823676 node_paths=PackedStringArray("Body", "Camera")]
+[node name="Player" type="Node3D" unique_id=26823676 node_paths=PackedStringArray("Body", "Collision", "Camera")]
script = ExtResource("1_gkqfc")
Body = NodePath("CharacterBody3D")
+Collision = NodePath("CharacterBody3D/CollisionShape3D")
Camera = NodePath("CharacterBody3D/Camera3D")
[node name="CharacterBody3D" type="CharacterBody3D" parent="." unique_id=1327672780]
diff --git a/src/Godot/Mappings/InputAction.cs b/src/Godot/Mappings/InputAction.cs
index f9a2dd4..e4fb564 100644
--- a/src/Godot/Mappings/InputAction.cs
+++ b/src/Godot/Mappings/InputAction.cs
@@ -8,6 +8,7 @@ public enum InputAction
MoveRight,
MoveForward,
MoveBack,
+ MoveDown,
Jump,
Cancel,
ToggleConsole
@@ -24,6 +25,7 @@ public static class InputActionExtensions
InputAction.MoveRight => "move_right",
InputAction.MoveForward => "move_forward",
InputAction.MoveBack => "move_back",
+ InputAction.MoveDown => "move_down",
InputAction.Jump => "jump",
InputAction.Cancel => "ui_cancel",
InputAction.ToggleConsole => "toggle_console",