GameMaker Studio 2

创建于:2017-04-19

创建人: dougen

192 信息 1091 成员
游戏开发工具 GameMaker Studio 2 的讨论小组

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

highway★ 2018-11-15

作者:Nathan Ranney

翻译:highway★

Image title

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


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

基础设置

此条目需要比上一个篇文章(绘制精灵)多一些动画,因此在开始之前,你需要将这些动画添加到工程中。 从此链接下载精灵并将其添加到你的项目中。 我已经恰当地命名了文件,因此只要确保精灵的名称与文件名相匹配,就可以将其添加到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_stateattack_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。 这可以防止在更改精灵时动画在错误的帧上启动。

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

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

animation_control();

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

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

(转发自:原日志地址

近期喜欢的会员

 

加入 indienova

  • 建立个人/工作室档案
  • 建立开发中的游戏档案
  • 关注个人/工作室动态
  • 寻找合作伙伴共同开发
  • 寻求线上发行
  • 更多服务……
登录/注册