Godot Wild Jam 55 - zzZ
Some more facts:
- jam submission
- source code
- two wildcards implemented:
- A fork in the road: four endings
- Kabom!: missed sheep explode
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:
- simplicity: implement solid game mechanic with low innovation which makes it easy to kill my darlings.
- full stack: instead of delegating tasks/picking the ones I’m good at, everything must be concepted and implemented by myself
- own assets:
- mspaint graphic: simple, sketchy, bare minimum that just works
- ableton/dawless audio: game jams are a good opportunity to get passive feedback about my side quest ‘music production’
- focus within godot: Does it add value to the player?, avoid early optimization/polishing
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
@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.
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
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)
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")
set_parallel(true)is needed, so multiple properties can be controlled with the same tween simultaneously
- Additionally a ticking clock gets louder the more the clock approaches the end/next morning. Since the tween increases volume linearly but dB is logarithmic, the sound is barely hearable until the last few seconds, which is a nice side effect.
await tween.finishedis new in Godot 4 and replaces the
- the sunrise event gets emitted after the tween is finished/the time is over
The tacho shows a half circle on which a needle indicates the state of sleepiness:
- left: fully awake, emit event
- middle: half asleep, game starts there
- right: fully asleep, emit event
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.
The game scene holds everything together while only waiting for three different events:
- sheep got collected: increment tacho, play sheep sound, free sheep node
- sheep felt into the byss: decrement tacho, play explosion animation+sound, free sheep node
- timer is over/ sunrise emitted by sunclock: change to game over scene
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.
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.
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.
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.
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”.
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()
The intro such as the dreaming-end have a music box to add a bit coziness/sleepiness to the scene.
The instrument was made with a preset such as a few effects:
- Ableton’s Collision with preset “Glockenspiel basic”: a typical music box sound but a bit to high
- Chorus: Add more vibrato/pitch modulation to sound more natural.
- Cabinet: Dampen the sound to make it more like it was recorded in a room.
- Cut off the lows is always good if they’re not needed to avoid dirty mixes/rumbling sound.
- Make the highs a bit more quiet additionally to the cabinet.
- Compressor: Nothing special, just make the sound more louder. This may be obsolete since I export all my samples normalized.
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.
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:
- Thanks to FGKeiji’s comment to teach me this phrase!
- A preset can be described by a few coefficients along with the used algorithm.
- No ‘Digital Audio Workstation’ which is often helpful on focusing since it isn’t flooded with features and possibilities.
- 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
- You’re jamming on your keyboard “I know easier approaches!”? -> firstname.lastname@example.org