Table of Contents

Getting Started with Godot

This event runs monthly on the 2nd Monday of the month. See the home page for the date of our next monthly event.

For our regular monthly tutorial nights, we're basing our teaching on the wonderful series by AJ's Learning Lab, Godot FPS Tutorial - Learn Godot by making Wolfenstein 3D.

He has made the assets he uses freely available (please consider donating) here: https://ajslearninglab.itch.io/boganstein-3d-learn-godot-4-with-wolfenstein-3d/

All the code we use is available on AJ's Learning Lab github - https://github.com/AJsLearningLab/GodotFPS

First Steps

Part 1

Goal: Learn about Godot, create a world/room (floor and walls)

Part 2

Goal: Create a player character that can be controlled by the keyboard

if Input.is_action_pressed("ui_left"):
  self.rotate_y(TURN_SPEED)
if Input.is_action_pressed("ui_right"):
  self.rotate_y(-TURN_SPEED)

Part 3

Goal: Add textures to the walls and floor

Part 4

Goal: Add a UI/HUD and a knife

ui.gd
extends CanvasLayer
 
var ammo = 0
var current_weapon = "knife"
 
func _ready():
	$AnimatedSprite2D.animation_finished.connect(_on_AnimatedSprite2D_animation_finished)
 
func _process(delta):
	if Input.is_action_just_pressed("ui_select"):
		if current_weapon == "knife":
			$AnimatedSprite2D.play("stab")
		elif current_weapon == "gun":
			if ammo > 0:
				$AnimatedSprite2D.play("shoot")
				ammo -= 1
 
func _on_AnimatedSprite2D_animation_finished():
	if current_weapon == "knife":
		$AnimatedSprite2D.play("knife_idle")
	elif current_weapon == "gun":
		$AnimatedSprite2D.play("gun_idle")

Part 5

Goal:Add a hand gun, firing, add a global script

else:
  current_weapon = "knife"
  $AnimatedSprite2D.play("knife_idle")

Part 6

Goal: More weapons

var time_since_last_shot = 0.0
var fire_rate = 1.0
$AnimatedSprite2D.play(Global.current_weapon + "_idle")
time_since_last_shot += delta
var can_shoot = time_since_last_shot >= (1.0 / fire_rate)

if Global.current_weapon != "knife" and Global.ammo <= 0:
  Global.current_weapon = "knife"
  $AnimatedSprite2D.play("knife_idle")
if Input.is_action_pressed("ui_select") and can_shoot:
else:
  $AnimatedSprite2D.play(Global.current_weapon + "_shoot")
  
time_since_last_shot = 0.0

if Global.current_weapon != "knife":
  if Global.ammo > 0:
    Global.ammo -= 1
    
match Global.current_weapon:
  "gun":
    fire_rate = 3.0
  "machine":
    fire_rate = 6.0
  "mini":
    fire_rate = 10.0
  "knife":
    fire_rate = 2.0
  _:
    fire_rate = 1.0

func _on_AnimatedSprite2D_animation_finished():
  $AnimatedSprite2D.play(Global.current_weapon + "_idle")

The full code for ui.gd should now be:

ui.gd
extends CanvasLayer
 
var time_since_last_shot = 0.0
var fire_rate = 1.0
 
 
func _ready():
	$AnimatedSprite2D.animation_finished.connect(_on_AnimatedSprite2D_animation_finished)
	$AnimatedSprite2D.play(Global.current_weapon + "_idle")
 
func _process(delta):
	time_since_last_shot += delta
	var can_shoot = time_since_last_shot >= (1.0 / fire_rate)
 
	if Global.current_weapon != "knife" and Global.ammo <= 0:
		Global.current_weapon = "knife"
		$AnimatedSprite2D.play("knife_idle")
 
	if Input.is_action_pressed("ui_select") and can_shoot:
 
		if Global.current_weapon == "knife":
			$AnimatedSprite2D.play("stab")
		else:
			$AnimatedSprite2D.play(Global.current_weapon + "_shoot")
 
		time_since_last_shot = 0.0
 
		if Global.current_weapon != "knife":
			if Global.ammo > 0:
				Global.ammo -= 1
 
	match Global.current_weapon:
		"gun":
			fire_rate = 3.0
		"machine":
			fire_rate = 6.0
		"mini":
			fire_rate = 10.0
		"knife":
			fire_rate = 2.0
		_:
			fire_rate = 1.0
 
func _on_AnimatedSprite2D_animation_finished():
	$AnimatedSprite2D.play(Global.current_weapon + "_idle")

Part 7

Goal: Add enemies

add_to_group("player")
extends CharacterBody3D
 
@onready var player : CharacterBody3D = get_tree().get_first_node_in_group("player")
 
const SPEED = 5.0
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")  # Get the gravity from the project settings to be synced with RigidBody nodes.
var dead = false
var is_attacking = false  
var attack_range = 5
 
func _ready():
	add_to_group("enemy")
 
func _physics_process(delta):
	if dead or is_attacking:  # Check if the enemy is dead or attacking
		return
 
	if player == null:
		return
 
	var dir = player.global_position - global_position
	dir.y = 0.0
	dir = dir.normalized()
 
	velocity = dir * SPEED
	# Add the gravity.
	if not is_on_floor():
		velocity.y -= gravity * delta
 
	move_and_slide()
	attack()
 
func attack():
	var dist_to_player = global_position.distance_to(player.global_position)
	if dist_to_player > attack_range:
		return
	else:
		is_attacking = true  # Set the attacking flag
		$AnimatedSprite3D.play("shoot")
		await $AnimatedSprite3D.animation_finished  # Wait for the animation to finish
		is_attacking = false  # Reset the attacking flag
 
 
func die():
	dead = true  # Corrected variable scope
	$AnimatedSprite3D.play("die")
	$CollisionShape3D.disabled = true

Part 8

Goal: Enemy Death and Raycasting

@onready var ui_script = $ui
@onready var ray = $Camera3D/RayCast3D
if Input.is_action_just_pressed("ui_accept"):
  if ui_script.can_shoot:
    shoot()
func shoot():
  if ray.is_colliding() and ray.get_collider().has_method("die"):
    ray.get_collider().die()
if Input.is_action_pressed("ui_accept"):

Part 9

Goal: HUD labels and player death

var player_health = 100
function update_player_health():
  $health.text = str(get_parent().player_health)
update_player_health()
func damage():
  player_health -= 10
  print(player_health)
  if player_health <= 0:
    queue_free()
if $RayCast3D.is_colliding() and $RayCast3D.get_collider().has_method('damage'):
  $RayCast3D.get_collider().damage()

Part 10

Goal: Ammo pickups

Next Steps

Goal: Add sound effects

var sound_player = $AudioStreamPlayer
match Global.current_weapon:
  "gun":
    sound_player.stream = preload("res://gun.ogg")
  "machine":
    sound_player.stream = preload("res://machine.ogg")
  "mini":
    sound_player.stream = preload("res://mini.ogg")
sound_player.play()