< up >
2023-03-25

Godot Wild Jam 55 - zzZ

I did my first solo submission at the godot wild jam 55 about the theme dreams and reached rank 29! It’s called zzZ where the player catches sheep to catch some z’s1.

Some more facts:

Contents

Idea

Johannes had the idea of falling sheeps that must get catched but “gifted” it to me, since he couldn’t do it himself due to lack of time. The core mechanic was quite simple, but still fun to play for a few minutes. I was not hyped about it and that was actually my jam driver:

Mechanics

Sheep spawner

Sheep are the essential entity of the game. They fall from the sky into the abyss unless the player catches them with the basket.

The spawner is a scene on its own deciding where and when to generate sheep such as registering the abyss event, that a sheep emits once it fell into the abyss. To know where the abyss is, the spawner awaits the position of it. Sheep get spawned once a second with random size, initial speed and position For choosing a random position, the spawner awaits another two points aka Marker2D:

@export var left_point:Marker2D
@export var right_point:Marker2D

func _on_timer_timeout():
	var sheep:Sheep = sheep_scene.instantiate()
	# ...some more initalization...
	sheep.global_position.x = randf_range(
		left_point.global_position.x, 
		right_point.global_position.x
		)
	sheep.global_position.y = left_point.position.y

Those points are visually placed above the game scene:

About every 10th sheep is special having one of the three complement colors cyan, magenta or yellow such as a smaller random size and a higher random initial speed. They’re worth double on collecting and triple on not collecting them.

Basket

The basket emits an event for every sheep it collides with. Godot 4 makes it easy to automate collision shape generation from a sprite with complex shape:

The basket’s x-axis can be controlled via mouse which is hidden (via game scene’s Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)):

extends Area2D

signal item_collected(body:Node2D)

func _input(event) -> void:
	if event is InputEventMouseMotion:
		global_position.x = event.position.x

func _on_body_entered(body) -> void:
	emit_signal("item_collected", body)

Sunclock

The sunclock is the timer of the game, visually similar to the one in minecraft. It consists of two sprite layers. The bottom “clock”-layer gets rotated one revolution in a specified amount of time.

This can be implemented via tween:

extends Node2D

signal sunrise()

@export var duration:int = 30

func _ready():
	var tween = create_tween().set_parallel(true)
	
	tween.tween_property($Clock, "rotation_degrees", 180, duration)
	tween.tween_property($ClockSound, "volume_db", -10, duration)
	await tween.finished
	emit_signal("sunrise")

Tacho (speedometer)

The tacho shows a half circle on which a needle indicates the state of sleepiness:

There are three animated faces that should transport the meaning of the tacho:

The score is spread evenly across the 180° range. The code is quite messy but I don’t want to hide anything:

extends Node2D

signal fallen_asleep()
signal woken_up()

@export var total:int = 30
@onready var score:int = 0

var event_emitted:bool = false

func _ready():
	$Emoji.play("half_sleeping")

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	$Needle.global_rotation_degrees = clamp(180/total*score + 90.0, 0, 180)
	
	var animation = "half_sleeping"
	if score >= total/4:
		animation = "asleep"
	elif score <= -total/4:
		animation = "awake"
	
	if $Emoji.animation != animation:
		$Emoji.play(animation)

func inc(val=1)->void:
	score = min(score+val, total)
	if score == total and !event_emitted:
		emit_signal("fallen_asleep")
		event_emitted = true

func dec(val=1)->void:
	score = max(score-val, -total)
	if score == total and !event_emitted:
		emit_signal("woken_up")
		event_emitted = true

event_emitted debounces the event, so it gets only emitted once even if the player collectes more sheeps while the event wasn’t consumed quickly enough by the game scene.

Game loop

The game scene holds everything together while only waiting for three different events:

All three events check if an end has been reached and if so, set the globals correctly, make the mouse visible again and change to the game over scene. This is where the wildcard A fork in the road is implemented. Additionally the timeout event can decide between two ends depending if the player is more awake (score is negative, needle is on the left) or more sleepy (score is positive, needle is on the right or in the middle).

Retrospectively looking at the code: There is too much responsibility shared between game<->tacho and game<->game over. If I’d invest more time into the game, I’d refactor this inconsistency. The game itself shouldn’t need to provide any content to game over. Also the game shouldn’t need to know how to use the tacho’s score for deciding if and which end has been reached.

Intro

The intro is basically an animation player blending in all the logos synced with the music box. In order to make the synchronization more reliable, the three main parts of the intro sound are split and played after each other. In theory, if the CPU is too slow, the break between the notes are higher, but that is totally fine.

Godot - instantiated scenes

In some cases it might be handy to change to a scene with some context. E.g. tell the game over screen which end has been reached. Sadly I couldn’t find an idiomatic solution to create such instantiated scenes. If you instantiate a scene with parameters, you can’t change to it. It is only possible to add it as a child or use a hacky alternative.

I added a plain auto load script Globals.gd and stored the state there. I dislike this approach while everyone could read/write the state.

Audio

Since my alter ego loves to produce and play around with audio, I couldn’t resist to invest some time into music and sound effect production.

Background music

Describing the creative process of making music isn’t that reasonable than e.g. programming where the problem-solution relation can be argued quite rational. I’ll try to describe it either way.

The first auditive association of dreamy game music for me was the Super Mario 64 Dire Dire Docks theme. Old consoles often use FM synthesis to generate complex instruments with a low memory footprint2. I decided to just play around with some c major chords (because this scale is the easiest one to play), with my dawless3 setup consisting of a MIDI Keyboard connected to the Korg FM2 which gets recorded by my digital recorder. The FM8 comes close to the Nintendo64 music engine while it also implements fm synthesis. I pressed record and played around with some chords and presets. I layered two chord progression takes but unfortunately haven’t saved the project file… So the first layer is the first and loud chord progression and the second layer is the more quite progression that can be heard in between to given some kind of “response” if the first layer gave a “call”.

Sheep

I sliced a few different sheep sounds from milkotz’s recording and pre loaded them, so playing them back has a low latency:

var sheeps = [
	preload("res://milkotz_sheep1.mp3"),
	preload("res://milkotz_sheep2.mp3"),
	preload("res://milkotz_sheep3.mp3"),
	preload("res://milkotz_sheep4.mp3"),
]

Since there are small and big sheep, why not pitching the sample in relation to the size? Quickly a formula pitch = 1 - (size-0.9) was crafted with educated guessing such as trial-and-error. The pitch’s default is set to 1. Basically it maps the range of the sheep scale factor within the range [0.1,1]4 onto the pitch range [0.9, 1.8] in reverse, because smaller sheep have higher pitches and vice versa. This means the smallest sheep with a size of 0.1 has a pitch of 1-(0.1-0.9) = 1.8 where the biggest sheep has a pitch of 1-(1-0.9) = 0.95.

To have limitless parallel sheep sounds, an audio player get initialized for each collected sheep:

func _on_basket_item_collected(body:Sheep):
	var sheep_player = AudioStreamPlayer.new()
	sheep_player.stream = sheeps[randi_range(0,len(sheeps)-1)]
	sheep_player.set_bus("SheepFX")
	sheep_player.pitch_scale -= (body.size-0.9)
	add_child(sheep_player)
	sheep_player.play()

Music box - intro + dreaming-end sound

The intro such as the dreaming-end have a music box to add a bit coziness/sleepiness to the scene.

Instrument

The instrument was made with a preset such as a few effects:

Notes

Although the whole-tone-scale is usually used for dream sequences, I wanted something more naive and chose the c major scale again. I tried to mimic the “ascending stairs” music box melodies often have and added a little offset to some notes so it sounds more analog.

Intro notes:

Dreaming-end notes:

Summary

The initial driver remained throughout the week and I learned some new things about Godot 4. Each mechanic could be outsourced to its own independent scene and connected to each other via events, eliminating a classical game loop.

Although everything works pretty bug free, the game scene holds more state than it should making it’s role inconsistent in relation to tacho and game over. I’d refactor this one if I’d invest more time into this game.

Keeping the graphics simple and just sketch them via mspaint within only a few minuted helped to focus on the overall progress in contrast to the other jams.

The submission took the 29th place:


  1. Thanks to FGKeiji’s comment to teach me this phrase!
  2. A preset can be described by a few coefficients along with the used algorithm.
  3. No ‘Digital Audio Workstation’ which is often helpful on focusing since it isn’t flooded with features and possibilities.
  4. The minimum and maximum of all random sizes define the absolute range of all possible sizes: https://github.com/Glow-Project/gwj-55/blob/main/sheep.gd#L25
  5. You’re jamming on your keyboard “I know easier approaches!”? -> teachme@evilcookie.de