├── Movement.cs ├── Player.tscn ├── PlayerLook.cs └── README.md /Movement.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System; 3 | using static Cmd; 4 | using static Godot.Input; 5 | 6 | public partial struct Cmd 7 | { 8 | public static Vector2 move_input; 9 | public static bool wishJump; 10 | } 11 | 12 | // *** IMPORTANT *** 13 | // Make sure the set your inputs (WASD keys) accordingly to ui keys (ui_left, ui_right, ui_up, ui_down) in the Editor 14 | //This is my first released script so please don't expect a fully fledged FPS Controller, still working on it 15 | 16 | public partial class Movement : CharacterBody3D 17 | { 18 | 19 | Action onLand, onAir, onJump; 20 | [Export] public PlayerLook playerLook; 21 | [Export] public bool MovementEnabled = true; 22 | // Movement Varabiles 23 | [Export] float friction = 6f; 24 | [Export] float moveSpeed = 7.0f; // Ground move speed 25 | [Export] float groundAccel = 14f; // Ground accel 26 | [Export] float groundDeaccel = 10f; // Deacceleration that occurs when running on the ground 27 | [Export] float airAccel = 2.0f; // Air accel 28 | [Export] float airDeaccel = 2.0f; // Deacceleration experienced when opposite strafing 29 | [Export] float jumpSpeed = 10f; // The speed at which the characters up axis gains when hitting jump 30 | public float gravity = ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle(); // should be around 28 personally 31 | [Export] bool holdJumpToBhop = true; // When enabled allows player to just hold jump button to keep on bhopping perfectly 32 | [Export] public bool Grounded = true; 33 | // All Vector Variables 34 | Vector3 playerVelocity, wishDir = Vector3.Zero; 35 | float playerTopVelocity = 0.0f; 36 | 37 | [Export] AudioStreamPlayer jumpSound, landSound; 38 | 39 | [Export] public Marker3D Head; 40 | [Export] Label speedLabel, wishDirLabel, TopSpeedLabel, FrameRateLabel; 41 | public override void _EnterTree() 42 | { 43 | onLand += doLanding; 44 | onLand += QueueJump; 45 | onAir += doOnAir; 46 | } 47 | public override void _ExitTree() 48 | { 49 | onLand -= doLanding; 50 | onLand -= QueueJump; 51 | onAir -= doOnAir; 52 | } 53 | public override void _UnhandledInput(InputEvent @event) 54 | { 55 | WishJumpLogic(@event); 56 | setDir(); 57 | } 58 | void HasLandedLogic() 59 | { 60 | if (Grounded != IsOnFloor()) 61 | { 62 | if (!Grounded) 63 | { 64 | onLand?.Invoke(); 65 | Grounded = true; 66 | } 67 | else 68 | { 69 | onAir?.Invoke(); 70 | Grounded = false; 71 | } 72 | } 73 | } 74 | void doLanding() 75 | { 76 | landSound.Play(); 77 | } 78 | void doOnAir() 79 | { 80 | //Do stuff the moment you start jumping 81 | } 82 | void WishJumpLogic(InputEvent @event) 83 | { 84 | if (@event.IsActionPressed("ui_accept") && !wishJump) 85 | wishJump = true; 86 | if (@event.IsActionReleased("ui_accept")) 87 | wishJump = false; 88 | } 89 | public override void _PhysicsProcess(double delta) 90 | { 91 | if (!MovementEnabled) return; 92 | HasLandedLogic(); 93 | 94 | if (Grounded) GroundMove(); 95 | else AirMove(); 96 | 97 | // This will move the player 98 | Velocity = playerVelocity; 99 | MoveAndSlide(); 100 | ShowMovementStats(); 101 | ShowFrameRateStats(); 102 | } 103 | public void setDir() 104 | { 105 | move_input = GetVector("ui_left", "ui_right", "ui_up", "ui_down"); // Y is inverted, don't ask why it's Godot 106 | wishDir = new Vector3(move_input.x, 0, move_input.y).Rotated(Vector3.Up, playerLook.Rotation.y).Normalized(); 107 | } 108 | void QueueJump() 109 | { 110 | if (holdJumpToBhop) 111 | wishJump = IsActionPressed("ui_accept"); 112 | } 113 | float wishSpeed() => wishDir.LengthSquared() * moveSpeed; 114 | 115 | //Allows for movement to slightly increase as you move through the air 116 | void AirMove() 117 | { 118 | //snap = Vector3.Down; 119 | Accelerate(wishDir, wishSpeed(), airAccel); // accel 120 | playerVelocity.y += -gravity * (float)GetPhysicsProcessDeltaTime(); 121 | } 122 | //[Export] Vector3 snap; 123 | private void GroundMove() 124 | { 125 | 126 | //snap = -GetFloorNormal(); 127 | // Do not apply friction if the player is queueing up the next jump 128 | ApplyFriction(!wishJump); 129 | 130 | Accelerate(wishDir, wishSpeed(), groundAccel); 131 | if (wishJump) 132 | { 133 | jumpSound.Play(); 134 | wishJump = false; 135 | //snap = Vector3.Zero; 136 | playerVelocity.y = jumpSpeed; 137 | onJump?.Invoke(); 138 | } 139 | } 140 | private void Accelerate(Vector3 wishdir, float wishspeed, float accel) 141 | { 142 | 143 | float addspeed, accelspeed, currentspeed; 144 | currentspeed = playerVelocity.Dot(wishdir); 145 | addspeed = wishspeed - currentspeed; 146 | if (addspeed <= 0) return; 147 | accelspeed = accel * (float)GetPhysicsProcessDeltaTime() * wishspeed; 148 | if (accelspeed > addspeed) { accelspeed = addspeed; } 149 | 150 | playerVelocity.x += accelspeed * wishdir.x; 151 | playerVelocity.z += accelspeed * wishdir.z; 152 | } 153 | 154 | 155 | //Applies friction to the player, called in both the air and on the ground 156 | private void ApplyFriction(bool enabled) 157 | { 158 | if (!enabled) return; 159 | Vector3 vec = playerVelocity; // Equivalent to: VectorCopy(); 160 | float lastSpeed, newspeed, control, drop; 161 | drop = 0f; vec.y = 0f; 162 | lastSpeed = vec.Length(); 163 | /* Only if the player is on the ground then apply friction */ 164 | if (Grounded) 165 | { 166 | control = lastSpeed < groundDeaccel ? groundDeaccel : lastSpeed; 167 | drop = control * friction * (float)GetPhysicsProcessDeltaTime(); 168 | } 169 | newspeed = lastSpeed - drop; 170 | //playerFriction = newspeed; 171 | if (newspeed < 0) { newspeed = 0; } 172 | if (lastSpeed > 0) { newspeed /= lastSpeed; } 173 | 174 | playerVelocity.x *= newspeed; 175 | playerVelocity.z *= newspeed; 176 | } 177 | 178 | //Some UI Stuff 179 | void ShowFrameRateStats() 180 | { 181 | FrameRateLabel.Text = $"FPS: {Engine.GetFramesPerSecond()}"; 182 | } 183 | void ShowMovementStats() 184 | { 185 | Vector3 labelVel = playerVelocity; 186 | labelVel.y = 0; 187 | 188 | // Show the player's current speed 189 | speedLabel.Text = $"Speed: {labelVel.Length()}"; 190 | // Show the player's current wishDir 191 | wishDirLabel.Text = $"WishDir:\n X = {wishDir.x}\nZ = {wishDir.z}"; 192 | 193 | if (labelVel.LengthSquared() > playerTopVelocity * playerTopVelocity) //avoiding sqrt for performance 194 | { 195 | playerTopVelocity = labelVel.Length(); 196 | } 197 | TopSpeedLabel.Text = $"TopSpeed: {playerTopVelocity}"; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://bqyrobfgejs2k"] 2 | 3 | [ext_resource type="Script" path="res://Movement.cs" id="1_1pgfc"] 4 | [ext_resource type="Script" path="res://PlayerLook.cs" id="2_657tq"] 5 | 6 | [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_plmxj"] 7 | 8 | [node name="Player" type="Node"] 9 | 10 | [node name="Movement" type="CharacterBody3D" parent="." node_paths=PackedStringArray("playerLook", "jumpSound", "landSound", "Head", "speedLabel", "wishDirLabel", "TopSpeedLabel", "FrameRateLabel")] 11 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) 12 | wall_min_slide_angle = 0.785398 13 | floor_stop_on_slope = false 14 | floor_snap_length = 0.01 15 | script = ExtResource("1_1pgfc") 16 | playerLook = NodePath("../PlayerLook") 17 | jumpSound = NodePath("jumpSound") 18 | landSound = NodePath("landSound") 19 | Head = NodePath("Head") 20 | speedLabel = NodePath("../SpeedLabel") 21 | wishDirLabel = NodePath("../WishDirLabel") 22 | TopSpeedLabel = NodePath("../TopSpeedLabel") 23 | FrameRateLabel = NodePath("../FrameRateLabel") 24 | 25 | [node name="CollisionShape3d" type="CollisionShape3D" parent="Movement"] 26 | shape = SubResource("CapsuleShape3D_plmxj") 27 | 28 | [node name="Head" type="Marker3D" parent="Movement"] 29 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) 30 | 31 | [node name="jumpSound" type="AudioStreamPlayer" parent="Movement"] 32 | 33 | [node name="landSound" type="AudioStreamPlayer" parent="Movement"] 34 | 35 | [node name="PlayerLook" type="Marker3D" parent="." node_paths=PackedStringArray("movement", "cam")] 36 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) 37 | script = ExtResource("2_657tq") 38 | movement = NodePath("../Movement") 39 | cam = NodePath("Camera3d") 40 | 41 | [node name="Camera3d" type="Camera3D" parent="PlayerLook"] 42 | 43 | [node name="FrameRateLabel" type="Label" parent="."] 44 | offset_right = 62.0 45 | offset_bottom = 26.0 46 | text = "Text" 47 | 48 | [node name="SpeedLabel" type="Label" parent="."] 49 | offset_top = 26.0 50 | offset_right = 62.0 51 | offset_bottom = 52.0 52 | text = "Text" 53 | 54 | [node name="TopSpeedLabel" type="Label" parent="."] 55 | offset_top = 52.0 56 | offset_right = 62.0 57 | offset_bottom = 78.0 58 | text = "Text" 59 | 60 | [node name="WishDirLabel" type="Label" parent="."] 61 | offset_top = 78.0 62 | offset_right = 62.0 63 | offset_bottom = 104.0 64 | text = "Text" 65 | -------------------------------------------------------------------------------- /PlayerLook.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System; 3 | using static Godot.Input; 4 | 5 | public partial class PlayerLook : Marker3D 6 | { 7 | [Export] Movement movement; 8 | [Export] Camera3D cam; 9 | [Export] public bool PlayerRotationEnabled = true; 10 | [Export] public float mouseSens = 0.2f; 11 | [Export] float cam_accel = 40f; 12 | [Export] public Vector2 _rotation; 13 | const float SENS_M = 0.2f; 14 | public override void _Ready() 15 | { 16 | Position = movement.Head.GlobalPosition; 17 | 18 | MouseMode = MouseModeEnum.Captured; //Sets the mouse to captured 19 | } 20 | 21 | public override void _UnhandledInput(InputEvent @event) 22 | { 23 | if (!PlayerRotationEnabled) return; 24 | if (@event.IsActionPressed("shoot")) //Mouse0 25 | { 26 | MouseMode = MouseModeEnum.Captured; 27 | } 28 | if (@event.IsActionPressed("ui_cancel")) // ESC 29 | { 30 | MouseMode = MouseModeEnum.Visible; 31 | } 32 | if (@event is InputEventMouseMotion mouseMotion) 33 | { 34 | // modify accumulated mouse rotation 35 | _rotation += (-mouseMotion.Relative * mouseSens * SENS_M); 36 | 37 | _rotation.y = Mathf.Clamp(_rotation.y, -89f, 89f); 38 | 39 | Rotation = new Vector3(0, Mathf.DegToRad(_rotation.x), 0); 40 | cam.Rotation = new Vector3(Mathf.DegToRad(_rotation.y), 0, 0); 41 | 42 | } 43 | } 44 | public override void _Process(double delta) 45 | { 46 | //Camera Interpolation to fix jittering 47 | if (Engine.GetFramesPerSecond() > Engine.PhysicsTicksPerSecond) 48 | { 49 | Position = Position.Lerp(movement.Head.GlobalPosition, cam_accel * (float)delta); 50 | } 51 | else 52 | { 53 | Position = movement.Head.GlobalPosition; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quake-Movement-in-Godot-4 2 | This is an experimental FPS controller ported from WiggleWizard's Quake Movement script for (Unity) into Godot 4 Beta. The player rotation node is separate (not a child of) the CharacterBody3D, that way you won't have any jittering camera movement because the camera is smoothly interlopated (Thanks Garbaj tutorials). 3 | 4 | Please note that this controller is my first released script so it is NOT a fully fledged FPS Controller. 5 | 6 | I recommend using this script as a basis and improving it further. 7 | 8 | Instructions: 9 | 10 | Extract all the files to your project folder, drag Player.tscn into your main scene. 11 | Make sure the set your inputs such as "shoot" as Mouse0 and (WASD keys) accordingly to ui keys (ui_left, ui_right, ui_up, ui_down) as followed in the editor. 12 | ![image](https://user-images.githubusercontent.com/32967925/190875146-56ea4db3-da53-44c8-b34f-0240ba8fcd47.png) 13 | ![image](https://user-images.githubusercontent.com/32967925/190875132-d72201f1-dc18-49dc-9c86-30b7a0c72c33.png) 14 | 15 | Thanks for using this asset! 16 | if you appreciate it, please consider leaving a star to this repo :) 17 | --------------------------------------------------------------------------------