GameMaker Studio 2

创建于:2017-04-19

创建人: dougen

148 信息 569 成员
游戏开发工具 GameMaker Studio 2 的讨论小组
GameMaker Studio 2小组导航 :介绍、愿景、内容
Linpean 2017-10-27

我最近半年开始学习GameMaker Studio 2(以下简称GMS2),向往着变成一名独立开发者,同时也在了解GMS2的引擎操作,阅读F1帮助文档,趴教程。可以说 GMS2 的中文用户很少,百度贴吧关注人数也未破万,资料很不成体系,很多前人的资料和文章都未被人整理,压在不知名的角落,我希望能够帮助其他的中文用户了解GMS2,所以建立一个快速导航,帮助大家快速开始GameMaker Studio 2的学习。

愿景和管理工作

希望大家不要在公开的帖子中留下联系方法或询问他人的联系方式,而是将有关一个话题的讨论都公开在帖子中,如果真的是要私下联系,可以使用私信的功能。我不希望看到的是一个人提出一个问题后通过私下的沟通解决了问题,而关于解决问题的过程并没有留在帖子中,无法给搜索到这个帖子的人提供帮助。

  • 希望大家不要重复发帖,将与同一话题的讨论集中到一个帖子中
  • 希望大家在发表帖子和回复之前先考虑一下措辞,事后发现错误也及时订正
  • 希望大家不要发表无价值的回复,当你只是觉得某一个帖子好的时候,请使用「点赞」功能

管理员的工作主要是维护秩序,维护的主要目标是方便他人检索和阅读信息,希望大家不要感到不满。

  • 修改帖子标题以符合帖子内容,通常会修改几乎所有帖子标题
  • 修改正文和回复中明显的错别字、将代码编辑代码块
  • 将重复的帖子导向讨论更多的帖子,并移除新的重复的帖子
  • 删除重复或无意义的回复,删除不当内容

导航模块

  • 快速索引社区:国内比较大的GameMaker中文社区和推荐的QQ群
  • 求助:提出你在使用过程中遇到的问题,寻找需要的功能和插件,管理员会不定期整理QA内容,防止日经QA
  • 开发:讨论与游戏开发有关的话题,或发布你自己开发的游戏
  • 分享:原创文章、资料汇总、翻译自官网的文章和文档

快速索引社区

  1. Yoyogames官网
  2. GMS2官方文档
  3. GMS2文档红色激情汉化版
  4. GameMaker开发者之家(维护中)
  5. GameMaker百度贴吧
  6. GameMakerStudio2 Wiki(正在创建中)
  7. GameMaker开发者之家QQ群:235271204
  8. GameMaker:Studio2 菜鸟群:102797189
  9. GameMaker:Studio2入门小站

求助整合

  1. 官网如何购买,如何注册?
  2. 国内更新太慢,怎么办?


开发

    待续

[ 分享:入门 ] Make an RPG:开始我们的RPG之旅
Linpean 2017-10-23

文档说明

本系列是油管上的HeartBeast[Beginner] Make an RPG课程的中文笔记,主要形式是截图的方式进行步骤上的说明。

由于原教程是基于GMS1版本的,我这里是用GMS2版本进行制作的,界面和部分函数都有变化,有错漏的地方,请参考原视频和官方帮助文档。接触GameMaker时,苦于国内没有完整的一个RPG教程,诺娃上的青铜的幻想GameMaker: Studio 中文教程可惜因为作者的工作,没有持续更新下去。而红激的教程也是以FC上的小游戏为切入点,只好上油管,HeartBeast的教程很丰富,有平台跳跃、射击、也有角色扮演。

面向对象:GameMaker新手,以学习一门脚本编程语言,制作一个RPG游戏为目标的爱好者。

主要包括以下内容(蓝色标识已完成,目前进度 :18/30):

相关参考资料:

  1. GMS官方说明文档
  2. [Beginner] Make an RPG--HeartBeast
  3. GameMaker: Studio 中文教程--青铜的幻想
  4. GMS2官方中文教程系列--顺子
北京独立游戏团队诚寻美术同伴!有追求的小伙伴请随时勾兑~
ethliu 2019-03-13

坐标北京,团队配备有资深策划一名、美术策划剧情全能的程序一名、发行商务一名、兼职美术好友若干

目前寻求可全职一起讨论和工作的美术伙伴一名,有偿~

我们做游戏的目的是追求优质的游戏体验,欢迎有同样追求自我追求的美术小伙伴前来勾兑勾兑!!

随时联系我都可以~ QQ: 139103270

译:[GMS2] SEALS OF THE BYGONE中随机生成平台游戏关卡的方法
highway★ 2019-02-23

作者:Logan Foster
译:highway★

Image title

游戏体验的多样性是Roguelite游戏的支柱之一。算法生成的关卡,从平台游戏的角度来说,存在很多挑战,对于刚入手GMS的新手来说也比较复杂。比较折中的方式是将预制的关卡区块链接组合在一起,下面是我在Seals of the Bygone中制作随机关卡的方法,用到了GMS2的图层系统。


摘要

从本质上说,我们要为关卡创建一些不同的区块(section)。每个区块包含子区块(subsections),其中包含我们的资产(asset)。通过使用代码来动态切换子区块的开关(on/off),我们就可以随机关卡了。(译注:当然这不如dead cells那种算法加预制的随机方法效果好,不过对于刚入坑这里的朋友来说应该也是种不错的选择,而且在区块中来控制关卡设计,也相对纯算法容易一些,比如做risk of rain类型的游戏,应该是很够用了)

Image title

我们没有在代码或外部资源中存储关卡区块信息,而是在Room Editor中创建它们,并存储在图层中,就跟做其他关卡一样。这也是我创建这个系统的主要原因,我想保留现有的设计流程,该流程基于Room Editor。


配置

第1步:创建父区块

首先,我们要为每个主要结构创建空白资产层。区块的数量并没有限制,在下面的示例关卡里,我们创建了3个区块,名为"Section1","Section2","Section3"。这些资产层基本上就相当于结构文件夹,也可以在代码中引用。

Image title

第2步:创建子区块

现在,创建子区块的其他空白资产层。这些图层将保存我们构成关卡的tile和object。这里我们将其命名为"Sub1_1","Sub1_2"等。


第3步:子区块的图层

最后,我们将构成关卡的tile、asset和object层也加入到这些子区块中。上图中,子区块"Sub2_1"是打开的,并存在"Collision2_1"和"Tiles2_1"这类图层。这些就是我们构建关卡要用到的层。

Image title

现在我们完成了所有设置,你可以切换父组的可见性,以便查看子区块的不同组合。我建议你在完成设计子区块后使用锁定功能,以确保不会意外的编辑它。


代码

我们已经搞定了关卡区块,并存储在不同的层中,现在可以来试试在关卡开始的时候随机选择哪些层了。在Room Start事件中敲下面的代码。

Image title

首先,我们定义要用于区块和子区块的前缀。我们将使用它们的名字来找到我们要用的层,请确保这些命名与你在Room Editor中的层相同。然后我们循环查看有多少不同的区块。

Image title

现在我们知道有多少区块了,遍历每个区块并查看有多少个子区块。然后选择一个随机的子区块来使用。

Image title

在决定使用哪个子区块之后,我们循环并销毁其他所有子区块(在该区块内)。最后我们通过设置层的可见性进行清理,以确保一切正确。如图,这里的图层名称是"硬编码"的,所以你需要将它们更改为你的区块包含的内容。我们可以动态的执行此操作,但这里没什么必要。

结论

完事儿了,很简单的解决方案,可以为你的关卡增添一些随机性。如果你想做一些更强大的东西,可以试试用layer_xlayer_y移动一些东西来扩展这个系统。

如果你有什么问题,可以随时联系我@rologfos
谢谢阅读!




2/23/2019
H

(转发自:原日志地址
求助帖,200毫秒内双击两次方向键进入奔跑状态
陈康 2019-01-21

部分步事件代码
/// @description 人物状态更新
#region 按键输入
CtrlArr[PRO_CTRL.Null] = keyboard_check(vk_nokey);
CtrlArr[PRO_CTRL.Left] = keyboard_check(vk_left);
CtrlArr[PRO_CTRL.Right] = keyboard_check(vk_right);
CtrlArr[PRO_CTRL.Up] = keyboard_check(vk_up);
CtrlArr[PRO_CTRL.Down] = keyboard_check(vk_down);
CtrlArr[PRO_CTRL.Fight] = keyboard_check(ord("X"));
CtrlArr[PRO_CTRL.DeputyFight] = keyboard_check_pressed(ord("Z"));
CtrlArr[PRO_CTRL.Elude] = keyboard_check_pressed(vk_space);
CtrlArr[PRO_CTRL.DeputyAction] = keyboard_check_pressed(ord("V"));
CtrlArr[PRO_CTRL.Jump] = keyboard_check_pressed(ord("C"));
CtrlArr[PRO_CTRL.Rest] = keyboard_check_pressed(ord("E"));

#endregion


JumpTrigger = CtrlArr[PRO_CTRL.Jump];//跳跃触发
IsTop = place_meeting(x,y-1,obj_land);//头顶
IsGround = place_meeting(x,y+1,obj_land);//地面
#region 垂直状态判定
switch (InitializeYState)
{
case PRO_YACTION.Stop :
if (!IsTop && JumpTrigger && (JumpMin < JumpMax))//跳跃按键和跳跃次数
{
InitializeYState = PRO_YACTION.Skip;
}else if (!IsGround)
{
InitializeYState = PRO_YACTION.Fall;
}
break;
case PRO_YACTION.Skip :
{
InitializeYState = PRO_YACTION.Rise;//上升状态
}
break;
case PRO_YACTION.Rise :
{
if (IsTop)
{
InitializeYState = PRO_YACTION.Stop;
}else if (!IsTop && JumpTrigger && (JumpMin < JumpMax))//按下跳跃且跳跃次数大于可跳跃次数
{
InitializeYState = PRO_YACTION.Skip;
}else if (Vsp >= 0)
{
InitializeYState = PRO_YACTION.Fall;//Y向上大于0,大于零在空中
}
}
break;
case PRO_YACTION.Fall :
{
if (JumpTrigger && (JumpMin < JumpMax))//按下跳跃且跳跃次数大于可跳跃次数
{
InitializeYState = PRO_YACTION.Skip;
}else if (IsGround)
{
InitializeYState = PRO_YACTION.Stop;
}
}
break;

}
#endregion


LeftHold = CtrlArr[PRO_CTRL.Left];
RightHold = CtrlArr[PRO_CTRL.Right];

SquatHold = CtrlArr[PRO_CTRL.Down];

LeftMeet = place_meeting(x-1,y,obj_land);
RightMeet = place_meeting(x+1,y,obj_land);

#region 水平状态判定
switch (InitializeXState)
{
case PRO_XACTION.Stand :
{
if (!LeftMeet && LeftHold && !RightHold && IsGround )//左边没碰到仅左键按下
{
InitializeXState = PRO_XACTION.LeftRush;
......
夜太深,脑子转不过来.如果有志同道合的人士,请赐教

下蹲移动和跳跃算是共存了

左右移动,C跳跃,下蹲,X攻击.问双击方向键的想法分享.

菜鸟学到的吐血教训:别写类似ds_list[| 0] = [ds_map1,"aaa",0]这样的代码……
Rusty 2018-12-02

因为在做一个带有卡牌要素的游戏,用ds_map和ds_list管理卡牌信息、牌序等等十分方便,于是当时就随手写出了类似ds_list[| 0] = [ds_map1,"aaa",0]这样的代码。现在想写存档系统了,结果一看教程,懵逼了。如果需要通过json_encode这种函数将ds数据转化为json,同时又是ds_map中含有ds_list或者ds_list中含有ds_map这种情况时,需要先使用ds_list_mark_as_map、ds_map_add_list等函数让ds_map和ds_list可以被正确写入和读取。而如果是前面这种ds_list含数组,数组里又含ds_map的形式,ds_list_mark_as_map、ds_map_add_list等函数貌似就不适用了,转成json来存档读档就会出错。于是乎不得不把ds连同相关的自定义function也改一遍……不幸中的万幸就是这部分代码本来就是刚学GMS的时候写的,也该优化了……

总之给同为新手的朋友一句话:如果想用json的格式来保存存档信息,就千万别因为一时方便就写成类似ds_list[| 0] = [ds_map1,"aaa",0]这样的代码,可以写成ds_list[| 0]=ds_map1; ds_map1[? "string"]="aaa"; ds_map[? "number"]=0;这样,把"aaa"、0这类写在ds_map里,才能正常用json函数创建/读取存档文件……

不过也可能有把ds_list[| 0] = [ds_map1,"aaa",0]这种函数正常转写成json的方法,只不过我太菜没注意到……

总之,供各位朋友参考了……含泪发布。

(转发自:原日志地址
【求助】steam版本的GMS2,代码中的中文显示不正常
慢好多拍 2018-11-20

Image titleImage title

如图中显示,steam版本的GMS2,中文在代码中显示为“?”,字体设置里面的中文也被显示为“?”,排除是输入法的问题。之后我即便整个汉化之后,问题还是存在,不知道有什么解决办法吗?非常感谢!


GAMEMAKER STUDIO 2中实现2D动态光影效果(PART 2)
顺子 2018-11-15

教程原文:Realtime 2D Lighting in GameMaker Studio 2 - Part 2

翻译:顺子

在上一篇文章中,我想您展示了如何创建 2D 光影系统的基础知识,本文将继续沿用上一篇中的示例,我们将进一步完善它使其更像真实的点光源。我们还将介绍一些着色器的使用以便于实现这一效果——没错,使用着色器会更方便!——然后让光源也具备颜色,让我们先来看看上一次我们做到哪儿了。

Image title

在上一篇文章最后,我们说接下来的任务是让光线半径以外所有的区域都变黑,因为这更像一个“点光源”,因此我们先来实现这个效果。在我们真正了解这个机制之前,我们先把之前绘制的阴影都放到一个表面(surface)上,因为这么做更方便后期使用着色器进行处理。请注意,以下的变化将穿插于不同的脚本之间,因此可能会有一点跳——让我们开始吧!在光源对象的创建事件中我们要添加一个新的变量:

surf = -1 ;

然后在绘制事件的顶部添加以下内容,这将会在首次运行绘制事件时创建一个新的表面——如果这个表面不见了也会重新创建。

if( !surface_exists(surf) ){
    surf = surface_create(room_width,room_height);
}

接下来我们找到检测 tile 的循环,并且在vertex_begin()之前,我们将该表面设置为当前表面,这样我们的阴影就会被渲染到这个表面上而不是屏幕上。我们将修改我们的循环代码,它现在看起来像这样......

surface_set_target(surf);
draw_clear_alpha(0,0);

vertex_begin(VBuffer, VertexFormat);
for(var yy=starty;yy<=endy;yy++)
{
    for(var xx=startx;xx<=endx;xx++)
    {
            // Shadow volume creation. 
    }
}
vertex_end(VBuffer);    
vertex_submit(VBuffer,pr_trianglelist,-1);
surface_reset_target();

draw_surface(surf,0,0);

现在,除了我们修改了阴影渲染的表面以外应该跟之前没有任何变化,你可能已经注意到了在代码中我们把这个表面设置为黑色并且 alpha 为 0。这一点非常重要,这样做我们才能把这个表面混合显示,然后使得没有阴影的区域正确显示出地面来。

OK,现在进入真正有趣的部分!我们现在要创建一个着色器(shader),并让它来处理这个新的表面。同样,目前来看这一切都没有任何的不同。我们在资源树中着色器一栏单击右键并选择“创建”,这将会自动添加一个新的着色器并打开编辑窗口。这将为我们自动生成一个简单的默认着色器,我们要做的就是在绘制先前那个表面之前先设置这个着色器,然后在绘制完表面后进行重置。这个操作(假设你调用了你的着色器 shader0)将只影响中间调用 draw_surface() 的部分。

Image title

shader_set(shader0);
draw_surface(surf,0,0);
shader_reset();

照旧,目前运行起来还是跟之前没有任何区别,但是我们现在可以做一些有趣的事情了!首先我们需要传递几个用户自定义的值,直接传给我们绘制灯光的事件。我们把这段代码添加到 Fragment Shader 中 void main() 的上方。

uniform vec4 u_fLightPositionRadius;        // x=lightx, y=lighty, z=light radius, w=unused

接下来,我们需要把场景的坐标从 Vertex Shader 传递到 Fragment Shader 中。这很容易,因为之前我们在场景中哪些坐标绘制阴影都已经保存下来了,只需要把这些坐标传递过去就行了。为此我们需要另外为 Vertex shader添加一个新的值,如下所示:

varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenPos;

然后在 Vertex Shader 脚本的底部“}”前添加

v_vScreenPos =  vec2( in_Position.x, in_Position.y );

你还需要把“varying vec2 v_vScreenPos;”这一行放进 Fragment shader 里。现在 Fragment shader 已经有了一切要素,只需要把光源的坐标传到绘制事件中即可。为此我们需要获取光源对象的创建事件中 u_fLightPositionRadius 变量的值。我们要在最开始就去处理,因为这个过程实在不够快,因此如果我们在一开始就去处理,在后面真正要用的时候就会更高效一些。让我们在光源的创建事件底部添加以下内容:

LightPosRadius = shader_get_uniform(shader0,"u_fLightPositionRadius");

现在我们可以直接在光源的绘制事件中使用它了,应当在我们设置 shader 以及绘制阴影表面之间

shader_set(shader0);
shader_set_uniform_f( LightPosRadius, lx,ly,rad,0.0 );
draw_surface(surf,0,0);
shader_reset();

现在,我们运行一下程序——没错,看上去还是没有任何变化——但至少要确保能正常运行!

到此为止,我们已经可以在 fragment shader 中使用这些额外的信息。包括获取场景坐标(v_vScreenPos)并计算其到光源的位置(u_fLightPositionRadius.xy)的距离,然后检查这个距离是否大于光照半径,如果不是则输出黑色像素。你还可以继续增大光照半径(绘制事件中的 rad变量 256 左右)。让我们来看看我们如何在 Fragment Shader 中使用它,以下是 shader 全文:

varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenPos;


uniform vec4 u_fLightPositionRadius;        // x=lightx, y=lighty, z=light radius, w=unused

void main()
{
    // Work out vector from room location to the light
    vec2 vect = vec2( v_vScreenPos.x-u_fLightPositionRadius.x, v_vScreenPos.y-u_fLightPositionRadius.y );

    // work out the length of this vector
    float dist = sqrt(vect.x*vect.x + vect.y*vect.y);

    // if in range use the shadow texture, if not it's black.
    if( dist< u_fLightPositionRadius.z ){
        gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
    }else{
        gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
}

如你所见,这是一个非常简单的计算,但是会给我们一个很棒的效果,可以很好的区分出点光源,并将光照半径之外的所有区域都变成黑色。

Image title

看这效果多酷!移动鼠标时我们可以发现所有的投影都会跟随我们鼠标而移动位置。我们还可以进一步提升效果,只需要略微降低ALPHA 值使画面更透明一些即可,只需要在 Fragment shader 最后的“}”之前添加以下内容

gl_FragColor.a *= 0.5 ;

这么做是为了获取最终的 alpha 值(在我们的示例中是在影子区域为 1.0,其它区域则为 0.0),降低透明度就会得到以下画面


Image title

在上面你可能会注意到,当我计算出 vect 变量时我调用了 * vec2 (xx,yy)* 。我这么做是为了让它更清晰,但实际上这并不是必须的。着色器作用于向量,这意味着我们可以同时执行多个操作。这条线应该是这样的:

vec2 vect = v_vScreenPos.xy-u_fLightPositionRadius.xy;

这么做是为了能在一个操作中获取屏幕上某个点的 xy 坐标并减去光源的坐标 xy 然后直接存储到 vec2 中。如果你能明白这一点,就使用这个新的语句,如果不明白就还是用原来的写法,这对这个着色器而言没什么区别但在其它某些着色器中可能区别就大了,你可以试着去尝试一些新的东西,这么做可以让你的代码更精炼,启动更快。

接下来我们要添加一些颜色,但在此之前,我们想要让光线的边缘变得不那么硬,而是一个渐渐削弱的效果。毕竟在现实中除非你非常靠近光源本身,否则你看不到这么强烈的光线边缘,事实上光线会逐渐消失,所以我们在这里添加这样的效果会使得整体效果更好。我们要让光线随着光照半径线性削弱,如果你足够聪明,可以在这里使用曲线或类似的方法来实现。这个衰减的计算非常简单,实际上我们已经拥有了所有需要的值。跟光源的距离以及光照半径就是我们所需要的所有数值。目前我们已经有一些简单的代码是在光照半径内去调节 alpha 值在 0.0 到 1.0 之间的缩放 (光照半径内外),因此现在我们用 LERP(线性插值)来处理阴影的值来完成整个阴影的渲染绘制,以下是 Fragment shader 中的内容:

//在范围内使用阴影纹理,否则全黑。
if( dist< u_fLightPositionRadius.z ){
    // 从光源中央往半径将值从 0 变为 1
    float falloff = dist/u_fLightPositionRadius.z;          
    // 获得阴影纹理
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );        
    // 阴影线性变为全黑
    gl_FragColor = mix( gl_FragColor, vec4(0.0,0.0,0.0,0.7), falloff);          
}else{
    // 超出半径的直接全黑
    gl_FragColor = vec4(0.0, 0.0, 0.0, 0.7);
}

着色器中的 LERP 方法名为 mix(很奇怪),但事实上就是一个东西。这个值会根据衰减值逐渐从 gl_FragColor 混合(个人猜测)处理成 0,0,0,0.7 (全黑)。当你从脚本尾部删除 gl_FragColor.a *= 0.5;以后,这就会呈现出一个很棒的衰减效果,更像真实的明暗处理。

Image title

接下来我们要添加颜色。首先,我们双击场景中的实例,并将颜色设置为绿色,如下所示:

Image title

完成这个操作后,再修改我们绘制表面的光线绘制事件,这样我们可以把颜色传进去,比如下面这样:

draw_surface_ext(surf,0,0,1,1,0,image_blend,0.5);

这将会把绿色(以及 0.5 的 alpha 值)传递给着色器,然后我们就可以用来设置光线颜色。接下来我们需要修改 Fragment shader 的代码让着色器也处理颜色。所以我们之前写着**gl_FragColor = v_vColour * texture2D()**的地方现在要改成:

    vec4 col =  texture2D( gm_BaseTexture, v_vTexcoord );
    if( col.a<=0.01 ){
        gl_FragColor = v_vColour;           
    }else{      
        gl_FragColor = col;
    }

我们检测 alpha 值(col.a 的值),当它为 1.0 时则为全黑阴影区域,0.0 则为非阴影区,所以这是个非常容易理解的数字。我们也没有必要拿这个值跟 0.0 进行精确比较,因为我们永远也不能确定这个值到底是多少,所以我们需要预留一点误差空间。现在我们运行程序的话,你会看到一道可爱的绿光!(译者:要想生活过得去,嗯~)

Image title

要想点亮多个光源,你只要重复以上操作即可,混合区域的处理会稍微有些复杂(即如何让黑色区域和着色区域保持本色),但这才是核心关键。如果你找不到正确的混合模式,那就把所有的内容传递给着色器并让它来帮你处理一切,记住默认的 Application Surface 也只是一个表面,同样可以用着色器来渲染。另外要记住你没法同时读取并写入同一个表面,因此你可能会需要一个临时表面。

终于……你做到了,你已经完成了一个属于自己的实时光影系统!在下面的图里你可以看到看到我把一个光源穿越多个动态光源的效果。点击此处可以查看相关效果的视频,其中使用的就是与我介绍的类似的系统,尽管我还做了一些更复杂的处理(比如在单个表面上绘制多个阴影),你当然没有必要去尝试并把这些东西复制到你自己的光影系统里,你的系统已经能够实现各种基础的功能了。

译者:最后这个图的效果,光照着本教程是无法完成的,算是原作者留的课后思考题吧,另外自己按照教程实现了一个小示例——网盘下载:密码:bymz,仅供参考

Image title


(转发自:原日志地址
【译文】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上关注我,并在我的网站上关注更多与游戏开发相关的内容。

(转发自:原日志地址
【译文】GAMEMAKER STUDIO系列:构建更好的动画系统
highway★ 2018-11-15

作者:Nathan Ranney

翻译:highway★


在开发Kerfuffle(译注:游戏挂了,过于追求视觉效果、没钱、再加上一些其他问题,他们现在在做Knight Club)时,我需要一个动画系统,允许我在游戏中保持任何单独的动画帧(译注:格斗游戏/动作游戏为了提升打击感,会采用帧冻结的技术),而无需手动添加或删除精灵帧。 我还需要能够根据当前动画帧触发某些动作。 使用此设置,我可以创建hitbox,播放声音或更改状态,同时完全控制屏幕上绘制的所有内容。

变量

这些是与动画系统相关的重要变量。 如果后面你感到困惑,请回头再仔细看看。


frameSpeed

The speed in FPS that the game counts through the frames to be displayed. Default is 1.游戏通过要显示的帧计算的FPS速度。 默认值为1。

frameCounter

Increases every frame by the frameSpeed.

每帧按frameSpeed递增


currentFrame

The frame of the sprite currently being drawn on the screen.

当前正在屏幕上绘制的精灵帧


frameData

Current list of frame data the game is reading from, based on the animation that needs to play. Idle, run, attack, etc.

游戏正在读取的当前帧数据列表,基于需要播放的动画。 如空闲,奔跑,攻击等


frameDuration

Total number of in game frames to display the current sprite frame.

显示当前精灵帧的游戏帧总数。


maxFrames

The total number of frames in any given sprite.

任何给定精灵中的帧总数。


animSprite

The actual name of the sprite resource in GameMaker. sprMomo_Idle, for example.

GameMaker中精灵资源的实际名称。 例如,sprMomo_Idle。


脚本

后面我们要用到的脚本。

frame_reset();

//将frameCounter和currentFrame重置为0
frameCounter = 0;
currentFrame = 0;

animation_set();

该脚本接受两个参数。 首先,frameData(相关的帧数据列表)和第二个是animSprite(你想要绘制的精灵资源)

//animation_set ( argument0, argument1 );

frameData = argument0;
animSprite = argument1;

帧数据

每个动画都需要一个帧数据列表。 这是一个列表,其中包含每帧动画播放的游戏帧数量。 每个数据列表都使用以下命名约定。 frameDataIdle,frameDataRun,frameDataDash等。 

(译注:如果你初次接触这些并对这些内容感兴趣,可以扩展阅读一下,google搜一下街霸系列的frame data,会有很多更详细的资料。btw  indienova如何插入表格呢?)

Momo(我们游戏中的一个角色)空闲动画的帧数据

Image title

请注意,所有列表和值都以0开头。因此,即使此动画有12帧,列表中的最大数字也是11。这包括你要显示的帧! 如果你希望它在游戏中显示5帧,则列表中的值应为4。

** GMS特定说明**

确保在不再使用时手动删除列表! 否则你可能会遇到内存泄漏!

帧计数器

现在我们有一个帧数据列表,我们需要实际根据该数据设置动画。 我们需要做的第一件事是弄清楚maxFrame是什么。

maxFrames = sprite_get_number( animSprite ) - 1;

然后,如果您的currentFrame恰好大于或等于最大帧,并当frameCounter大于或等于精灵帧应出现在屏幕上的最大帧数,则重置为第一帧。

if ( currentFrame >= maxFrames - 1 && frameCounter == frameDuration ) 
{
     frame_reset();
}