This is an old revision of the document!
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
Goal: Learn about Godot, create a world/room (floor and walls)
Goal: Create a player character that can be controlled by the keyboard
const TURN_SPEED = 0.05if Input.is_action_pressed("ui_left"):
self.rotate_y(TURN_SPEED)
if Input.is_action_pressed("ui_right"):
self.rotate_y(-TURN_SPEED)
Goal: Add textures to the walls and floor
Goal: Add a UI/HUD and a knife
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")
Goal:Add a hand gun, firing, add a global script
ammo -= 1 line, add:else:
current_weapon = "knife"
$AnimatedSprite2D.play("knife_idle")
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.player("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:
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")
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
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()