|
|
|
|
Reply From: |
Hammer Bro. |
I’ve got an FSM at the heart of a platformer engine (see it in action here), and it’s been a breeze to use. It’s entirely in GDScript, and not a lot of script at that. There’s no need to make a specific C++ implementation, and in fact doing so would prove too restrictive when you wanted to make minor customizations.
At its heard is the State superclass, State.gd. Note that it’s slightly specialized for my own purposes:
# Superclass (faux interface) common to all finite state machine / pushdown automata.
var player = null
# Return the unique string name of the state. Must be overridden.
func get_name():
assert(false)
# Handle any transitions into this state. Subclasses should first chain to this method.
func enter(player):
self.player = player
_animate()
# Exit the current state, enter a new one.
func set_state(state):
player.state.exit()
player.state = state
state.enter(player)
# Transition to a new animation; by default, one matching the name of the State (if it exists).
# Can be overridden without chaining.
func _animate():
var name = get_name()
if player.animation_player.has_animation(name):
player.animation_player.play(name)
# Handle input events.
func _input(event):
pass
# Update physics processing.
func _fixed_process(delta):
pass
# Handle exit events.
func exit():
pass
For an object that makes use of this FSM, I’ve got the following bits of code. I won’t explain them here since you’re already familiar with them, but for anyone who’s interested, there’s a fantastic tutorial at http://gameprogrammingpatterns.com/state.html
func _ready():
# Initialize state.
state = Starting_State.new()
state.enter(self)
# The current state gets to intercept input events.
func _input(event):
# Send the input to the current state if we haven't already handled it.
var new_state = state._input(event)
# Switch states but don't forward input, because presumably that event was handled.
if new_state != null:
printt("Input", new_state.get_name())
state.set_state(new_state)
# Let the current state handle the processing logic; also handle the changing of states.
func _fixed_process(delta):
# Update the current state; handle switching.
_state_loop("_fixed_process", delta)
# Call the given function with the given arg and iterate if state changes.
func _state_loop(function, arg):
# Keep a list of old states to prevent cycles.
var old_states = []
var new_state = state.call(function, arg)
while new_state != null:
var new_state_name = new_state.get_name()
# Throw an exception if we re-enter a previously visited state this frame.
assert(old_states.find(new_state_name) == -1)
old_states.append(new_state_name)
set_state(new_state)
# Let our new state run this cycle since our old state ended.
new_state = state.call(function, arg)
The state loop is a bit nonstandard – I wanted to make sure states would instantaneously advance to a final resting state when multiple conflicting conditions were received, and I wanted to make sure that there weren’t any infinite loops. So far, so good.
Creating new states is dead simple. Just extend my custom State.gd class and override whatever the desired functionality is. For instance, Standing.gd:
extends "res://Player/States/State.gd"
const Backflipping = preload("res://Player/States/Backflipping.gd")
const Ducking = preload("res://Player/States/Ducking.gd")
const Falling = preload("res://Player/States/Falling.gd")
const Jumping = preload("res://Player/States/Jumping.gd")
const Walking = preload("res://Player/States/Walking.gd")
func get_name():
return "Standing"
func enter(player):
.enter(player)
# Reset double-jumps upon landing.
player.has_extra_jump = player.can_double_jump
func _input(event):
if player.should_jump(event) and not player.is_attacking():
player.get_tree().set_input_as_handled()
# Backflip.
if Input.is_action_pressed("player_up"):
return Backflipping.new()
# Regular Jump.
return Jumping.new()
func _fixed_process(delta):
if player.is_grounded() == false:
return Falling.new()
if Input.is_action_pressed("player_duck") or player.is_roof_blocked():
return Ducking.new()
if player.get_horizontal_movement() != 0 and not player.is_attacking():
return Walking.new()
I think I’ve got about 13 states at present, most of which ought to be present in that video. It’s definitely worth the effort to set up and get comfortable with a state machine; complex behaviors are so much more elegant and the separation of code is manageable.
A snapshot of the source code for the project in the video is available here.
Thanks for showing a showcase of FSM in GDS.
hexdump | 2016-08-09 08:51
This is a great example and has helped me a lot in my current project. The tutorial in the link is great too. Thanks!
Do you have an example of the set_state method? I’m trying to implement it in my project but I cannot find success.
Taceor | 2018-04-07 15:25
I’ve updated the answer to include set_state and a link to the entire project’s code for reference.
Hammer Bro. | 2018-04-08 17:01