Creators – Week 7

It was time this week to finish up Roll-A-Ball. After the session, I did some additional tweaking. There are changes to the materials and camera position. I also added a splash screen, background sound, a font resource and a sound effect when a pickup is collected. Each level has an associated target time and you advance to the next level depending on whether you beat that time. There are four levels in total. At the end, depending on how you did, you get “You won” or “Game over” message and the option to restart. Here it is in action:

Collision

Although the pickups were technically just over the ground, and not intersecting it, we found previously that for many of us, when we ran, several of the pickups detected contact with the ground and deleted it. We “fixed” that by temporarily lifting the CollisionShape3D in the pickup up by 0.01m, but we returned to this to correct it.

Every node that collides with another, in our specific case Area3D and RigidBody3D, derives from the CollisionObject3D node and has a Layer and Mask property:

Layer is the collision layer that this node is in. Note that something can be in more than one layer.

Mask is the collision layer that we watch for collisions with. Anything not in these nominated layers will be ignored.

All we had to do to fix this, was to set the Mask for the Pickup to 2, and the Layer for the Player to 2. Now the pickup ignores all collisions except with the player (everything else, including the ground, being in Layer 1 by default).

Dynamic Level Loading

We wanted the GameManager to be able to load levels automatically. First we created our level, by adding a Node3D called “Level 1” to our Main scene and dragging all pickups into it. We then right-clicked and chose “Save Branch as Scene…” to make it a scene of its own. We then deleted the instance of the level from the scene, since we were going to have the Game Manager create it.

We added a new variable to Game Manager:

@export var level : PackedScene

PackedScene is the type used for a scene saved to disk. We were then able to assign our level scene to this variable in the inspector.

Next we had to add some code to get this level created. We added these lines at the bottom of the GameManager’s _ready() function:

var current_level = level.instantiate()
add_child(current_level)

This takes the level from the disk and actually creates nodes from it (instantiate()). We save that to a variable current_level and the use add_child() to actually add this to the current scene. When we ran, all was as normal, but looking at the remote tree, we could see that Level 1 was now a child of Game Manager.

We then created a few extra levels and tried loading them instead, by selecting them in the inspector.

Handing Multiple Levels Automatically

We then went back and decided to add support for multiple levels, having the game change to the next level automatically. We added these variables to the game_manager.gd script:

@export var levels : Array[PackedScene]

var current_level : Node3D
var current_level_index : int = -1

An Array, as you might remember from the first few weeks, is a variable that can store multiple values like a list. We then removed the last two lines from _ready() and created this function instead:

func load_next_level() -> void:
	if (current_level != null):
		current_level.queue_free()

	current_level_index += 1
	current_level = level[current_level_index].instantiate() as Level
	add_child(current_level)

This function:

  • Removes the current level, if there’s one loaded
  • Increments the level counter
  • Loads the next level and adds it to the scene

Note that it doesn’t know when to stop yet and will keep trying to load even when there’s no levels left.

Finally we had to call this function from somewhere. All that was needed was to edit _process() as follows:

func _process(delta: float) -> void:
	pickups_count_label.text = "Remaining Pickups: " + str(pickup_count)

	if (pickup_count == 0):
		load_next_level()

If we’re at the start of the game, when the pickup_count is also zero, or we’ve just collected all the pickups i a level, the game will call load_next_level() and load a level.

We tested this and it worked.

Reseting the Player

We wanted to reset the player. This means returning it to its starting position and clearing any linear and angular velocity from the RigidBody3D. For a RigidBody3D, all changes to physics variables should happen in _physics_process().

To make this work, we first added three variables to player.gd:

var initial_pos : Vector3
var initial_rot : Vector3

var reset_requested : bool = false

The first two are to remember the initial position and rotation of the player and the third is a switch we can toggle to true when we want to trigger a reset.

To remember the initial position and rotation, we just needed to add these two lines to _ready():

	initial_pos = position
	initial_rot = rotation

Then in _physics_process() we change it to read as follows:

func _physics_process(delta : float) -> void:
	if (reset_requested):
		position = initial_pos
		rotation = initial_rot
		linear_velocity = Vector3.ZERO
		angular_velocity = Vector3.ZERO
		reset_requested = false
	else:
		var input = Input.get_vector("Up", "Down", "Left", "Right")
		apply_torque(power * delta * Vector3(input.x, 0, -input.y))

If reset_required is true, then we put the ball position and rotation back to their original values and set both linear_velocity and angular_velocity to zero. Finally we set reset_requested back to false so that we won’t run this code again until it’s needed.

To actually trigger this, in game_manager.gd, we added a variable for the player:

@onready var player: RigidBody3D = $"../Player"

And we updated _process() to also reset the player:

func _process(delta: float) -> void:
	pickups_count_label.text = "Remaining Pickups: " + str(pickup_count)

	if (pickup_count == 0):
		load_next_level()
		player.reset_requested = true

Handing Running out of Levels

To handle running out of levels, we updated our load_next_level() function in game_manager.gd to check that there actually was another level to load and to return false if there wasn’t. We changed the return type to -> bool first:

func load_next_level() -> bool:
	if (current_level != null):
		current_level.queue_free()

	current_level_index += 1
	if (current_level_index >= len(level)):
		return false

	current_level = level[current_level_index].instantiate() as Level
	time_remaining = current_level.target_seconds
	add_child(current_level)

	return true

This uses the len() function to check the size of the level array and if there isn’t another level to load return false.

Now the game doesn’t crash when it runs out of levels. Finally we added this logic to game_manager.gd’s _process() to hide the player once we run out of levels:

func _process(delta: float) -> void:
	pickups_count_label.text = "Remaining Pickups: " + str(pickup_count)
	
	if (pickup_count == 0):
		if (load_next_level() == false):
			player.hide()

Getting the Code

All our code for this year is available on our GitHub.

Creators – Week 6

This week we added some pickups to our game, some signals (Godot’s term for messages or events) to indicate their creation or collection, and a game manager to keep track of everything.

Pickups

To create our pickups we created a new scene with an Area3D as the root node. An Area3D, in conjunction with a CollisionShape3D, allows us to define an area where bodies entering and exiting this area can be detected.

We paired this Area3D with a cylinder CollisionShape3D and a cylinder MeshInstance3D, both about the same height as our player ball.

We created a new StandardMaterial3D resource for our pickup and used the checker texture again, this time scaled to appear multiple times on the object, to give it some visual interest.

Signals

Signals are Godot’s way for nodes to tell other nodes when something’s happened. There are many built-in signals in the existing nodes and you can easily define others yourself.

To demonstrate, we created a new script attached to the root node of the pickup scene, and with this same node selected, we then went to the tab underneath the Inspector tab, a tab called Node.

This shows the list of signals on this node available to connect to. We right clicked on body_entered() and connected it to the pickup script, creating a new function called _on_body_entered() in that script.

We just set the following code there to show that we’d detected something entering the area (as defined by the collider):

func _on_body_entered(body: Node3D) -> void:
	print(body.name + " entered the area")

We created an instance of the pickup in our Main scene by using the “Instantiate Child Scene” button at the top of the scene view, and selecting the pickup scene:

We moved this pickup off centre and tested our code by playing it and rolling the player into the pickup. The message “Player entered the area” was printed to the Output window.

We updated this code by adding queue_free() at the end of the function:

func _on_body_entered(body: Node3D) -> void:
	print(body.name + " entered the area")
	queue_free()

This function means “please queue this node (and all it’s child nodes) for deletion at the end of this frame”. When we test again, we see that rolling the player into the pickup now makes the pickup disappear.

A Handy Way To Handle Events

In Godot connecting a node to another’s signal normally means that the first node has to have a reference to the second.

There’s a nice pattern that simplifies things. We make a central place that holds the signals, nodes that want to “emit” (or cause the signal to fire) and nodes that want to know when the signal was emitted just need to know about this central place, they don’t need to know about each other.

Godot has a system whereby a script can be set to autoload when the game starts, it doesn’t have to be connected to a node in a scene. Even more convenient, it will declare a global (visible everywhere) variable for this class instance that everyone can easily access.

This might sound complex, but it’s super easy in practice. We created a new script called events.gd. The contents were as follows:

extends Node

signal pickup_created
signal pickup_collected

We then went to Project Settings | Globals and under Autoload, selected the events.gd script and pressed “+ Add”

Firing the Events from Pickup

In pickup.gd we added two lines to our _ready() and _on_body_entered() functions to cause the appropriate signals to be emitted when the pickup was created and collected respectively:

func _ready() -> void:
	Events.pickup_created.emit()

func _on_body_entered(body: Node3D) -> void:
	print(body.name + " entered the area")
	queue_free()
	Events.pickup_collected.emit()

See that we’re using the global variable “Events” here to access the signals.

We tested that we could read these signals by putting code into Player, but as this was just for quick testing, we won’t replicate it here.

Game Manager

We created a new Node3D in the Main scene, called it Game Manager, and created a new script attached to it.

In this script we added two variables to the top of the class:

extends Node3D

@export var pickup_count : int = 0
@export var game_time : float = 0

One is an integer variable to keep track of the number of pickups. The second is a float variable that tracks how long the game has been running for. Both have “@export” at the start, this means that they both appear as variables in the Inspector, which is handy.

For the pickup count, we first added two new functions, to respond to the creation and collection signals respectively:

func _on_pickup_created() -> void:
	pickup_count += 1
	
func _on_pickup_collected() -> void:
	pickup_count -= 1

All these to is increase or decrease the pickup_count variable. We need then to connect them to the signals, so that they get called if the signal is emitted. We do that in _ready():

func _ready() -> void:
	Events.pickup_created.connect(_on_pickup_created)
	Events.pickup_collected.connect(_on_pickup_collected)

For game_time, all we have to do is keep adding the delta values available in _process(), which represent the time in seconds since the last frame was drawn, together:

func _process(delta: float) -> void:
	game_time += delta

To see these running we ran our game and in the Scene view, switched to the “Remote” tree. The remote tree is the tree of the game that’s running. Looking at the Game Manager node in the inspector, we could verify the variables were working as expected.

Getting the Code

All our code for this year is be available on our GitHub.

Creators – Week 5

This week we continued working on Roll-A-Ball.

More Walls and Resource Copies

We left off last week with two walls. This week we copied one of those to create a third. It’s new transform position is (1.05, 0, 0) and it’s transform rotation is (0, 90, 0). That rotation represents 90 degrees around the vertical axis (y-axis).

It was immediately clear that we ideally needed to close the gaps at the corners where the walls meet.

To adjust the size of the third node, we select its MeshInstance3d child node. To adjust the mesh size, we click on the picture of the mesh in the inspector to see it’s properties and then adjust the size to 2.1m:

We can see all the walls change size. This is because they are all using the same mesh resource. We set the size back to (2.0, 0.3, 0.1) first. Now, we can make this mesh resource separate to the one used by the other two walls. Click on the drop-down arrow next to the mesh picture and then select “Make Unique”

Now when we change the size to (2.2, 0.3, 0.1) it doesn’t change the other two walls.

Finally we copy this third wall and move it to (-1.05, 0, 0). Now we have four walls.

Materials

We created a wall material in a very similar fashion to how we created the original ground material and assigned it to all four walls.

We also created a new material and assigned it to the ball. We gave it a strong red colour in ‘Albedo’. Because the ball was going to be impossible to tell if it was rolling, being all one uniform colour, we decided to apply a checker board texture to it. Here’s a texture we used today:

We placed this in a textures folder in the FileSystem and then assigned it to the Texture slot under ‘Albedo’ in the ball material. That gave the ball a checkered look.

We also looked at some of the other most commonly used material settings, including roughness, metallic and transparency.

Turning the Ball into the Player

We wanted to create a Player object and make it it’s own scene. We renamed “ball” in the Main scene to Player and right-clicked on and chose “Save branch as scene”. This created a new scene called “Player” which opened a new tab in the centre of the editor.

The Player node still had the transformation from the main scene (lifted up along the y-axis). We removed this and saved the Player scene. Switching back to Main we could see the Player node was now cutting through the ground so we moved it upwards again. Note that changes in the child scene were reflected in the parent scene, but not the other way around.

Creating an Input Map

To capture user input, we set up some Input Actions. Under the Project menu, we selected Project Settings and then selected the Input Map tab:

Where it says “Add New Action” we typed the name of each action in turn (Up, Down, Left and Right) and pressed the Enter key or the “+ Add” button to create it.

We then selected each action in turn and pressed the + at the right hand side of it’s row. We then just pressed the key we wanted for that action and closed the dialog to confirm the selection. Above I’ve chosen the arrow keys, but others chose WASD. Either is fine.

Making the Player Move

We next want to make our player move in response to input.

First we created a scripts folder in our file system

In the Player scene, we selected the Player node and pressed the Attach Scripts button. We then change the path on the dialog that appeared to make sure to place our new script in the scripts folder (instead of the scenes folder).

Our script automatically opens in the editor. It extends RigidBody3D, which is the type of the existing Player node. It has two empty functions, _ready() and _process(), by default. _ready() is called when the object is first created and _process() is called every time the scene is drawn.

We’d like to apply physical forces to our object, specifically torque, which is a turning force. There is another function where we should put code that interacts with the physics system: _physics_process(). Here’s what that empty function should like:

func _physics_process(delta : float) -> void:
      pass

This is a function that takes one parameter called “delta” which is a float. A float is a number with a decimal point. The “-> void” means that this functions doesn’t send back a value to the code that calls it. You must have at least one line inside a function in GDScript; “pass” doesn’t do anything, it’s just there to prevent the function from having no lines in it.

We can get our input values like this:

func _physics_process(delta : float) -> void:
	var input = Input.get_vector("Up", "Down", "Left", "Right")

These are the names of the input actions we’ve already defined. This is going to return a Vector2 value which we’re storing in the variable called “input”. As a Vector2, this has an x and a y value. The x value will vary between 1, -1 and 0 depending on whether “Up” or “Down” or neither are being pressed. Similarly the y value will vary between 1, -1 and 0 depending on whether “Left” or “Right” or neither are being pressed.

We can now actually apply torque (turning force) to our ball:

func _physics_process(delta : float) -> void:
	var input = Input.get_vector("Up", "Down", "Left", "Right")
	apply_torque(Vector3(input.x, 0, -input.y))

The function apply_torque() is part of RigidBody3D and expects a Vector3 which indicates how much to apply around the x, y and z axes respectively. We don’t want any applied around the y-axis as this would just spin the ball around the vertical axis and not roll it along he ground. So we make a Vector3 where the “Up”/”Down” actions add torque around the X-axis and the “Left”/”Right” actions add torque around the z-axis. When testing, because of our camera angles, we find the “Left”/”Right” is backwards to what we want, so we use – to flip the direction.

The ball now rolls! Too fast and there’s a problem with colliders, which we need to fix – the wall colliers are the wrong size, but once that’s corrected we have something not too bad.

Getting the Code

All our code for this year is be available on our GitHub. Please see Creators – Week 4 for more details.

Creators – Week 4

This week we started our first Godot game. We downloaded the engine from the Godot website: https://www.godotengine.org

To install Godot on Windows, you just need to download the ZIP file and extract the two files inside to a folder on your computer. Double-click on the larger of the two to open Godot. Once run, you can pin the program icon to the taskbar to easily run it again.

Godot opens with the Project List. Here we see projects we’ve already created and can create new ones. We made a new project called “Roll a Ball”.

We found some machines had trouble with opening the project if the rendering mode was set to “Forward+”. For those people we found switching it to “Compatibility” fixed that issue. It seems that the underlying issue here is support for the Vulkan graphics API in the “Forward+” rendered. Switching back to “Compatability” uses the OpenGL API instead, which is more widely supported. Vulkan is intended as the successor to OpenGL.

Godot Scenes, Nodes and Resources

Scenes

The key building blocks of Godot are Nodes. Scenes are nothing more than a collection of nodes.

Each scene has a single node at the very top of the list. It can have nodes below it that are known as its children. Each of those child nodes can themselves have children. We call this structure a “tree” because of how it branches from a “root” – notwithstanding that the tree is upside down with the root at the top!

Nodes

There are lots of different types of Nodes and they all have specialised jobs to do. Most nodes “extend” other simpler node types, meaning that they do everything that the simpler node does, plus a bit more. For examples, the XRCamera3D, which is a camera used for VR and AR development looks like this:

Node -> Node3D -> Camera3D -> XRCamera3D

So an XRCamera3D is a specialised version of a Camera3D, which is a specialised version of a Node3D which is a specialised version of a Node.

Later, when we’re writing our own scripts, we’ll also effectively be extending existing nodes and adding our functionality on top of their existing functionality.

Resources

Resources are not nodes, but they are used by nodes. They provide extra information that Nodes need. Examples are meshes (3D shapes) and materials (which define the finish of 3D shapes).

Resources can often be created directly attached to the Node that needs them, but they can also be created as a file within the project. Having them as a file is good when you want to share the resource with several nodes and quickly find it later to edit it.

Godot Editor Layout

Unlike some other game engines, Godot has a fairly fixed layout. With reference to the image above, we will take a look at the most important parts.

The upper left is the Scene view, this is where we see the tree of nodes in the current scene.

Below the Scene view is the File System view. This shows the files in the current project. We can easily create folders here, rename and reorganise things. We can also create resources.

In the upper centre of the program is the main editor area. Using the buttons above it, it can be switched between 2D, 3D Script and Asset Lib views.

On the right hand side is the Inspector. This shows all the properties of the currently selected node or resource and allows us to edit them.

Building our Scene

We created a scene with a plane and a ball, both as physics objects. We dropped the ball and watched it bounce on the plane.

Both objects were build similarly, but the root node in each was slightly different. Let’s look at the ball first:

A RigidBody3D is a node that the physics engine will take care of moving. On it’s own, however, it can’t do anything. It needs to know what shape it’s simulating. As humans we also need to be able to see it. We accomplish this with two child nodes, The CollisionShape3D (containing a SphereShape2D resource) defines the shape, from a collision viewpoint, so the RigidBody3D can now do it’s work. So we can see something, a MeshInstance3D (containing a SphereMesh) is added to give us something we can see.

If might seem odd that we have to specify one shape for collision and one for visual purposes, but actually that really makes sense. Calculating physics and collisions is expensive (in terms of computer time). It’s really normal to use as simple a collision shape as we can. If this mesh was actually an irregular boulder, rather than a perfect sphere, we might still be able to get away with a simple collision shape, if it was close enough.

The ground was built like this:

Very similar, but instead of RigidBody3D, we have StaticBody3D. StaticBody3D is also managed by the physics engine, but it never moves. Like RigidBody3D it needs a collision shape, and again we need something we can see. For that we have a CollisionShape3D node (with a WorldBoundaryShape3D resource) and a MeshInstance3D (with a PlaneMesh resource). WorldBoundaryShape3D is interesting because it’s infinite in size. Even though our visible plane is only 2x2m, the ball will never fall off the side. This will not matter because we’ll be adding walls around our ground.

Running our Scene

Before we can meaning fully run our scene, we have to add a light and camera to it. We added two nodes, one a Camera3D and the other a DirectionalLight3D and positioned them appropriately. To run the scene, you just need to press the play button on the upper right-side and the game opens in a new window.

Getting the Code

All our code for this year will be available on our GitHub. This will contain all code we write this year. At any point, use the green button on that page to download a ZIP file containing all the up-to-date code:

In Godot, just use the “Import” button from the Project List to load a project that’s not already listed.

Creators – Week 3

Hi Folks,

We continued with Learn GDScript from Zero, completing lessons 18-23. There are four remaining lessons, which I encourage you to explore in your own time, we won’t be covering them in Creators.

When we return after the Halloween break, we will install Godot and start our first game.

If you want to practice or catch-up, the link is here: https://gdquest.github.io/learn-gdscript/

Bodgers Day 1 – Getting started

Hello Bodgers

It was great to be back and to get started for another year. We spent most of last Saturday getting set up for the year ahead. We began by installing our Python Editor Mu, you can download it here.

We also installed Paint.Net which we will use for editing images for our sprites, you can get Paint.Net here.

I have created some resources for our games that are available on Dropbox here, download the folder called CoderDojo and save it to your Desktop.

Launch Mu and load the file called day_1.py from the CoderDojo folder you just downloaded and enter the code from the worksheet below..

Click Play and your first Python game should run.

Looking forward to seeing you all again next Saturday.

Declan, Rob and Kevin