Lemur Logic Adventure
Technical details
Engine: Godot
Version: 4.3
Genre: Turn-based tower defence
Team of 4 people:
- me as a programmer, audio designer, game designer,
- my friend as a 2D artist,
- newly met 2 programmers.
Assets: Created by my team member, from itch.io and audio from Zapsplat
Itch.io: Link
Code: Link
Description
The game was created for HackYeah 2024, during 24h. We have created a game for a theme, the family of lemurs, where we chose to go with education sub-theme. We wanted to teach the basics of logic and logic gates through gameplay. Basics are taught through an interactive quiz, while logic gates are taught through a tower defence game. My responsibilities focused on entire UI, programming the tower defence, entire audio expect the quiz sounds. The idea was to include logic gates where player would place towers on different squares connected to logic gates to gain some buffs. The game is in Polish language as it was required by the rules. The game was created in Godot Engine 4.3.
Tower
First thing I programmed was the tower and it's behaviour. The tower has a range, which is shown by the circle around it. When the enemy is in the range, the tower is shooting at it. The tower has a specific attack speed, which can be changed in the editor. Also it focuses on the first enemy that enters the range, changing the target only when the current one is out of range or dead. In the _process function there are 3 states: no enemies in range, current_enemy is not set and attack. The code for this is shown below.
extends Node2D
class_name Tower
@export var animated_sprite: AnimatedSprite2D
var enemy_in_range_list: Array[Enemy] = []
var current_enemy: Enemy = null
var timer = Timer.new()
func _ready():
(...)
func _process(delta):
if current_enemy == null && len(enemy_in_range_list) == 0:
return
elif current_enemy != null && len(enemy_in_range_list) == 0:
animated_sprite.stop()
timer.stop()
current_enemy = null
elif current_enemy != null:
if do_rotation:
animated_sprite.look_at(current_enemy.global_position)
animated_sprite.rotation_degrees -= 90
if timer.is_stopped():
timer.start()
else:
_assign_curr_enemy()
animated_sprite.play()
# Enemy entered the range
func _on_range_area_entered(area: Node2D):
if is_instance_of(area, Enemy):
enemy_in_range_list.append(area)
# Enemy exited the range
func _on_range_area_exited(area: Node2D):
if area in enemy_in_range_list:
enemy_in_range_list.erase(area)
# Remove enemy on death
func _on_enemy_died(enemy: Enemy):
if enemy in enemy_in_range_list:
enemy_in_range_list.erase(enemy)
# Get the first enemy in the range
func _assign_curr_enemy():
current_enemy = enemy_in_range_list[0]
func _attack():
(...)
Tower placement UI
Here is the script responsible for tower placement UI. The script is attached to the button that is responsible for each of the tower placement. When the button is clicked and holded, the temporary tower is instantiated and follows the mouse. Depending on where the mouse is, the tower is shown in red if it is on the enemy path or in white if it is not. When the left mouse button is released, the tower is placed on the map. The tower can be placed only on the place that is not path, neither is UI. The code for this is shown below.
extends Panel
# Tower scene associated with button
@export var tower_scene: PackedScene
@export var tower_type: String
var curr_tile
func _on_gui_input(event):
# Instantiate temporary tower for placing visualisation
var tempTower = tower_scene.instantiate()
tempTower.set_z_index(10)
var root_scene = get_tree().get_root()
# Get the tile at tilemap for the current tower position
var mapPath: TileMapLayer = root_scene.get_child(root_scene.get_child_count() - 1).get_node("Path")
var mapPathRect = mapPath.get_used_rect()
var tile = mapPath.local_to_map(get_global_mouse_position())
# Check if the tile is on the enemy walk path - as our designer created 3 tilemaps for each of the parts of game it had to be checked whether the tile is on "Path"
var is_on_path = (mapPathRect.position.x <= tile.x && tile.x <= mapPathRect.size.x) && (mapPathRect.position.y <= tile.y && tile.y <= mapPathRect.position.y + mapPathRect.size.y - 1)
# On left mouse button click add the temporary tower to the scene
if event is InputEventMouseButton && event.button_mask == 1:
add_child(tempTower)
tempTower.process_mode = Node.PROCESS_MODE_DISABLED
# On left mouse button hold and mouse movement show the temporary tower and check if it is on the path
elif event is InputEventMouseMotion && event.button_mask == 1:
if get_child_count() > 2:
get_child(2).global_position = event.global_position
get_child(2).get_node("Area").show()
if is_on_path:
get_child(2).get_node("Area").modulate = Color(255, 0, 0, 0.2)
else:
get_child(2).get_node("Area").modulate = Color(255, 255, 255, 0.2)
# On left mouse button release check if the tower can be placed and place it
elif event is InputEventMouseButton && event.button_mask == 0:
if get_child_count() > 2:
get_child(2).queue_free()
if event.global_position.x < 900:
if !is_on_path:
var path = root_scene.get_child(root_scene.get_child_count() - 1).get_node("Towers")
path.add_child(tempTower)
tempTower.global_position = event.global_position
tempTower.get_node("Area").hide()
AudioPlayer.play_sfx("build_tower")
else:
if get_child_count() > 2:
get_child(2).queue_free()
Audio System extended
Here I have extended the audio system functionality from The Blessed Ones project, I have added random change of pitch for sound effects - here it was for attacks. Then I have added music change based on the game state - menu, pause, wave, prepare phase, which was achieved by signals - the Observer pattern. The code for this is shown below.
var pitch_array = [0.9, 1.0, 1.1]
# Function to randomly change the pitch of the sound effect
func play_pitch_sfx(sfx_name: String):
if sfx.has(sfx_name):
var asp = AudioStreamPlayer.new()
asp.stream = sfx[sfx_name]
asp.name = "SFX"
var pitch = pitch_array.pick_random()
print(pitch)
asp.pitch_scale = pitch
asp = add_to_bus(asp, sfx_name)
add_child(asp)
asp.play()
await asp.finished
asp.queue_free()
# Function to manage signal of enter / exit menu
func _on_is_menu(value: bool):
if (value):
play_music("menu_music")
else:
curr_music.queue_free()
# Function to manage signal of enter / exit pause menu
func _on_is_paused(value: bool):
if (value):
play_music("pause_music")
else:
curr_music.queue_free()
# Function to manage signal of enter / exit penguin wave
func _on_is_wave(value: bool):
if (value):
play_music("wave_music")
elif (!value && curr_music != null):
curr_music.queue_free()
# Function to manage signal of enter / exit prepare phase
func _on_is_prepare(value: bool):
if (value):
play_music("prepare_music")
elif (!value && curr_music != null):
curr_music.queue_free()
Key takeouts
Another 24h game jam, but this time the theme did not fit me so well, but I think that we have managed to create a game that is fun and educational. Also it has a lot of potential and features, also building on the code from previous projects. Extension of audio system, UI - these things are polished and extended 3rd time looking at previous projects. First time I have tried my hand at tower defence game, which was a great experience and thought me a lot about game design and programming. I have extended my knowledge regarding Observer pattern and signals in Godot Engine, which right now is pretty handy for me. Also management of inputs and masks regarding Mouse events was a new thing for me, which I have learned and used in this project. I have previously managed GUI events but not that much of mouse events, so it was a great experience for me. I am looking forward to use this knowledge in future projects. Task planning and estimation or needs regarding the project also increased my knowledge and experience in project management.