译文|GAMEMAKER STUDIO 系列:简单状态机

作者:highway★
2018-12-10
10 11 2

作者:Nathan Ranney

翻译:highway★


按照设计,状态机一次只能处于一种状态。 由我们来定义对我们的情况有意义的状态,以及它们之间的关系。 在本文中,我们将使用状态机来控制在任何给定时间可用的玩家操作,允许我们设置角色并定义角色可以执行的操作。

大家好, 今天我想告诉你如何设置一个简单的状态机。 状态机是一种数据结构,顾名思义,它跟踪不同的状态。 例如,我们的游戏可能有三种状态:“游戏运行”,“游戏暂停”和“游戏结束”。我们可能会使用状态机来记住哪一个处于活动状态,并定义如何从一个状态转换到另一个状态(请参阅 上面的图片)。

基础设置

此条目需要比上一个篇文章(绘制精灵)多一些动画,因此在开始之前,你需要将这些动画添加到工程中。 从此链接下载精灵并将其添加到你的项目中。 我已经恰当地命名了文件,因此只要确保精灵的名称与文件名相匹配,就可以将其添加到 GMS 中。 继续添加所有精灵 - 甚至是敌人的精灵 - 因为我们将在以后的文章中需要这些精灵。 确保每个精灵的原点是 (16,32)

枚举,控制器和持久性

为了设置我们的状态机,我们首先确定哪些状态是可能的,以及我们如何在代码中识别它们。 由于这个例子都是关于角色动作的,所以让我们定义那些动作是什么,并给每个动作一个整数 id。 最简单的方法是使用 enum(枚举,Enumeration 的缩写),它是在自定义变量类型下保存的常量的集合。 如果你熟悉 Gamemaker 中的 Macros(宏),那么枚举就是这样的。 我更喜欢使用枚举,因为它们比宏更容易管理和跟踪。

创建一个脚本并将其命名为enum_init。 添加以下行。
//states enum states 
{     
    normal,     
    crouch,      
    attack,     
    hit 
}

请注意,我们不必设置每个条目的值,枚举自动将值0指定为“正常”,然后在每个条目后递增。 我们可以随时获得值

var example = states.attack; 
show_message(string(example));  //output: 3

实际上,你可以通过为每个条目指定值来覆盖枚举的自动编号,但重要的是要重申枚举是常量。 它们定义后无法更改!

枚举也是全局的,这意味着任何对象都可以访问它们。 这对我们的状态机来说非常完美。

现在我们有了枚举,我们在哪里实例化它? 从非持久对象调用这些变量,比如我们的 oPlayer 对象,不是最好的主意。 我们想要做的是创建一个持久的控制器对象(它始终存在),以管理许多对象可以访问的枚举和其他数据类型之类的东西。 继续创建一个新对象并将其命名为“con”(译注:建议还是 oController 这样与其他对象保持相同的命名前缀,在看代码时会比较易读。)。 我喜欢保持我的控制器名称简短,因为它更容易返回。选中新对象上的“持久(Persistent)”框。 最后,将 Create 事件添加到对象,并添加以下面的代码:

///init 初始化 
enum_init();

con 对象放在你的房间里。 由于此对象是持久的,因此除非您明确销毁,否则它将继续存在! 无需在每个房间放置此物体。

Switch cases

既然我们已经在枚举中定义了状态,我们就可以从我们的玩家对象中访问它们了。 打开我们在上一个条目中创建的 oPlayer 对象,并将以下行添加到 create 事件中。

attack = false; 

//states 
currentState = 0; 
lastState = 0; 

//movement 
xSpeed = 0; 
ySpeed = 0; 
lastSprite = sprite;

End Step 事件添加到 oPlayer,然后添加一些代码。

xPos = x; 
yPos = y; 

x += xSpeed; 
y += ySpeed;  

//animation 
frame_reset();

现在让我们跳到 step 事件。 我们可以删除我们在之前那篇文章中添加的几乎所有代码,因为大多数代码只是为了展示 draw_sprite_ext 的不同部分。 查看下面的代码,并确保你的 step 事件看起来完全相同。

//buttons 
player_buttons(); 
 
//animation 
frame_counter();  

//state switch 
switch currentState 
{     
    case states.normal:         
        normal_state();     
    break;      
    
    case states.crouch:         
        crouch_state();     
    break;      

    case states.attack:         
        attack_state();     
    break; 
}

如果你之前从未见过 switch 语句,你可能会想知道到底发生了什么。 我稍后会解释,但首先我们需要创建三个新脚本:normal_state,crouch_state attack_state。 我喜欢使用不同状态的脚本,因为它使代码更容易阅读。 你可以弹出所需的任何脚本(译注:在 GMS2 中,在代码中的脚本上按下鼠标中键即可弹出对应的脚本,并链接在当前对象窗口),并在该特定状态下工作。

好吧,所有这一切究竟意味着什么呢? 什么是 switch 语句以及它是如何工作的? 将 switch 语句视为 if 语句的更具体版本。if 语句用于布尔值检查,条件满足则执行,switch 语句用于根据变量的值执行代码。 看看下面的代码块。

//if statement 
if(currentState == states.normal)
{     
    normal_state(); 
} else if(currentState == states.crouch) {     
    crouch_state(); 
}  

//switch statement 
switch currentState 
{     
    case states.normal:         
        normal_state();     
    break;  
    
    case states.crouch:         
        crouch_state();     
    break; 
}

这两段代码在功能上都是相同的。 它们都将根据 currentState 变量的当前值运行我们想要的脚本,但 switch 语句要清晰得多。 当我们添加状态时,使用 if 语句变得难以管理。 switch 语句更容易管理。

最后,我们需要在 player_buttons 脚本中添加一个新的按钮变量。 打开该脚本并添加此行:

attack = keyboard_check_pressed(ord("Z"));

状态机

我们已经定义了一个可能状态的枚举,以及变量 currentState 来跟踪哪个状态是活动的。 现在我们知道了 switch 语句的工作原理,我们可以创建在每个状态下执行的代码,以及在它们之间进行转换的规则。 switch 语句可以很容易地显示我们的状态机是什么以及它正在做什么。 如果我们的 currentState 变量等于语句中的一个 case,则执行与该 case 相关的代码。 由于我们为每个状态创建了脚本,因此请继续打开 normal_state 脚本并添加以下代码

//移动 
if(left)
{     
    xSpeed = -2; 
} else if(right) {     
    xSpeed = 2; 
} else {     
    xSpeed = 0; 
}

//切换到下蹲状态 
if(down)
{     
    currentState = states.crouch; 
}
  
//切换到攻击状态 
if(attack)
{     
    currentState = states.attack; 
}

这段代码非常简单。 对我来说,正常状态意味着角色的默认状态。 他们没有执行任何特殊操作,例如攻击或使用道具,玩家可以完全控制角色。 在这里,我们有左右移动,并转换到蹲和攻击状态。 如果你现在运行游戏,你将无法看到我们的状态机的全部效果。 如果你按 Z,你将改变状态,不再能够移动。 接下来让我们定义蹲状态。 打开 crouch_state 脚本并添加以下代码:

xSpeed = 0;  
if(!down)
{     
    currentState = states.normal; 
}

蹲下时(按住向下箭头键)我们停止玩家的水平移动(xSpeed = 0)。 如果他们释放向下键,我们将返回正常状态。 这将是一个在蹲下时添加不同动作的好地方,比如爬行或者可能是蹲下的攻击。
打开我们创建的最后一个状态脚本,attack_state,并添加以下代码:

xSpeed = 0;  

if(frame > sprite_get_number(sprite) - 1)
{     
    currentState = states.normal; 
}

我们再次将水平速度归零,并且当动画结束时我们将玩家状态设置回正常。 但是......我们还没有设置我们的动画,是吗? 动画控制是状态机和 switch 的另一个重要用途! 创建一个新脚本并将其命名为 animation_control。 添加以下代码:

xScale = approach(xScale,1,0.03); 
yScale = approach(yScale,1,0.03);  

//动画控制 
switch currentState 
{     
    case states.normal:         
        if(left)
        {             
            facing = -1;         
        } else if(right) {             
            facing = 1;         
        }                  
        if(left || right)
        {             
            sprite = sprPlayer_Run;         
        } else {             
            sprite = sprPlayer_Idle;         
        }     
    break; 
     
    case states.crouch:         
        sprite = sprPlayer_Crouch;     
    break;      

    case states.attack:         
        sprite = sprPlayer_Attack;     
    break; 
} 
 
//如果精灵更改,则将帧重置为0 
if (lastSprite != sprite)
{     
    lastSprite = sprite;     
    frame = 0; 
}

通过使用另一个 switch 语句,我们可以轻松控制播放器动画。 请注意,我们可以在 switch case 中使用 if 语句! 我们没有将动画控制与我们创建的初始 switch 语句组合在一起的原因有几个。 首先,我们希望我们的动画在所有代码的最后发生。 动画是之前发生的一切的结果! 其次,它让代码更好读。 上面代码底部的最后一个表达式会在精灵更改时将帧重置为 0。 这可以防止在更改精灵时动画在错误的帧上启动。

请注意,我们将 xScaleyScale 代码移动到 animation_control 的顶部。 这对以后很重要。

oPlayer 对象中打开 end step 事件,并将以下行添加到代码的底部。 这将确保它在其他一切之后发生。

animation_control();

来吧,运行游戏。 你应该有一个能够左右奔跑,空闲,蹲和攻击的角色了! 我们能够根据当前状态区分角色的行为。 除了管理优势之外,设置状态机还可以更轻松地跟踪错误,添加新行为以及跟踪对象的整体结构。 我经常使用状态机和 switch 语句来控制比如要显示的菜单屏幕,当前的游戏模式以及定义给敌人的 AI 类型等内容。

谢谢阅读! 在 Twitter 上关注我,并在我的网站上关注更多与游戏开发相关的内容。

本文为用户投稿,不代表 indienova 观点。

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. yellow 2018-12-10

    标题那个图在《游戏编程模式》(Robert Nystrom ) 介绍状态模式的时候也提到了,@highway★,可以看看。

  2. 黑轮酱 2018-12-10

    我刚用以前别人那个脚本改的差不多了……

您需要登录或者注册后才能发表评论

登录/注册