编者按
indienova 会员青铜的幻想为希望了解学习 GameMaker: Studio 的中文读者专门撰写了本系列教程。
本栏目已经有了专门的专题页面,请参看这里。根据教程四的结尾处的小调查,吃瓜群众们表示对人物的攻击技能和敌人 AI 十分有兴趣,因此接下来的几次教程将围绕这个相关方向展开。
欢迎读者朋友在文章后留言,以便作者能够继续针对性地安排接下来的教程内容。
教程目标
添加伊瑟拉的普通攻击和技能。
准备工作
这个系列教程的项目/代码及原始美术素材全部都在 GitHub 项目库。这个教程的内容基于目录 GMS_TUT_04 下的内容开始,完成后的项目文件放在目录 GMS_TUT_05。此次教程所需的美术资源可以在这里下载。
导入美术资源
首先需要导入相关的人物动画,讲到这里我很开心。因为呢,我对我们这个游戏《冰杖秘闻》的像素美术是十分欣赏的,也很希望能够分享更多的人物动画出来(更多视频及原画请移步官网欣赏)。这里是本次教程将要用到的两套动画——伊瑟拉的普通攻击(侧面、正面和背面)以及技能攻击:




在策划上,普通攻击是用手里的法杖发出一个魔法球,而技能攻击是一个伤害较低的范围减速,于是我们还需要一个飞行的魔法球以及范围减速效果的动画:


以上就是这次教程里用到的所有美术资源的动图,现在需要把它们导入 GMS 中去。
具体的导入操作在教程二中有详细的描述,特别要记得正确的把 Sprite 的原点设置在人物的脚下。下面是我所采用的 Sprite 命名,我建议的是所有的 Sprite 资源名称都以 spr_ 开头,同时尽量让名称能够表示该资源的用途或含义:
- 普通攻击侧面: spr_ysera_attack_side
- 普通攻击正面: spr_ysera_attack_front
- 普通攻击背面: spr_ysera_attack_back
- 特殊技能: spr_ysera_skill
- 魔法球: spr_ysera_magic_bullet
- 特殊技能效果: spr_ysera_skill_effect
将这些资源都放入 Sprites 分类里之前建立的 Character 目录中(红框中的是这次新导入的 Sprite 资源):

温馨小提示
如果你觉得在左侧的资源导航栏中,所显示的资源图片太小不容易看清,你可以通过以下设置来显示资源的大图标。在 File(文件)菜单的 Preferences(选项)中,勾选 Big Resource Tree Icons(资源树中显示大图标):

然后重启 GMS,图标就会放大一些,更容易看清对应的人物动作一点:

但这样也使得每屏能显示的资源数量变少了,所以路怎么走,你们自己选。
建立人物控制的脚本
在开始敲代码之前先回顾一下之前的人物控制相关的脚本,我们打开obj_ysera,可以看到当前她响应三个事件Create、Step以及与obj_scene_base的碰撞:

其中 Create 事件对应的脚本是:
phy_fixed_rotation = 1; //为了不让人物发生旋转
Step 事件对应的脚本是:
event_inherited(); YseraStep();
而碰撞事件只是为了注册人物和物体间的碰撞,暂时并没有脚本需要运行。
关于这几个脚本我要做的第一件事是在资源导航栏的 Scripts 分类中建立一个新的名为 YseraCreate 的脚本文件,并将 phy_fixed_rotation = 1 这句代码从 Create 事件的脚本移入这个脚本,然后将 Create 事件的脚本修改至如下:
event_inherited(); YseraCreate();
这个改动的目的如下:
obj_ysera作为“场景中的动态物体”(obj_scene_dynamic)的子类,理应执行任何在父类的 Create 事件中执行的脚本(通过调用event_inherited);- 将脚本内容移至 YseraCreate 脚本文件中,这样可以避免一部分代码放在事件窗口中,一部分代码放在脚本文件里的混乱。在这样调整之后,我们就可以很清楚的知道,所有和伊瑟拉这个人物相关的脚本是 YseraCreate 和 YseraStep 这两个脚本文件,下面的改动也都在这两个脚本文件中进行。
定义人物的朝向
再回顾一下 YseraCreate 和 YseraStep 这两个脚本的内容,是这样的。
YseraCreate:
phy_fixed_rotation = 1;
YseraStep:
image_speed = 0.25;
if(keyboard_check(ord('A'))){
phy_position_x = phy_position_x - 4;
sprite_index = spr_ysera_walk_side;
image_xscale = 1;
}
else if(keyboard_check(ord('D'))){
phy_position_x = phy_position_x + 4;
sprite_index = spr_ysera_walk_side;
image_xscale = -1;
}
else if(keyboard_check(ord('W'))){
phy_position_y = phy_position_y - 4;
sprite_index = spr_ysera_walk_back;
}
else if(keyboard_check(ord('S'))){
phy_position_y = phy_position_y + 4;
sprite_index = spr_ysera_walk_front;
}
else{
sprite_index = spr_ysera_idle;
}
接下来,首先我想要在YseraCreate脚本中加一个枚举(enum)来定义人物当前的朝向,因为在目前的代码中,人物移动时 phy_position_x 和 phy_position_y 的加减、人物在 x 轴的缩放 image_xscale、移动时需要采用的动画这几个因素都是和人物的朝向有关的,而且可以想象将要添加的攻击动画、魔法球的生成位置、魔法球的飞行方向还与朝向有关。
因为我们只有四个方向的动画,所以朝向定义为四个——上、下、左、右,然后在枚举定义后添加用于记录人物朝向的变量 m_playerDirection:
enum PlayerDirection{
UP,
DOWN,
LEFT,
RIGHT
}
m_playerDirection = PlayerDirection.DOWN;
(注释:为便于查看,此次教程中新增加的代码用浅橙色高亮显示)
默认的朝向设置成下方,因为我们设置的人物的初始图像是朝下的。然后我们在 YseraStep 脚本中根据玩家按键,将朝向设置成对应的值,例如玩家按A键时方向应设置为向左:
if(keyboard_check(ord('A')))
{
phy_position_x = phy_position_x - 4;
sprite_index = spr_ysera_walk_side;
image_xscale = 1;
m_playerDirection = PlayerDirection.LEFT;
}
播放攻击与技能动画
在记录了人物的朝向以后,我们就可以正确的选择当前的攻击动画了,所以接下来加入的功能是在玩家按键时播放攻击和技能的动画。
我不记得有没有在之前的教程里说过这个小提示,就是假如你当前还不清楚一个功能要怎么下手实现,你可以先尝试做些简单的改动让你的游戏看起来有这个功能。在尝试的过程中你就会更清楚下一步要怎样做。例如现在我们要做的就是这样,如果还不清楚人物的攻击过程一共包含哪些要做的步骤,那至少可以先播放一个人物动画。
还可以举一个例子,比如你在做游戏的背包系统,如果你觉得不知从何开始,那么不如先做一个最简单的功能就是当玩家按下打开背包的按键时,显示一个背包的 UI 出来。接下来再看这个 UI 需要哪些玩家数据以及如何让这个UI正确关联到玩家数据。至少这种方法对我个人在开发过程中一直有很好的效果。
接下来回到我们教程的项目,我们选择 J和 K 键分别作为攻击和技能的按键,那么在 YseraStep 脚本中响应的代码如下:
if(keyboard_check(ord('J'))){
switch(m_playerDirection)
{
case PlayerDirection.UP:
sprite_index = spr_ysera_attack_back;
break;
case PlayerDirection.DOWN:
sprite_index = spr_ysera_attack_front;
break;
case PlayerDirection.LEFT:
sprite_index = spr_ysera_attack_side;
break;
case PlayerDirection.RIGHT:
sprite_index = spr_ysera_attack_side;
break;
}
image_index = 0;
}
else if(keyboard_check(ord('K'))){
sprite_index = spr_ysera_skill;
image_index = 0;
}
为不同的情况设置 sprite_index,即当前需要播放的动画。其中语句 image_index=0 的意思是每次攻击和技能的动画的从第一帧开始播放。
增加人物状态变量
但如果仅仅是把这段代码加进 YseraStep 脚本中,你会发现人物还是没法正确的播放攻击和技能动画,这是因为之前控制人物移动的代码中的存在一个 else 语句会在玩家没有任何按键的时候播放玩家的站立动画,这样会导致在按下 J 键后,玩家正要播放攻击动画时,这个站立动画会立刻取代攻击动画。
所以这里需要再次整理一下我们想要的逻辑。之前的行走控制代码的逻辑是,当玩家按下四个方向的行走按键时,移动角色并播放相应的行走动画,若玩家没有按下任何按键,则播放站立动画。在加入了攻击和技能动画后,这个逻辑被打破了,因为在播放攻击和技能动画的过程中,即使这时玩家没有再按键,也应当等到动画播放完毕才能回到站立动画。与此同时,在攻击和释放技能的过程中,人物也应该无法移动。(在攻击的过程中无法移动会影响操作手感,此处为了教程内容的安排简化处理)
根据以上的逻辑,需要再为人物增加两个新的变量用来表示人物是否正在攻击过程中(m_isAttacking)和人物是否正在释放技能过程中(m_isInSkill)。在 YseraCreate 脚本中初始化这两个变量:
m_isAttacking = false; m_isInSkill = false;
同时在YseraStep脚本处理攻击和技能按键的代码处设置它们的值为 true:
if(keyboard_check(ord('J')))
{
m_isAttacking = true;
…...
}
else if(keyboard_check(ord('K')))
{
m_isInSkill = true;
…...
}
那么如何将这两个变量的值在动画播放完毕后设置成 false呢?这里需要引入一个新的事件类型——动画结束(Animation end)。这里对事件与脚本的处理与之前一致,我们需要做的是建立一个名为 YseraAnimationEnd 的脚本,并与 Animation end 事件关联:

然后在 YseraAnimationEnd 脚本中添加如下代码:
if(m_isAttacking && sprite_index == spr_ysera_attack_side
|| sprite_index == spr_ysera_attack_front
|| sprite_index == spr_ysera_attack_back){
m_isAttacking = false;
}
if(m_isInSkill && sprite_index == spr_ysera_skill){
m_isInSkill = false;
}
这里的含义是,如果角色处在攻击状态且攻击的动画结束了,那么取消角色的攻击状态,对释放技能这里同理。到此时为止,m_isAttacking(是否在攻击)和 m_isInSkill(是否在施法)这两个变量的值已经能够正确的反映角色状态了。
在具有了这两个变量以后,就可以用来改写攻击与技能动画的播放代码了,由于代码过长因此这里只写出简化的代码结构:
if(m_isAttacking == false && m_isInSkill == false)
{
if(按下攻击键)
播放攻击动画
else if(按下技能键)
播放技能动画
else if(按下A键)
播放向左行走动画
else if(按下D键)
播放向右行走动画
else if(按下W键)
播放向上行走动画
else if(按下S键)
播放向下行走动画
else
播放站立动画
}
在完成这部分代码后,人物应该可以正常的播放攻击和技能动画了:

生成魔法球与技能效果
接下来要做的是配合动画,在合适的时间释放出魔法球和技能效果。首先为它们建立两个 Object:obj_ysera_magic_bullet 和 obj_ysera_skill_effect,并分别将 spr_ysera_magic_bullet 和 spr_ysera_skill_effect 作为它们的 Sprite。
根据美术上的设定,释放魔法球和技能效果的出现都是在动画的第二帧,因此在 YseraStep 脚本的最后加上这段代码来生成它们:
if(sprite_index == spr_ysera_attack_side
|| sprite_index == spr_ysera_attack_front
|| sprite_index == spr_ysera_attack_back){
if(image_index > 2 && m_fired == false){
instance_create(x, y, obj_ysera_magic_bullet);
m_fired = true;
}
}
if(sprite_index == spr_ysera_skill){
if(image_index > 2 && m_fired == false){
instance_create(x, y, obj_ysera_skill_effect);
m_fired = true
}
}
这段代码的意思是,如果当前在播放攻击动画,那么在大于第二帧的时候,就在玩家的位置生成一个魔法球,对于技能动画同理。这里用到了一个新的函数 instance_create,它的作用是在指定的位置生成一个 Object。
其实当初我在实现这个功能的时候踩了一个坑,我最早的代码是这样写的:
if(在播放攻击动画时){
if(image_index == 2){
生成魔法球
}
}
然后这段代码的结果是什么都没有出现。为什么呢?秘密在这里,因为 image_index 并不是一个整数,这意味着image_index==2 这个条件可能永远都不会实现。所以应该把条件改成 image_index > 2。但如果只是把这个条件改掉:
if(在播放攻击动画时){
if(image_index > 2){
生成魔法球
}
}
那结果会在第二帧以后产生许多个魔法球,因此还需要添加一个变量 m_fired 来标记这次攻击动画过程中是否已经生成过魔法球了,这样就变成了上面那段完整代码所表达的逻辑。
另外在代码实现上还需要在 YseraCreate 脚本里初始化变量:
m_fired = false;
然后在每次按键开始播放攻击动画的时候将这个变量重置为 false:
if(keyboard_check(ord('J')))
{
...省略
m_fired = false;
}
else if(keyboard_check(ord('K')))
{
...省略
m_fired = false;
}
好吧,敲完了这么多代码终于又可以运行测试一下了:

这个结果是不是你预想之中的呢?因为我们只是生成了魔法球和技能效果,就没有再管它们了啊。在游戏开发中,对于每一个在场景中动态生成的物体,由于是我们通过代码创建的,因此我们就需要我们自己来关注在何时销毁它们。而与之相对的是,通过关卡编辑器预先放置在场景中的墙、木桶什么的,就不需要我们操心了,引擎会负责它们的创建和销毁。
那么对于这两个物体来说,它们的逻辑又各有不同。其中魔法球应该在生成后沿着射出的方向飞行,并在飞出屏幕以后自动消除;而技能效果只需要在播放完一遍以后消失就可以了。
魔法球的飞行与消除
首先为物体 obj_ysera_magic_bullet 的 Create、Step 和 Outside Room 事件分别建立三个脚本 MagicBulletCreate、MagicBulletStep 和 MagicBulletOutsideRoom。你们看,一切都是套路,和之前对 obj_ysera 做的事情差不多,唯一的不同是多了一个 Outside Room 事件,这个事件是指物体移动到了房间以外。
魔法球的飞行过程需要两个变量:飞行方向和速度。在实现上,其实这两个变量可以合并在一起变成m_speedX和m_speedY——飞行速度在X轴和Y轴的分量。在MagicBulletCreate脚本中初始化这两个变量:
m_speedX = 0; m_speedY = 0;
另外谈到初始化,实际上对于 GML 编程语言来说并不是一个必要的步骤,也就是说你可以在任何时候写一个新的变量来保存你的数值,并给后面使用。唯一会出错的情形是,如果你尝试读取一个变量,而这个变量之前没有任何地方给它赋值过,那 GMS 会报出以下错误信息:
Variable xxx(yyy, zzz) not set before reading it.
所以我总是在 Create 事件对应的脚本中给我想要用的变量设置一个初始值,以免后面出错。另外能在 Create 脚本中看到这个物体需要用到的所有变量也是一件好事对不对。
飞行的过程就很简单了,在 MagicBulletStep 脚本中根据速度更新物体的位置就好了:
x = x + m_speedX; y = y + m_speedY;
在MagicBulletOutsideRoom脚本中销毁物体:
instance_destroy();
最后要做的就是在生成魔法球的时候正确的设置它的飞行速度变量:
if(sprite_index == spr_ysera_attack_side
|| sprite_index == spr_ysera_attack_front
|| sprite_index == spr_ysera_attack_back){
if(image_index > 2 && m_fired == false){
var magicBullet = instance_create(x, y, obj_ysera_magic_bullet);
switch(m_playerDirection){
case PlayerDirection.UP:
magicBullet.m_speedY = -10;
break;
case PlayerDirection.DOWN:
magicBullet.m_speedY = 10;
break;
case PlayerDirection.LEFT:
magicBullet.m_speedX = -10;
break;
case PlayerDirection.RIGHT:
magicBullet.m_speedX = 10;
break;
}
m_fired = true;
}
}
因为GMS的坐标系是这样的:

所以按向上的时候,Y方向的速度值是负数;向下的时候是正数。
技能效果的消除
在技能效果的动画播放完成一次以后将它销毁,如果前面你都看过了就会发现这就像做选择题一样简单:
对 obj_ysera_skill_effec 的[ 1 ]事件添加脚本[ 2 ]并在其中调用[ 3 ]函数。
选择1:
- 创建(Create)事件
- 更新(Step)事件
- 动画播放完成(Animation End)事件
- 移动到房间以外(Outside Room)事件
选择2:
- MagicBulletCreate
- MagicBulletAnimationEnd
- SkillEffectAnimationEnd
- SkillEffectCreate
选择3:
- instance_create
- instance_find
- instance_destroy
- instance_copy
都是送分题哈~ 这节课就不讲了。
之前有一期教程的留言中,有网友提到希望能够为游戏开发的新人讲解如何通过拖拽的方式来完成游戏的功能,虽然我不太提倡使用这些拖拽的功能,但还是可以演示一下的:

从功能上来讲,这种拖拽的方式所实现的功能与之前调用脚本里的 instance_destroy 是完全一致的。
魔法球的两个问题
再次运行游戏验证之前的功能是否已经实现:

可以看到魔法球往不同朝向的飞行、以及技能效果的销毁都已经正常了。但还存在两个明显的问题,其一是当伊瑟拉向右侧发射魔法球的时候,发射的点位与动画不匹配。这是因为之前生成魔法球的代码:
var magicBullet = instance_create(x, y, obj_ysera_magic_bullet);
它是将魔法球初始化在了人物的 x、y 坐标,即人物自身的脚下。因此需要在这里加一个偏移值才能和动画对上,这个偏移值的大小可以打开攻击动画 spr_ysera_attack_side 来找到:

从这张图中可以看到人物的双脚之间的坐标值是 (100, 120),而魔法球在发出的前一瞬间的位置是 (35, 87)。那么 (-65, -33) 就是人物向左发射魔法球时的偏移值,向右就是 (65, -33)。用同样的办法可以得到向下发射的偏移值是 (0, 7),向上发射时的偏移值是 (8, -89),但为了左右对称起见,我们采用 (0, -89) 这个值。
另一个问题是魔法球飞行时的尾迹方向不对,这可以通过设置魔法球的 image_angle 参数来解决。
将这两个改动反映到代码里就是:
var magicBullet = instance_create(x, y, obj_ysera_magic_bullet);
var deltaX = 0;
var deltaY = 0;
switch(m_playerDirection)
{
case PlayerDirection.UP:
magicBullet.m_speedY = -10;
magicBullet.image_angle = 270;
deltaY = -89;
break;
case PlayerDirection.DOWN:
magicBullet.m_speedY = 10;
magicBullet.image_angle = 90;
deltaY = 7;
break;
case PlayerDirection.LEFT:
magicBullet.m_speedX = -10;
deltaX = -65;
deltaY = -33;
break;
case PlayerDirection.RIGHT:
magicBullet.m_speedX = 10;
magicBullet.image_angle = 180;
deltaX = 65;
deltaY = -33;
break;
}
magicBullet.x += deltaX;
magicBullet.y += deltaY;
m_fired = true;
次回预告
接下来就是最后的完美运行测试了:

本次教程的内容就完成了!
恭喜!
你已经完成了六章 GMS 教程的学习,获得初窥门径成就,请再接再厉。
但对于目前来说,发出的魔法球和技能效果还仅仅是视觉上的表现而已,在接下来的教程中,我们将要逐步实现它们的功能!
附录:教程资源链接
该系列教程的项目/代码及原始美术素材全部更新至 GitHub 项目库。


终于更新了
你好。请问能提供下最新的函数接口文档吗?官网上我找不到啊
@kamimika:http://docs.yoyogames.com/
又学习了不少,谢谢!希望继续更新更多内容啊。
谢啦谢啦!!☆⌒(*^-゜)v
不错不错!楼主辛苦了,写这么多!GMS是一个人可以玩转的强大引擎~
其实,再加上了两种攻击动作以后,楼主可以讲讲state的用法了!将移动和不同的攻击分开到不同的script里更便于管理撒~不然等不同类型的动作多了再讲,大家剪切粘贴代码都会忙不过来了:D
image_angle属性对物理的object无效吗?
找到了phy_rotation
恩,谢谢:)会在合适的时候讲下这个。
啊啊啊好棒!!!学习了!!感谢楼主不辞辛劳的讲解!
我猜楼主下一篇教程会在31号更新。
顺便问下,GMS里有没有状态机的概念?差不多是上面某位同学说的state。
楼主辛苦了。感谢分享。顺便期待下寻路方面的教程。
@Jeason1997:嗯,是每周三更新。state其实只是一种编程的方式吧,每种语言都可以实现。
一路学到这里,发现原来是每周三更新
最近由 SandCas 修改于:2016-09-05 19:36:19测试的时候出了个问题,目前进行到了AnimationEnd阶段,
最近由 SandCas 修改于:2016-09-06 17:55:59提示enum"PlayerDirection" has already been defined。
昨天做的时候还没有这个问题,而且PlayerDirection这个定义的话,全脚本找了找好像就这一个呀?
代码痴真是懵头了……
(问题已解决)
@SandCas:那你把这个删掉看看呢?是会出错吗?
@青铜的幻想:删掉之后解决了,因为个人的粗心大意,create和step里写了两次枚举……
最近由 SandCas 修改于:2016-09-07 15:27:14另外新人朋友不要像我一样把true写成了ture……
@SandCas:…………拼写错了。是 true 不是ture ,23333
@OMFG:是的我刚刚发现,刚想上来修改问题233333
最近由 SandCas 修改于:2016-09-06 23:52:12大兄弟心细啊
您好,我想问一下,我按照教程做但是Ysera的技能球发射点一直不变,deltaX和deltaY那一段代码是放在YseraStep中吗?同时因为发射点没法改变的情况技能球会跟Ysera体积冲突,不把Ysera的密度改为0的话技能球发射的时候会将人物推出一段距离,如果可以麻烦您解答一下吧,谢谢
@JohnKenwayCN:你可以直接下载项目文件看原始代码,然后和你的代码比较一下
青铜。
如果我想做一个“双击A”就可以向左奔跑的效果,该怎么实现呢。
我主要想了解一下您解决问题的方式,这样我就有些自主性,可以自己尝试去解决问题了。
@oKamiNemo:你英语怎么样?这有个官方论坛讨论双击奔跑的实现方式
https://forum.yoyogames.com/index.php?threads/how-to-double-tap-for-dashing.1569/
感谢您的分享!
这一章啃了两天,总算过关了。
学会了switch...case...语句的使用,知道了一个简单的拼写错误会浪费很久时间去排查,以及自我思考。
彻底蒙圈,放弃了。实在看不明白作者省略的代码写在哪个脚本里,还有GML语法规范到底是什么,看来不学gml别想弄了。代码不逐条解释根本看不懂或有疑惑。
最近由 dingwuwo 修改于:2017-01-29 14:02:16=和==有什么区别吗?
我试着把一些==改成了= 也可以正常运行
@TomasLiu:=是赋值,把符号后面的值赋给符号前面的变量
==是判断符号前面和后面是否相等。 本质上不一样,可以正常运行应该是凑巧
@TomasLiu:因为习惯了这种写法再去学其他语言容易死的很惨。
这个动作模型感觉太简单了……没有抽象出动作状态,没有抽象出硬直,就没办法取消硬直、没办法实现Dash攻击什么的
该评论已删除
最近由 **狸花猫 修改于:2017-09-05 15:45:10该评论已删除
最近由 **狸花猫 修改于:2017-09-04 23:03:27话说GMS2里面是不能直接写image_angle = 270;这种语句的吗?代码没变,但是没实现发射出去的子弹转向的功能
最近由 sima 修改于:2018-05-19 02:06:59已经解决了,原来是magic_bullet勾选了uses physicsl选项,去掉就好了
如果人物WASD和JK写在同一个if elseif里面和作者写的有什么分别吗?
还有WASDJK全部独立一个if这种呢?
感谢大侠,我在GMS2里面发现instance_create已经取消了,检索一下API发现已经更改成
instance_create_layer ,参数(x,y,层(实际是房间),obj) 就通过了
instance_create_layer(x, y,rm_test, obj_ysera_skill_effect);
大佬,是不是GMS1.2是不支持enum函数的?一直在报错,是不是换成最新版本比较好?
@hanchiqi:同问