This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
godot [2024/03/17 19:57] admin |
godot [2024/09/09 13:50] (current) admin [Part 5] |
||
---|---|---|---|
Line 1: | Line 1: | ||
====== Getting Started with Godot ====== | ====== 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]]. | 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]]. | ||
Line 6: | Line 7: | ||
All the code we use is available on AJ's Learning Lab github - https://github.com/AJsLearningLab/GodotFPS | 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 | ||
+ | <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) | ||
+ | </code> | ||
+ | * 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 ==== | ==== Part 3 ==== | ||
- | <code file hud.gd> | + | **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: | ||
+ | <code file ui.gd> | ||
extends CanvasLayer | extends CanvasLayer | ||
Line 34: | Line 95: | ||
</code> | </code> | ||
+ | * Add the ui scene to the player scene (Drag drop from file system onto player node) | ||
- | ==== Part 7 ==== | + | ==== 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: | ||
+ | <code> | ||
+ | else: | ||
+ | current_weapon = "knife" | ||
+ | $AnimatedSprite2D.play("knife_idle") | ||
+ | </code> | ||
+ | * 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: | ||
+ | <code> | ||
+ | add_to_group("player") | ||
+ | </code> | ||
+ | * Select the guard root node. Create a new script, use the code below: | ||
<code guard.gd> | <code guard.gd> | ||
extends CharacterBody3D | extends CharacterBody3D | ||
Line 86: | Line 178: | ||
$CollisionShape3D.disabled = true | $CollisionShape3D.disabled = true | ||
+ | </code> | ||
+ | |||
+ | |||
+ | ==== 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 | ||
+ | <code> | ||
+ | var time_since_last_shot = 0.0 | ||
+ | var fire_rate = 1.0 | ||
+ | </code> | ||
+ | * Add a line to the _ready() function | ||
+ | <code> | ||
+ | $AnimatedSprite2D.play(Global.current_weapon + "_idle") | ||
+ | </code> | ||
+ | * Add some lines to the start of the _process() function | ||
+ | <code> | ||
+ | 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") | ||
+ | </code> | ||
+ | * Tweak first if statement, change to: | ||
+ | <code> | ||
+ | if Input.is_action_pressed("ui_select") and can_shoot: | ||
+ | </code> | ||
+ | * Remove everything after the stab line, and add | ||
+ | <code> | ||
+ | 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") | ||
+ | </code> | ||
+ | |||
+ | The full code for ui.gd should now be: | ||
+ | <code file 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") | ||
+ | |||
+ | |||
+ | </code> | ||
+ | |||
+ | ==== 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 | ||
+ | <code> | ||
+ | @onready var ui_script = $ui | ||
+ | @onready var ray = $Camera3D/RayCast3D | ||
+ | </code> | ||
+ | * At the bottom, just above move_and_slide(), add the following code: | ||
+ | <code> | ||
+ | if Input.is_action_just_pressed("ui_accept"): | ||
+ | if ui_script.can_shoot: | ||
+ | shoot() | ||
+ | </code> | ||
+ | * Add the shoot function | ||
+ | <code bash> | ||
+ | func shoot(): | ||
+ | if ray.is_colliding() and ray.get_collider().has_method("die"): | ||
+ | ray.get_collider().die() | ||
+ | </code> | ||
+ | * To allow strafing, change the ''If Input.is_action_just_pressed("ui_accept"):'' to | ||
+ | <code> | ||
+ | if Input.is_action_pressed("ui_accept"): | ||
+ | </code> | ||
+ | |||
+ | ===== Part 9 ===== | ||
+ | **Goal: HUD labels and player death** | ||
+ | * Add a player health variable to the player.gd script | ||
+ | <code> | ||
+ | var player_health = 100 | ||
+ | </code> | ||
+ | * Create two new Label nodes as a child of the UI, name one 'health'. | ||
+ | * Create a new function in the ui.gd script | ||
+ | <code> | ||
+ | function update_player_health(): | ||
+ | $health.text = str(get_parent().player_health) | ||
+ | </code> | ||
+ | * Add the function to the bottom of the _process() function in the ui.gd script | ||
+ | <code> | ||
+ | update_player_health() | ||
+ | </code> | ||
+ | * Edit the player script and add a new function: | ||
+ | <code> | ||
+ | func damage(): | ||
+ | player_health -= 10 | ||
+ | print(player_health) | ||
+ | if player_health <= 0: | ||
+ | queue_free() | ||
+ | </code> | ||
+ | * Add a RayCast3D to the guard scene | ||
+ | * Set Y = 0, Z = 5 | ||
+ | * After the ''play(shoot)'' line in the attack() function, add: | ||
+ | <code> | ||
+ | if $RayCast3D.is_colliding() and $RayCast3D.get_collider().has_method('damage'): | ||
+ | $RayCast3D.get_collider().damage() | ||
+ | </code> | ||
+ | |||
+ | ===== 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: | ||
+ | <code> | ||
+ | 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() | ||
</code> | </code> |