This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
godot [2024/03/18 12:07] 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 ===== | ===== First Steps ===== | ||
Line 42: | Line 45: | ||
* In the world scene, drag the player scene into the world | * 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 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 Convext Collision Sibling' mesh to the player's MeshInstance3D | + | * 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 | * Set up WASD controls in the Project Settings | ||
Line 103: | Line 106: | ||
$AnimatedSprite2D.play("knife_idle") | $AnimatedSprite2D.play("knife_idle") | ||
</code> | </code> | ||
- | * Set the ammo and current_weapon to 'gun'. Test. | + | * Set the ammo to '3' and current_weapon to 'gun'. Test. |
* Create a global script. File -> New Script (based on Node). Name it global. | * Create a global script. File -> New Script (based on Node). Name it global. | ||
- | * Move the ammo and current weapon variables from the player script to the global script. | + | * 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 | * Open the project settings, in Autoload, add the global script and mark it global | ||
* Tweak the project setting page, allow fullscreen too | * 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 | * 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 | * 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> | ||
+ | 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 | ||
+ | |||
+ | </code> | ||
+ | |||
==== Part 6 ==== | ==== Part 6 ==== | ||
Line 131: | Line 201: | ||
if Global.current_weapon != "knife" and Global.ammo <= 0: | if Global.current_weapon != "knife" and Global.ammo <= 0: | ||
Global.current_weapon = "knife" | Global.current_weapon = "knife" | ||
- | $AnimatedSprite2D.player("knife_idle") | + | $AnimatedSprite2D.play("knife_idle") |
</code> | </code> | ||
* Tweak first if statement, change to: | * Tweak first if statement, change to: | ||
Line 163: | Line 233: | ||
$AnimatedSprite2D.play(Global.current_weapon + "_idle") | $AnimatedSprite2D.play(Global.current_weapon + "_idle") | ||
</code> | </code> | ||
- | ==== Part 7 ==== | ||
- | <code guard.gd> | + | The full code for ui.gd should now be: |
- | extends CharacterBody3D | + | <code file ui.gd> |
+ | extends CanvasLayer | ||
- | @onready var player : CharacterBody3D = get_tree().get_first_node_in_group("player") | + | var time_since_last_shot = 0.0 |
+ | var fire_rate = 1.0 | ||
- | 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(): | func _ready(): | ||
- | add_to_group("enemy") | + | $AnimatedSprite2D.animation_finished.connect(_on_AnimatedSprite2D_animation_finished) |
+ | $AnimatedSprite2D.play(Global.current_weapon + "_idle") | ||
- | func _physics_process(delta): | + | func _process(delta): |
- | if dead or is_attacking: # Check if the enemy is dead or attacking | + | time_since_last_shot += delta |
- | return | + | var can_shoot = time_since_last_shot >= (1.0 / fire_rate) |
- | + | ||
- | if player == null: | + | |
- | return | + | |
- | + | ||
- | var dir = player.global_position - global_position | + | |
- | dir.y = 0.0 | + | |
- | dir = dir.normalized() | + | |
- | velocity = dir * SPEED | + | if Global.current_weapon != "knife" and Global.ammo <= 0: |
- | # Add the gravity. | + | Global.current_weapon = "knife" |
- | if not is_on_floor(): | + | $AnimatedSprite2D.play("knife_idle") |
- | velocity.y -= gravity * delta | + | |
+ | 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") | ||
- | move_and_slide() | ||
- | attack() | ||
- | func attack(): | + | </code> |
- | 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 | + | |
+ | ==== 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> | ||
- | func die(): | + | ===== Part 9 ===== |
- | dead = true # Corrected variable scope | + | **Goal: HUD labels and player death** |
- | $AnimatedSprite3D.play("die") | + | * Add a player health variable to the player.gd script |
- | $CollisionShape3D.disabled = true | + | <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> |