GameMaker Studio 2

创建于:2017-04-19

创建人: dougen

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

译文:GameMaker Studio基础系列:Hitbox和hurtbox(攻击/受击判定盒)

highway★ 2018-11-07

Image title

作者: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,如下:

Image title


上图里,我们看到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跟随玩家了。

Image title

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)。每一部分都会持续一定的帧数。看看下面的图表会理解的更清晰。

Image title

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

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

Image title


我们的启动帧是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对象)。 查看下面的图表,可以直观地了解正在发生的事情。

Image title

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;
}
}
}
}
}
}
}