Lesson 09

Selling the 'glitch': visual flavor and feedback

GlitchFall should feel corrupted and alive. We add cheap, high-impact effects: floating/tilted geometry, a subtle pickup pop, and a screen tint on hazard hits — all with tools you already know.

▸ By the end of this lesson: A world that reads as 'glitched' plus satisfying feedback on pickups and hits

9.1Floating, drifting glitch shapes

Take a few of the scenery meshes from Lesson 2 and make them hover and rotate. Create a tiny reusable script glitch_float.gd and attach it to any MeshInstance3D you want to animate:

glitch_float.gdgdscript
extends MeshInstance3D

# Gentle bob + spin to make static geometry feel unstable/corrupted.
@export var bob_height: float = 0.4
@export var bob_speed: float = 2.0
@export var spin_speed: float = 0.6

var _base_y: float = 0.0
var _t: float = 0.0

func _ready() -> void:
	_base_y = position.y

func _process(delta: float) -> void:
	_t += delta
	position.y = _base_y + sin(_t * bob_speed) * bob_height
	rotate_y(spin_speed * delta)

@export makes it tweakable

@export exposes a variable in the Inspector, so you can give each floating shape different bob height and speed without editing code. Reuse one script across many nodes, vary the numbers per instance.

  1. Attach glitch_float.gd to a few scenery meshes. Tilt some of them (set an odd rotation) before running so they float at broken angles.
  2. Run — the world now drifts and spins subtly. That instability is the glitch aesthetic.

9.2A pickup pop

Give fragments a quick scale-up-and-vanish instead of an instant disappear. Update fragment.gd's collection to tween before freeing:

fragment.gd (replace _on_body_entered)gdscript
func _on_body_entered(body: Node3D) -> void:
	if body.is_in_group("player"):
		collected.emit(self)
		_play_pickup_and_free()

func _play_pickup_and_free() -> void:
	# Stop detecting further hits during the animation.
	set_deferred("monitoring", false)
	# Scale the fragment up quickly, then remove it.
	var tw: Tween = create_tween()
	tw.tween_property(self, "scale", scale * 1.8, 0.15)
	tw.tween_callback(queue_free)

Area3D has no modulate

Unlike 2D CanvasItem nodes, 3D nodes have no simple modulate alpha, so we can't fade them in one line. A scale pop is the clean, reliable cross-node effect. If you want a fade too, you'd tween the mesh material's albedo_color alpha instead — but the scale pop alone reads well.

9.3Hazard screen flash

Flash the screen red when the player hits a hazard. Add a full-screen ColorRect to the HUD and pulse it from Main.

  1. Under HUD, add a ColorRect named DamageFlash. Set its anchors to Full Rect so it covers the screen, color to red, and its Modulate alpha to 0 (invisible).
main.gd (replace _on_player_hit)gdscript
@onready var damage_flash: ColorRect = $HUD/DamageFlash

func _on_player_hit(_body: Node3D) -> void:
	_respawn_player()
	_flash_damage()

func _flash_damage() -> void:
	damage_flash.modulate.a = 0.5
	var tw: Tween = create_tween()
	tw.tween_property(damage_flash, "modulate:a", 0.0, 0.4)
  1. Run with F6. Collect a fragment — it pops before vanishing. Touch a hazard — the screen flashes red as you respawn.
  2. Floating glitch shapes drift in the background the whole time.

Checkpoint — expected state