作者:Matt Thorson(注:配色是蔚蓝哦~ 什么,不是蔚蓝么)
译者注:本文没有配图,可能挺枯燥。要是各位爷有耐性看完了,嘿~ 感觉还挺有帮助,给咱刷个跑车什么的,心里刷就行。给作者多刷几个,大游艇大火箭您随意。
我被问到很多关于TowerFall还有Celeste中物理工作原理的问题。这是个非常简单的系统,不过我花了大概十多年研究基于tile的平台游戏,才整明白。我很久以前写gamemaker的平台游戏(platformer)引擎用过类似的基本概念,从那时候开始,我对它做了很多简化和改进。Celeste和TowerFall的引擎是用c#写的,因此我们有很多出色的功能,像委托(delegates )、和结构(structs ),这让一切都变得更好。虽然这倒不是啥有突破性的东西,不过我觉着还是写下来好,没准儿会对谁有帮助呢!
(这篇文章本来是关于Towerfall的,在我们做Celeste之前就已经写过。这也适用于Celeste,我更新了一下,这也会对那些以Celeste为参考点的读者更有帮助。)
我们所有的物理都由两个类来处理:Solids和Actors(这里不翻译了,保留原单词比较好)。Solid,当然就是关卡内那些可碰撞的几何体。Actor,就是物理对象,比如玩家、弓箭、怪物、宝箱什么的。一切需要移动并与关卡内几何体产生互动的任何对象都是Actor。该系统有下面这些简单的约束:
- 所有的collider都有轴对齐的边界框(AABBs)(译注:如果这位爷您跟我一样不了解AABB是啥,请看这里)
- 所有collider的位置、宽和高都是整数
- 除了特殊情况外,Actor和Solid永远不会重叠
- Solid之间不会相互作用
Actor基础
咱们先来假设一下,Solid永远不会移动 --- 然后我们要怎样确保不管Actor怎么移动都不与Solid重叠?Actor有一个非常简单的核心API,这个个API有这俩函数:
public void MoveX(float, Action); public void MoveY(float, Action);
这俩老哥都将移动量作为float,将碰撞动作回调作为C#委托。Actor对自己的速度、加速度或者重力都没有任何概念。扩展Actor的每个class都会搞定这些,保持追踪它的速度,并在适当的时候传递给这些函数。
咱们来看看MoveX函数:
public void MoveX(float amount, Action onCollide) { xRemainder += amount; int move = Round(xRemainder); if (move != 0) { xRemainder -= move; int sign = Sign(move); while (move != 0) { if (!collideAt(solids, Position + new Vector2(sign, 0)) { //旁边没有Solid Position.X += sign; move -= sign; } else { //撞上Solid了! if (onCollide != null) onCollide(); break; } } } }
首先,我们将amount( 移动量)添加到我们的“remainder”计数器中。由于位置是用整数表示的,所以我们只能以像素为单位移动,不能1/2像素、1/4像素这样移动,因此我们用round函数处理一下remainder,只有move不等于0时候我们处理移动。
现在咱们知道了我们要移动多远,我们只需要每次搞定一个像素就行了。对于每个像素,我们都会提前检查一遍是否有障碍物,如果没有,我们就移动。如果撞到墙了,我们就停止移动,然后调用传入的碰撞委托。
我们为什么要传递碰撞委托?后面再说,基本上,它让我们可以适用相同的MoveX和MoveY函数来完成很多不同类型的移动。这也意味着扩展Actor的类,这样在碰撞的时候我们可以轻松的切换行为。
现在,我们让Actor可以移动了,并且不会与Solid相交 -- 假设Solid不会移动(我们前面说过了,这里我再提一句)
移动Solid的准备
这里有点儿费劲了。这些年来,我看过也写过很多关于移动Solid的有缺陷的实现方法。在我十几岁那会,玩过一些游戏,有些游戏在移动平台物理处理这方面做的非常残暴,我现在终于找到了解决方案,咱们向下看。
public void Move(float, float);
Solid只需要一个移动函数,并且不需要碰撞委托,因为Solid之间不能发生任何碰撞。如果您让Solid往右挪30像素,那甭管旁边杵着几个Solid老铁跟那儿挡着,它都会跟您说,“没毛病,哥肯定给您挪到那儿。”
但是呢,这30像素的漫长道路上,要是有Actor们跟这儿挡道儿,咱就得处理一下子了,因为Solid不能跟Actor重叠啊。
在咱们了解Solid移动函数之前呢,咱先在Actor的API里加点儿东西:
public virtual bool IsRiding(Solid solid); public virtual void Squish();
Actor里咱加了个IsRiding,咱就把移动的Solid想象成一匹马,Actor要是站上面了,就是在骑它,IsRiding就用来检测Actor骑没骑它。但是呢,某些Actor吧,可能希望重写这个函数来更改行为,比如在Towerfall中,玩家在抓着Solid的边缘悬挂的时候也能骑Solid,但飞行的怪物就永远不会骑Solid。在Celeste里,Madeline(就是您操控的内个小姑娘)站在Solid上或者贴边儿的时候都会骑。
第二个我们加的东西,就是挤压函数,在两个Solid直接,Actor会被挤压。默认情况下,就是销毁Actor。
开始搬东西
当Solid与Actor交互时,它可以通过两种方式:载客(译注:这个Solid是位的哥,脾气火爆,你要不上车,它就撞您) 或者 推动。要是Actor站Solid上面了,那就被带走,但是要是Solid的移动导致他们重叠了,那Actor就会被推走。重要的一点,推动 优先于 载客 ---- 就是说如果两种情况同时出现了,那就算作是推动。
下面是Solid.Move:
public void Move(float x, float y) { xRemainder += x; yRemainder += y; int moveX = Round(xRemainder); int moveY = Round(yRemainder); if (moveX != 0 || moveY != 0) { //遍历关卡中的每个Actor,如果actor.IsRiding为true就添加到list里 List riding = GetAllRidingActors(); //关闭此Solid的碰撞,让通过它移动的Actor不会卡住。 Collidable = false; if (moveX != 0) { xRemainder -= moveX; Position.X += moveX; if (moveX > 0) { foreach (Actor actor in Level.AllActors) { if (overlapCheck(actor)) { //往右推动 actor.MoveX(this.Right — actor.Left, actor.Squish); } else if (riding.Contains(actor)) { //往右载客 actor.MoveX(moveX, null); } } } else { foreach (Actor actor in Level.AllActors) { if (overlapCheck(actor)) { //往左推动 actor.MoveX(this.Left — actor.Right, actor.Squish); } else if (riding.Contains(actor)) { //往左载客 actor.MoveX(moveX, null); } } } } if (moveY != 0) { //Y轴移动 … } //重新启用此Solid的碰撞 Collidable = true; } }
这块儿咱写了不少代码,哈,所以下面咱得看看都写了啥。
首先,将amount移动量添加到remainder中。Solid共享整数锁定位置的Actor约束,他们必须得使用相同的系统才能知道啥时候要移动。
其次,我们创建了一个在此Solid上的每个Actor的List,也就是我们应该载客的名单(译注:看着没,老哥转行不开出租了,可能是开的大公交,一下上一大堆人)。简单的循环检测一下每个Actor的IsRiding就可以搞定。在实际移动之前执行此操作很重要,因为移动可能会让我们超出IsRiding检测的范围。
第三,咱先暂时关闭此Solid的碰撞。当推动和载着Actor时,咱通过调用Actor.Move函数来解决。我们不想让Actor在移动过程中考虑这个Solid是在推它还是载着它走。
接着,我们一次移动一个轴,并开始解决Actor交互。我们要进行重叠检测来看看我们是否需要推动任何Actor。这里有个要注意的地方,我们要在移动之前进行载客检测,移动之后进行推动检测。
如果发现自己与任何Actor重叠,我们就要推动他们。无论我们是否也随身载着这些Actor,都要优先考虑,因为前面我们说过了,推动的优先级高于载客。如果我们没有推动给定的Actor,那我们就要检测它是不是已经放入我们要载客的Actor列表里了。如果该检测也没有通过,那这个Actor就跟咱无关,咱就甭管它了。
现在,咱们来看看我们推动Actor和载着Actor时的区别。
推动Actor无法获得我们的全部移动量 - 只能推动我们的领先边缘与其最接近边缘之间的像素差。这是为了确保Actor保持与正在执行推动的Solid的侧边平齐。
推动还使用了Actor.Squish回调,回想一下,默认情况下,这会销毁Actor。如果Solid将一个Actor推入另一个Solid中,有些不好的事就要发生,虽然咱们没那么残暴,但是我们不允许重叠啊,咱也只能销毁Actor。看到这里,您可能想尝试其他操作,比如让Actor BLA BLA BLA,您的想法我虽然看不到,不过可能会很有趣吧~ 在Towerfall和Celeste中,根据游戏的玩法,我们有很多不同的处理方式。
载着Actor可以获得全部的平台移动速度,而且没有碰撞回调。这里没有被挤压的危险 --- 就算Actor撞墙了,也没事儿。
就是这样!希望对您有所帮助 :)
(译注:就是这样,这么突然的结束,我缓冲一下啊,翻译文章比写代码画图想系统还累,我就吐个槽,各位爷吉祥。)
(译注2:还没吐完,我想到了小学时代课本上那位网红,他说“人的生命是有限的,可是,为人民服务是无限的,我要把有限的生命,投入到无限的为人民服务之中去。” 这一刻,我体会到了那种修仙的冲动。就让我们在这崇高的名言的洗礼下结束吧,回见了您呐~ 对喽,各位爷别忘了在心里给matt刷大游艇大火箭啊~)
来自DEV的凝视
-H
2020/06/07
附原文地址 https://medium.com/@MattThorson/celeste-and-towerfall-physics-d24bd2ae0fc5
如有翻译不当的地方,请指正。