6. Coding a First-Person Controller in C# – Moving from Unity to Godot: An In-Depth Handbook to Godot for Unity Users

© Alan Thorn 2020
A. ThornMoving from Unity to Godothttps://doi.org/10.1007/978-1-4842-5908-5_6

6. Coding a First-Person Controller in C#

Alan Thorn1 
(1)
High Wycombe, UK
 
Unlike Unity, Godot doesn’t ship with any built-in character controllers, neither third-person nor first-person. So, in this chapter, we’ll create a reusable first-person controller rig from start to end in C# for Godot. Once created, we’ll be able to drag and drop first-person controls directly into any scene. The WASD keyboard keys will move the camera forward, left, backward, and right, respectively. Holding down the Shift key enables run mode and the space bar initiates a jump. Mouse movement will control head orientation, and the first-person controller overall will support many physical interactions, including gravity, collisions, ramp movement, and more. See Figure 6-1.
Figure 6-1

Creating a First-Person Controller Rig

Getting Started – Creating a Camera Scene

Let’s begin the first-person character controller by creating a new scene to contain our objects. In Godot, a Scene behaves like a Unity Prefab. It allows us to create an asset in isolation, which can be added and reused across multiple scenes. To create a new Scene, select SceneNew Scene from the application menu, and then choose 3D scene from the Create Root Node menu. See Figure 6-2.
Figure 6-2

Creating a 3D Scene

Next, right-click the root node from the Scene Tree, and then choose Add Child Node to show the Create Node Dialog. From here, create a KinematicBody Node (similar to a character controller in Unity – or a Rigidbody set to Kinematic Mode). A Kinematic Body is useful for building player-controlled characters or AI-controlled characters that need to react with physics and collision bodies. This node will become the root node – or main node – of the player character. See Figure 6-3.
Figure 6-3

Creating a Kinematic Body Node

The newly created Kinematic Body node should become the root of the scene, that is, the topmost node. It’ll be the topmost node of the player character, and there’ll be several child nodes. Our current scene should contain only the player character – it’s not intended to be played stand-alone but should be embedded into another scene, instantiated as a node, wherever first-person controls are needed. To do this, right-click the KinematicBody node in the Scene Tree and then select Make Scene Root from the context menu.
Figure 6-4

Making the Kinematic Body the Scene Root

As the Kinematic Body becomes the root, the previous root node (a 3D Spatial) will now become a child, and it may be safely deleted or left as is. We’ll need a spatial node for later anyway. Every KinematicBody needs a CollisionShape child node to express the character’s volume and size (equivalent to a Unity Collider). Let’s add one now, specifically a CollisionShape node. See Figure 6-5.
Figure 6-5

Adding a CollisionShape Node

The CollisionShape node begins as an empty spatial object with no width, height, or depth. To assign it shape, volume, and form, simply select the CollisionShape node and then move to the Shape field in the Inspector. From here, choose New CapsuleShape. The Capsule best approximates the volume of a humanoid character. See Figure 6-6.
Figure 6-6

Creating a Capsule Shape for the Kinematic Body

You may need to rotate the CollisionShape by 90 degrees on the X axis – or a different axis (depending on your object’s orientation) – and then tweak the CapsuleShape’s settings to adjust its Radius and Height. The idea is to resize the CollisionShape to approximate a humanoid character, such as the player or an NPC. See Figure 6-7.
Figure 6-7

Resizing the CapsuleShape

Now let’s focus on building the character’s head, which will be the center of the camera’s view. To start, create an empty spatial node – a child of the root – or use an existing spatial if you have one. Rename the node to “Head” and position it toward the top of the capsule at the center of the head location. Remember, the blue forward arrow should always point in the direction of the character’s view. See Figure 6-8.
Figure 6-8

Positioning the Character’s Head

Note

The blue arrow of the transform gizmo is the forward vector. It must represent the direction in which the character is facing. All nodes within the player hierarchy should share the same orientation, with the forward vector pointing forward.

Having now used a spatial node to mark the head position, we should add a camera as a child object. Simply add a camera node using the Node Creation Window , and then rotate its transform if needed by 180 degrees around the Y axis to face forward. See Figure 6-9. The Player Rig is now completed and ready to code. Excellent!
Figure 6-9

Completing the Character Rig

Player Movement and Key Bindings

One of the most important mechanics supported by any first-person controller is movement , both walking and running, specifically moving forward and backward using the W and S keys and strafing – or side stepping – left and right with A and D. To read input from these keys on the keyboard or to read input from buttons on a gamepad, we must first configure the game’s Input Actions. This is simple to achieve. To do this, select ProjectProject Settings from the application menu, and then switch to the Input Map tab. See Figure 6-10.
Figure 6-10

Accessing the Input Action Settings from Project ➤ Project Settings

We’ll need to create four distinct Actions to read input from WASD: left, right, up, and down. In Godot, an Action is a form of key binding. It converts a button press or an analog control into numerical data that drives gameplay. To add Actions, simply type a name for each action into the Action field, and then click the Add button for each action to add four new actions in total. I’ve used the names move_forward, move_backward, move_left, and move_right, respectively. See Figure 6-11.
Figure 6-11

Adding Input Actions from the Project Settings Dialog

Each Input Action defines one or more key bindings or mappings, a conversion from a button press to axis data. Let’s explore how to create bindings for the move_forward Action; and the remaining Actions are a repeat of that process. Click the Add Event button (+ icon), located beside the Input Action name. This creates a new key binding. On clicking Add Event, select Key from the context menu. This will link a keyboard button to the Input Action. See Figure 6-12.
Figure 6-12

Create a New Key Binding

After clicking Key from the context menu, Godot prompts you to press the relevant keyboard key to form the key binding. For the move_forward Action, press the W key. See Figure 6-13.
Figure 6-13

Detecting a Keyboard Press for a Key Binding

You can also add multiple keys to the same Action. Simply click Add Event again, and then add a new key binding. For the move_forward action, the W and Up arrow keys are most appropriate. See Figure 6-14 for the complete move_forward binding, along with all other actions, move_backward, move_left, and move_right. See Figure 6-14.
Figure 6-14

Completing the Key Bindings for Forward, Backward, Left, and Right

Reading Input Actions for Movement

The previous section demonstrated how to configure four input actions for player movement and all its associated key bindings. This links forward, backward, left, and right movement to the WASD keys and to the directional arrows. In this section, we’ll create a new script file for the PlayerCharacter, which reads input through the Actions. To get started, create a new C# script file named FPSControl.cs and attach the script to the topmost KinematicBody node, as shown in Figure 6-15. The newly created script is shown in Listing 6-1.
Figure 6-15

Creating an FPSControl.cs Script File for Handling Player Movement

using Godot;
using System;
public class FPSControl : KinematicBody
{
    // Declare member variables here. Examples:
    // private int a = 2;
    // private string b = "text";
    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
    }
//  // Called every frame. 'delta' is the elapsed time since the previous frame.
//  public override void _Process(float delta)
//  {
//
//  }
}
Listing 6-1

Godot Auto-Generated FPSControl Script

Godot offers the GetActionStrength function to read input data from the specified Action. This function returns a smoothed floating-point value in the 0 to 1 range; 0 means “not pressed” and 1 means “fully pressed.” These values, read from four Actions, can effectively be translated across two axes of movement. Specifically, move_forward and move_backward together represent the Vertical Axis. And move_left and move_right represent the Horizontal Axis. We can visualize both Horizontal and Vertical in the –1 to 1 range. –1 means down or left, and 1 means right or forward. 0 means neither Action is pressed, or both are pressed together (because –1 + 1 = 0). To read input across the four Actions and to map them into our two axes of character movement, let’s first add Export string variables to our class, allowing us to customize axis names from the inspector, which will be plugged into the function GetActionStrength . See the following code. Export is equivalent to the SerializedField attribute in Unity.
    [Export]
    private string LeftAxis, RightAxis,
   UpAxis, DownAxis = string.Empty;
Next, we’ll use the _process event to read input from the actions and map them to the two axes. See Listing 6-2.
public override void _Process(float delta)
{
        float Vertical = -Input.GetActionStrength(DownAxis) + Input.GetActionStrength(UpAxis);
        float Horizontal = Input.GetActionStrength(LeftAxis) + -Input.GetActionStrength(RightAxis);
}
Listing 6-2

Converting Input Actions to 2D Movement, Horizontal and Vertical

Establishing Move Direction

Input read from Actions using GetActionStrength can be converted easily into two dimensions of local motion: horizontal and vertical – left and right and forward and backward, respectively. This motion may be expressed in two floating-point values only. This motion is local insofar as horizontal and vertical movement always relate to the direction in which the player is currently facing. Forward always means forward for the player, as opposed to forward in the world, for example. For this reason, we’ll need to convert the local movement strength to a world space direction for moving the player based on input. To do that, consider the revised _process event in Listing 6-3.
public override void _Process(float delta)
{
        float Vertical = -Input.GetActionStrength(DownAxis) + Input.GetActionStrength(UpAxis);
        float Horizontal = Input.GetActionStrength(LeftAxis) + -Input.GetActionStrength(RightAxis);
        MoveDirection = Vector3.Zero;
        MoveDirection = HeadNode.Transform.basis.z * Vertical + HeadNode.Transform.basis.x * Horizontal;
        MoveDirection = MoveDirection.Normalized();
}
Listing 6-3

Converting Local Movements to World Velocity

The code in Listing 6-2 depends on two additional class variables. The first is a Vector3 variable, MoveDirection, which always represents the normalized velocity of the player character, that is, the direction of movement. This variable is normalized and has a unit length, allowing it to be scaled by any speed needed. The other is the HeadNode variable, which refers to the empty spatial node that is the parent of the camera. Chapter 3 explores how to retrieve node references from a NodePath using the GetNode function. The head always represents the direction of travel for the player. This is because when moving forward and backward or left and right, the character is always moving in the direction of stare. Here’s how you can read the Head Node and Camera Node objects at application startup, from NodePath variables defined in the Inspector.
   public override void _Ready()
    {
        HeadNode = GetNode(HeadPath) as Spatial;
        CamNode = GetNode(CamPath) as Spatial;
        Input.SetMouseMode(Input.MouseMode.Captured);
    }
Note

Notice the Input.SetMouseMode function is used to hide the system cursor, allowing for more intuitive head motion.

The MoveDirection variable is calculated each frame by using the basis member of the Head Node. The basis variable expresses three vectors, which are the local X, Y, and Z axes in world space. This means that the basis.z variable is the forward vector in world space and the basis.x is the right vector in world space.

Applying Gravity

The previous section detailed how to convert the player’s move direction into world space using a normalized vector. This vector can be multiplied by a speed to offset the player along its velocity smoothly and seamlessly, frame by frame. However, this direction doesn’t account for gravity, which is a force normally pulling everything downward toward the ground at an accelerated rate. To apply gravity, let’s create a representation of it using a Vector3 variable, expressing direction and strength.
    [Export]
    public Vector3 Gravity = Vector3.Zero;

A gravity vector of type Vector3.Zero will have no strength. For the player character, a vector of (0, –30, 0) works well in most cases. This can be specified in the Inspector. Other interesting values include (0, 30, 0), which would push an object upward, like a perpetual jump, and (–30, 0, 0), which would continually pull an object sideways.

Completing Player Movement

In this section, we’ll complete the crucial functionality started in previous sections, namely, input, direction calculation, and gravity. Here, we’ll complete the movement controls for the first-person character, specifically WASD motion. This code will span two functions, namely, _Process and _PhysicsProcess. _Process will be used to read input directly from the keyboard, and _PhysicsProcess will convert that input to physics-based movement for the KinematicBody. Consider the following _Process and _PhysicsProcess functions in Listing 6-4.
public override void _PhysicsProcess(float delta)
    {
        Vector3 TargetVel = Velocity;
        TargetVel.x = MoveDirection.x * SpeedMultiplier * MoveSpeed;
        TargetVel.z = MoveDirection.z * SpeedMultiplier * MoveSpeed;
        Velocity = Velocity.LinearInterpolate(TargetVel, Acceleration * delta);
        Velocity += Gravity * delta;
        Velocity.y = MoveAndSlideWithSnap(Velocity, Vector3.Down, Vector3.Up, true, 4, Mathf.Deg2Rad(FloorMaxAngle)).y;
    }
//  // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(float delta)
    {
        float Vertical = -Input.GetActionStrength(DownAxis) + Input.GetActionStrength(UpAxis);
        float Horizontal = Input.GetActionStrength(LeftAxis) + -Input.GetActionStrength(RightAxis);
        MoveDirection = Vector3.Zero;
        MoveDirection = HeadNode.Transform.basis.z * Vertical + HeadNode.Transform.basis.x * Horizontal;
        MoveDirection = MoveDirection.Normalized();
    }
Listing 6-4

Reading Input and Moving the Kinematic Body

In Listing 6-4, the _Process event uses the Input.GetActionStrength function to read input data from the keyboard – on both the horizontal and vertical axes – and converts that to numerical data within the –1 to 1 range. This data is then constructed as a Vector3 structure, representing object velocity. The Transform.basis member expresses the three local axes of an object (Up, Forward, and Right) in world space. These are expressed in basis.y, basis.z, and basis.x, respectively.

Note

More information on Input.GetActionStrength can be found at the Godot Documentation here: https://docs.godotengine.org/en/3.2/classes/class_input.html#class-input-method-get-action-strength.

More information on Transform.basis can be found at the Godot Documentation here: https://docs.godotengine.org/en/3.2/classes/class_transform.html#class-transform-property-basis.

Next, the _PhysicsProcess event uses the Vector3.LinearInterpolate function to smoothly transition the player’s current position to a new position, based on our input velocity. The MoveAndSlideWithSnap function will move an object along its velocity but consider important physical properties about the world. Specifically, MoveAndSlideWithSnap ensures that, firstly, the player moves around successfully without passing through solid objects, like walls and enemies; secondly, the player can move up or down ramps and inclines; and thirdly, the player will move cleanly when standing on movable surfaces, like conveyor belts or ascending platforms. The SlopeAngle and Floor Normal parameters determine these behaviors.

Note More information on MoveAndSlideWithSnap can be found at the Godot Documentation here: https://docs.godotengine.org/en/3.2/classes/class_kinematicbody.html#class-kinematicbody-method-move-and-slide-with-snap.

Notice also that our code calculates gravity. That is, for each _PhysicsProcess, our velocity is calculated and gravity applied to continually pull an object downward. This code uses the Gravity vector defined in the previous section.

Head Movement and Orientation

In the preceding section, the _PhysicsProcess function generated a velocity vector for the player character, based primarily on direct keyboard input. Pressing the W key or the Up arrow always moves the player forward, that is, forward in the direction in which the camera is facing. This section explores how to determine our look direction in code and how to control it using mouse movement. Let’s start by reading input from the mouse, specifically mouse movement on the horizontal and vertical axes. This is important because Horizontal mouse movement (sliding left or right) controls head rotation around the Y axis, while movement along the Vertical direction controls head rotation around the X axis. The following code can be added to our FPSControl class. See Listing 6-5.
public override void _Input(InputEvent motionUnknown)
{
    InputEventMouseMotion motion = motionUnknown as InputEventMouseMotion;
    if (motion != null)
        MouseMove = motion.Relative;
}
Listing 6-5

Reading Mouse Movement Input

The _Input event is a native function of the Node class, which can be overridden by any script to handle input events, like key presses and mouse movement. This function will always fire when an input event changes. Here, this function is used simply to record the Relative mouse motion, that is, how much the mouse position has changed in screen space since the previous input event. The MouseMove variable is simply a Vector2 class variable used to express the current mouse movement.

Note More information on Input event can be found online here at the Godot Documentation: https://docs.godotengine.org/en/3.2/tutorials/inputs/inputevent.html.

Next, having collected mouse movement from our Input event, we can re-code the _Process function as in Listing 6-6.
   public override void _Process(float delta)
    {
        float Vertical = -Input.GetActionStrength(DownAxis) + Input.GetActionStrength(UpAxis);
        float Horizontal = Input.GetActionStrength(LeftAxis) + -Input.GetActionStrength(RightAxis);
        MoveDirection = Vector3.Zero;
        MoveDirection = HeadNode.Transform.basis.z * Vertical + HeadNode.Transform.basis.x * Horizontal;
        MoveDirection = MoveDirection.Normalized();
if(MouseMove.Length() <= 0) return;
        Vector2 MouseResult = MouseMove * MouseSensitvity * delta;
        MouseResult = new Vector2(MouseResult.x, Mathf.Clamp(MouseResult.y, -PitchLimit, PitchLimit));
        HeadNode.Transform = HeadNode.Transform.Rotated(Vector3.Up, -Mathf.Deg2Rad(MouseResult.x));
        HeadNode.Transform = HeadNode.Transform.Rotated(HeadNode.Transform.basis.x, Mathf.Deg2Rad(MouseResult.y));
        HeadNode.RotationDegrees = new Vector3(Mathf.Clamp(HeadNode.RotationDegrees.x, -PitchLimit,PitchLimit),
             HeadNode.RotationDegrees.y, HeadNode.RotationDegrees.z);
        MouseMove = Vector2.Zero;
    }
Listing 6-6

Handling Head Rotation

The revised _Process function now supports head rotation based on mouse movement. The complete head rotation is calculated from the MouseResult Vector2 structure, where MouseResult.X expresses rotation around the camera’s local Y axis and MouseResult.Y expresses rotation around the camera’s local X axis. The MouseSensitivity floating-point variable is a multiplier to either strengthen or weaken the rotation effect. Notice the Spatial variable HeadNode references the Spatial parent of the camera. This node rotates according to mouse input.

Head rotation is a multistep process, iterating through Pitch (rotation around X) and Yaw (rotation around Y). First, the calculated Pitch is constrained by the Mathf.Clamp function and a PitchLimit . The PitchLimit variable is a Vector2 object whose X and Y components define the minimum and maximum rotation in degrees, respectively, that the head may rotate from its starting orientation around the local X axis. This prevents the camera from rotating upside down, which would normally happen if the player continually moved the mouse up or down without stopping or reversing direction. Second, the Transform.Rotated function is called to rotate the Camera (or more specifically, it’s empty parent) around its local Y and then its local X. Notice the utility function Mathf.Deg2Rad is called during this process, converting angles in degrees to radians, as expected by Godot’s Rotated function.

Note  More information on Transform.Rotated can be found at the Godot online documentation here: https://docs.godotengine.org/en/3.2/classes/class_transform.html#class-transform-method-rotated.

The final step is ensuring the Spatial.RotationDegrees variable (a Vector3 object) is clamped within our acceptable pitch range. RotationDegrees parallels Unity’s EulerAngles variable. It’s a Vector3 structure expressing Pitch, Yaw, and Roll – rotation around each local axis. So by rotating the camera based on mouse movement as we have done here, the CameraNode.basis.z variable always expresses our forward direction in world space – the direction we’re facing.

Jumping and Being Grounded

Our first-person player character must support a jumping behavior, that is, the ability to jump in the air and then to fall down again by gravity. Jumping introduces two important states for the player, namely, grounded and not grounded. Grounded is true whenever the player is in contact with the floor and false when not. Knowing when a character is grounded is important for preventing double jumps – for not being able to jump while you’re already jumping or falling and for enabling animations or character behaviors that change depending on whether the character is jumping, falling, or standing. For our character, a space bar press initiates a jump. To code this, let’s first configure the relevant Input Action, as with movement, by simply accessing the ProjectProject Settings menu and by adding a Jump action from the Input Map, as demonstrated earlier in this chapter. See Figure 6-16.
Figure 6-16

Configuring the Jump Action…

In addition, let’s add three new variables: JumpPower, a float describing the strength of our jump; Grounded, to describe whether we are currently touching the ground or not; and Snap, which will be plugged into the MoveAndSlideWithSnap function, inside _PhysicsProcess. When we’re jumping, Snap should be Vector3.Zero to ensure we can lift off without being connected to the floor, and when falling, it should be Vector3.Down, to ensure we fall back to ground.
private bool Grounded = false;
[Export]
private float JumpPower = 15f;
private Vector3 Snap = Vector3.Down;
Now we’ve added our variables, let’s refine the _Process function to detect and handle a Jump situation. See Listing 6-7.
if(Input.IsActionJustPressed("move_jump") && Grounded)
{
         Velocity.y = JumpPower;
         Snap = Vector3.Zero;
}
else
         Snap = Vector3.Down;
Listing 6-7

Detecting a Jump

A jump works like inverted gravity. Gravity pulls you down with acceleration, and a jump pulls you up with deceleration. Listing 6-7 detects a jump press to initiate a jump, but it depends on the Grounded variable being accurate, representing our contact status with the floor. Godot provides a convenient function in the KinematicBody class to detect floor contact, namely, IsOnFloor. This function returns true if the KinematicBody is on the ground. The _PhysicsProcess function can therefore be written in full as in Listing 6-8.
public override void _PhysicsProcess(float delta)
{
    Vector3 TargetVel = Velocity;
    Grounded = IsOnFloor();
    TargetVel.x = MoveDirection.x * SpeedMultiplier * MoveSpeed;
    TargetVel.z = MoveDirection.z * SpeedMultiplier * MoveSpeed;
    Velocity = Velocity.LinearInterpolate(TargetVel, Acceleration * delta);
    Velocity += Gravity * delta;
    Velocity.y = MoveAndSlideWithSnap(Velocity, Snap, FloorNormal, true, 4, Mathf.Deg2Rad(FloorMaxAngle)).y;
}
Listing 6-8

Checking for Being Grounded

Excellent work! We now have a first-person controller that can move and jump, and its head can rotate from mouse movement. Let’s move forward and create walk and sprint modes.

Note More information on the IsOnFloor function can be found at the Godot documentation online here: https://docs.godotengine.org/en/3.2/classes/class_kinematicbody.html#class-kinematicbody-method-is-on-floor.

Walking and Sprinting

Basically, sprinting can be coded as “fast walking.” Previously, our first-person controller had a walk speed determining how quickly it moved around the scene when horizontal and vertical input was given. Here, we’ll adjust the code to support two different speeds depending on whether the Shift key is being held down. A Shift key press should be configured as an Action from the Input Map – “move_sprint.” To support running, we’ll create a SpeedMultiplier float variable. A value of 1f is the “default speed.” As a result, run functionality can be added to the _Process function with the Listing 6-9.
if(Input.IsActionPressed("move_sprint"))
    SpeedMultiplier = RunMultiplier;
else
    SpeedMultiplier = 1f;
Listing 6-9

Add Sprint Functionality

Head Bobs and Sine Waves

This section adds our final, polish feature to the first-person controller, specifically the Head Bob, that is, the smoothed, bobbing (up and down) motion of the head as it naturally adjusts to character locomotion, walking through the scene. A Head Bob is, simply put, a Y axis position adjustment, back and forth that applies only when the character is moving. To implement a Head Bob, we’ll use a Sine Wave. This is a smooth, repeated curve that we can programmatically move through to determine the Y position of the character’s head over time. This position can be generated from just two float variables, Amplitude and Frequency. Amplitude determines the maximum height of the curve and thereby the strength of the bob. Frequency determines the smoothness of the effect, the ratio of bobs to linear distance. Let’s get started with Head Bobbing by creating three new class variables, which we’ll use later.
    [Export]
    public float HeadAmplitude, HeadFrequency = 1f;
    //Current Y-Pos of the head
    private float BobHeight = 0f;
Next, we’ll code a new and separate function to handle Head Bobbing independently, which will be called every frame, inside _Process. See Listing 6-10.
private void HeadBob(float delta)
{
    Transform T = CamNode.Transform;
    if(MoveDirection.y != 0 && Grounded)
    {
        BobHeight += delta;
        BobHeight = Mathf.Wrap(BobHeight,0f,360f);
        float LerpedHeight = Mathf.Sin(BobHeight * HeadFrequency) * HeadAmplitude;
        T.origin.y = LerpedHeight;
        CamNode.Transform = T;
        return;
    }
    T.origin.y = Mathf.Lerp(T.origin.y, 0f, delta);
    CamNode.Transform = T;
}
Listing 6-10

Coding a Head Bob

Excellent! You’ve now coded a great looking, and fully tweakable, head bob. Let’s break down the code here. Firstly, the HeadBob function accepts a delta value, which can be passed from the _Process event. This is simply a deltaTime value. Inside the _Process event, the HeadBob function begins by checking our Grounded status to ensure we’re on the ground. It then proceeds to wrap an incremental, numerical value between 0 and 360 into Mathf.Sin to retrieve a value along the since curve. This becomes the LerpedHeight and updates the camera origin on Y.

Completing the FPS Controller

So, we’re done. The first-person controller is now fully coded. Great job! Godot doesn’t ship with a C# first-person controller, so we’ve just made an important contribution. Check out the complete code for the controller in Listing 6-11.
using Godot;
using System;
public class FPSControl : KinematicBody
{
    [Export]
    private string LeftAxis, RightAxis, UpAxis, DownAxis = string.Empty;
    private Vector3 MoveDirection = Vector3.Zero;
    private Vector3 FloorNormal = Vector3.Up;
    [Export]
    public float MoveSpeed = 10f;
    [Export]
    public Vector3 Gravity = Vector3.Zero;
    private bool Grounded = false;
    [Export]
    private float Acceleration = 8f;
    [Export]
    private float JumpPower = 15f;
    public Vector3 Velocity;
    [Export]
    public float FloorMaxAngle = 45f;
    private Vector3 Snap = Vector3.Down;
    private Vector2 MouseMove = Vector2.Zero;
    [Export]
    public float MouseSensitvity = 10f;
    [Export]
    public NodePath HeadPath;
    private Spatial HeadNode;
    [Export]
    public NodePath CamPath;
    private Spatial CamNode;
    [Export]
    public float HeadAmplitude, HeadFrequency = 1f;
    private float BobHeight = 0f;
    [Export]
    public float PitchLimit = 45f;
    [Export]
    public float RunMultiplier = 1f;
    private float SpeedMultiplier = 1f;
    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        HeadNode = GetNode(HeadPath) as Spatial;
        CamNode = GetNode(CamPath) as Spatial;
        Input.SetMouseMode(Input.MouseMode.Captured);
    }
    public override void _PhysicsProcess(float delta)
    {
        Vector3 TargetVel = Velocity;
        Grounded = IsOnFloor();
        TargetVel.x = MoveDirection.x * SpeedMultiplier * MoveSpeed;
        TargetVel.z = MoveDirection.z * SpeedMultiplier * MoveSpeed;
        Velocity = Velocity.LinearInterpolate(TargetVel, Acceleration * delta);
        Velocity += Gravity * delta;
        Velocity.y = MoveAndSlideWithSnap(Velocity, Snap, FloorNormal, true, 4, Mathf.Deg2Rad(FloorMaxAngle)).y;
    }
//  // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(float delta)
    {
        float Vertical = -Input.GetActionStrength(DownAxis) + Input.GetActionStrength(UpAxis);
        float Horizontal = Input.GetActionStrength(LeftAxis) + -Input.GetActionStrength(RightAxis);
        MoveDirection = Vector3.Zero;
        MoveDirection = HeadNode.Transform.basis.z * Vertical + HeadNode.Transform.basis.x * Horizontal;
        MoveDirection = MoveDirection.Normalized();
        HeadBob(delta);
        if(Input.IsActionJustPressed("move_jump") && Grounded)
        {
            Velocity.y = JumpPower;
            Snap = Vector3.Zero;
        }
        else
            Snap = Vector3.Down;
        if(Input.IsActionPressed("move_sprint"))
            SpeedMultiplier = RunMultiplier;
        else
            SpeedMultiplier = 1f;
        if(MouseMove.Length() <= 0) return;
        Vector2 MouseResult = MouseMove * MouseSensitvity * delta;
        MouseResult = new Vector2(MouseResult.x, Mathf.Clamp(MouseResult.y, -PitchLimit, PitchLimit));
        HeadNode.Transform = HeadNode.Transform.Rotated(Vector3.Up, -Mathf.Deg2Rad(MouseResult.x));
        HeadNode.Transform = HeadNode.Transform.Rotated(HeadNode.Transform.basis.x, Mathf.Deg2Rad(MouseResult.y));
        HeadNode.RotationDegrees = new Vector3(Mathf.Clamp(HeadNode.RotationDegrees.x, -PitchLimit,PitchLimit),
                    HeadNode.RotationDegrees.y, HeadNode.RotationDegrees.z);
        MouseMove = Vector2.Zero;
    }
    private void HeadBob(float delta)
    {
        Transform T = CamNode.Transform;
        if(MoveDirection.y != 0 && Grounded)
        {
            BobHeight += delta;
            BobHeight = Mathf.Wrap(BobHeight,0f,360f);
            float LerpedHeight = Mathf.Sin(BobHeight * HeadFrequency) * HeadAmplitude;
            T.origin.y = LerpedHeight;
            CamNode.Transform = T;
            return;
        }
        T.origin.y = Mathf.Lerp(T.origin.y, 0f, delta);
        CamNode.Transform = T;
    }
    public override void _Input(InputEvent motionUnknown)
    {
        InputEventMouseMotion motion = motionUnknown as InputEventMouseMotion;
        if (motion != null)
            MouseMove = motion.Relative;
    }
}
Listing 6-11

The Complete First-Person Controller

Testing the Controller

You’ve built a first-person controller. Great! Now let’s test it. To do this, create a completely new empty scene. And then add a selection of mesh instances to it, boxes, cubes, cylinders, and others, to build some scenery. Ensure that you generate static bodies for each mesh to enable collisions. See Figure 6-17. Remember, the associated book companion files feature a basic collision scene for your testing.
Figure 6-17

Creating a World for Testing Our Controller…

Next, let’s add the player character. The first-person controller was created as a completely separate scene, which means it can be dragged and dropped into any other scene, just like a Unity Prefab or an Additive Scene. See Figure 6-18.
Figure 6-18

Adding the Player Scene

Once the player is added, you’ll need to set your starting properties for the first-person controller. By selecting the player object, you can adjust its fields from the inspector, customizing them for active scene, as opposed to the original. See Figure 6-19.
Figure 6-19

Customizing the First-Person Controller

Now you’re ready to try out the new first-person controller. Simply save the scene, and then press the Play Scene button from the toolbar. See Figure 6-20. Great work!
Figure 6-20

Playing the First-Person Controller Scene…

Summary

Congratulations. In this chapter, you coded a first-person controller in C# for Godot, complete with run, jump, and head-control mechanics. In reaching this far, you’ve seen varied examples for reading input, both event-based and polling-based; ways to move objects during _PhysicsProcess to account for physical interactions; how to use scenes as Prefabs, nested inside other scenes; and how to create input actions and more.