Godot Engine - Procedural Generation - 2.5D

Built a 10-round procedurally generated hidden-object game in 50 hours as sole programmer of a 2-person team. Packed up to 90 non-overlapping sprites per round with a greedy AABB rejection sampler (80 attempts/sprite). Reached 254th in Narrative by creating an event-based dialogue handler for in-dialogue position/animation editing.

Notable Features

  • Defined Clown Sprite position in 3D space by syncing the Dialogic plugin's signals with game state

  • Created a Round class and instantiated for each round, specifying round architecture, difficulty, and targets to speedline the level design process

extends Resource

class_name WaldoRoundResource

@export_category('Round Settings')
@export var description: String
@export var scale: bool
@export var rotate: bool
@export var mirror: bool
@export var ends_early: bool # lies and adds 1 to waldo_count on display
@export var can_spawn_edge: bool


@export_category('Waldo Sprite')
@export var waldo_sprite: PackedScene
@export var waldo_count: int # very much depends on round

@export_category('Other Sprites')
@export var sprites: Array[PackedScene]
@export var num_normal_sprites:

Game Manager code snippet (dialogue and state)
extends Node3D

@export var tlines: Array[DialogicTimeline]
@export var clown_positions: Dictionary[String, Marker3D]
@export var clown_sprites: Dictionary[String, Sprite3D]

@onready var waldo_ui: Control = $UI/WaldoUI
@onready var black_out: ColorRect = $UI/BlackOut
@onready var music: AudioStreamPlayer = $Music
@onready var start_end_screen: VBoxContainer = $UI/StartEndScreen
@onready var time_label: Label = $UI/StartEndScreen/TimeLabel
@onready var blood: AudioStreamPlayer = $Blood
@onready var shine: AudioStreamPlayer = $Shine

var curr_round := 0

var time_elapsed: float = 0.0
var is_running: bool = false

var curr_clown_sprite: Sprite3D

func _ready():
	randomize()
	
	curr_round = 0
	time_elapsed = 0
	
	Dialogic.timeline_ended.connect(_on_timeline_ended)
	Dialogic.signal_event.connect(_on_dialogic_timeline_signal)
	
	waldo_ui.hide()
	black_out.hide()
	
	start_end_screen.show_start()
	start_end_screen.show()

func start_game():
	Dialogic.start(tlines[curr_round])
	is_running = true
	
	if not music.playing:
		music.play()

func _process(delta: float) -> void:
	if is_running: # stopwatch
		time_elapsed += delta

func _on_timeline_ended():
	if curr_clown_sprite != null:
		curr_clown_sprite.hide()
	
	if curr_round >= 10:
		is_running = false
		display_end_screen()
		return
		
	if not waldo_ui.visible:
		waldo_ui.start_round(curr_round)
		if not music.playing and curr_round < 4:
			music.play()

func _on_waldo_ui_waldo_done() -> void:
	curr_round += 1
	Dialogic.start(tlines[curr_round])
	
func _on_finger_signal():
	black_out.show()
	music.stream_paused = true
	
	if curr_round < 5:
		await get_tree().create_timer(1.0).timeout
		shine.play()
		await get_tree().create_timer(2.0).timeout
		blood.play()
		await get_tree().create_timer(2.0).timeout
	else:
		await get_tree().create_timer(2.5).timeout
		blood.play()
		await get_tree().create_timer(2.5).timeout
	
	black_out.hide()
	music.stream_paused = false
	
func _on_head_signal():
	black_out.show()
	await get_tree().create_timer(1.0).timeout
	shine.play()
	await get_tree().create_timer(2.0).timeout
	blood.play()
	await get_tree().create_timer(2.0).timeout
	black_out.hide()
	
func _on_dialogic_timeline_signal(s: String):
	if s == 'finger':
		_on_finger_signal()
	elif s == "music":
		if not music.playing:
			music.play()
	elif s == "hide_clown":
		if curr_clown_sprite:
			curr_clown_sprite.hide()
	elif s == 'head':
		_on_head_signal()
	else:
		handle_dialogic_clown(s)

func handle_dialogic_clown(s: String):
	# animate clown based on specification in string
	# e.g. happy::close
	var words = s.split('::')
	update_clown(words[0], words[1])
	
func update_clown(clown_sprite: String, clown_pos: String):
	if curr_clown_sprite:
		curr_clown_sprite.hide()
	
	if clown_sprites.has(clown_sprite):
		curr_clown_sprite = clown_sprites[clown_sprite]
		curr_clown_sprite.show()
	
	if clown_positions.has(clown_pos):
		curr_clown_sprite.global_position.x = clown_positions[clown_pos].global_position.x
		curr_clown_sprite.global_position.z = clown_positions[clown_pos].global_position.z

func display_end_screen():
	black_out.show()
	start_end_screen.show_end()
	start_end_screen.show()
	
	var minutes: int = int(time_elapsed / 60)
	var seconds: int = int(time_elapsed) % 60
	var milliseconds: int = int((time_elapsed - int(time_elapsed)) * 100)
	time_label.text = "%02d:%02d:%02d" % [minutes, seconds, milliseconds]

func _on_start_end_screen_start_game(is_end: bool) -> void:
	if (is_end):
		_ready()
	else:
		start_game()

Fun fact! I created the 3D room and some of the textures in Microsoft Paint. Both the red walls and the ceiling also integrated small visual shaders. I also made the SFX and music in Reaper :)


I plan to revisit the game and balance the level design. I also received feedback on whether to introduce timers in levels or win/loss routes that I'm considering.