Lesson 04

Shooting Bullets

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.

THE SHOOT BUTTON

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.

THE BULLET SCENE

"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 SHADED LINES ARE OPTIONAL

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.

New code in this lesson

None of the earlier lessons use these. Each one shows the line where it appears here.

const

A value that never changes while the game runs. Written in CAPITALS by habit.

player.gd
const BULLET_SCENE: PackedScene = preload("bullet.tscn")

preload()

Loads a file, like a saved scene, once when the game starts, so it is ready the moment you need a copy.

player.gd
const BULLET_SCENE: PackedScene = preload("bullet.tscn")

PackedScene

A saved scene, like bullet.tscn, waiting to be copied into the game.

player.gd
const BULLET_SCENE: PackedScene = preload("bullet.tscn")

wait_time and start()

A Timer node counts down from wait_time seconds once start() is called, then sends timeout.

player.gd
shoot_timer.wait_time = fire_rate

timeout

The signal a timer sends when it reaches zero.

player.gd
shoot_timer.timeout.connect(_on_shoot_timer_timeout)

Input.is_mouse_button_pressed()

True on every frame that mouse button is held down.

player.gd
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):

return

Ends the function straight away. With a value after it, it hands that value back to whoever called the function.

player.gd
return

instantiate()

Makes a brand-new copy of a saved scene. It is not in the game until you add_child() it.

player.gd
var bullet: Node2D = BULLET_SCENE.instantiate()

add_child()

Puts a node into the game, under another node.

player.gd
get_tree().current_scene.add_child(bullet)

current_scene

The scene running right now: the level itself.

player.gd
get_tree().current_scene.add_child(bullet)

create_timer()

A one-time countdown made in code, no Timer node needed. It sends timeout after that many seconds.

bullet.gd
var timer: SceneTreeTimer = get_tree().create_timer(lifetime)

Vector2.RIGHT

A ready-made arrow one pixel long. There is also Vector2.LEFT, UP and DOWN.

bullet.gd
position += Vector2.RIGHT.rotated(rotation) * speed * delta

position

Where a node is, measured from its parent.

bullet.gd
position += Vector2.RIGHT.rotated(rotation) * speed * delta

rotated()

The same arrow, turned by an angle. Vector2.RIGHT.rotated(rotation) points the way the node faces.

bullet.gd
position += Vector2.RIGHT.rotated(rotation) * speed * delta

-=

Takes away from what a variable already holds.

enemy.gd
health -= 1

The scenes

■ UI node · ■ 2D node · % unique name · .tscn instanced scene

game.tscn
  • Game Node2D
  • Player CharacterBody2D player.tscn
  • Enemy1 StaticBody2D enemy.tscn
  • Enemy2 StaticBody2D enemy.tscn
  • Enemy3 StaticBody2D enemy.tscn
  • Enemy4 StaticBody2D enemy.tscn
bullet.tscn
  • Bullet Area2D bullet.gd
  • Polygon2D
  • CollisionShape2D
enemy.tscn
  • Enemy StaticBody2D enemy.gd
  • Polygon2D
  • CollisionShape2D
player.tscn
  • Player CharacterBody2D player.gd
  • Sprite2D
  • Gun Polygon2D
  • Muzzle Marker2D %
  • ShootTimer Timer optional %
  • CollisionShape2D

FileSystem

  • game.tscn
  • bullet.tscn
  • bullet.gd
  • enemy.tscn
  • enemy.gd
  • player.tscn
  • player.gd

The code

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 = true