adding state machine week 3
This commit is contained in:
parent
c811c75e49
commit
efae634f41
@ -12,7 +12,7 @@ config_version=5
|
||||
|
||||
config/name="GodotSpeedRun"
|
||||
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"
|
||||
|
||||
[file_customization]
|
||||
|
@ -1,7 +1,7 @@
|
||||
[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" path="res://scripts/CharacterBody2D.gd" id="1_u4sui"]
|
||||
[ext_resource type="Script" uid="uid://cjym0i568q43q" path="res://scripts/gamecontroller.gd" id="1_77wyw"]
|
||||
[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"]
|
||||
|
||||
[sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_3u4a8"]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[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"]
|
||||
radius = 60.2993
|
||||
|
1
week1/GodotSpeedRun/scripts/CharacterBody2D.gd.uid
Normal file
1
week1/GodotSpeedRun/scripts/CharacterBody2D.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cxtom61f1i8rm
|
1
week1/GodotSpeedRun/scripts/Trigger.gd.uid
Normal file
1
week1/GodotSpeedRun/scripts/Trigger.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bopx7olk57553
|
1
week1/GodotSpeedRun/scripts/gamecontroller.gd.uid
Normal file
1
week1/GodotSpeedRun/scripts/gamecontroller.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cjym0i568q43q
|
158
week3/character_update.md
Normal file
158
week3/character_update.md
Normal 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")
|
||||
```
|
143
week3/updated_project/scripts/player.gd
Normal file
143
week3/updated_project/scripts/player.gd
Normal 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
|
Loading…
Reference in New Issue
Block a user