Dana Vrajitoru
I355/C490/C590 3D Games Programming

I355/C490/C590 Lab 2 / Homework 2 Godot Version

Date: Wednesday, September 11, 2024. To be turned in by Wednesday, September 18. Here is a snapshot of the game:

In this lab, we will follow the online demo:

Godot 3D Platformer Demo (https://www.youtube.com/watch?v=sVsn9NqpVhg).

Lab Part

Ex. 1. In this lab we'll create a simple 3D platformer game. You can either follow the video linked above or the instructions below. The homework part is not in the video.

Here are some details from the video that are different in Godot 4.3:

Environment

Open Godot and create a new project called 3DPlatformer. Switch the project type to Forward+ and the Version Control Metadata to None, then click Create & Edit.

In the Hierarchy, click on Node 3D to create a new node. You can leave it with the default name. Click on the + above this node to add a new node as its child. Type "camera" into the search box and add a Camera3D object.

Click on the 3-dot button above the scene editor (left of Transform). In the dialog that opens, click Add Sun to Scene. Then open it again and click Add Environment to Scene. Click the Play Scene button above the Inspector. It will ask you to save the scene first. Rename the scene Level.tscn and then save it. After the game window opens, you can close it.

Ground

Select the Node3D in the hierarchy. Click the + to add a new node of type MeshInstance3D (do a search with "mesh" and it should appear). In the Inspector, click on <empty> next to Mesh and select a New PlaneMesh.

Click on the plane object to the right of Mesh in the inspector and it should show the shape's properties. Set the Size of this object to 10 x 10.

Camera Setup

Select the Camera3D object in the hierarchy. Pull the green arrow up a little bit.

To update its position and orientation using the inspector, scroll down in the inspector to the Transform and expand it. Set the camera's Y coordinate in the Position to 2.

Project Settings

In the top menu, click on Project and then Project Settings. Click on Window under Display and set your window size to 1200 x 800, or some other size that works well for your screen. Then under the Mode select Maximized. Below that, in the Stretch, make sure that the Aspect choice is keep.

Without closing the settings panel, scroll down on the left to the Rendering section and click on Anti-Aliasing. For MSAA 3D choose 2x. In the top-right corner of the panel, toggle on Advanced Settings. Then also in Rendering, click on Lights and Shadows. Under Soft Shadow Filter Quality, choose Soft Medium (Average). You can close the settings panel now.

Player and Scene.

We will now create an object for the player and we will make it its own scene. Sometimes we do that when an object is complex enough or when we want to create copies of it more easily.

From the Scene menu at the top, select New Scene. For the root of this scene, click on the Other Node option. This opens the node selection dialog. Type "rigid" in the search bar and select RigidBody3D. Rename the node Player in the hierarchy.

Mesh. We still need to add a mesh and a collider for this object to work properly. Add a child of the node Player of type MeshInstance3D. Then in the Inspector, set the Mesh of this object as a New CapsuleMesh.

Collider. Then with the mesh object selected, click on the Mesh button above the scene editor, to the right of View. Choose Create Collision Shape. In the dialog that opens, make sure that the top choice is Sibling. Then for the Collision Shape Type, select Simplified Convex, then click Create. You can click the eye next to the mesh in the hierarchy to hide it so that you can see the collider better, then click to show it again. It should be fitting the object pretty well.

Then click on the Player object, expand the Deactivation category under RigidBody3D, and click to enable Lock Rotation.

Script. Right-click on the Player object in the Hierarchy and choose Attach Script. The default name is fine, so click Create. Now the script editor should show in place of the scene editor. You can switch back and forth between them using the buttons just above the editor.

Replace the instruction pass in the function _process with the following lines:

var input = Input.get_action_strength("ui_up")
apply_central_force(input * Vector3.FORWARD * 1200 * delta)

Save the scene with Ctrl-S or Cmd-S. The default name is OK.

Level Integration. In the project browser on the lower left side, double-click on the scene Level.tscn to reopen it. Then click on the 3D button above the editor to switch back to the scene editor.

Select the Node3D root node of the scene. Then right-click on player.tscn in the project browser and select Instantiate. A new child of the root should have appeared with the name Player.

Select the Player object, then in the inspector, expand its Transform and set the position to 0 x 2 x -2. This should place it in front of the camera and above the ground.

Ground Collider

To avoid the player falling through the fall, let's add a collider to the ground. Select the object MeshInstance3D and click on the Mesh button above the scene (next to View). Select Create Collision Shape. In the placement option at the top choose Static Body Child, and the shape type below Trimesh. Two thin blue triangles should have appeared over the ground. This is a collider made of triangles.

Input Configuration

Let's configure the input to accept WASD keys in addition to the arrows. At the top of the window, click on the Project menu and select Project Settings. At the top of the settings window, click on the Input Map tab.

Click in the box showing Add New Action and type in move_forward, then enter. This creates a new action that we can listen to in the program through the Input class. To the right of this action, click on the + button to add keys attached to this event. Hit the W key. This should find the corresponding code in the list and you can just click OK to add it. Similarly, add the up arrow key. Now the object will respond to both of these keys. Repeat the process setting the move_left action associated with the A key, move_right with D, and move_back with S. You can close the settings dialog.

Going back to the script, we need to change the actions that we are listening to. In the function _process, replace the two lines with the following code:

var input := Vector3.ZERO
input.x = Input.get_axis("move_left", "move_right")
input.z = Input.get_axis("move_forward", "move_back")

Run the program now. The player should be moving in all 4 directions.

To improve the movement, switch back to the 3D editor, select the player in the hierarchy, and go to the Inspector. Here, expand Linear below RigidBody3D, and set the Damp value to 3.

Camera Tracking Player

Let's have the camera track the player and rotate with as as the player is moving around. Click on the player tab above the scene editor to go back and edit the player scene. You can also double-click on it in the project browser. With the Player node selected, click on the + and add a new node of type Node3D as its child. Rename this node TwistPivot. Add another node of the same type Node3D as a child of TwistPivot and call it PitchPivot.

Create a node node of type Camera3D as a child of the PitchPivot node. With the camera selected, in the Inspector set its Z value in the Position (under Transform) to 3. Save the changes to this scene.

Return to the scene level and delete the old Camera3D node. Play the game to see the camera follow the player around.

Now we'll position the camera a little better. Go back to the player scene and select the camera. Click on the Preview button in the scene editor on the top left, just below the label Perspective.

In the Hierarchy, select the object TwistPivot, expand the Transform in the Inspector, and set the Y coordinate in the Position to 1. Then select the PitchPivot object, expand its Transform, and set the X coordinate in its Rotation to -10. Click on the Preview button again to exit this view.

Save the scene and run the level scene again to see the effect.

Mouse Control

We would like to make the mouse invisible on screen and confined to the application window while playing. However, when the player hits the ESC key, the mouse should become visible again and unconfined.

To have the mouse control the view of the scene, go back to the script and add the following line to the function _ready:

Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

This will make the mouse invisible and will force it to remain in the running window. Before we test this, in the function _process, add the following code:

if Input.is_action_just_pressed("ui_cancel"):
    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

This will allow us to return to a normal mouse mode when the ESC key is pressed. Try this to see if it works.

Then, we want to capture mouse movement in our scene. First, at the top of the class, below the extends line but before the first function, add the following variables:

var mouse_sensitivity := 0.001
var twist_input := 0.0
var pitch_input = 0.0

Adding them at the top makes them available to all the functions in this class.

Then add the following function at the bottom of the script:

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
            twist_input = -event.relative.x * mouse_sensitivity
            pitch_input = -event.relative.y * mouse_sensitivity

Now we need to apply this input to the camera. In the function _process, add the following line at the end:

$TwistPivot.rotate_y(twist_input)

Make sure that the object has the exact name in the line above. The $ sign in front of the name identifies that object in the scene tree. You can try this functionality: when you move the mouse around, the camera should rotate around the player vertically.

To avoid having to switch to the level scene to add it, we can set it up as the main scene. For that, click on the level tab above the editor, then click the Play button instead of Play This Scene. If you haven't done this before, it will ask you to set up a main scene, and will offer to use the current scene. You can just click OK. That way, no matter which scene you are editing, the Play button will play the level scene.

To deal with the other rotation, add the following lines to the function _process:

$TwistPivot/PitchPivot.rotate_x(pitch_input)
$TwistPivot/PitchPivot.rotation.x = clamp($TwistPivot/PitchPivot.rotation.x, -0.5, 0.5)
twist_input = 0
pitch_input = 0

The expression $TwistPivot/PitchPivot lets us select the object PitchPivot from the subtree of the object TwistPivot. The function clamp restricts the value to the the interval [-0.5, 0.5]. This is done to avoid rotating the camera too much.

To simplify the code a little but, add these two variables at the top of the script. The keyword @onready means that they will be defined just before the function _ready is called, meaning when the scene tree already exists at runtime.

@onready var twist_pivot = $TwistPivot
@onready var pitch_pivot = $TwistPivot/PitchPivot

Here, we're simply storing a reference to those expression in some simpler variables. Replace those expressions in the function _process with these variables. Also, to improve readability, replace the 0.5 value in the clamp function with deg_to_rad(30) and similarly for the negative value. This will use the angle of 30 degrees and will convert it to radians because the rotation expects such a value.

Now, we'd like the player to move forward in the direction of the camera, and not in the global direction. To do this, in the call to apply_central_force inside the function _process, multiply the input by the expression twist_pivot.basis without deleting all the other things multiplied by it. Try it now and notice how the movement of the player has changed.

Jump

We'd like to add a jump feature to the player triggered by pressing the spacebar. There is a built-in input manager option for that called "ui_accept". But first, we need to be able to check if the object is grounded, because it should only be allowed to jump when it's grounded. Add the following function:

func is_grounded():
    return linear_velocity.y > -0.1 and linear_velocity.y < 0.1

At the top of the class, add a variable called jump_speed and assign it the value 10. Then add the following code to the function _process just before the check for "ui_cancel":

if Input.is_action_just_pressed("ui_accept") and is_grounded():
    apply_impulse(Vector3(0, jump_speed, 0))

You can adjust the jump speed later. Verify that this new feature works. This is the end of the lab.

Homework Part

Create a few other plane or box objects places around the ground one, accessible to the player by jumping, to create a more complex level. Apply some materials to the objects to make the game look better.

Add a few collectible objects that the player can look for. You can make them yellow cylinders with a small height, and lay them on the side to look like coins. Add a counter to them and increment it every time there is a collision. If you name this object "Coin##", in the function _on_body_entered you can check if the body (the coin) has this name with

if body.name.contains("Coin")

You will need to connect the Player object with the body_entered signal (see Lab 1). Make sure to enable both Continuous CD and Contact Monitor in the Solver in the Inspector for the player and set the Max Contacts value high enough. Then to delete the coin that collided with the player, you can do

body.queue_free()

To create multiple coins, you can also save the subtree as its own scene.

That's it for the homework. Submit the script files (.gd) and the scene files (.tscn), as well as a screenshot of the program (png or jpg image), in a single zip file.

Homework Submission

Submit a zip file with the scripts, the scenes, and a screen shot of the running program, to Canvas, Assignments - Homework 2.