Skip to main content

Overview

This guide walks you through creating a new minigame using Nyuron’s established patterns. We’ll use food_catch as a reference.

Step 1: Create Directory Structure

Create a new folder in minigames/ with this structure:
minigames/your_game/
├── scenes/
├── scripts/
└── assets/

Step 2: Create Main Scene

Create scenes/Main.tscn with these essential nodes:
Node2D (root)
├── Background
├── Player/Character
├── CanvasLayer (UI)
│   ├── ScoreLabel
│   ├── GameOverPanel
│   ├── backButton
│   └── TouchControls (if needed)
└── intro_panel (for game instructions)

Step 3: Main Script Template

Create scripts/main.gd with this structure:
extends Node2D

# UI References
@onready var score_label: Label = $CanvasLayer/ScoreLabel
@onready var panel: Panel = $CanvasLayer/GameOverPanel
@onready var title: Label = $CanvasLayer/GameOverPanel/Title
@onready var score_lbl: Label = $CanvasLayer/GameOverPanel/Score
@onready var retry_btn: TextureButton = $CanvasLayer/GameOverPanel/Buttons/RetryButton
@onready var back_btn: TextureButton = $CanvasLayer/GameOverPanel/Buttons/BackButton
@onready var backButton: Button = $CanvasLayer/backButton

# Intro Panel
@onready var intro_panel: Control = $intro_panel
@onready var play_button: Button = $intro_panel/Button

# Game State
var is_paused := false
var last_coins_gained: int = 0
var score: int = 0

func _ready() -> void:
	# Setup UI process modes
	panel.visible = false
	retry_btn.process_mode = Node.PROCESS_MODE_ALWAYS
	back_btn.process_mode = Node.PROCESS_MODE_ALWAYS
	retry_btn.mouse_filter = Control.MOUSE_FILTER_STOP
	back_btn.mouse_filter = Control.MOUSE_FILTER_STOP
	
	# Connect buttons
	backButton.pressed.connect(_on_backButton_pressed)
	retry_btn.pressed.connect(_on_retry_pressed)
	back_btn.pressed.connect(_on_back_pressed)
	play_button.pressed.connect(_on_play_pressed)
	
	# Show intro
	intro_panel.visible = true
	backButton.visible = false
	
	# Initialize game
	_update_hud()

func _on_play_pressed() -> void:
	# Hide intro with animation
	var t := create_tween()
	t.tween_property(intro_panel, "modulate:a", 0.0, 0.4)
	await t.finished
	intro_panel.visible = false
	
	# Start game
	backButton.visible = true
	# Start your game logic here

func _update_hud() -> void:
	score_label.text = "Puntos: %d" % score

func _check_game_over() -> void:
	# Your game over condition
	if game_is_over:
		# Stop game logic
		await get_tree().create_timer(0.8).timeout
		
		# Save score
		if score > 0:
			var score_manager = get_node("/root/ScoreManager")
			if score_manager:
				score_manager.save_high_score("your_game", score)
				var coins_earned = int(score * 0.1)  # Adjust multiplier
				if coins_earned > 0:
					score_manager.add_coins(coins_earned)
					last_coins_gained = coins_earned
		
		_show_game_over()

func _show_game_over() -> void:
	title.text = "¡Fin del Juego!"
	score_lbl.text = "Puntos: %d" % score
	$CanvasLayer/GameOverPanel/CoinsEarned.text = "Monedas obtenidas: +%d" % last_coins_gained
	panel.visible = true
	backButton.visible = false

func _on_retry_pressed() -> void:
	get_tree().reload_current_scene()

func _on_back_pressed() -> void:
	# Return to portrait mode and main menu
	DisplayServer.screen_set_orientation(DisplayServer.SCREEN_PORTRAIT)
	get_tree().root.set_content_scale_size(Vector2i(270, 480))
	get_tree().change_scene_to_file("res://scenes/main_menu.tscn")

func _on_backButton_pressed() -> void:
	if not is_paused:
		pause_game()
	else:
		resume_game()

func pause_game():
	is_paused = true
	# Pause your game logic
	panel.visible = true
	title.text = "Pausa"
	score_lbl.text = "Puntos: %d" % score

func resume_game():
	is_paused = false
	# Resume your game logic
	panel.visible = false

Step 4: Set Screen Orientation

Determine if your game is portrait or landscape, then set it in the launcher function.

Portrait (270x480)

# In scenes/main_menu.gd
func _on_your_game_pressed():
	DisplayServer.screen_set_orientation(DisplayServer.SCREEN_PORTRAIT)
	get_tree().root.set_content_scale_size(Vector2i(270, 480))
	get_tree().change_scene_to_file("res://minigames/your_game/scenes/Main.tscn")

Landscape (480x270)

func _on_your_game_pressed():
	DisplayServer.screen_set_orientation(DisplayServer.SCREEN_LANDSCAPE)
	get_tree().root.set_content_scale_size(Vector2i(480, 270))
	get_tree().change_scene_to_file("res://minigames/your_game/scenes/Main.tscn")

Step 5: Integrate with ScoreManager

Register Your Game

Add your game to scripts/ScoreManager.gd.gd:
var high_scores = {
	"memorice": 0,
	"turtle_runner": 0,
	"worm_catch": 0,
	"food_catch": 0,
	"counting_animals": 0,
	"nyuron_color": 0,
	"your_game": 0  # Add your game here
}

Save Scores

In your game over logic:
var score_manager = get_node("/root/ScoreManager")
if score_manager:
	score_manager.save_high_score("your_game", final_score)
	var coins_earned = int(final_score * 0.1)
	if coins_earned > 0:
		score_manager.add_coins(coins_earned)

Step 6: Add to Main Menu

Edit scripts/main_menu.gd to add your game:
# Add button reference
@onready var your_game_button := $CanvasLayer/MenuSlidePanel/.../YourGameButton

# In _ready()
your_game_button.pressed.connect(_on_your_game_pressed)

# Add launcher function
func _on_your_game_pressed():
	DisplayServer.screen_set_orientation(DisplayServer.SCREEN_PORTRAIT)
	get_tree().root.set_content_scale_size(Vector2i(270, 480))
	get_tree().change_scene_to_file("res://minigames/your_game/scenes/Main.tscn")
Also add to the progress HUD launcher:
# In _on_progress_hud_play_game()
match game_key:
	"turtle_runner": _on_turtle_pressed()
	"your_game": _on_your_game_pressed()  # Add this line

Step 7: Create Game Over Panel

Add this to your Main.tscn’s CanvasLayer:
GameOverPanel (Panel)
├── Title (Label)
├── Score (Label)
├── CoinsEarned (Label)
└── Buttons (HBoxContainer)
    ├── RetryButton (TextureButton)
    └── BackButton (TextureButton)

Step 8: Implement Touch Controls (if needed)

For mobile-friendly controls:
@onready var touch_left = $CanvasLayer/TouchLeft
@onready var touch_right = $CanvasLayer/TouchRight

func _ready():
	touch_left.gui_input.connect(_on_touch_input.bind(-1))
	touch_right.gui_input.connect(_on_touch_input.bind(1))

func _on_touch_input(event: InputEvent, dir: float) -> void:
	if event is InputEventScreenTouch or event is InputEventMouseButton:
		if event.pressed:
			# Handle touch press
			player.move(dir)
		else:
			# Handle touch release
			player.stop()

Common Patterns

All games show an intro panel with instructions. Hide it with a fade animation:
var t := create_tween()
t.tween_property(intro_panel, "modulate:a", 0.0, 0.4)
await t.finished
intro_panel.visible = false
Flash labels to provide visual feedback:
func _flash_label(label: Label, flash_color: Color, in_time := 0.06, out_time := 0.22):
    var base := label.modulate
    var t := create_tween()
    t.tween_property(label, "modulate", flash_color, in_time)
    t.tween_property(label, "modulate", base, out_time)
Spawn floating text for score feedback:
func _spawn_floating_text(text: String, color: Color) -> void:
    if floating_text_scene == null:
        return
    var ftxt = floating_text_scene.instantiate()
    ftxt.position = spawn_position
    add_child(ftxt)
    ftxt.show_text(text, color)
Implement pause that stops timers, animations, and game logic:
func pause_game():
    is_paused = true
    $SpawnTimer.paused = true
    # Set entities to PROCESS_MODE_DISABLED
    # Set animation speed_scale to 0.0
    panel.visible = true
    title.text = "Pausa"

Testing Checklist

1

Intro displays correctly

Verify intro panel shows with instructions and play button works
2

Gameplay works

Test core mechanics and scoring
3

Pause functions

Press back button to pause, verify game stops, resume works
4

Game over triggers

Ensure game over panel appears with correct score and coins
5

Score saves

Verify high score and coins are saved to ScoreManager
6

Navigation works

Test retry button and back to main menu
7

Mobile controls

If applicable, test touch input on device

Next Steps

UI Components

Learn about reusable UI patterns

Mobile Adaptation

Optimize for mobile devices

Build docs developers (and LLMs) love