Tweens, Particles, Bombs & Autoloads
Add juice & build the complete game loop
Last lecture: we sliced fruit and detected collisions
Today: animations, particles, bombs, and scene management
From playable but flat → fully juiced game with game-over state
match statementToday: we turn the left into the right.
Before we code, let's see what's possible
.new()You already know .instantiate()
Loads a saved .tscn scene
NEW: .new() creates a node purely from code
No .tscn file needed
Perfect for temporary effects: particles, labels, lines, sounds
When to use:
.instantiate() = complex saved scenes (Fruit, Bomb).new() = simple dynamic objects (Label, AudioStreamPlayer)var audio = AudioStreamPlayer.new()
audio.stream = load("res://assets/sounds/slash.wav")
audio.volume_db = -5
add_child(audio)
audio.play()
Steps:
.new()stream, volume_dbadd_child().play()Same pattern for Label, Line2D, GPUParticles2D, Sprite2D…
All created with .new() because they're temporary visual effects
dynamic_nodes_demo.tscnClick anywhere on screen
Colored circles spawn where you click
Each shrinks and fades, then vanishes
That shrink-fade effect? Created with tweens. (More on that in 3 slides.)
Problem: Score resets when changing scenes
Each scene is a fresh node tree — all variables reset
Solution: Autoload — a persistent singleton
Project Settings → Autoload tab → add script
That node lives forever, persists across all scene changes
Accessed by name: Global.current_score
global.gdGlobal.current_scoreLike a global variable, but better: it's a real node with methods
current_scorereset_score()set_score(score)get_score()Both autoloads: set up once, used everywhere
The autoload (Global) never unloads, even as scenes change below it
autoload_demo.tscnPress the button to increment a counter
Counter increments on-screen
Now press: "Change Scene" button
The counter persists in the new scene!
That's an autoload preserving state across scene changes.
awaitRecap: We used await audio.finished in Lecture 13
What does await actually do?
In many languages, a "sleep" or "wait" command freezes the entire program.
In Godot, await is asynchronous.
CRITICAL RULE
await pauses THIS SPECIFIC FUNCTION until a signal fires.
The rest of the game (drawing, physics, `_process`) keeps running perfectly!
await_demo.tscnThe function was paused for 2 seconds.
But the Apple sprite kept spinning in _process()!
awaitSyntax: await [object].[signal]
# 1. Wait for a sound to finish
await audio.finished
# 2. Wait for an animation/tween to finish
await tween.finished
# 3. Wait for an exact amount of time (super useful!)
await get_tree().create_timer(2.5).timeout
We will use ALL of these today to choreograph the bomb sequence.
Bringing animations to life
Animate any property from A → B over time
Manual: if time < 0.3: scale += (1/30) * delta
With tweens: create_tween().tween_property(fruit, "scale", Vector2(1,1), 0.3)
Tweens handle the math. You describe the endpoint.
var tween = create_tween()
tween.tween_property(node, "scale", Vector2(1, 1), 0.3)
.from(Vector2(0.1, 0.1))
.set_trans(Tween.TRANS_BOUNCE)
.set_ease(Tween.EASE_OUT)
Key methods:
tween_property(node, prop, target, duration) — animate a property.from(start_value) — override starting value.set_trans(curve) — transition curve (SINE, BOUNCE, LINEAR, CUBIC, etc.).set_ease(direction) — IN, OUT, or IN_OUTCurves control feel. Choose wisely:
Sequential:
tween.tween_property(
label, "scale",
Vector2(1, 1), 0.2)
tween.tween_property(
label, "position",
pos - Vector2(0, 50), 0.5)
Scale, then move
Parallel:
var tween = create_tween()
tween.set_parallel(true)
tween.tween_property(
label, "scale",
Vector2(1, 1), 0.2)
tween.tween_property(
label, "position",
pos - Vector2(0, 50), 0.5)
Scale and move together
Or use .parallel() method on individual properties
Tweens aren't just for nodes and sprites!
You can tween any variable on any object.
# Example: Animate a score variable from 0 to 1000
var tween = create_tween()
tween.tween_property(self, "current_score", 1000, 2.0)
If current_score has a setter function that updates a label, you instantly get a smooth rolling score counter!
tween_demo.tscnClick the buttons to see exactly how transition curves and parallel logic behave in real-time.
In game.gd, spawn_fruit() function:
add_child(fruit)
# NEW: Spawn animation
var tween = create_tween()
tween.tween_property(fruit, "scale", Vector2(1, 1), 0.3)\
.from(Vector2(0.1, 0.1))\
.set_trans(Tween.TRANS_BOUNCE)\
.set_ease(Tween.EASE_OUT)
Create tween, animate scale from 0.1 to 1.0 over 0.3 seconds
BOUNCE curve with OUT easing = playful pop-in
Test: Run game, fruits pop in with a bounce
In slash_detector.gd, create_fruit_splash():
func create_fruit_splash(pos: Vector2):
var splash = Sprite2D.new()
splash.texture = preload("res://assets/images/splash_01.png")
splash.position = pos
splash.z_index = 100
add_child(splash)
# Tween the splash
var tween = create_tween()
tween.set_parallel(true) # All properties at once
tween.tween_property(splash, "scale", Vector2(1.2, 1.2), 0.4)\
.from(Vector2(0.5, 0.5))\
.set_trans(Tween.TRANS_BOUNCE)\
.set_ease(Tween.EASE_OUT)
tween.tween_property(splash, "modulate:a", 0.0, 0.4)\
.set_trans(Tween.TRANS_SINE)\
.set_ease(Tween.EASE_IN)
# Cleanup when done
await tween.finished
splash.queue_free()
await tween.finished then queue_free()Result: splash appears, grows and fades, disappears
No manual cleanup code needed; tween handles it
In game.gd:
func _on_fruit_sliced():
score += 10
score_label.text = "Score: %d" % score
# NEW: Score popup
score_popup(get_global_mouse_position())
func score_popup(pos: Vector2):
var label = Label.new()
label.text = "+10"
label.add_theme_font_size_override("font_size", 48)
label.position = pos
label.z_index = 1000
add_child(label)
var tween = create_tween()
# Bounce in: scale 0.1 -> 1.0
tween.tween_property(label, "scale", Vector2(1, 1), 0.3)\
.from(Vector2(0.1, 0.1))\
.set_trans(Tween.TRANS_BOUNCE)\
.set_ease(Tween.EASE_OUT)
# Then parallel: float up + fade
tween.set_parallel(true)
tween.tween_property(label, "position", pos - Vector2(0, 50), 0.6)\
.set_trans(Tween.TRANS_SINE)\
.set_ease(Tween.EASE_OUT)
tween.tween_property(label, "modulate:a", 0.0, 0.6)\
.set_trans(Tween.TRANS_SINE)\
.set_ease(Tween.EASE_IN)
await tween.finished
label.queue_free()
Creation:
Label.new() — create label from codeadd_child() — add to sceneAnimation (2-stage):
Cleanup:
await tween.finishedqueue_free()Test: Slice fruit → "+10" pops up, bounces, floats, fades
Burst effects for slices
GPUParticles2D
Instead of manually moving 100 sprites, we let the GPU do it.
We provide a Material that defines the rules:
particle_demo.tscnNotice how changing just 3 variables completely alters the visual effect.
A function that belongs to the class, not an instance
Normal function: fruit.slice() — called on an object
Static function: ParticleBurst.create_fruit_burst() — called on the class
# Regular function (instance method)
func slice(): ...
fruit.slice()
# Static function (class method)
static func create_fruit_burst(parent, pos):
...
ParticleBurst.create_fruit_burst(game, Vector2(100, 200))
Particle bursts are:
Static function benefits:
New file: scripts/particle_burst.gd
extends Node2D
static func create_fruit_burst(parent: Node, pos: Vector2):
"""Create a sparkle burst for fruit slices"""
var particles = GPUParticles2D.new()
particles.texture = preload("res://assets/images/star_01.png")
particles.position = pos
particles.z_index = 50
parent.add_child(particles)
# Configure particle behavior
var material = ParticleProcessMaterial.new()
material.direction = Vector3(1, 0, 0) # Emission axis
material.spread = 180.0 # 180° = full circle
material.particle_flag_disable_z = true # Stay in 2D
material.initial_velocity_min = 100.0
material.initial_velocity_max = 250.0
material.angular_velocity_min = -360.0
material.angular_velocity_max = 360.0
material.damping_min = 50.0
material.damping_max = 100.0
material.scale_min = 0.5
material.scale_max = 1.5
material.gravity = Vector3(0, 200, 0)
particles.process_material = material
particles.lifetime = 1.0
particles.amount = 30
particles.explosiveness = 1.0
particles.one_shot = true
particles.emitting = true
# Auto-cleanup
await particles.finished
particles.queue_free()
texture — image for each particleprocess_material — ParticleProcessMaterial with behavioramount — number of particles (30 for fruit, 100 for bomb)lifetime — how long particles liveone_shot = true — emit all at onceexplosiveness = 1.0 — emit all immediatelyemitting = true — start emittingMaterial properties control physics:
direction, spread — emission shapeinitial_velocity_min/max — speed rangeangular_velocity — spingravity — pull downwarddamping — air frictionAdd to particle_burst.gd:
static func create_bomb_burst(parent: Node, pos: Vector2):
"""Create a large explosive burst for bomb detonation"""
var particles = GPUParticles2D.new()
particles.texture = preload("res://assets/images/star_01.png")
particles.position = pos
particles.z_index = 100 # Higher than fruit burst
parent.add_child(particles)
var material = ParticleProcessMaterial.new()
material.direction = Vector3(1, 0, 0)
material.spread = 180.0
material.particle_flag_disable_z = true
# MORE intense than fruit burst
material.initial_velocity_min = 300.0 # Faster
material.initial_velocity_max = 500.0
material.angular_velocity_min = -720.0
material.angular_velocity_max = 720.0
material.damping_min = 100.0
material.damping_max = 200.0
material.scale_min = 1.0
material.scale_max = 2.5 # Bigger
material.gravity = Vector3(0, 300, 0) # More gravity
particles.process_material = material
particles.lifetime = 1.5 # Longer
particles.amount = 100 # More particles
particles.explosiveness = 1.0
particles.one_shot = true
particles.emitting = true
await particles.finished
particles.queue_free()
In fruit.gd, in the slice() function:
func slice(slash_pos: Vector2):
# ... existing code ...
# Emit particle burst at slash position
var ParticleBurst = preload("res://scripts/particle_burst.gd")
ParticleBurst.create_fruit_burst(get_parent(), slash_pos)
preload() loads the script, ParticleBurst.create_fruit_burst() calls the static method
get_parent() is the game node (parent of all fruits)
Test: Slice fruit → stars burst outward
That's the fruit burst effect — instant visual feedback for successful slices
Game over with style
In bomb.gd (fill in stubs):
func play_fuse_sound():
fuse_audio = AudioStreamPlayer.new()
fuse_audio.stream = preload("res://assets/sounds/bomb-fuse.wav")
fuse_audio.volume_db = -10
add_child(fuse_audio)
fuse_audio.play()
func slice(slash_pos: Vector2):
if is_sliced:
return
is_sliced = true
# Stop fuse sound
if fuse_audio:
fuse_audio.stop()
# Stop movement
velocity = Vector2.ZERO
# Signal to game: bomb was sliced
emit_signal("bomb_sliced", self)
func detonate():
# Swap to explosion texture
texture = explosion_texture
# Play explosion sound
play_explosion_sound()
# Particle burst
var ParticleBurst = preload("res://scripts/particle_burst.gd")
ParticleBurst.create_bomb_burst(get_parent(), global_position)
# At top of bomb.gd
signal bomb_sliced(bomb: Node2D)
This signal passes the bomb node itself as an argument
Used in game.gd to react: emit_signal("bomb_sliced", self)
In game.gd connect: bomb.bomb_sliced.connect(func(b): game_over())
In game.gd:
const BOMB_SPAWN_CHANCE = 0.15 # 15% chance
func spawn_fruit():
# Roll for bomb
if randf() < BOMB_SPAWN_CHANCE:
spawn_bomb()
return
# ... existing fruit spawn code ...
func spawn_bomb():
var bomb = bomb_scene.instantiate()
bomb.position = Vector2(randf_range(100, 1180), -50)
add_child(bomb)
# Connect signal
bomb.bomb_sliced.connect(_on_bomb_sliced)
15% of the time, spawn bomb instead of fruit
Connect the signal to handle bomb detonation
Random offset on camera each frame
var is_shaking = false
var shake_intensity = 0.0
func _process(delta):
if is_shaking:
camera_2d.offset = \
Vector2(randf_range(-shake_intensity, shake_intensity),
randf_range(-shake_intensity, shake_intensity))
That's it! Random offset makes the screen jitter
Control intensity to feel impact (0 = steady, 20 = wild)
In game.gd:
func _on_bomb_sliced(bomb: Node2D):
# Stop game immediately
spawn_timer.stop()
# Run the explosion sequence
await execute_bomb_sequence()
# Save score and end game
Global.set_score(score)
get_tree().change_scene_to_file("res://scenes/score_screen.tscn")
func execute_bomb_sequence():
# T=0: Start camera shake
is_shaking = true
shake_intensity = 8.0
# T=0 to 2: Shoot rays at intervals
for i in range(5):
create_ray()
await create_timer(0.4).timeout
# T=2: Bomb detonates
bomb.detonate()
shake_intensity = 12.0
# Wait a bit
await create_timer(2.5).timeout
# Stop shake
is_shaking = false
camera_2d.offset = Vector2.ZERO
| Time | Event | Intensity |
|---|---|---|
| T=0s | Slice bomb, timer stops | — |
| T=0s | Camera shake starts | 8 |
| T=0–2s | Rays shoot (5×, 0.4s apart) | 8 |
| T=2s | Bomb detonates | 12 (increase) |
| T=2.5s | Shake stops, camera resets | 0 |
| T=4.5s | Scene changes to score screen | — |
Choreographed chaos: each moment timed for maximum impact
This code is pre-written in the final project
Read it to understand the pattern
Rays are Line2D nodes created dynamically
Animate with tween: start point → extended point (with parallax offset)
Parallel tween: fade out alpha while extending
Lambda cleanup on tween.finished
Rays combine: .new(), tweens, trigonometry (cos/sin/TAU)
This is "bonus content" — study in final code if interested
Full 4.5-second game-over sequence — dramatic, but under complete control
Wiring the complete flow
New: scripts/global.gd
extends Node
var current_score = 0
func reset_score():
current_score = 0
func set_score(score: int):
current_score = score
func get_score() -> int:
return current_score
Simple but essential: holds score across scenes
Project Settings → Autoload tab
res://scripts/global.gdGlobalNow use it anywhere: Global.current_score
Godot instantiates autoloads automatically on startup
extends Control
func _ready():
# Reset score on menu open
Global.reset_score()
# Connect play button
$PlayButton.pressed.connect(_on_play_pressed)
func _on_play_pressed():
get_tree().change_scene_to_file("res://scenes/game.tscn")
On ready: reset global score and connect button
Button press: change to game scene
extends Control
func _ready():
# Display final score from Global
$ScoreLabel.text = "Final Score: %d" % Global.get_score()
# Connect buttons
$RestartButton.pressed.connect(_on_restart)
$MenuButton.pressed.connect(_on_menu)
func _on_restart():
get_tree().change_scene_to_file("res://scenes/game.tscn")
func _on_menu():
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
On ready: display score from Global, connect buttons
Restart: go to game. Menu: go to main menu.
Full game loop:
Project Settings → General → Main Scene
Set to: res://scenes/main_menu.tscn
When you run the game, it starts here
Press F5 (Run Project)
✓ Main menu appears
✓ Click Play → Game starts
✓ Slice fruits, score increases
✓ Hit bomb → Explosion, game over
✓ Score screen shows final score (from Global!)
✓ Click Restart → Back to game (score reset)
✓ Click Menu → Back to main menu
What we built today
| Lecture 13 | Lecture 14 | Lecture 15 |
|---|---|---|
✓ match |
✓ Tweens | Extensions |
| ✓ Gesture input | ✓ Particles | & Polish |
| ✓ Trail collision | ✓ Static functions | |
| ✓ String format | ✓ Autoloads | |
| ✓ Simple effects | ✓ Scene transitions |
14 new concepts in 2 lectures → Complete playable game
What We Wrote
Game Now Has
~350 lines of code total (starter + our builds)
Pair programming challenges
Extend the game with new features:
You have all the tools — now customize!
Try these on your own:
Two lectures, three new game mechanics
L13: Slash detection, scoring, match statements
L14: Juice, particles, bombs, full game loop
You now understand game feel, polish, and state management.