Targets keep appearing, so destroy fifteen before they crowd you out. Assembles Mouse Aiming, Shooting Bullets, and Enemy Spawner.
Loading the game engine. This takes a moment the first time.
This is a project, not a lesson: no new idea lives in this folder. Aim, shoot, and destroy fifteen targets. They keep appearing whether you keep up or not.
Timer plus instantiate(), spawning targets instead of enemies, at random spots instead of the screen edges.Each spawned target has a local "destroyed" signal, and since this file creates every target, it connects to each one right after instantiate(), the same parent-reaches-down pattern the Enemy Spawner lesson uses. The count lives here; the HUD hears about it on the bus.
■ UI node · ■ 2D node · % unique name · .tscn instanced scene
game_events.gd
Switch it on once, under Project > Project Settings, on the Globals tab. Not made yet? Press Create Script to make it right there. Already made it? Press Select Script/Scene, pick it, and press Add. Its Name column should say GameEvents, which Godot fills in for you. That's the name your code uses.
extends Node2D
## Which scene to copy each time we spawn. Set to target.tscn in Godot.
@export var target_scene: PackedScene
## How many seconds to wait between spawns.
@export var spawn_interval: float = 1.1
## The most targets allowed on screen at once.
@export var max_targets: int = 6
## Destroy this many to win.
@export var targets_to_win: int = 15
var _destroyed: int = 0
var _alive: int = 0
var _game_over: bool = false
@onready var spawn_timer: Timer = %SpawnTimer
@onready var player: Player = $Player
func _ready() -> void:
spawn_timer.wait_time = spawn_interval
spawn_timer.timeout.connect(_on_spawn_timer_timeout)
spawn_timer.start()
func _on_spawn_timer_timeout() -> void:
if _game_over or _alive >= max_targets:
return
if target_scene == null:
return
var target: BlitzTarget = target_scene.instantiate()
# Anywhere in the middle of the play field, away from the edges and
# never right on top of the player's corner.
target.global_position = Vector2(randf_range(260, 1080), randf_range(90, 580))
# We made this target, so we can connect to its local signal. No bus
# needed to hear from something this file built itself.
target.destroyed.connect(_on_target_destroyed)
target.tree_exiting.connect(_on_target_removed)
add_child(target)
_alive += 1
func _on_target_destroyed() -> void:
if _game_over:
return
_destroyed += 1
# hud.gd connects to this bus signal and redraws the score.
GameEvents.target_destroyed.emit()
if _destroyed >= targets_to_win:
_game_over = true
spawn_timer.stop()
player.set_physics_process(false)
# hud.gd shows the win message.
GameEvents.goal_reached.emit()
_restart_soon()
func _on_target_removed() -> void:
_alive -= 1
func _restart_soon() -> void:
var timer: SceneTreeTimer = get_tree().create_timer(2.0)
timer.timeout.connect(_on_restart_timeout)
func _on_restart_timeout() -> void:
get_tree().reload_current_scene()extends Area2D
## How fast the bullet flies.
@export var speed: float = 520.0
## How many seconds the bullet lives before erasing itself.
@export var lifetime: float = 2.0
func _ready() -> void:
# Bullets that miss would fly on forever. This one-time timer erases
# them when their time is up.
var timer: SceneTreeTimer = get_tree().create_timer(lifetime)
timer.timeout.connect(queue_free)
body_entered.connect(_on_body_entered)
func _physics_process(delta: float) -> void:
position += Vector2.RIGHT.rotated(rotation) * speed * delta
func _on_body_entered(body: Node2D) -> void:
# "BlitzTarget" is the class_name at the top of target.gd.
if body is BlitzTarget:
body.take_hit()
# Erase the bullet no matter what it hit, even a wall.
queue_free()extends CanvasLayer
## Keep in step with "targets_to_win" on the level. It is worth seeing that
## these are two different numbers that merely agree: the level owns the
## rule, the HUD only owns the words.
@export var targets_to_win: int = 15
var _destroyed: int = 0
@onready var score_label: Label = %ScoreLabel
@onready var result_label: Label = %ResultLabel
@onready var home_button: Button = %HomeButton
func _ready() -> void:
GameEvents.target_destroyed.connect(_on_target_destroyed)
GameEvents.goal_reached.connect(_on_goal_reached)
home_button.pressed.connect(_on_home_pressed)
_update_label()
func _on_target_destroyed() -> void:
_destroyed += 1
_update_label()
func _on_goal_reached() -> void:
result_label.text = "You win!"
func _update_label() -> void:
score_label.text = "Targets: %d / %d" % [_destroyed, targets_to_win]
# Loads the lesson list screen, which replaces this whole scene.
func _on_home_pressed() -> void:
get_tree().change_scene_to_file("res://shared/ui/lesson_select.tscn")class_name Player
extends CharacterBody2D
## How fast the player moves.
@export var speed: float = 200.0
# The scene to copy for each bullet. It lives in this same folder.
const BULLET_SCENE: PackedScene = preload("bullet.tscn")
## How many seconds to wait between shots.
@export var fire_rate: float = 0.25
# True when the gun is loaded. Goes false the instant we fire.
var _can_shoot: bool = true
@onready var shoot_timer: Timer = %ShootTimer
@onready var muzzle: Marker2D = %Muzzle
@onready var sprite: Sprite2D = $Sprite2D
func _ready() -> void:
shoot_timer.wait_time = fire_rate
shoot_timer.timeout.connect(_on_shoot_timer_timeout)
func _physics_process(_delta: float) -> void:
# Move with WASD: the Top-Down Movement block.
var direction: Vector2 = Input.get_vector(
"move_left", "move_right", "move_up", "move_down"
)
velocity = direction * speed
move_and_slide()
# Turn to face the mouse: the Mouse Aiming block.
look_at(get_global_mouse_position())
# Aiming left would turn the picture upside down, so mirror it back.
sprite.flip_v = abs(rotation) > PI / 2
# Hold to keep firing: the Shooting Bullets block.
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and _can_shoot:
_shoot()
func _shoot() -> void:
_can_shoot = false
shoot_timer.start()
var bullet: Node2D = BULLET_SCENE.instantiate()
bullet.global_position = muzzle.global_position
bullet.rotation = rotation
# Add the bullet to the LEVEL, not to the player. If it were our child,
# it would swing around with us every time we turned.
get_tree().current_scene.add_child(bullet)
func _on_shoot_timer_timeout() -> void:
_can_shoot = trueclass_name BlitzTarget
extends StaticBody2D
## How many hits this target survives.
@export var health: int = 1
# A LOCAL signal: it belongs to this one target. The level made us, so the
# level connects to it. game_events.gd explains when local is the right call.
signal destroyed
## Called BY bullet.gd, not by anything in this file.
func take_hit() -> void:
health -= 1
if health <= 0:
# Emit FIRST, then erase: erased nodes cannot send messages.
destroyed.emit()
queue_free()extends Node
# All of the game's signals are declared below, grouped by the lesson
# that uses them. A signal with brackets, like coin_collected(value: int),
# passes that value to every function connected to it.
# This shows only the signals this lesson uses. The full file declares
# every lesson's signals.
# Platformer Movement lesson
signal goal_reached
# Target Blitz project
signal target_destroyed