状态设计模式
简介
当有许多状态需要处理,但一次只能将一个脚本附加到一个节点上时,编写游戏脚本是很困难的.与其在玩家的控制脚本中创建一个状态机,不如将状态分离出来,分成不同的类,这样会使开发更加简单.
用Godot实现状态机的方法有很多,下面是一些其他方法:
玩家的每一个状态都可以有一个子节点,在使用时会被调用.
Enums可以与匹配语句一起使用.
状态脚本本身可以在运行时动态地从一个节点上换掉.
本教程将只专注于添加和删除附加有状态脚本的节点.每个状态脚本将是不同状态的实现.
注解
这里有一个很好的资源来解释状态设计模式的概念 : https://gameprogrammingpatterns.com/state.html
脚本设置
继承的特性对于开始使用这个设计原则是很有用的.应该创建一个类来描述玩家的基本功能.现在,一个玩家将被限制为两个动作. 向左移动 , 向右移动 .这意味着将有两种状态.**闲置** 和 运行 .
下面是通用状态,所有其他状态都将从该状态继承.
GDScript
# state.gd
extends Node2D
class_name State
var change_state
var animated_sprite
var persistent_state
var velocity = 0
# Writing _delta instead of delta here prevents the unused variable warning.
func _physics_process(_delta):
persistent_state.move_and_slide(persistent_state.velocity, Vector2.UP)
func setup(change_state, animated_sprite, persistent_state):
self.change_state = change_state
self.animated_sprite = animated_sprite
self.persistent_state = persistent_state
func move_left():
pass
func move_right():
pass
对上面的脚本做一些说明.首先,这个实现使用了一个``setup(change_state, animated_sprite, persistent_state)``方法来分配引用.这些引用将在这个状态的父体中被实例化.这有助于在编程中被称为*内聚的东西.玩家的状态不希望承担创建这些变量的责任,但确实希望能够使用它们.然而,这确实使状态与状态的父体*耦合.这意味着,状态高度依赖于它是否有一个包含这些变量的父体.所以,请记住,当涉及到代码管理时,*耦合*和*内聚*是重要的概念.
注解
有关内聚力和耦合的更多详情,请参见以下网页:https://courses.cs.washington.edu/courses/cse403/96sp/coupling-cohesion.html
其次,脚本中还有一些移动的方法,但没有实现.这一点很重要.
第三,这里实际上实现了 _physics_process(delta)
方法.这使得状态可以有一个默认的 _physics_process(delta)
实现,其中 velocity
用于移动玩家.状态可以修改玩家移动的方法是使用定义在其基类中的 velocity
变量.
最后,这个脚本实际上被指定为一个名为 State
的类.这使得重构代码变得更容易,因为在Godot中使用 load()
和 preload()
函数的文件路径将不再需要.
所以,现在有了基础状态,前面讨论的两种状态就可以实现了.
GDScript
# idle_state.gd
extends State
class_name IdleState
func _ready():
animated_sprite.play("idle")
func _flip_direction():
animated_sprite.flip_h = not animated_sprite.flip_h
func move_left():
if animated_sprite.flip_h:
change_state.call_func("run")
else:
_flip_direction()
func move_right():
if not animated_sprite.flip_h:
change_state.call_func("run")
else:
_flip_direction()
GDScript
# run_state.gd
extends State
class_name RunState
var move_speed = Vector2(180, 0)
var min_move_speed = 0.005
var friction = 0.32
func _ready():
animated_sprite.play("run")
if animated_sprite.flip_h:
move_speed.x *= -1
persistent_state.velocity += move_speed
func _physics_process(_delta):
if abs(persistent_state.velocity.x) < min_move_speed:
change_state.call_func("idle")
persistent_state.velocity.x *= friction
func move_left():
if animated_sprite.flip_h:
persistent_state.velocity += move_speed
else:
change_state.call_func("idle")
func move_right():
if not animated_sprite.flip_h:
persistent_state.velocity += move_speed
else:
change_state.call_func("idle")
注解
由于 Run
和 Idle
状态是从 State
延伸出来的,而 State
又是 Node2D
的延伸,所以函数 _physics_process(delta)
是从 底向上 调用的,也就是说 Run
和 Idle
将调用它们的实现 _physics_process(delta)
.然后 State
将调用它的实现,然后 Node2D
将调用它自己的实现,以此类推.这可能看起来很奇怪,但它只与预定义函数有关,如 _ready()
、 _process(delta)
等.自定义函数使用正常的继承规则,即覆盖基础实现.
有一种迂回的方法可以获得一个状态实例.可以使用状态工厂.
GDScript
# state_factory.gd
class_name StateFactory
var states
func _init():
states = {
"idle": IdleState,
"run": RunState
}
func get_state(state_name):
if states.has(state_name):
return states.get(state_name)
else:
printerr("No state ", state_name, " in state factory!")
这将在字典中查找状态,如果找到则返回状态.
现在,所有的状态都用自己的脚本定义了,现在是时候弄清楚如何实例化那些传递给它们的引用了.由于这些引用不会改变,所以调用这个新脚本``persistent_state.gd``是有意义的.
GDScript
# persistent_state.gd
extends KinematicBody2D
class_name PersistentState
var state
var state_factory
var velocity = Vector2()
func _ready():
state_factory = StateFactory.new()
change_state("idle")
# Input code was placed here for tutorial purposes.
func _process(_delta):
if Input.is_action_pressed("ui_left"):
move_left()
elif Input.is_action_pressed("ui_right"):
move_right()
func move_left():
state.move_left()
func move_right():
state.move_right()
func change_state(new_state_name):
if state != null:
state.queue_free()
state = state_factory.get_state(new_state_name).new()
state.setup(funcref(self, "change_state"), $AnimatedSprite, self)
state.name = "current_state"
add_child(state)
注解
persistent_state.gd
脚本包含检测输入的代码.这是为了使教程简单化,但通常这样做不是最好的做法.
项目设置
本教程做了一个假设,即它要连接的节点包含一个子节点,这个子节点是一个 AnimatedSprite.还有一个假设是,这个 AnimatedSprite.
注解
本教程中使用的骆驼的压缩文件是:下载: here <files/llama.zip> .源自 piskel_llama ,但我在那个页面上找不到原创作者的信息……还有一个好的精灵动画教程已经有了.参见 2D精灵动画 .
所以,唯一必须附加的脚本是 persistent_state.gd
,它应该附加在玩家的顶部节点上,这是一个 KinematicBody2D .
现在玩家已经利用状态设计模式实现了它的两种不同的状态.这种模式的好处是,如果想要添加另一个状态,那么就需要创建另一个类,而这个类只需要关注自己以及如何变化到另一个状态.每个状态在功能上是分离的,并且是动态实例化的.