Creators – Week 9

This week we:

  • Added a basic GameManager class
  • Made a ShipKiller class that interfaces with GameManager
  • Added an enemy that wanders around the screen

ShipKiller and GameManager

Godot nodes can emit a bunch of signals that indicate when things happen. For something like Area2D we’re usually most interested in signals like body_entered, which emits when something crossed into that area.

We can connect to these signals from our code. This works well when we have a small number of things to connect, but it’s not so useful when we have a lot of things to connect or for allowing us to connect to things dynamically created at run-time.

For our ShipKiller, we wanted a script that we could attach to any Area2D node and not require any further set-up for it to work. This will allow us to really easily use it for static obstacles, enemies and even enemy projectiles.

To make this work so simply, we made the GameManager class and attached it to a node of the same name. We gave this node a unique name. This means two things:

  1. There can only ever be one node in the tree called GameManager
  2. We can always just address the GameManager object in code as %GameManager and we don’t need to know the relative path. This makes it easy to use from anywhere.

Here’s out basic game_manager.gd script:

extends Node2D

@export var ship : Node2D

func inform_body_entered(body) -> void:
	if (body == ship):
		ship.queue_free()

and here’s our ship_killer.gd script:

extends Area2D

func _on_body_entered(body: Node2D) -> void:
	%GameManager.inform_body_entered(body)

Here is the sequence of actions:

  1. The Ship (a CharacterBody2D) crosses into the area of the ShipKiller (an Area2D)
  2. This causes the Area2D to emit the body_entered signal which ship_killer.gd is connected to with it’s function _on_body_entered()
  3. ShipKiller tells the GameManager that something has just crossed its area
  4. GameManager checks to make sure it was the Ship, and if it was, deletes it

Enemy and Wandering Behaviour

We designed a multi-frame animated sprite at https://www.piskelapp.com/ and imported it into Godot as a spritesheet.

We created an Area2D node and called it Enemy0. Under that we placed a CollisionShape2D node, with a 32×32 RectangleShape2D defining it’s shape. We also placed an AnimatedSprite2D and used our enemy sprite sheet to define the animation.

We also added a Node2D under Ememy0, called it Mover and added a new script to it. This script is intended to be attached to the child of an existing node and so we’ll always be moving the parent, not the node the script is attached to. Again, this is to help us reuse this general purpose script.

Here is the version of mover.gd that we wrote:

extends Node2D

@export var direction : Vector2
@export var speed : float
@export var wander_time_min : float
@export var wander_time_max : float

var parent : Node2D
var vp_rect : Rect2
var wander_change_dir_time : float

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	parent = get_parent() as Node2D
	vp_rect = get_viewport_rect() 
	set_wander_change_dir_time()

func set_wander_change_dir_time() -> void:
	wander_change_dir_time = randf_range(wander_time_min, wander_time_max)

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	if (parent == null):
		return
	
	wander_change_dir_time -= delta
	
	if (wander_change_dir_time < 0):
		pick_random_direction()
		set_wander_change_dir_time()
		
	correct_dir_for_bounds()
	parent.position += direction.normalized() * speed * delta

func correct_dir_for_bounds() -> void:
	if (direction.x < 0 && parent.position.x < 0):
		direction.x = 1
	if (direction.x > 0 && parent.position.x > vp_rect.size.x):
		direction.x = -1
	if (direction.y < 0 && parent.position.y < 0):
		direction.y = 1
	if (direction.y > 0 && parent.position.y > vp_rect.size.y):
		direction.y = -1

func pick_random_direction() -> void:
	direction = Vector2.ZERO
	while (direction == Vector2.ZERO):
		direction.x = randi_range(-1, 1)
		direction.y = randi_range(-1, 1)

The exported variables in the script define a direction of travel, a speed and a minimum and maximum time before the item changes direction.

Private variables are used to store the parent node and the viewport rectangle (the bounds of the screen) and the time until we are going to change direction next.

In the _ready() function we set the parent and viewport rect variables and we call a function set_wander_change_dir_time() which randomly picks a time until the next direction change between the minimum and maximum values we’ve supplied.

In _process() we first make sure that the parent variable has been set; if it hasn’t we leave immediately. We then take delta (the time since the last frame) away from the time until the next direction change. This has the effect of counting it town to zero.

Once the time until the next direction change has dropped to zero or below, we call pick_random_direction() to pick a new direction and we also call set_wander_change_dir_time() to set the time to the next direction change.

The function pick_random_direction() is pretty simple, but it is setup to keep picking random directions until the direction isn’t (0, 0), which would cause the item to stop dead. The function will return one of eight directions: up, up right, right, down right, down, down left, left or up left.

Finally the code compares the direction we’re travelling in and how close we are to the edge of the screen in that direction and reverse the direction we’re travelling if we’re going to cross the edge and go off the screen.

Once all that’s done, we actually move the parent’s position.

Getting the Code

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

Creators – Week 9

This week we started a new game called Gem Search. Here are some of the key design points:

  1. The player can wander freely in a low-poly 3D environment with a first-person perspective
  2. There are random gems hidden around the environment
  3. Gems come in four levels
  4. Each level has several variants of gem
  5. The player holds a detector in their hands; a gauge and an indicator light show proximity to a hidden gem
  6. When very close to a gem, the user can dig to recover it and it will be placed in their inventory
  7. Each gem has a score value proportional to its level and scarcity
  8. The detector can initially only detect the lowest level gems
  9. Gems can be combined to make higher level gems
  10. Gem combination recipes are randomised each play-through
  11. When combining gems the original gems are destroyed.,
  12. If gem combination is successful, a single gem of the next level is produced
  13. Once a gem of a higher level is owned, it can be installed in the detector; the detector can then detect gems of that level
  14. The player is against the clock to get the highest possible score

Creating a New Project

We started first by creating a new project called “Gem Search”, using the 3D core template.

Asset Store

We took two free assets from the Unity Asset store as a basis for our game:

We logged into the Asset Store using the same login we use for Unity, searched for the above assets and on the web page for each one, chose “Add to my assets”.

After that, we returned to Unity and opened the Package Manager from the Window menu. In the package manager we changed the option from “Packages: In Project” to “My Assets”.

Our two packages from the Asset Store were then visible. We downloaded and imported them both. For the Starter Assets – First Person Character Controller package, there was an import warning about switching to the new input system, which we accepted.

We then spent a little time examining the assets we’d acquired; both asset packs have demo scenes in them.

Setting up our Main Scene

We opened the LowPoly Environment Pack\Demo\Demo 3 scene and, after creating a Scenes folder at the top-level of our project, saved a a copy of this scene there as “Main”.

We then did a few adjustments. First we deleted the existing Main Camera. Immediately the fog in the scene was apparent (settings on the camera was suppressing it). We looked at where the fog settings were specified (select Window | Rendering | Lighting and then look on the Environment tab of this panel) and saw how it was causing a problem for the Scene View if we were zoomed out far enough to see the entire scene. We disabled the fog in the Scene View (it will still be visible in the game) from this drop down:

We noted that the entire terrain was offset by -2.6m vertically and rotated by 100% around the vertical. It woulds suit us better if it was square to the world and at (0, 0, 0). The difficulty was the if we moved or rotated the terrain, nothing would move with it and all the trees, bushes, rocks, etc. would end up misplaced. The solution that we followed was as follows:

  1. Move the Terrain_2 GameObject out to the top-level of the hierarchy
  2. Move every other GameObject with a mesh (but not the empties they were previously children of) to be a child of the Terrain_2 GameObject
  3. Reset the transform on Terrain_2. As all other GameObjects are children, they move with it
  4. Move all the children from under Terrain_2 back under the empties that originally held them
  5. Move Terrain_2 itself back inside “Environment”

Now the environment is still organised as before, but everything is neat and square.

Raycasting and Defining the Ground

The ground isn’t level and we are going to be looking for the height of the ground at any point we want to position a gem using the something called Physics.Raycast(). A ray-cast shoots an invisible ray looking to see if it hits a collider of any sort. We can control many things about this operation including:

  1. The location that the ray starts from
  2. The direction the ray points in
  3. The maximum distance the ray can project
  4. Which Layers to look at

All GameObjects start out in the “Default” layer, but it’s often handy to put things in specific layers when we want to be specific about them. This is one of those times. With any GameObject selected, we can add a layer by clicking the Layer drop-down at the upper right of the Inspector and choosing “Add Layer”. We took the first non-assigned Layer and called it “Ground”.

We then filtered the hierarchy to isolate those GameObjects with a MeshCollider by typing “t:MeshCollider” in the box at the top of the hierarchy. This showed that terrain and the rocks were the only ones; this is exactly what we want. We used Shift to select all of them and then changed them to layer “Ground”. We then cleared the box at the top of the Hierarchy to remove the filter.

Creating a Prefab for Testing

We made a Prefabs folder at the top-level of the project, right-clicked and choose Create | Prefab. We renamed this prefab Lamp Post and combined a plane, a cylinder and a lamp, using some materials that came from the LowPoly Environment Pack, into a simple lamp-like shape 2m tall:

Creating the Item Scatterer

We set out to create something that could scatter gems across our ground surface, comprised of the terrain and the rocks.

We made a scripts folder and created as new C# script there called ItemScatterer.cs.

Editing this we added a single property to start:

public Vector3 size;

This is to represent the size of the area over which we’ll be scattering our gems. It would be nice to be able to see this area. Unity allows you to draw Gizmos which show up in the Scene View (but never in the game). We add the following code:

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireCube(transform.position, size);
    }

This is going to draw a yellow wireframe box at the position of the GameObject this component is attached to at the requested size.

We add an empty to our game, and call it “Item Scatterer”. We then attach our latest script to it. Setting the size to something like (150, 150, 150) we can see a large yellow box outlining our area. I move the area slightly to centre it closer to where the most trees are on the environment.

We progressed the scatter code a little, but I’ll cover it fully in next weeks notes.

Code Download

The code for this week’s project is on our GitHub, as always. 

Modellers – Week 9

This week, we took the mug we’d created the week before and UV unwrapped it again. We’d covered that last week too, and it’s in last week’s notes, but because we’d rushed a little we went over it again.

One the model was unwrapped, we created an image texture and manually painted it. We then used the GIMP image editing program to add an image to our texture.

Step-by-step notes can be found here:

The model file can be downloaded from here.

Explorers Week 9 – Guessing Game!

Hello Everyone,

Great to see you all on Saturday.

We made a slight departure from the games we have done in previous weeks. This weeks game was a mathematical Guessing Game.

DUCK

We only had one sprite and one large block of code. We had to create variables and figure out all the possible situations that could occur when a guess was made.

guess

Here are the notes in PDF form from this weeks session CDA-S8-Week_09-GuessNumbers.pdf

Martha

Iseult, Julie, Ruaidhrí and Eoin

Creators – Tanks

tank-1530043_640

The main purpose of this week’s session was to explore the idea of transforms by using them to make a little top-down tank that we could drive about the screen. I close a tank because:

  • It’s easy to draw a recognisable tank with a few simple shapes
  • It’s easy to see which way is forward
  • Tanks move in a very simple way (either turn or drive forward or backwards)

Origin and Axes

We don’t often use the word, but the top left of the screen (0, 0) can be referred to as the origin.

Another useful word is axes (plural of axis). We have two axes in 2D – the X-axis which normally runs horizontally left-to-right on the screen and the Y-axis that runs top-to-bottom.

Transforms

With these words in hand, let’s talk transform. P5 has three functions that allow us to make transforms. Once we call them, they effect everything that’s subsequently drawn. Here they are:

  • translate() – Move the origin by a given amount along the x and y axes.
  • rotate() – Rotate about the origin (wherever it currently is).
  • scale() – Scale along x and y axes.

Design of our Tank class

Our Tank class has five main things:

  • A constructor that takes and stores a vector for the tank’s position [this.pos]
  • A property to store the tank’s angle [this.angle]
  • A function to draw the tank [draw()]
  • A function to tell the tank to turn left/right [turn()]
  • A function to tell the tank to drive forwards/backwards [drive()]

We draw our tank as if it’s centered on (0, 0) and facing right.  We then can use translate(this.pos) to move it and rotate(this.angle) to change its angle.

For turn() all we need to do is to to change this.angle by a requested amount.

For drive(), we do a little more. We:

  • Create a copy of the current position
  • Create a vector (1, 0) which is facing right (representing the tank’s forward direction)
  • Multiply that vector by how fast we want the tank to move
  • Rotate that vector to point in the tank’s actual direction this.angle
  • Add to the copy of the current position we took initially
  • Check that new position to make sure we wouldn’t end up off-screen and, as long as we we wouldn’t, update the tanks actual position this.pos

Getting user input

We then need to get input from the user. In sketch.js we created a new function getInput() and put a call to it in our draw() function.

In getInput() we just look for the arrow keys. If we see Left or Right then we tell the tank to turn. If we see Up or Down we tell the tank to drive. We use the P5 function keyIsDown() for this.

Download

The files for this week can be found on our GitHub repository. The actual files uploaded have expanded the game a little. There are now two tanks and the Tank constructor takes two new arguments for the tank’s colours. The second tank is controlled not with the arrow keys but with WSAD and we use the P5 variables keyIsPressed and key to detect those being held down (as they’re different to the arrow keys).

Explorers Week 9 – Maths Game

Hi everyone,

You all did great work on Saturday, there was some quite complex thinking to be done to figure it out but you did great!

CDA-S5-Challenge_10-Maths game-how to

  1. the player picks a level of difficulty and the computer chooses 2 random numbers to add (subtract or multiply- whichever you choose!) together and show the numbers to the player. Fr this we needed 2 SPRITES and 4 VARIABLES called SCORE, LEVEL, NUMBER1 and NUMBER2 as well as 2 BACKDROPS. CDA-S5-Challenge_10-Maths game-ask questions1CDA-S5-Challenge_10-Maths game-ask questions
  2. The player then has to enter an answer to the equation and the computer tells them whether they are wrong or right. CDA-S5-Challenge_10-Maths game-decisions alternative end
  3. We repeated the ask/answer questions 5 or 10 times. Can you figure out where the REPEAT loop fits?
  4. We also had a second sprite who reacted positively to correct answers BROADCAST and negatively to wrong answers BROADCAST. You can use whatever sprites you like and change their look whatever way you like. One coder added a puppy as their second and had him bark whenever an answer was correct. CDA-S5-Challenge_10-Maths game-sprite 2
  5. After all the questions were asked we had the 1st Sprite SAY – Game Over! and BROADCAST Game over so that the backdrop changed and music played. There are two ways to change the backdrop- see below!CDA-S5-Challenge_10-Maths game-alt backdrop CDA-S5-Challenge_10-Maths game-backdropCDA-S8-Week_9_18-MathsGame.pdfCan you improve our game??! Can you figure out how to subtract or multiply instead of add? Can you get the computer to add three numbers together or give the user 3 level options like: easy, medium or hard? The notes for the Maths Game are here:

See you Saturday

 

Martha

Julie, Ruaidhrí and Eoin

Explorers Week 9 – Paint Program

Hi everybody,

Hope you all enjoyed finishing up our drawing program. We made good use of costumes this week to make it look like we were dipping our paint brush in each of the different colours.

We also used a variable similarly to how we used in the Piano Game we made. We made it into a slider and then code select the Pen Size we wanted.

Here are the notes in PDF. CDA-S6-Week_08-Paint

See you next week for something a little Christmas themed!

 

Martha

 

Week 9, Explorers – Pen Commands

Hello Everyone,

Thank you all for coming again this week. This week we looked at pen commands, we have not done this before in the Explorers group so it was new for everyone.

pencommand

We also created some variables which we set as sliders which again is something we had not done before with this group.sliders

And lastly we added buttons to our game. We added two separate buttons, a Start and Stop.

Here are the full Pdf version of my notes from this weeks session. cda-s6-challenge-pen-command.pdf

Martha