====== Getting Started with Godot ======
This event runs monthly on the **2nd Monday** of the month. See the [[start|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 [[https://www.youtube.com/@AJsLearningLab|AJ's Learning Lab]], [[https://www.youtube.com/playlist?list=PL4vjw0qHwNZIQZScBFaON0WGkz-BMyoCh|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
Glasgow Social Game Dev Chatroom Link (Matrix): https://glasgow.social/matrix
===== First Steps =====
* Download Godot from https://godotengine.org/download/ and install it (it's available on all platforms)
* Download the assets from AJ's Learning Lab here: https://ajslearninglab.itch.io/boganstein-3d-learn-godot-4-with-wolfenstein-3d/
==== Part 1 ====
**Goal: Learn about Godot, create a world/room (floor and walls)**
* New scene: click 3D Scene, rename to 'world'
* Create floor. Add MeshInstance3D to scene
* Set Mesh type to 'New PlaneMesh'
* Change size. Transform -> scale to 20
* Create collision mesh. Using the 'Mesh' top button, choose 'Trimesh Static Body'. That automatically creates a StaticBody3D of the correct type, and then adds a CollisionShape3D.
* New wall. Create another new mesh. This time the Mesh should be New BoxMesh'. Move walls to edge, set z to 20
* Tweak the wall height, 'y to the sky', set to 10
* Duplicate for the other three walls
==== Part 2 ====
**Goal: Create a player character that can be controlled by the keyboard**
* New scene. Click + next to the world scene. Other node. Search for CharacterBody3D. Name it 'player'.
* Add subnode. Click +, search for MeshInstance3D. Set Mesh shape to Capsule shape.
* Fix the height. Transform, adjust Y to 1m.
* Select player node, add new node, Camera3D.
* Adjust Camera height to eye height.
* Create script. Select player node. Use script+/scroll button. Select a template for Basic movement.
* Add new variable. ''const TURN_SPEED = 0.05''
* Add new code
if Input.is_action_pressed("ui_left"):
self.rotate_y(TURN_SPEED)
if Input.is_action_pressed("ui_right"):
self.rotate_y(-TURN_SPEED)
* Comment out the jump code
* In the world scene, drag the player scene into the world
* In the three dot menu (world scene), use 'Add Sun to scene', 'add environment to scene'
* In the player scene, use the Mesh button to add a 'Simplified Convex Collision Sibling' mesh to the player's MeshInstance3D (not 'Single', that'll cause performance issues - why?)
* Set up WASD controls in the Project Settings
==== Part 3 ====
**Goal: Add textures to the walls and floor**
* Import a texture by dragging it into the file system.
* Select a wall. Use Geometry -> Material Override. Select New StandardMaterial3D. Click the sphere. Expand Albedo. Drag the texture to the 'Texture' input.
* Find UV1, experiment with the sizing, approx 30 x 10
* Apply to floor/other walls.
==== Part 4 ====
**Goal: Add a UI/HUD and a knife**
* Create new scene, other node: 'CanvasLayer', rename to 'ui'
* Create new node, AnimatedSprite2D
* Create new node, ColorRect
* In 2D view, size the ColorRect. Layout -> Anchors -> Bottom Wide. Set the colour to dark blue.
* Import wolfweapons.png
* Select Animated Sprite. Expand Animation -> Sprite Frame. Click New SpriteFrames. Click it again to view the animation frame at the bottom.
* Rename default to 'knife_idle', set as default.
* Click grid, adjust, set up idle frame.
* Adjust position of animatedspirteframe in 2D view, use Transform->Scale to size up to 5
* Create new animation called 'stab'
* Add new stab animations, disable loop, set FPS to 16
* Click UI root node, add script. Enter the following code:
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")
* Add the ui scene to the player scene (Drag drop from file system onto player node)
==== Part 5 ====
**Goal:Add a hand gun, firing, add a global script**
* Add new handfun sprite animations ('gun_idle' and 'shoot'). Disable looping. (Adjust to 5x4 for grid)
* Edit the ui code, after the ''ammo -= 1'' line, add:
else:
current_weapon = "knife"
$AnimatedSprite2D.play("knife_idle")
* Set the ammo to '3' and current_weapon to 'gun'. Test.
* Create a global script. File -> New Script (based on Node). Name it global.
* Move the ammo and current weapon variables from the ui script to the global script.
* Open the project settings, in Autoload, add the global script and mark it global
* Tweak the project setting page, allow fullscreen too
* Edit the ui.gd script and change all references to current_weapon and ammo to be Global.current_weapon and Global.ammo
* Test everything still works
==== Part 7 ====
**Goal: Add enemies**
* Create new scene. Other node->CharacterBody3D. Name it guard.
* Add two children to the enemy node. A CollisionShape3D (set to capsule), and a AnimatedSprite3D.
* Set the Y to 1, to make it on the floor. Adjust the capsule size to be 2m.
* Import the guard sprite to the file system.
* Select the AnimatedSprite3D, Click Sprite Frame-> New Sprite Frame. Click it again to set a default animation using the grid, set to 7x2
* Adjust the eize, choose AnimatedSprite3D. Transform -> y =1, scale = 4 ish.
* A new animations, 'die' and 'shoot'
* Select the AnimatedSprite3D, in the inspector find 'Flags', enable 'Billboard - Y:Billboard'
* Edit the player script and add this line to the _ready() function:
add_to_group("player")
* Select the guard root node. Create a new script, use the code below:
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 6 ====
**Goal: More weapons**
* Edit the ui AnimatedSprite2D, add new animations/sprites
* Rename 'shoot' to gun_shoot
* Add new variables to ui.gd script
var time_since_last_shot = 0.0
var fire_rate = 1.0
* Add a line to the _ready() function
$AnimatedSprite2D.play(Global.current_weapon + "_idle")
* Add some lines to the start of the _process() function
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")
* Tweak first if statement, change to:
if Input.is_action_pressed("ui_select") and can_shoot:
* Remove everything after the stab line, and add
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")
==== Part 8 ====
**Goal: Enemy Death and Raycasting**
* Edit the ui.gd script. Tweak the can_shoot variable, set it in the object scope (top) and remove "var" from "var can_shoot" from the _process()
* Access the 3D view of the player. Add a new node as a child of the camera 3D, type: RayCast3D
* Set the target position to y:0, z: -20
* Make sure the raycast has access to collision mask layers 1+2
* Check the guard scene, collision should be on layer 2, mask 1+2
* Check Player is layer 1, mask is 1+2
* Edit the player script, add new const at the top
@onready var ui_script = $ui
@onready var ray = $Camera3D/RayCast3D
* At the bottom, just above move_and_slide(), add the following code:
if Input.is_action_just_pressed("ui_accept"):
if ui_script.can_shoot:
shoot()
* Add the shoot function
func shoot():
if ray.is_colliding() and ray.get_collider().has_method("die"):
ray.get_collider().die()
* To allow strafing, change the ''If Input.is_action_just_pressed("ui_accept"):'' to
if Input.is_action_pressed("ui_accept"):
===== Part 9 =====
**Goal: HUD labels and player death**
* Add a player health variable to the player.gd script
var player_health = 100
* Create two new Label nodes as a child of the UI, name one 'health'.
* Create a new function in the ui.gd script
function update_player_health():
$health.text = str(get_parent().player_health)
* Add the function to the bottom of the _process() function in the ui.gd script
update_player_health()
* Edit the player script and add a new function:
func damage():
player_health -= 10
print(player_health)
if player_health <= 0:
queue_free()
* Add a RayCast3D to the guard scene
* Set Y = 0, Z = 5
* After the ''play(shoot)'' line in the attack() function, add:
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**
* Import the sound effect files (gun.ogg, machine.ogg, mini.ogg)
* In the player scene, add a new node, AudioStreamPlayer.
* Add the following code to the player shoot() function:
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()