译文 | GameMaker Studio 之中的攻击与受击判定盒

作者:highway★
2018-11-08
23 25 6

作者:Nathan Ranney

翻译:highway★

注意:本文面向初学者!!!

原文部分函数为GMS,译文中已经改为GMS2的对应函数

观看操作视频或阅读

这篇博客文章概述了设置hitbox和hurtbox的所有步骤和代码。您也可以按照以下视频进行操作:

https://youtu.be/NbOVd4ycZkg (要爬出去)

什么是hitboxes和hurtboxes?

注:这里我还是用原文,原意是攻击判定盒 & 受击判定盒,如果你常玩FTG、ACT或者FPS类型的游戏,对hitbox这玩意儿肯定了解很多。不了解的朋友还请仔细阅读。

基本上,hitbox和hurtbox只是专门的碰撞检测(碰撞检测允许您确定对象何时接触或重叠)。hitbox通常与某种形式的攻击相关联,并描述该攻击的有效范围。hurtbox通常与角色(或游戏中的任何其他“可击中”对象)关联。每当他们俩碰撞时,我们认为攻击已“达成”,我们将其效果应用于目标。下面的内容我会用FTG类型游戏做主要的例子。在我看来,格斗游戏提供了最明显的hitbox和hurtbox示例,使它们非常容易理解。 我们来看看街霸4,如下:


上图里,我们看到Makoto表演了她的一个特殊动作,吹上攻击。这招儿就是向上出拳,通常用来防空,可以击中向你跳跃的对手。红色矩形是hitbox,而绿色矩形是hurtbox。如果Makoto用她的hitbox碰到别人的hurtbox,那么另一个玩家将被“击中”。

现在,默念“box”一千遍,好了,咱们开始设置。

Hurtbox 设置

首先!我们需要一个精灵图用于我们的hurtbox。创建一个新的sprite,命名为sprHurtbox,1 x 1像素,并将其着色为绿色。我们只需要一个像素,因为我们将在实例化hurtbox时将其缩放到我们需要的任意大小。另一种方法是为每个可能需要hurtbox的游戏对象创建一个自定义大小的精灵图,这样很……浪费资源,也很……无聊。

现在我们创建一个object(对象),命名为oHurtbox,精灵图指定为sprHurtbox。添加create事件,敲下面这些。

image_alpha =0.5; //让hurtbox半透显示
owner =-1; //将绑定到创建它的任意对象的id,比如oPlayer
xOffset =0; //用来跟owner对齐位置
yOffset =0; //同上

现在我们需要创建一个hurtbox并给它一个持有者。

创建一个script(脚本),命名为hurtbox_create。敲入下面这些代码。(咳咳,哥们儿你别复制粘贴啊……

_hurtbox = instance_create_layer(x, y, layer, oHurtbox);//创建oHurtbox对象,注,如果你想用其他的layer来显示这玩意,就把layer改为你想要显示的layer名,“layer name”
_hurtbox.owner = id; //存储该对象的id
_hurtbox.image_xscale = argument0;
_hurtbox.image_yscale = argument1;
_hurtbox.xOffset = argument2;
_hurtbox.yOffset = argument3;

return _hurtbox;

如果你以前没怎么写过script(脚本)的话,可能觉得这个看起来有点儿多啊,但其实很简单。首先,我们创建一个oHurtbox对象,并将该对象的ID存储在_hurtbox的owner变量中。然后,使用_hurtbox的变量,我们传入所有者(调用此脚本的任意对象),接着定义了hurtbox的大小和偏移量。现在脚本已经写好了,我们可以来调用一下试试看。打开oPlayer对象把下面的代码加到create事件里。

//hurtbox
hurtbox = hurtbox_create(18,24,-9,-24);

// hitbox
hitbox =-1;

使用我们刚刚创建的hurtbox_create脚本,我们可以很方便的地设置比例和偏移量,并将oHurtbox对象的ID存储在oPlayer对象可以使用的变量中。脚本中使用的数字以像素为单位。我们创建的hurtbox是18像素宽x24像素高,偏移玩家精灵左侧9像素,并偏移玩家精灵上方24像素(注:这说的可够真详细的 =_=)。好了,现在运行游戏看看,hurtbox好像没有跟随你的角色。

我们得解决这个问题。在oPlayer对象中打开end step事件并添加以下代码。如果你看了本系列的前几篇教程,我把这些代码加到了animation code底下。

//hurtbox
with(hurtbox){
    x = other.x + xOffset;
    y = other.y + yOffset;
}

with和other如果你还没用过的话,我在这里简单解释一下(如果还是不明白的话,还是去仔细看一下F1比较好)。当你使用with后跟对象名称(或特定对象ID)时,花括号里的代码将执行,就像该对象正在执行它一样。so,当我们写with(hurtbox)时,我们正在更新存储在hurtbox变量中的特定oHurtbox对象的x和y位置。

由于我们使用with,我们也可以使用other。这段代码用到other时,它将引用此代码运行的原始对象。在这种情况下,就是我们的oPlayer对象。

好了,现在hurtbox跟随玩家了。

Hitbox设置

现在我们有hurbox了,我们得打它啊! hitbox所需的设置跟hurtbox差不多,但它还有更多功能。 简单理解一下hitbox,首先我们来检测碰撞,要是碰撞了,然后决定接下来要做什么。(哎,我车还没碰到你,你怎么倒了呢?面对一些老年碰瓷者,有可能是咱们的车hitbox出毛病了,要么就是他们的hurtbox的offset或者scale出毛病了吧……这时候可能就需要交警和行车记录仪来debug了 =_=

就像hurtbox一样,我们需要创建一个精灵和一个对象。创建一个名为sprHitbox的单像素精灵,红色。然后创建oHitbox对象并指定sprHitbox精灵。添加create,step,end step和destroy事件,打开create事件并敲入以下代码。

image_alpha =0.5;
owner =-1;
xOffset =0;
yOffset =0;
life =0;//hitbox存活时间
xHit =0;//用来击退 x方向
yHit =0;//用来击退 y方向
hitStun =60;//击晕时间
ignore =false;
ignoreList = ds_list_create();

与我们的hurtbox一样,我们需要设置所有者和偏移量。然而,与受伤害的盒子不同,hitbox并不是一直存在的,它只存在于攻击期间。life变量将用于确定数据框将存在多少帧并保持活动状态。 xHit和yHit是我们的击退变量。hitStun确定我们击中的角色被打中后眩晕的时间。最后,ignore变量和ignoreList列表将用于确保我们不会多次击中一个角色。后面你会看到它是如何工作的。

击中眩晕是一个角色在被击中后被击晕的时长。如果玩家被击晕,除了等着被揍或者祈祷,他们什么都做不了(当然你也可以写成疯狂按键可以稍微减少眩晕时长)!格斗游戏里这玩意儿很常见。你要是把对手打晕了的话,嗯……先来一个挑衅动作,然后一套连招KO好了~ (或者…你也可以点一个轻攻击让对方恢复正常,接着继续干死他…有点儿更藐视对手,是的,我跑题了 : p

打开destroy事件并加上下面的代码。

owner.hitbox =-1;
ds_list_destroy(ignoreList);

这可以确保hitbox在销毁后,其所有者停止尝试与其进行交互,并在不再需要时删除ignoreList。如果列表未被删除,则可能导致内存泄漏。
之后打开step事件,加入下面代码:

life --;

这将在hitbox处于活动状态时从生命周期中减去(就是计时器)。当life变量达到0时,删除hitbox。最后到end step事件,加入下面这一小段:

if(life <=0){
    instance_destroy();
}

当一个对象被破坏时,就像我们上面所做的那样,将调用destroy事件(如果存在)。OK,hitbox设置已经完成了, 但对于实际对象!还有很多事情要做。就像hurtbox一样,接着我们要干嘛?对了,脚本。创建一个新脚本,命名为hitbox_create,然后敲入以下代码(上面的我加了注释,下面的注释我就不加了,作者讲的很细)。

_hitbox = instance_create_layer(x, y,,layer, oHitbox);
_hitbox.owner = id;
_hitbox.image_xscale = argument0;
_hitbox.image_yscale = argument1;
_hitbox.xOffset = argument2;
_hitbox.yOffset = argument3;
_hitbox.life = argument4;
_hitbox.xHit = argument5;
_hitbox.hitStun = argument6;

return _hitbox;

跟hurtbox那个差不多,多了几样东西,life,xHit和hitStun。 完事儿了吗?我们差不多已经完成了一半。回到oPlayer对象的end step事件,在hurtbox代码段下面加上
这些:

//hitbox
if(hitbox !=-1){
    with(hitbox){
        x = other.x + xOffset;
        y = other.y + yOffset;
    }
}

这与hurtbox代码略有不同,我们要先在攻击那一刻确认此时是否已经有hitbox存在,也就是检查我们的hitbox变量是否不等于-1。

现在,最后一步,我们需要在攻击期间的正确时间实际创建hitbox。但在我们这样做之前,我需要简要介绍一下格斗游戏中攻击的实际构成。所有攻击都分为三个部分。启动(Start up),活跃(Active)和恢复(Recovery)。每一部分都会持续一定的帧数。看看下面的图表会理解的更清晰。

(译注:这个图不翻译了,gif弄着太麻烦了,见谅,但是这个图是精华,一定要仔细看懂)

启动是攻击变为活动所需的时间,然后衔接到出拳。活跃是hitbox能够实际击中敌人的时间。恢复是角色完成攻击并返回中立状态(译注:这里对状态不太熟悉的话,请先详细了解一下状态机)所需的时间,之后才可以再执行其他操作。让我们看看我们的角色精灵,以确定我们的启动,活动和恢复帧应该在哪里。


我们的启动帧是0-2帧。相当于攻击动作的发条。活跃帧为3-4帧,恢复5-7帧。我们需要在第3帧创建我们的hitbox,它需要在第5帧开始之前一直处于活动状态。在我的项目中,我的frameSpeed变量为0.15并且游戏以60 fps运行,我的精灵动画以大约每秒四帧。所以,我的hitbox的生命需要为8帧。


打开attack_state脚本并添加以下行(译注:这个脚本在之前的教程中)。

//在合适的时间创建hitbox
if(frame ==3&& hitbox ==-1){
    hitbox = hitbox_create(20* facing,12,-3* facing,-16,8,3* facing,45);
}

我们要检查我们是否在正确的帧上,并且hitbox不存在,再使用hitbox_create脚本创建hitbox。在创建hitbox时,我们需要将水平值(xscale和xOffset)乘以角色面向的方向。这确保了hitbox始终与角色的方向对齐。然后我们设置了8帧的存活时间,然后是水平击退和击晕。现在运行游戏并按下攻击,你应该会看到hitbox出现并按预期消失。现在我们得让它能打东西了!

TIPS:hitbox越大,它就越强大。生活也一样。 hitbox活动的时间越长,它就越强。在格斗和动作游戏中,巨大的,持续时间长的hitbox总是非常强大(想想那些恶心人的BOSS吧)。在设计攻击时请记住这一点!

敌人设置

拳击手需要沙袋,而我们,需要一个敌人。这将非常简单,因为敌人将使用与我们的玩家相同的许多代码(译注:通常玩家和敌人会隶属于一个Entity的父类对象,这样就不用重写类似的代码了)。现在,我们需要添加一些新的精灵。你可以使用你想要的任何精灵,或者用我正在使用的相同精灵(译注:效果可能没那么好,比如受击、跳)。

以与创建玩家精灵相同的方式创建精灵。确保精灵原点是(16,32),就像上次一样!你应该有两个精灵:sprEnemy_Idle和sprEnemy_Hurt。 复制oPlayer对象并将其命名为oEnemy(译注:如果你有一个oEntity的对象的话,就可以更方便了)。将sprEnemey_Idle sprite分配给对象,然后打开create事件。我们需要添加一些新变量:

hit =false;
hitStun =0;
hitBy =-1;

hit是一个简单的布尔值,我们将在应用命中效果时使用到它。接下来,hitStun是被击中后敌人在hitStun中停留的时间。最后,hitBy将是击中它们的对象的ID。

接着打开step事件。删除与player按键和状态切换有关的代码段(译注:如果你有一个oEntity的对象的话,没必要这么麻烦了)。当我们按下按钮时,我们不希望敌人执行动作,我们需要重写状态切换。加入以下代码。

//状态切换
switch currentState {
    case states.hit:
        hit_state();
        break;
}

由于我们的敌人只会站着或被击中,我们现在不需要任何其他状态。但是我们确实需要创建hit_state脚本。立即执行此操作并添加以下代码。

xSpeed = approach(xSpeed,0,0.1);

hitStun --;

if(hitStun <=0){
    currentState = states.normal;
}

如果你已经读到这里了,那这对你来说应该很熟悉。首先,我们降低敌人的水平速度,直到达到零。接下来,我们让hitStun倒计时,并在hitStun达到零时将敌人恢复到默认正常状态。很简单吧! 再打开end step事件。首先,把animation_control()改成 animation_control_enemy();然后在hurtbox代码下面添加这个。

//被打了~~
if(hit){
    squash_stretch(1.3,1.3);
    xSpeed = hitBy.xHit;
    hitStun = hitBy.hitStun;
    facing = hitBy.owner.facing *-1;
    hit =false;
    currentState = states.hit;
}

这是我们应用命中效果的地方,如击退,挤压和拉伸,屏震(如果你想要这种效果的话),等等。它还将敌人状态更改为受击状态,这会阻止他们在击中昏迷时执行任何其他操作。 现在,我们要创建animation_control_enemy脚本。这是玩家使用的相同类型的脚本,但是简化了,因为敌人的动画和行为比玩家少很多。加入下面的代码(注意精灵名是否与你的资源匹配):

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

//animation control
switch currentState {
    case states.normal:
        sprite = sprEnemy_Idle;
        break;
    case states.hit:
        sprite = sprEnemy_Hurt;
        break;
}

//reset frame to 0 if sprite changes
if(lastSprite != sprite){
    lastSprite = sprite;
    frame =0;
}

这里没什么好说的。我们所做的就是根据状态设置精灵,就像我们对玩家一样。 OK,敌人设置完成!放在房间里一两个敌人。下面到了比较难的部分了...检查hitbox / hurtbox碰撞(重叠),并解决该碰撞。

击中检测和确定攻击

这部分有点儿绕。还记得with和other么?嗯...我们还要用到它们,但嵌套在自己内部。告诉对象在另一个对象内部的另一个对象内部做什么!对象开始!好吧也许它并不复杂,但有时读起来就有点儿费劲...... 不管怎样,咱们先回到oPlayer对象并打开end step事件,你可以在其中更新一下hitbox代码段,让它看起来像这样。

//hitbox
if(hitbox !=-1)
{
    with(hitbox)
    {
        x = other.x + xOffset;
        y = other.y + yOffset;
        //check to see if the hurtbox is touching your hitbox
        with(oHurtbox)
        {
            if(place_meeting(x,y,other)&& other.owner != owner)
            {
                //do some stuff
            }
        }
    }
}

快速回顾一下这里发生的事情。我们检查当时是否确实有一个hitbox,如果有,我们会检查所有的hurtbox对象,看看它们是否与这个特定的hitbox实例发生碰撞。使用with时请务必注意,如果您只使用对象的名称(如oHurtbox)而不是对象的实例ID,则将从该对象的所有实例中运行代码。现在我们是两层深,并且正在检查来自hurtbox的碰撞,所以当我们使用other时,它不再引用运行所有这些代码的主对象(oPlayer对象),而是作为一个层的对象在这一个之上(oHitbox对象)。 查看下面的图表,可以直观地了解正在发生的事情。

oPlayer用于与oHitbox通信,然后oHitbox使用with与oHurtbox进行通信。每次调用都会为代码创建一个新层。当一个对象正在使用其他对象时,它会引用它上面的层。必须了解这些层以及with/other,才能完全理解这些碰撞检测将如何工作。


最后,我们需要解决碰撞。我们已经检查了hitbox和hurtbox是否发生了碰撞,现在我们需要决定接下来会发生什么。好了,我们的ignore、ignoreList登场啦。首先,我们需要检测,看看hitbox是否已经击中了hurtbox。

//hitbox
if(hitbox !=-1)
    {
    with(hitbox)
    {
        x = other.x + xOffset;
        y = other.y + yOffset;
        //检测hurtbox是否碰到了hitbox
        with(oHurtbox)
        {
            if(place_meeting(x,y,other)&& other.owner != owner)
            {
                //ignore检测
                //检测来自hitbox对象的碰撞
                with(other)
                {
                    //检查你的目标是否在忽略列表中
                    //如果是,不要再次击中它
                    for(i =0; i < ds_list_size(ignoreList); i ++)
                    {
                        if(ignoreList[|i]= other.owner)
                        {
                            ignore =true;
                            break;
                        }
                    }
                }
            }
        }
    }
}

好多花括号......甭担心。在确定我们的hitbox与一个hurtbox相撞后,我们不得不再做一个功能,并且这两个判定盒有不同的Owner(持有判定盒的对象)。Owner检查可防止hitbox与属于玩家的hurtbox碰撞,从而阻止玩家自己打到自己。

接下来检测我们要忽略的敌人列表。如果你之前从未使用过for循环,这可能会让人感到有些困惑,但它看起来要简单得多。 for循环执行一定代码块一定次数。在这儿,它执行的次数与ignoreList中的数据实例一样多。它会检查列表中的每个位置,并将其与刚刚碰撞的hurtbox的owner进行比较。如果列表中的任何数据与hurtbox的owner匹配,则忽略owner,并且不会被命中,我们将使用break停止检查列表。我们这样做是为了防止同一个敌人在我们的攻击活跃的每一帧被击中。如果这个忽略检测不存在,则敌人将在8帧中被击中8次。

你可能想知道ignoreList 如何填充数据,我们后面再说。如果我们的第一次检查失败,也就是说,如果不应忽略敌人,我们可以击打它们并将其数据添加到列表中。对你的代码进行以下更改。

//hitbox
if(hitbox !=-1)
{
    with(hitbox){
        x = other.x + xOffset;
        y = other.y + yOffset;

        //check to see if the hurtbox is touching your hitbox
        with(oHurtbox){
            if(place_meeting(x,y,other)&& other.owner != owner){
                //ignore check
                //checking collision from the hitbox object
                with(other){
                    //check to see if your target is on the ignore list
                    //if it is on the ignore list, dont hit it again
                    for(i =0; i < ds_list_size(ignoreList); i ++){
                        if(ignoreList[|i]= other.owner){
                            ignore =true;
                            break;
                        }
                    }

                    //if it is NOT on the ignore list, hit it, and add it to
                    //the ignore list
                    if(!ignore){
                        other.owner.hit =true;
                        other.owner.hitBy = id;
                        ds_list_add(ignoreList,other.owner);
                    }
                }
            }
        }
    }
}

如果ignore为false,那么hurtbox(other.owner)的持有者就会被击中!我们需要告诉它被击中的对象(other.owner.hit = true)以及击中它们的对象(other.owner.hitBy = id)。然后将它们添加到忽略列表中,这样我们就不会在下一帧再次点击它们(ds_list_add(ignoreList, other.owner)。现在运行游戏,去揍你的敌人们吧!他们应该会被击中、击退、并被击晕(译注:当然,在动作或格斗游戏中,单一的普通攻击是不会击晕敌人很长时间的,这里的敌人硬直时间可能稍长,击退距离也略显长,这里是教程作者为了便于理解有意为之)

最后的想法

哇。好累啊!很高兴你能看完,真棒~ 当我开始写这篇文章时,我并没有预料到需要这么长时间(译注:嗯,别说写了,我都没想到要花这么长时间来翻译,真的很累……)。我很高兴能够展示很多有趣的概念,例如with / other,for循环,ds_lists和简单的碰撞检测。

我非常感谢你花时间阅读这篇文章,我希望你能学到新东西。你可以在Twitter上关注我,并在我的网站上关注更多与游戏开发相关的内容!有关本文中介绍的一些新主题的更多信息,请查看以下链接。

重要链接

嗯,我就是,又帅又爱分享的Nathan

近期点赞的会员

 分享这篇文章

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

参与此文章的讨论

  1. highway★ 2018-11-08

    文章后面一部分 漏掉了

    • eastecho 2018-11-08

      @highway★ :原文里面有大量的垃圾 tag,可能是复制过来的问题,正在处理!

    • highway★ 2018-11-08

      @eastecho:可能是有道笔记贴过来的问题 =_=

  2. narn 2018-11-08

    咳,咳,好干。水!给我水!

  3. DarkWave Studio 2018-11-09

    印度诺瓦的好文章越来越多了!!!作者加油!!!

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

登录/注册