Dana Vrajitoru
I254/C297 2D Games Programming

I254/C297 Lab 11 / Homework 11

Date: Wednesday, Wednesday, April 3, 2024.
Due date: Wednesday, April 17, 2024.

In this lab and homework, we will implement a tower defense game featuring object placement and determined path movement.

Lab Part

Ex. 1. We will create a tower defense game that looks like this:

a. Setup

Create a 2D project called Ship Defense with a root node and a script attached to it called main.gd. Change the size of the window to

Download the following images and add them to the project:

Add the ground image to the scene as a TextureRect. Scale it up so that it covers the whole viewport vertically. Align its top left corner with the 0x0 position. An empty area should be left on the right for the interface items.

Add one of the turrets as a Sprite2D in the top left corner. Add a script to it called Turret.gd, then save it as a scene called turret.tscn. Then move it to the empty area on the right. This will be our choice of defense weapon to begin with. Create two copies of the scene, change the texture in the sprite to the other two turrets, and also move them to the right.

Add a label called ShowPoints with the text "Points: 0" and place it above the turrets in the interface area. Also add a button with the text "New Game" and place it at the bottom on the right.

Create a ColorRect object in a highlight color and place it over the top turret and call it. Highlight Set the z index of the rectangle to 0 in Ordering and the z index of the 3 turrets to 1.

Create a CharacterBody2D object in the top left corner called Enemy with the invader image added as a Sprite2D as a child. Add a capsule collision shape to it. Set the collider's rotation to 90, then fit it over the sprite. Add a script to the character body object. Then save the branch as a scene. Place the object a little way up from the beginning of the blue path, so that it's not visible on the screen when running the game. Remove everything except for the call to move_and_slide from its _physics_process function.

We also need something for the bullets. In the top left corner, create a CharacterBody2D object called Bullet. Add as a child a small ColorRect object in the color you want and a rectangular collision shape. Add a script to it and remove everything but the call to move_and_slide from its _physics_process function. Save it as a scene and then delete it from the stage. We will instantiate it in the code.

b. Control Path Movement

The invaders need to move on a controlled path that follows the blue line. We need to define this path first.

Take a look at the current position of the invader object. In its script, add a static variable called initial_pos and assign to it a Vector2i(x, y) where in place of x and y you write in the current coordinates of the object. Then add a function _ready where you assign the value of this variable to the position of the object.

Now, we want to define the coordinates of the path. Declare a static class variable called path and assign to it an empty array.

Next, we want to add points on the path to this array. In the scene, move the enemy to the beginning of the path. Back in the script, add the coordinates of the object to the array inside extra brackets with a comma. The array should now look like this: [[208, -21]] or whatever the coordinates are for you. Then move the enemy somewhere in the middle of the vertical path and add those coordinates to the array inside another set of brackets, separated by a comma from the first, like [[208, -20], [208, 300]]. Continue adding points like this to define the whole path towards the exit. Make sure to capture the points where the direction of movement changes. The last position should be above the board where the object is not visible anymore.

We are now ready to define its movement on the path. We need a non-static class variable called focus and another one called target. The first one will be the index in the array of the next target, and the target will contain the position as a vector. Add the following function:

func set_target(n):
	if n < len(path):
		focus = n
		target = Vector2i(path[n][0], path[n][1])
	else:
		queue_free()

In the function _ready, call this function with 0 for the parameter.

Change the speed of the object to 200. In the function _physics_process, add the following code before the call to move_and_slide:

if position.distance_to(target) < 3:
	set_target(focus + 1)
var dir = global_position.direction_to(target)
velocity = dir * SPEED

Add one more class variable called friendly and set it to false in the function _ready.

c. Bullets

Let's create the functionality of the bullets. In preparation, add a static class variable to the class main (or root if you called it that) called points and initialize it to 0 in the function _ready. Add a label that shows its value, then a function called inc_points that increases the value, then updates the displayed value.

Then in the class Bullet, add a reference to root node as

@onready var root = $".."

Add a class variable target and add a move towards the target like in the code above in the function _physics_process. Initialize the target as a Vector2i(0, 0) in the declaration. Also add a class variable called friendly and set its value to true in the function ready.

Add a function check_target_hit(item) to this class. Here, check if the friendly attribute of the item is false, and if it is, queue the item to be freed, and call the function inc_points from the root variable. After the conditional testing the friendly condition, add a call to queue_free without a parameter to delete the bullet itself as well when it collides with anything.

Make the speed of the bullets higher, like 500. Then add the following code to check for collisions in the function _physics_process before the call to move_and_slide:

var collision_info = move_and_collide(velocity * delta, true)
if collision_info:
	check_target_hit(collision_info.get_collider())

We also want the bullets to delete themselves when they get out of the viewport. In _physics_process, after the call to move_and_slide, check if the coordinates are less than 0 and queue the bullet to be freed. Also, check if the coordinates are larger than the width and height of the viewport, and also do the same.

c. Turrets

For the turrets, we need to be able to place them in the scene, and once they are placed, they need to shoot bullets at the incoming enemies. For that, let's edit their scene and add and Area2D as parent.

Let's start by making the highlight object switch places when we click on a turret.

Edit the turret scene and add a child to the sprite of type Area2D. Add another child to the turret scene of type CollisionShape2D with a circle as the shape. This will be the area of influence where the turret will detect enemies. Make it a lot larger than the object.

Then make the turrets on the right side of the image with editable children and then replace the collider shape for each of them with a new one in a size and shape that fits them better. These 3 objects don't need to detect enemies, only mouse clicks. You can change the shape of the collider to a rectangle for the second and third turrets.

Add a variable in the Turret class called active. In the function _ready, check if the name is "Turret1", 2, or 3, and if that's the case, make active false, otherwise make it true.

Then add a reference to the highlight object in the class and to the root object:

@onready var highlight = $"../Highlight"
@onready var root = $".."

In the Turret scene, connect the signal input_event from the Area2D object to the class Turret/tt>. In the function created by this, add the following code:

if not active:
	if event is InputEventMouseButton and \
	event.button_index == MOUSE_BUTTON_LEFT and \
	event.is_pressed():
		highlight.position.x = position.x - highlight.size.x/2
		highlight.position.y = position.y - highlight.size.y/2

In the root script, add a variable called tour_selection, initialized as 1. Then store references to the turret scene (not objects), to the images for the 3 turrets, and to the enemy scene like

const TURRET = preload("res://turret.tscn")
const TURRET_1 = preload("res://turret1.png")
const TURRET_2 = preload("res://turret2.png")
const TURRET_3 = preload("res://turret3.png")
const ENEMY = preload("res://enemy.tscn")

In the class Turret, in the function for the input signal, after changing the position of the highlight, assign to tour_selection the right value with

root.tour_selection = int(name.replace("Turret", ""))

That way, the root will know which turret we want to place when we click in the scene.

Next, we'd like the turrets to detect when objects enter the area and start shooting at them. Again, in the scene Turret, connect both the signals body_entered and body_exited to functions in the Turret class. Basically, when an enemy enters the area, the turret will add it to an aiming list. When it exits, it will remove it from the list. For now, print a small message in each of these functions to make sure they work.

To test this, add a 4th turret to the scene somewhere close to the path of the enemies. Run the program to see if the messages are printed when the enemies goes through the area.

Declare a variable in the class called aim_list and initialize it as an empty pair of brackets (an array). In the function entering the area, append the parameter to the list if the turret is active and the body is not friendly. Then in the function exiting the area, erase the body from the list under the same conditions.

Next, we want to place the turrets in the scene when the mouse is clicked. If we don't want to add another area object over the scene, we can treat this as an unhandled input. Add the following function to the root script:

func _unhandled_input(event):
	if event is InputEventMouseButton and \
	event.button_index == MOUSE_BUTTON_LEFT and \
	event.is_pressed() and event.position.x <= 885:
		var turret = TURRET.instantiate()
		add_child(turret)
		turret.position = event.position
		if tour_selection == 2:
			turret.texture = TURRET_2
		elif tour_selection == 3:
			turret.texture = TURRET_3

d. Root

Now to put it all together, we need to add some things to the main (or root) script. The game will be working on the following finite state machine:

To implement it, let's add variables to the class called state, wave, level, as well as a constant for the maximum number of waves. Add some labels to display the wave number and the level number. Add some counts for the number of enemies spawned in the wave, as well as for the maximum number of enemies to spawn in a wave. Add 4 constants for the 4 state names.

We'll also need a couple of timing variables. Declare variables enemy_wait and wave_wait, as well as maximum values for both. Initialize the first one as 1.0 and the second as 3.0 (you can calibrate them later).

Add the following function to the root class to spawn an enemy:

func spawn_enemy(delta):
	enemy_wait -= delta
	if enemy_wait <= 0 and count_enemies < max_enemies:
		var ship = ENEMY.instantiate()
		add_child(ship)
		count_enemies += 1
		enemy_wait = max_enemy_wait

In the function _process, add a conditional where you test the value of the state being equal to waven or whatever you called the first state constant. In that case, call spawn_enemy(delta), then if the number of enemies is greater than or equal to the maximum for the wave, make the state become interm or the name you gave to the second state.

Homework Part

Ex. 2. a. Main FSM

Complete the FSM in the root script.

b. Turrets Choice

Figure out how many points you want the player to have to be able to place a turret of type 2 and 3, and how many turrets on the board you want to limit the player to, maybe also based on the number of points. Implement these changes in the turret and main classes.

b. Shooting Bullets

Implement the functionality in the Turrets class where the active turrets spawn a bullet at regular intervals. The bullets should spawn somewhere between the turret and the target. Use the aim_list array to choose a target to aim at. After creating the bullet, set its target to be the position of the chosen enemy object maybe plus something (its size over 2?).

Note that some of the enemies in the aim_list may have already been freed by colliding with bullets. The test is_instance_valid(aim_list[i]) will tell you if the element at index i in this array is still in the game. Only aim the bullets at existing enemies.

b. Waves and Levels

Calibrate the number of enemies in each wave, the speed of the enemies based on the wave and level number, and the delay between enemies based on all of this.

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. Create a zip file containing the project folder (the screenshot can be inside) and the executable folder. Submit both of them to Canvas, Assignments - Homework 11.