feat: split player movement into ground and noclip strategies

This commit is contained in:
2026-08-22 22:22:51 +02:00
parent 45c66ca7db
commit 76694a40f6
14 changed files with 123 additions and 62 deletions
+5
View File
@@ -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) "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] [physics]
@@ -0,0 +1,13 @@
namespace Game.Features.Players.Contracts;
public enum MovementMode
{
Ground,
Noclip
}
public interface IMovementModeSwitcher
{
MovementMode ActiveMovementMode { get; }
void SetMovementMode(MovementMode movementMode);
}
@@ -0,0 +1 @@
uid://c5nmjyb5iw6rh
@@ -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;
}
}
@@ -0,0 +1 @@
uid://v4pc5k482usm
@@ -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);
}
@@ -0,0 +1 @@
uid://b3bcwkq278ojo
@@ -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;
}
}
@@ -0,0 +1 @@
uid://c27csmmeunnld
@@ -1,54 +0,0 @@
using Godot;
namespace Game.Features.Players.Domain;
/// <summary>
/// Per-frame input for movement calculation.
/// </summary>
/// <param name="Right">World-space right direction of the body.</param>
/// <param name="Back">World-space back direction of the body.</param>
/// <param name="Direction">Movement input, X = left/right, Y = forward/back.</param>
/// <param name="IsOnFloor">Whether the body is currently grounded.</param>
/// <param name="JumpPressed">Whether jump was just pressed this frame.</param>
/// <param name="Delta">Physics frame delta time in seconds.</param>
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;
/// <summary>
/// Calculates next-frame velocity from input and ground state.
/// </summary>
/// <param name="currentVelocity">Body's velocity before this frame.</param>
/// <param name="input">Per-frame movement input.</param>
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;
}
}
@@ -1 +0,0 @@
uid://chnyf8efw3xw2
+28 -6
View File
@@ -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;
using Game.Features.Players.Domain.Movement;
using Game.Godot; using Game.Godot;
using Game.Godot.Extensions; using Game.Godot.Extensions;
using Game.Godot.Mappings; using Game.Godot.Mappings;
@@ -7,26 +9,44 @@ using Godot;
namespace Game.Features.Players.Godot; namespace Game.Features.Players.Godot;
public partial class PlayerController : Node3D public partial class PlayerController : Node3D, IMovementModeSwitcher
{ {
[Export] public CharacterBody3D Body; [Export] public CharacterBody3D Body;
[Export] public CollisionShape3D Collision;
[Export] public Camera3D Camera; [Export] public Camera3D Camera;
[Export] public float MoveSpeed = 4.0f; [Export] public float MoveSpeed = 4.0f;
[Export] public float JumpVelocity = 4.5f; [Export] public float JumpVelocity = 4.5f;
[Export] public float NoclipFlySpeed = 8.0f;
[Export] public float MouseSensitivity = 0.003f; [Export] public float MouseSensitivity = 0.003f;
[Export] public float MaxLookAngleDegrees = 89.0f; [Export] public float MaxLookAngleDegrees = 89.0f;
private PlayerMovement _movement; private Dictionary<MovementMode, IMovementStrategy> _movementStrategies;
private IMovementStrategy _currentMovementStrategy;
private PlayerLook _look; 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() public override void _Ready()
{ {
this.AssertRequiredExports(); this.AssertRequiredExports();
float gravity = (float)ProjectSettings.GetSetting(ProjectSetting.Gravity.SettingPath());
_movement = new PlayerMovement(MoveSpeed, JumpVelocity, gravity);
_look = new PlayerLook(MouseSensitivity, MaxLookAngleDegrees); _look = new PlayerLook(MouseSensitivity, MaxLookAngleDegrees);
float gravity = (float)ProjectSettings.GetSetting(ProjectSetting.Gravity.SettingPath());
_movementStrategies = new Dictionary<MovementMode, IMovementStrategy>
{
[MovementMode.Ground] = new GroundMovement(MoveSpeed, JumpVelocity, gravity),
[MovementMode.Noclip] = new NoclipMovement(NoclipFlySpeed)
};
_currentMovementStrategy = _movementStrategies[ActiveMovementMode];
} }
public override void _UnhandledInput(InputEvent @event) public override void _UnhandledInput(InputEvent @event)
@@ -53,10 +73,12 @@ public partial class PlayerController : Node3D
Direction: inputDirection, Direction: inputDirection,
IsOnFloor: Body.IsOnFloor(), IsOnFloor: Body.IsOnFloor(),
JumpPressed: Input.IsActionJustPressed(InputAction.Jump.ActionName()), JumpPressed: Input.IsActionJustPressed(InputAction.Jump.ActionName()),
AscendPressed: Input.IsActionPressed(InputAction.Jump.ActionName()),
DescendPressed: Input.IsActionPressed(InputAction.MoveDown.ActionName()),
Delta: (float)delta Delta: (float)delta
); );
Body.Velocity = _movement.CalculateVelocity(Body.Velocity, input); Body.Velocity = _currentMovementStrategy.CalculateVelocity(Body.Velocity, input);
Body.MoveAndSlide(); Body.MoveAndSlide();
} }
} }
+2 -1
View File
@@ -290,9 +290,10 @@ _surfaces = [{
}] }]
blend_shape_mode = 0 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") script = ExtResource("1_gkqfc")
Body = NodePath("CharacterBody3D") Body = NodePath("CharacterBody3D")
Collision = NodePath("CharacterBody3D/CollisionShape3D")
Camera = NodePath("CharacterBody3D/Camera3D") Camera = NodePath("CharacterBody3D/Camera3D")
[node name="CharacterBody3D" type="CharacterBody3D" parent="." unique_id=1327672780] [node name="CharacterBody3D" type="CharacterBody3D" parent="." unique_id=1327672780]
+2
View File
@@ -8,6 +8,7 @@ public enum InputAction
MoveRight, MoveRight,
MoveForward, MoveForward,
MoveBack, MoveBack,
MoveDown,
Jump, Jump,
Cancel, Cancel,
ToggleConsole ToggleConsole
@@ -24,6 +25,7 @@ public static class InputActionExtensions
InputAction.MoveRight => "move_right", InputAction.MoveRight => "move_right",
InputAction.MoveForward => "move_forward", InputAction.MoveForward => "move_forward",
InputAction.MoveBack => "move_back", InputAction.MoveBack => "move_back",
InputAction.MoveDown => "move_down",
InputAction.Jump => "jump", InputAction.Jump => "jump",
InputAction.Cancel => "ui_cancel", InputAction.Cancel => "ui_cancel",
InputAction.ToggleConsole => "toggle_console", InputAction.ToggleConsole => "toggle_console",