DeruDeru
DeruDeru

Reputation: 75

How to implement player state machine

I am very new to godot, and making 2d action platformer. I am trying to implement player state machine by following Finite State Machine in Godot by Nathan Lovato from GDQuest. However I am not sure how exactly I should write my code. I would really appreciate if someone could teach me how implement player state machine.

Current Player script. Player can move right and left, jump, and double jump.

extends KinematicBody2D

const UP_DIRECTION := Vector2.UP

export var can_move = true

export var speed := 200.0
export var jump_strength := 450
export var maximum_jumps := 2
export var double_jump_strength := 400
export var gravity := 1200

var _jumps_made := 0
var _velocity := Vector2.ZERO


onready var position2D = $Position2D
onready var _animation_player: AnimationPlayer = $Position2D/PlayerSkinIK/AnimationPlayer



func _physics_process(delta: float) -> void:

    #Left and Right Movement Direction
    var _horizontal_direction = (
        Input.get_action_strength("move_right")
        - Input.get_action_strength("move_left")
    )

    #X and Y velocity
    _velocity.x = _horizontal_direction * speed
    _velocity.y += gravity * delta

    #Player State
    var is_falling := _velocity.y > 0.0 and not is_on_floor()
    var is_jumping := Input.is_action_just_pressed("jump") and is_on_floor()
    var is_double_jumping := Input.is_action_just_pressed("jump") and is_falling
    var is_jump_cancelled := Input.is_action_just_released("jump") and _velocity.y < 0.0
    var is_idling := is_on_floor() and is_zero_approx(_velocity.x)
    var is_running := is_on_floor() and not is_zero_approx(_velocity.x)


    #Jump Counter
    if is_jumping:
        _jumps_made += 1
        _velocity.y = -jump_strength
    elif is_double_jumping:
        _jumps_made += 1
        if _jumps_made <= maximum_jumps:
            _velocity.y = -double_jump_strength
    elif is_jump_cancelled:
        _velocity.y = 0.0
    elif is_idling or is_running:
        _jumps_made = 0

    if (can_move == true):
        #Velocity Calculation
        _velocity = move_and_slide(_velocity, UP_DIRECTION)     

    #Flip Sprite 
    if get_global_mouse_position().x > $Position2D/PlayerSkinIK.global_position.x:

        position2D.scale.x=1
    else:

        position2D.scale.x=-1

    #Play WalkForward animation when moving towards mouse, play WalkBackward animation when moving away from mouse
    if position2D.scale.x==1:

        if Input.is_action_pressed("move_right"):
            print("forward1")
            if is_on_floor():
                _animation_player.play("Player-Run IK")
        elif Input.is_action_pressed("move_left"):
            print("backward1")
            if is_on_floor():
                _animation_player.play("Player-Run IK Backward")

    elif position2D.scale.x==-1:

        if Input.is_action_pressed("move_left"):    
            print("forward2")           
            if is_on_floor():
                _animation_player.play("Player-Run IK")
        elif Input.is_action_pressed("move_right"):
            print("backward2")
            if is_on_floor():
                _animation_player.play("Player-Run IK")

    #Animation Control
    if is_jumping or is_double_jumping:
        _animation_player.play("Player-Jump IK")
    elif is_falling:
        _animation_player.play("Player-Fall IK")
    elif is_idling:
        _animation_player.play("Player-Idle IK")

Player Script:

extends KinematicBody2D

class_name Player

enum States {ON_GROUND, IN_AIR, GLIDING}

var _state : int = States.ON_GROUND


export var speed := 200.0

var _velocity := Vector2.ZERO

var glide_gravity := 1000.0
var base_gravity := 4000.0
var glide_acceleration := 1000.0
var glide_max_speed := 1000.0
var glide_jump_impulse := 500.0
var jump_impulse := 1200.0


func _physics_process(delta: float) -> void:
    
    var input_direction_x: float = (
        Input.get_action_strength("move_right")
        - Input.get_action_strength("move_left")
    )
    
    var is_jumping: bool = _state == States.ON_GROUND and Input.is_action_pressed("jump")
    
    if Input.is_action_just_pressed("glide") and _state == States.IN_AIR:
        _state = States.GLIDING

    # Canceling gliding.
    if _state == States.GLIDING and Input.is_action_just_pressed("move_up"):
        _state = States.IN_AIR

    # Calculating horizontal velocity.
    if _state == States.GLIDING:
        _velocity.x += input_direction_x * glide_acceleration * delta
        _velocity.x = min(_velocity.x, glide_max_speed)
    else:
        _velocity.x = input_direction_x * speed

    # Calculating vertical velocity.
    var gravity := glide_gravity if _state == States.GLIDING else base_gravity
    _velocity.y += gravity * delta
    if is_jumping:
        var impulse = glide_jump_impulse if _state == States.GLIDING else jump_impulse
        _velocity.y = -jump_impulse
        _state = States.IN_AIR

    # Moving the character.
    _velocity = move_and_slide(_velocity, Vector2.UP)

    # If we're gliding and we collide with something, we turn gliding off and the character falls.
    if _state == States.GLIDING and get_slide_count() > 0:
        _state = States.IN_AIR
    
    
    
    if is_on_floor():
        _state = States.ON_GROUND

    
func change_state(new_state: int) -> void:
    var previous_state := _state
    _state = new_state
    

Player Idle:

extends PlayerState

func enter(_msg := {}) -> void:
    player.velocity = Vector2.ZERO

func physics_update(_delta: float) -> void:
    if not player.is_on_floor():
        state_machine.transition_to("Air")
        return
        
    if Input.is_action_pressed("jump"):
        state_machine.transition_to("Air", {do_jump = true})
    elif Input.is_action_pressed("move_left") or Input.is_action_pressed("move_right"):
        state_machine.transition_to("Move")

Player Move:

extends PlayerState

func physics_update(delta: float) -> void:
    
    if not player.is_on_floor():
        state_machine.transition_to("Air")
        return
    
    var input_direction_x: float =(
        Input.get_action_strength("move_right")
        - Input.get_action_strength("move_left")
    )
    
    player.velocity.x = player.speed * input_direction_x
    player.velocity.y += player.graivty * delta
    player.velocity = player.move_and_slide(player.velocity, Vector2.UP)        
    
    
    if Input.is_action_pressed("jump"):
        state_machine.transition_to("Air", {do_jump = true})
    elif is_equal_approx(input_direction_x, 0.0):
        state_machine.transition_to("Idle")

Player State:

extends State

class_name PlayerState

var player: Player

func _ready() -> void:
    
    yield(owner, "ready")
    
    player = owner as Player
    
    assert(player != null)

State:

# Virtual base class for all states.
class_name State
extends Node

# Reference to the state machine, to call its `transition_to()` method directly.
# That's one unorthodox detail of our state implementation, as it adds a dependency between the
# state and the state machine objects, but we found it to be most efficient for our needs.
# The state machine node will set it.
var state_machine = null


# Virtual function. Receives events from the `_unhandled_input()` callback.
func handle_input(_event: InputEvent) -> void:
    pass


# Virtual function. Corresponds to the `_process()` callback.
func update(_delta: float) -> void:
    pass


# Virtual function. Corresponds to the `_physics_process()` callback.
func physics_update(_delta: float) -> void:
    pass


# Virtual function. Called by the state machine upon changing the active state. The `msg` parameter
# is a dictionary with arbitrary data the state can use to initialize itself.
func enter(_msg := {}) -> void:
    pass


# Virtual function. Called by the state machine before changing the active state. Use this function
# to clean up the state.
func exit() -> void:
    pass

State Machine:

# Generic state machine. Initializes states and delegates engine callbacks
# (_physics_process, _unhandled_input) to the active state.
class_name StateMachine
extends Node

# Emitted when transitioning to a new state.
signal transitioned(state_name)

# Path to the initial active state. We export it to be able to pick the initial state in the inspector.
export var initial_state := NodePath()

# The current active state. At the start of the game, we get the `initial_state`.
onready var state: State = get_node(initial_state)


func _ready() -> void:
    yield(owner, "ready")
    # The state machine assigns itself to the State objects' state_machine property.
    for child in get_children():
        child.state_machine = self
    state.enter()


# The state machine subscribes to node callbacks and delegates them to the state objects.
func _unhandled_input(event: InputEvent) -> void:
    state.handle_input(event)


func _process(delta: float) -> void:
    state.update(delta)


func _physics_process(delta: float) -> void:
    state.physics_update(delta)


# This function calls the current state's exit() function, then changes the active state,
# and calls its enter function.
# It optionally takes a `msg` dictionary to pass to the next state's enter() function.
func transition_to(target_state_name: String, msg: Dictionary = {}) -> void:
    # Safety check, you could use an assert() here to report an error if the state name is incorrect.
    # We don't use an assert here to help with code reuse. If you reuse a state in different state machines
    # but you don't want them all, they won't be able to transition to states that aren't in the scene tree.
    if not has_node(target_state_name):
        return

    state.exit()
    state = get_node(target_state_name)
    state.enter(msg)
    emit_signal("transitioned", state.name)

Upvotes: 1

Views: 1039

Answers (1)

NEXE
NEXE

Reputation: 49

Most of the time I start like this. I hope you will find it helpful.

enum States {IDLE, MOVE, JUMP}  
var current_state = States.IDLE  

func _state_logic():  
    match current_state:
        States.IDLE:
            #  STATE TRANSITION: IDLE -- MOVE
            if Input.is_aciton_pressed("move_player"):
                current_state = State.MOVE

            #  STATE TRANSITION: IDLE -- JUMP
            if Input.is_aciton_pressed("move_player"):
                current_state = State.JUMP

        States.MOVE:
            movement_direction = input_direction

            #  STATE TRANSITION: IDLE -- JUMP
            if Input.is_aciton_pressed("move_player"):
                current_state = State.JUMP

            #  STATE TRANSITION: MOVE -- IDLE
            if Input.is_action_released("move_player"):
                current_state = State.IDLE


        States.JUMP:
            handle_player_jump()

            # OTHER STATE TRANSITION CODE

Upvotes: -1

Related Questions