Dana Vrajitoru
I355/C490/C590 3D Games Programming

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

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

We assume that you have successfully installed Godot version 4.3 on your computer based on the installation instructions.

Lab Part

Ex. 1. In this lab we'll get used to the Godot environment and write a small application implementing a 3D game of Racquetball.

Open the Godot executable. This should show a list of projects you have created with it. Click on the + Create to create a new project. The Compatibility option should be fine. In the Version Control you can switch to None. Choose a folder where to save the project and call it Racquetball. Then click on Create & Edit.

Resources - Scene. In the Project area in the bottom left quadrant, note a folder called res://. The res (short for resources) folder is where we normally store all the resources needed for the project. Create a folder inside called Scenes, by right-clicking on the res folder.

First, we need to create a scene. At the top, below Create Root Node, click on 3D Scene. This will create a scene node for the scene. Rename it Root. Hit Ctrl-S to save the scene. Select the Scenes folder and call it root.tscn. Remember to save it from time to time.

Camera and World Environment. In Godot, a camera is not added by default to a 3D scene, so let's create one. With the root node selected, click on the + above its name on the left, then follow on Node3D and then Camera3D. You can leave it where it is for now.

In the toolbar above the scene editor, click on the 3-dot button to the left of Transform. Click Add Sun to Scene and Add Environment to Scene. Then click on the play button above the inspector. It will ask you to set the main scene. Selecting the current is just fine. A new window will open showing the view of the game, showing the sky box and the ground.

Creating a Box. Let's create a box to hold the Racquetball game.

First, let's create a plane for the box to sit on. Click the root node to select it, so that the new node is added as a child of this one. The click the + above it to create a new node. To locate the type of node we need more easily, type mesh into the search bar. Among the options, you will see MeshInstance3D. Double-click on it to create such a node. Rename the object Ground using the Hierarchy on the left side.

The new object will not show anything on the screen yet. We need to set its geometry for that first. In the Inspector, below MeshInstance3D, you will see a property Mesh showing empty. Click on the arrow next to it and select a New PlaneMesh. A square should appear on the ground. Set its Size to 12 by 12.

A little below that, still in the Mesh category, there is a property Material with <empty> next to it. Click on that and choose a New StandardMaterial3D. Clicking on it brings up a bunch of properties below. Click on the Albedo and select a dark green color.

Note that the horizontal plane sits on the X and Z axes and that the vertical is the Y axis. You can take a moment to get used to the navigation in the scene editor:

With the object selected, above the scene editor, a button called Mesh has appeared. Click on it and then select Create Collision Shape. In the dialog that opens, for the Collision Shape placement select Static Body Child, and for the Collision Shape Type select Trimesh (default option), the click Create. Note that a StaticBody3D was added as child to the ground node with a CollisionShape3D added as its child. The yellow shape surrounding the plane is the collision shape.

In Godot, colliders are not created by default to match the dimensions of the objects they are attached to. In our case, another collider shape will work better. Click on the CollisionShape3D object, and then in the Inspector, next to Shape, click on the menu to replace the current entry with a New BoxShape. You will notice a smaller box with a thin blue outline appearing, with 6 small red circles on it defining its dimensions. Switch to the Top view and drag those red circles to the border of the plane to give the collider the same shape as the mesh. Then switch to the Front view and drag the red circle on top towards the level of the plane, so that objects don't bounce off of it too soon. You can leave the bottom circle as is.

Box. Let's create a box for the game to take place in. We will create it out of 5 walls in addition to the ground.

Create another node of type MeshInstance3D and name it Wall1. Then set the Mesh as a New BoxMesh. Set its dimensions as 10 x 10 x 0.5. Then in the inspector at the bottom, under Node 3D, expand the Transform property. Set the Position as 0 x 5 x 5.

Repeat the procedure for the ground to add a collider to this box. Also replace its shape with a New BoxShape and adjust its dimensions to fix the wall.

We don't need to repeat the procedure for all the other nodes. Instead, we will duplicate the node to create more walls.

Right-click on the Wall1 object in the hierarchy and select Duplicate. The new node should have the name Wall2. In this new object, you only need to change the Position in the Transform and set Z to -5 instead of 5.

Repeat the procedure creating Wall3. Because we want here to have the mesh with different dimensions, replace the current mesh of this object with a New BoxMesh. You can set the Size to 0.5 x 10 x 10 and set the Position in the Transform to 5 x 5 x 0.

Since this wall has different dimensions than the first one, it will need a different collider too. Just adjusting the dimensions of the collider won't work because it will also modify the collider of the first two walls. So instead, replace the shape of its collider with a New BoxShape and then adjust the dimensions.

You can duplicate Wall3 to create Wall4 and change the position to -5 x 5 x 0. The collider doesn't need to change since it has the same shape.

For the top wall, you can duplicate one of the objects but replace the mesh with a new one so that you can change the size to 10 x 0.5 x 10, then set its position to 0 x 10 x 0. Then also replace the shape in the collider and adjust its dimensions. Next, from the Hierarchy, add another 3D Object of type Cube. Set the scale of this object to 10 x 10 x 10. Set its position to (0, 5, 0). This is the shape of the box we want to create. However, we will make it out of 5 different boxes because of the following reason:

If you look in the Inspector, the box came with a component called a Box Collider. This allows Unity's physics engine to detect collisions between this object and others that have colliders. However, it will not function well if the ball is already inside it when the game starts. So we'll create a box for each wall that will each have its own collider. Also, looking at the ground, that object also came with a pre-made collider.

Let's make this object thinner and move it away from the center. Set the X scaling to 0.5 and the X coordinate in the Position to 5. Rename this object Wall1.

With the object still selected, right-click on it and select Duplicate. Rename the new object as Wall2 and set its X coordinate to -5. Repeat the procedure to create two more walls where the X scale is back to 10 and the Z scale is 0.5. Set these walls at coordinates (0, 5, 5) and (0, 5, -5). The top view is great to see the layout here. Finally, for the 5th wall, set the Y scale to 0.5 and the Y coordinate to 10.

Camera Setup. To be able to run the game inside the box, we need to set the camera so that we can see the inside the box.

Select the camera object. Pull on the yellow arrow to bring it up to about half way through the height of the box. Then pull the blue arrow forward to bring it close to the front wall of the box. The top view can help with that. If you want to be more precise, you can edit the Transform - Position of the camera and set the Y coordinate to 5 and the Z to 4.5. You can also raise the sun to the top of box without getting it passed the roof. Play the game to make sure the view if fine. You can also click on the Preview button below the Perspective mark to test what the camera is seeing.

Then you can also increase the FOV property of the camera to increase the perspective effect and make it look more dramatic. Choose a setting that looks good to you.

The Ball. Let's create the ball that will be bouncing against the walls. Select the front wall of the cube, the one close to the camera, and toggle the eye mark next to its name in the Hierarchy Inspector. The wall should be hidden now.

With the root object selected, add an object of type RigidBody3D and rename it Ball. We need to use this kind of object because the ball will be moving and colliding with other objects. Add another object of type MeshInstance3D as a child of the rigid body, and add a new mesh to it of type New SphereMesh. Set the Material to a vivid red or another outstanding color. Make sure that the radius is 0.5 and the position 0 x 5 x 0.

We also need a collider for this ball. Click on the rigid body object and add a child to it of type CollisionShape3D. For this shape, in the inspector, the Shape attribute is currently empty. Click on that and set it as a New SphereShape3D.

We also need some settings for these objects because for the ball to bounce against the walls, it needs to have a physics material that allows it to do it. Click on the rigid body and set the Gravity Scale to 0 and the object's mass to 0.1. Then set the Physics Material to a new one. Click on it to open its properties and set the Friction to 0 and the Bounce to 1.

If you run the program now, nothing happens. This is because the ball doesn't use gravity and its initial speed is 0, so there's no reason for it to move.

To make it move, we need to give it an initial impulse that initiate the motion. For that, first we need to add a script component to it. Select the Ball object and at the bottom of the Inspector, next to Script where it says empty, click on it. It will offer to create a new script called ball.gd. This name is fine, so you can leave it. After you click to create it, add a new folder called Scripts under res and move the script into it.

The script should contain two pre-defined functions, _ready and _process. The function _ready is only called once on each object when the program is loaded at runtime. In this function we need to define a variable to hold a random number generator to randomize the ball's speed in the beginning and as needed later. Declare a variable rand before the function _start, like

var rand

and the add the following line to the function _ready:

rand = RandomNumberGenerator.new()

In Godot, we cannot set the velocity of a rigid body directly. We need to apply a force to it instead. Add the following lines to the same function:

var force = Vector3(rand.randf_range(-10, 10), rand.randf_range(-10, 10), rand.randf_range(-10, 10))
apply_impulse(force)

Next, we would like to be able to calibrate the ball's speed more easily. Let's add an attribute speed in the code at the top of the class:

var speed = 15

Then replace 10 everywhere in the force by this variable. Now we can just give it a new value when we want to calibrate it.

We also need to set the collision detection as continuous. Select the Ball object, then expand the Solver and turn on the Continuous CD and Contact Monitor options. Then set the Max Contacts to 10.

Racket. Let's create an object that will play the role of the racket, so that the user can interact with the game. The scene editor has probably turned into a code editor. You can switch back to the 3D scene editor by clicking on the 3D button above the editor. That area lets you go back and forth between the script and the scene editors.

Add a new object to the scene of type MeshInstance3D as a child of the root object. Set its mesh as New CylinderMesh. Click on the mesh in the inspector and set its top and bottom radii to 1.5 and the height to 0.05. Then in the Transform, set the Y coordinate of the position to 5 and the rotation over x to 90o. Switch to the Front view and move it to the top right quadrant. Then switch to the Left side and pull it close to the camera but still in front of it so that it's visible on screen.

Set a material for the racket where the Alpha component of the Albedo is somewhere in the middle. Then expand the Transparency of the material and set it as Alpha. That way, we'll be able to see the ball through the racket. Switch to a side view and move the racket closer to camera but maybe not too close. You can use the camera preview to find a good distance for this object. Rename the object Racket.

Add a collider to this the same way as for the walls with a StaticBody3D object to control it. For the collider shape, switch to a New CylinderShape3D. Pull the red dot to the edge of the bottom to align the collider with the object. Then switch to the Left view and adjust the height of the collider. You can hide the needed walls during this operation and make them visible again afterwards. Rename the static body object as RacketBody. This is because we want to be able to tell it apart from the wall bodies when it collides with the ball.

Now, we need to be able to move the racket around to try and catch the ball. For that, we also need to add a script to it. Add a new script component to the racket and call it racket.gd. Add the following variables at the top of the class:

var speed = 5.0
var velocity = Vector2(0, 0)
var fixedZ = transform.origin.z

The expression transform.origin gives us the object's position that we see in the Transform in the inspector.

Then add the following code to the function _process:

var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
if direction:
    velocity.x = direction.x * speed
    velocity.y = -direction.y * speed
else:
    velocity.x = move_toward(velocity.x, 0, speed)
    velocity.y = move_toward(velocity.y, 0, speed)
transform.origin += Vector3(velocity.x * delta, velocity.y * delta, 0)

First, the code gets the direction of movement indicated by the user with the arrow keys, given by the system class Input. Then if there was indeed input from the user, we multiply the user's key strokes by the set speed of the object. The input from the user is only 2D, and the movement of the racket is supposed to be in the (X, Y) plane, so that works out fine.

If there is no input from the user, we gradually reduce the speed to 0 with the function move_towards.

On the last line, we multiply the velocity by the value of delta and add it to the position of the object. This is a simple linear movement where we displace the object by a distance equal to the velocity times the time (delta).

Collision. In the last part of the lab, we will handle the collision between the racket and the ball. We would like to have a score in the game and to have it increase every time there is a collision between the ball and the racket.

In the script ball.gd, add a variable score at the top and initialize it to 0.

Then click on the ball object (its rigid body) and note a tab labeled Node next to the Inspector. Click on that. These are all the possible "signals" that the object can receive. A signal is a notice of an event happening.

Click on the signal body_entered and then on Connect at the very bottom of the window. The dialog that opens lets you select an object to react to this event happening to this object. The object itself is fine, since it already has a script. The signal body entered is generated when another collider has entered this object's collider. The dialog also lets you select what function to be called in case of this event. If there isn't a function you already have for this, it will create one for you. Click on Connect to do just that.

Notice that in the script ball.gd, a new function was added at the bottom. In this function, replace the pass instruction with a print statement where you print the body.name. The body parameter is the object that collided with the ball, in fact, its RigidBody3D or StaticBody3D part of the subtree. Try the program to see what happens.

If everything works well, we just need to test if the body the object collided with has the name of the racket body and if so, increment the score and print it. Replace the print in the function with the following:

if body.name == "RacketBody":
    score += 1
    print("Score: %d" %(score))

Test the program to see if it works. This is the end of the lab, and the project will be continued homework.

Homework Part

Set some materials with the colors you like to the walls of the box and to the ball. You can play with the transparency in the Albedo component of the materials to see if that makes it look better.

Change the formula for the initial speed of the ball so that the Z component of the speed is between 0.5 * speed and speed.

Your game should be ready to go. Test it to make sure it works in both cases. Comment out any debug statements.

Experiment with the angular drag, freezing the ball's rotation, and other factors in the rigid body to find an optimal setting for the ball's movement. You can add some extra velocity to the ball when it hits the racket.

Create another cylindrical object called Target with the goal of giving the player extra points if the ball hits it. Set the target close to the back wall and give it a tag called "target". Add a case in the collision function where if it hits this target, you add 10 points to the score and output it.

Homework Submission

Take a screen shot of your running program, showing the content of the screen while the game is running, and save it as png or jpg. Submit it along with the root.tscn (scene), and all the .gd (script) files, to Canvas, Assignments - Homework 1 as a zip file.