User Tools

Site Tools


godot

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
godot [2024/03/15 14:16]
admin created
godot [2024/04/08 19:33] (current)
admin
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 5: Line 6:
 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/​ 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 =====
 +  * 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 ====
 +**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
 +
 +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"​)
 +
 +
 +</​code>​
 +  * 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:
 +<​code>​
 +else:
 +  current_weapon = "​knife"​
 +  $AnimatedSprite2D.play("​knife_idle"​)
 +</​code>​
 +  * Set the ammo 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 player 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 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 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 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>​
godot.1710512201.txt.gz ยท Last modified: 2024/03/15 14:16 by admin