adding state machine week 3

This commit is contained in:
OddlyTimbot 2025-04-28 15:56:42 -04:00
parent c811c75e49
commit efae634f41
8 changed files with 308 additions and 4 deletions

View File

@ -12,7 +12,7 @@ config_version=5
config/name="GodotSpeedRun" config/name="GodotSpeedRun"
run/main_scene="res://scenes/game.tscn" run/main_scene="res://scenes/game.tscn"
config/features=PackedStringArray("4.3", "Forward Plus") config/features=PackedStringArray("4.4", "Forward Plus")
config/icon="res://icon.svg" config/icon="res://icon.svg"
[file_customization] [file_customization]

View File

@ -1,7 +1,7 @@
[gd_scene load_steps=8 format=3 uid="uid://y083suj12rld"] [gd_scene load_steps=8 format=3 uid="uid://y083suj12rld"]
[ext_resource type="Script" path="res://scripts/gamecontroller.gd" id="1_77wyw"] [ext_resource type="Script" uid="uid://cjym0i568q43q" path="res://scripts/gamecontroller.gd" id="1_77wyw"]
[ext_resource type="Script" path="res://scripts/CharacterBody2D.gd" id="1_u4sui"] [ext_resource type="Script" uid="uid://cxtom61f1i8rm" path="res://scripts/CharacterBody2D.gd" id="1_u4sui"]
[ext_resource type="PackedScene" uid="uid://bqdsxxxm7gmj4" path="res://scenes/trigger.tscn" id="3_8dqk0"] [ext_resource type="PackedScene" uid="uid://bqdsxxxm7gmj4" path="res://scenes/trigger.tscn" id="3_8dqk0"]
[sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_3u4a8"] [sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_3u4a8"]

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://bqdsxxxm7gmj4"] [gd_scene load_steps=3 format=3 uid="uid://bqdsxxxm7gmj4"]
[ext_resource type="Script" path="res://scripts/Trigger.gd" id="1_4rgu6"] [ext_resource type="Script" uid="uid://bopx7olk57553" path="res://scripts/Trigger.gd" id="1_4rgu6"]
[sub_resource type="CircleShape2D" id="CircleShape2D_x8qck"] [sub_resource type="CircleShape2D" id="CircleShape2D_x8qck"]
radius = 60.2993 radius = 60.2993

View File

@ -0,0 +1 @@
uid://cxtom61f1i8rm

View File

@ -0,0 +1 @@
uid://bopx7olk57553

View File

@ -0,0 +1 @@
uid://cjym0i568q43q

158
week3/character_update.md Normal file
View File

@ -0,0 +1,158 @@
Updating the character
There are two improvements we can make to the character.
1. Animating the player character to make them look cool.
2. Improving the standard character controller.
Lets start with the second option, and improve the feel of the character.
## Jump Buffer
A jump buffer is a way to allow the player to jump even if their feet are not firmly planted on the ground. Call it a "forgiveness" for trying to jump when they are close to landing from a previous jump, but not quite there.
One way to achieve this is with a timer. When the player presses the jump button, the timer begins and continues to tick along for the set amount of time.
Every time the game loop runs, we can then check for if the player is on the ground, and if the timer still has time left in it we know they previously hit the jump button, and we can trigger the jump.
```
if Input.is_action_just_pressed("ui_accept"):
##start the timer, but don't jump yet...
jump_buffer_timer.start()
if is_on_floor() && jump_buffer_timer.time_left > 0:
velocity.y = JUMP_VELOCITY
jump_buffer_timer.stop()
```
This can provide a very nice way of improving the feel of the character, improving the perception of responsiveness.
## Acceleration or Deceleration of Running
Our second way of improving responsiveness is to introduce acceleration and decelleration into the character controller.
First, we need a variable we can use for control of acceleration:
`@export var acceleration:int = 5 `
Now we can apply this factor to movement.
```
var direction := Input.get_axis("ui_left", "ui_right")
## Apply acceleration and deceleration to movement
if direction == 0:
velocity.x = move_toward(velocity.x, 0, acceleration)
else:
velocity.x = move_toward(velocity.x, SPEED * direction, acceleration)
```
The move_toward function accepts three values: from, to, and a "delta", in other words how much it should move by.
Since this function will be called repeatedly by the game loop, a higher delta will cause the time to accelerate or decelerate to be shorter.
## Hard Gravity
Sometimes in games it produces a better result for game mechanics to change the effect of gravity depending on the state of the player. As an example, we can have gravity affect the player more during the downward part of a jump (in other words while they are falling).
This is possible because during the game loop gravity is always being applied to the player character, pushing them downward at a certain force. When they jump, an opposing force (velocity) is applied that allows them to overcome the downward push of gravity. But the gravity eats away at that jump force until it is overcome, and the player begins to fall. This is the apex of their jump, after which they are no longer moving upwards but rather moving downwards.
That "falling" state can be detected by a simple check:
`if not is_on_floor() && velocity.y > 0:`
In this scenario, the player is not on the floor, but their velocity is greater than zero - in other words gravity has kicked in stopping their jump upwards movement and is now pulling them downwards. If that is the case, they are now falling and we can take the opportunity to apply a harder gravity.
Here is how we normally apply gravity:
`velocity += get_gravity() * delta`
All we have to do is use a different gravity when the player is in the falling state, which we detected as above:
`velocity += get_gravity() * hard_gravity * delta`
In our physics process loop, we are constantly applying the gravity. If the player is in the "jump" state we should apply normal gravity, and if they are in the "falling" state we should apply the hard gravity.
```
func _physics_process(delta: float) -> void:
# Add the gravity.
if not is_on_floor():
velocity += get_gravity() * delta
```
To complete the function, we need to start keeping track of what state the player is in. This calls for a code pattern known as a **finite state machine**
Let's begin our state machine by using it to keep track of whether the player is in the "jump" state, or the "falling" state.
## State Machine
To begin, we can use an Enum to list out the possible states of the player. (You recall we used an Enum to list out the directions the player could be facing in the last lesson.)
```
enum State{IDLE, WALK, JUMP, FALLING}
var current_state: State = State.IDLE
```
Lets take another look at our Jump Buffer, implemented above. We have the perfect place to identify the JUMP state there.
```
if is_on_floor() && jump_buffer_timer.time_left > 0:
velocity.y = JUMP_VELOCITY
current_state = State.JUMP
jump_buffer_timer.stop()
```
Note the addition of the state change to `State.JUMP`. That is the main thing we need!
```
if current_state == State.JUMP:
#apply normal gravity during up of jump action
velocity += get_gravity() * delta
else:
#Character is falling, apply hard gravity
velocity += get_gravity() * hard_gravity * delta
```
Now if the player is in the "JUMP" state they get normal gravity, otherwise they get the hard gravity.
This solution works well, but the issue right now is that our state machine is only tracking the JUMP state. We need to flesh it out with rules for changing state to our other possible states such as IDLE, and WALK.
For that reason, we should put the state handling into it's own function. A nice way to do this is using a `match` conditional, also known as a switch-case in other languages.
```
func update_states()->void:
match current_state:
State.IDLE when velocity.x !=0:
current_state = State.WALK
##player stops walking, or starts falling
State.WALK:
if velocity.x == 0:
current_state = State.IDLE
if not is_on_floor() && velocity.y > 0:
current_state = State.FALLING
##when jump peaks, we start to fall
State.JUMP when velocity.y > 0:
current_state = State.FALLING
##player lands, either still or moving
State.FALLING when is_on_floor():
if velocity.x == 0:
current_state = State.IDLE
else:
current_state = State.WALK
```
Now that we are tracking all the states, a function to update the animation based on the state should be straightforward.
```
func update_animation()-> void:
match current_state:
State.IDLE:
playerGraphic.play("idle")
State.RUN:
playerGraphic.play("run")
State.JUMP:
playerGraphic.play("jump")
State.FALLING:
playerGraphic.play("fall")
```

View File

@ -0,0 +1,143 @@
extends CharacterBody2D
##week2 additions:
## states
## acceleration/deceleration
## hard gravity
## jump buffer
## shove attack
@onready var jump_buffer_timer: Timer = $jump_buffer_timer
@onready var coyote_timer: Timer = $coyote_timer
@onready var right_cast: RayCast2D = $RightCast
@onready var left_cast: RayCast2D = $LeftCast
@onready var playerGraphic: AnimatedSprite2D = $AnimatedSprite2D
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
const PUSH_FORCE:int = 500
@export var bump_power = 50
@export var acceleration:int = 5
@export var hard_gravity:int = 3
enum State{IDLE, RUN, JUMP, FALLING}
var current_state: State = State.IDLE
enum FaceDirection{LEFT, RIGHT}
var facing:FaceDirection = FaceDirection.RIGHT
var pushTarget:RigidBody2D
var pushEnabled:bool = false
func _physics_process(delta: float) -> void:
handle_input()
update_movement(delta)
update_states()
update_animation()
move_and_slide()
handle_collisions()
func handle_input() -> void:
#Jumping action
if Input.is_action_just_pressed("ui_accept"):
jump_buffer_timer.start()
if is_on_floor() && jump_buffer_timer.time_left > 0:
velocity.y = JUMP_VELOCITY
current_state = State.JUMP
jump_buffer_timer.stop()
#Walking action
var direction := Input.get_axis("ui_left", "ui_right")
#######REFACTOR################
## Apply acceleration and deceleration to movement
if direction == 0:
velocity.x = move_toward(velocity.x, 0, acceleration)
else:
velocity.x = move_toward(velocity.x, SPEED * direction, acceleration)
if direction <0:
facing=FaceDirection.LEFT
playerGraphic.flip_h = true
else:
facing = FaceDirection.RIGHT
playerGraphic.flip_h = false
if Input.is_action_just_pressed("shove") && pushEnabled:
match facing:
FaceDirection.RIGHT:
#push right
pushTarget.apply_central_impulse(Vector2(1,0) * PUSH_FORCE)
pushEnabled = false
FaceDirection.LEFT:
#push left
pushTarget.apply_central_impulse(Vector2(-1,0) * PUSH_FORCE)
pushEnabled = false
##Add the hard gravity in here
func update_movement(delta:float)-> void:
# Add the gravity.
if current_state == State.JUMP:
#apply normal gravity during up of jump action
velocity += get_gravity() * delta
else:
#Character is falling
velocity += get_gravity() * hard_gravity * delta
func update_states()->void:
match current_state:
State.IDLE when velocity.x !=0:
current_state = State.RUN
##player stops walking, or starts falling
State.RUN:
if velocity.x == 0:
current_state = State.IDLE
##player walks off of a ledge (no jump)
if not is_on_floor() && velocity.y > 0:
current_state = State.FALLING
##when jump peaks, we start to fall
State.JUMP when velocity.y > 0:
current_state = State.FALLING
##player lands, either still or moving
State.FALLING when is_on_floor():
if velocity.x == 0:
current_state = State.IDLE
else:
current_state = State.RUN
func update_animation()-> void:
match current_state:
State.IDLE:
playerGraphic.play("idle")
State.RUN:
playerGraphic.play("run")
State.JUMP:
playerGraphic.play("jump")
State.FALLING:
playerGraphic.play("fall")
func handle_collisions()-> void:
#react to objects around
for i in get_slide_collision_count():
var c = get_slide_collision(i)
if c.get_collider() is RigidBody2D:
c.get_collider().apply_central_impulse(-c.get_normal() * bump_power)
if right_cast.is_colliding() && facing==FaceDirection.RIGHT:
var collider = right_cast.get_collider()
if collider is Node && collider is RigidBody2D:
pushTarget = collider
pushEnabled = true
if left_cast.is_colliding() && facing==FaceDirection.LEFT:
var collider = left_cast.get_collider()
if collider is Node && collider is RigidBody2D:
pushTarget = collider
pushEnabled = true
if not right_cast.is_colliding() and not left_cast.is_colliding():
pushEnabled = false