Click to fire. Every bullet is a brand-new copy of a saved scene, created the instant you press the button.
Loading the game engine. This takes a moment the first time.
This lesson builds new nodes while the game runs. Every bullet is a fresh copy of bullet.tscn, made the instant you click.
Shooting asks Godot about the LEFT MOUSE BUTTON directly, by name: MOUSE_BUTTON_LEFT. Moving is different: move_left and its three partners are names from Project > Project Settings > Input Map, each one holding the real keys (WASD and the arrow keys) that set it off.
"preload" loads bullet.tscn as the script is read, so it is ready before the game starts. bullet.tscn sits in this same folder, so the path is just its file name. Nothing to set in the Inspector.
The fire rate, the _can_shoot flag and the ShootTimer node are all one feature: a cooldown, so that holding the button does not make sixty bullets a second. Leave every shaded block out and the gun still fires, just faster than you probably want. Add them back when that bothers you.
None of the earlier lessons use these. Each one shows the line where it appears here.
A value that never changes while the game runs. Written in CAPITALS by habit.
const BULLET_SCENE: PackedScene = preload("bullet.tscn")Loads a file, like a saved scene, once when the game starts, so it is ready the moment you need a copy.
const BULLET_SCENE: PackedScene = preload("bullet.tscn")A saved scene, like bullet.tscn, waiting to be copied into the game.
const BULLET_SCENE: PackedScene = preload("bullet.tscn")A Timer node counts down from wait_time seconds once start() is called, then sends timeout.
shoot_timer.wait_time = fire_rateThe signal a timer sends when it reaches zero.
shoot_timer.timeout.connect(_on_shoot_timer_timeout)True on every frame that mouse button is held down.
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):Ends the function straight away. With a value after it, it hands that value back to whoever called the function.
returnMakes a brand-new copy of a saved scene. It is not in the game until you add_child() it.
var bullet: Node2D = BULLET_SCENE.instantiate()Puts a node into the game, under another node.
get_tree().current_scene.add_child(bullet)The scene running right now: the level itself.
get_tree().current_scene.add_child(bullet)A one-time countdown made in code, no Timer node needed. It sends timeout after that many seconds.
var timer: SceneTreeTimer = get_tree().create_timer(lifetime)A ready-made arrow one pixel long. There is also Vector2.LEFT, UP and DOWN.
position += Vector2.RIGHT.rotated(rotation) * speed * deltaWhere a node is, measured from its parent.
position += Vector2.RIGHT.rotated(rotation) * speed * deltaThe same arrow, turned by an angle. Vector2.RIGHT.rotated(rotation) points the way the node faces.
position += Vector2.RIGHT.rotated(rotation) * speed * deltaTakes away from what a variable already holds.
health -= 1■ UI node · ■ 2D node · % unique name · .tscn instanced scene
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.3optional
# True when the gun is loaded. Goes false the instant we fire.
var _can_shoot: bool = true
@onready var shoot_timer: Timer = %ShootTimer
# Muzzle is an empty node parked at the tip of the gun, so bullets start
# there instead of in the middle of the player.
@onready var muzzle: Marker2D = %Muzzle
@onready var sprite: Sprite2D = $Sprite2D
func _ready() -> void:optional
shoot_timer.wait_time = fire_rate
# When the timer runs out, "_on_shoot_timer_timeout" reloads the gun.
shoot_timer.timeout.connect(_on_shoot_timer_timeout)
func _physics_process(_delta: float) -> void:
# Move with WASD.
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 same as the Mouse Aiming lesson.
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
# "is_mouse_button_pressed" stays true the whole time you HOLD the left
# mouse button, so you can hold it down to keep firing.
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
_shoot()
# Makes one bullet. The "_" in front means only this file uses it.
func _shoot() -> void:
# Still reloading, so this press does nothing.
if not _can_shoot:optional
return
# Lock the gun, then start the countdown that unlocks it again.
_can_shoot = false
shoot_timer.start()
# "instantiate()" makes one real copy of bullet.tscn.
var bullet: Node2D = BULLET_SCENE.instantiate()
# Start it at the gun tip, pointed the way the player is facing.
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:optional
_can_shoot = trueextends Area2D
## How fast the bullet flies.
@export var speed: float = 500.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 and slowly clog the game up.
# "create_timer()" makes a one-time timer, and we hand it "queue_free"
# so the bullet erases itself when the 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:
# "Vector2.RIGHT" is an arrow pointing right. ".rotated(rotation)" turns
# that arrow to match the way the bullet is facing. Multiplying by speed
# and delta gives exactly how far to travel this frame.
position += Vector2.RIGHT.rotated(rotation) * speed * delta
# "body" is whatever the bullet ran into.
func _on_body_entered(body: Node2D) -> void:
if body is Enemy:
# "body_entered" already handed us the target as "body", so we just
# call a function on it. No signal needed.
body.take_hit()
# Erase the bullet no matter WHAT it hit, even a wall.
# This line sits outside the "if" on purpose.
queue_free()class_name Enemy
extends StaticBody2D
## How many hits this target survives.
@export var health: int = 3
## Called BY bullet.gd, not by anything in this file.
func take_hit() -> void:
health -= 1
if health <= 0:
queue_free()