在“业余程序员”系列,我想分享一下作为一名业余程序员的经验。我的分享有两个主要目的:一是证明非专业的程序员也能制作出原型;二是向专业程序员展示设计师是怎么做程序的,也许能得到一些反馈和建议吧!最后,本系列也是对我本人工作的一些反思。这次我先介绍我如何使用Unity为个人项目制作自定义操作器:
为什么使用自定义操作器,而不使用Unity默认的那个?
你可能会问自己:“但为什么不使用Unity自带的角色控制器呢?”事实上,我试过了,至少在一开始的时候。但后来我遇到了一些问题:
1、Unity的默认角色控制器是基于Unity的物理引擎的:如果你想制作一款物理平台游戏(游戏邦注:如《Little Big Planet》、《Trine》等),这也许是个不错的解决方案。然而,我需要一种“敏锐”而准确的操作,像老式2D平台游戏(《马里奥》、《索尼克》、《超级食肉男孩》等)那样的,所以Unity的物理引擎太难调整了。
2、Unity默认的角色操作器是基于胶囊状碰撞器:所以,玩家能够轻易地在斜坡上行走,但当你要做的是朝右移动的平台游戏时,胶囊就会卡在边缘上,这是不可行的:
2d Platformer-01
3、最后,当我想到以后的AI寻径,我决定自己做一个控制器:我认为在一般的平台游戏中,使用简单的功能如跑(速度)和跳(高度)可以更容易控制AI在路径上的移动,并结合游戏世界的限制条件(碰撞和重力)。
基本原则 没什么特别的,关于2D游戏的碰撞有很多网上教程,我采用的方法不过是结合了我看过的东西和我自己的(不成功的)经验。
因为我的原型是一款贴图平台游戏,我本可以使用简单的系统来检查我想移动的贴图是不是墙体。出于若干原因,主要是因为我之前不懂得用这种方法处理斜坡,我选择了更通用的系统,即使用光线投射来检测碰撞。
所以,基本的碰撞概念是非常简单的:控制器以一定的速度值移动,除非这个速度导致它的碰撞器遇到另一个碰撞器。所以,我们只需要检测这些可能的碰撞!
为了检测这些碰撞,正如我前面所说的,我使用光线投射:当有一个速度值时,就有光线会搜索这个速度的方向(X轴和Y轴)的碰撞。如果光线发现碰撞,命中点会确定这个控制器在这个轴上能够达到的最大值。
对于四个方向上的各个光线,伪代码如下:
If speed on x axis > 0
If raycast to the right hits a collision on point (Px,Py)
Set xMaxLimit = Px
取决于控制器碰撞器的大小和贴图的大小,我必须在每一边放两束光线,即一个角一束。但如果你的两束光比你的贴图要大,那么你使用两束光就会遇到如图所示的问题:
2d Platformer-02
对一边的光线不止一束的情况,我还必须比较它们的命中点P1和P2,并保留最接近的一个:
Set xMaxLimit = Minimum value between P1x and P2x
最后,我检测控制器的各边是否被阻塞,如果是则速度设为0。如果没有,则控制器将仍然被阻塞,但保持原来的速度值。所以,如果你突然想走另一条路,你必须等到相反的加速度弥补实际速度,最终将速度增加到另一个方向上:它使玩家感觉到“被卡住”。
解决办法相当简单:If controller position on x axis >= xMaxLimit – 1 (I added a 1 pixel buffer to prevent errors)
Set rightBlocked to true
If rightBlocked is true AND speed on x axis > 0
Set speed on x axis = 0
我想这是非常简单和传统的办法吧……
重力问题 重力就是指,当控制器在空中下落时,每一帧,Y轴上的速度会在重力的作用下加上一个均匀加速度:
speed on y axis = speed on y axis + gravity acceleration * delta time
(在这里我就不谈delta time(时间增量)了,因为程序员知道那是什么东西,而非程序员可以很容易在网上找到解释。)
我现在不想在这里详细地解释跳跃系统(因为我想另写一文介绍)。这里的重点是,当跳跃输入被调用时,一个冲击速度会在Y轴上被赋加给控制器,然后重力一步一步地抵销高度,使控制器进入下落状态。
然后我使用相同的系统来确定移动限制和着陆标记:
If raycast to the bottom hits a collision on point (Px,Py)
Set yMinLimit = Px
If controller position on y axis <= yMinLimit + 1
Set grounded to true
If grounded is true AND is not jumping
Set speed on y axis = 0
Else
Apply gravity (see above)
你可以看到上述代码的一些变体: 1、如果底部方向到光线上有速度,则不必检测,因为总是有速度(由持续的重力加速度产生)。
2、相同地,如果着陆,则不必检测。
3、如果控制器正在跳跃,则在设置Y轴上的速度为0以前需要检测:否则,当跳跃冲力被赋到Y轴上的速度时,它直接回到0,因为控制器在下一帧里可能仍然贴着地面。
2d Platformer-03
光源的重要性 这不是一个大问题,我认为对专业的程序员来说是小菜一碟。但我自己花了一些时间,我真的想在这里分享一下我是怎么理解它的:
一开始,我的光线是由控制器碰撞器的角产生的。问题是,控制器不能正确地处理右边的碰撞。现在我知道它是一个代码执行顺序的问题,用下图更容易解释:
2d Platformer-04
后来我发现了一个解决办法:光线从更远处投射,以产生“缓冲区”:
2d Platformer-05
斜坡问题 斜坡……当想到碰撞系统时,我总是很怕斜坡。我怕到不得不去寻找“无斜坡”的做法!但是,我最终克服了斜坡障碍!
令人吃惊的是,在我整合基本的碰撞系统后,控制器居然能够应付斜坡了。好吧,虽然处理得不是很漂亮,但至少不会卡在斜坡上了,这极大地鼓励了我。事实上爬坡很好,因为X轴上的极限位置总是“推回”。然而,下坡很成问题,因为当玩家跑得非常快时,他会先在X轴上移动,然后受重力作用在斜坡上下落,这就产生了“弹跳”下坡现象!
另外,在老式2D平台游戏中,玩家碰到斜坡的底部中心。但当我的基本碰撞系统运作时,控制器碰到的是斜坡地面的最接近底部角度的地方。
2d Platformer-06
为了让控制器固定在斜坡地面上,我尝试了多种解决办法,从各种教程到自己购买方案,我最终决定使用比较简单的一个。基本上就是:
1、检测控制器是否与斜坡接触
2、如果是,则直接设置Y轴关联到斜坡碰撞的Y轴位置
检测是否与斜坡接触 首先,为了检测控制器是否与斜坡接触,我检测它是否“在斜坡的上方”。为此,当一束底部光线发现碰撞时,我只要检测这个碰撞法向量和右边的单位向量之间的差异:
If raycast to the bottom hits a collision on point (Px,Py)
If angle between collision hit normal vector and right unit vector differs from 90°
set slopeOnHitPoint to true
当两束底部光线之一不能确定控制器是否真的在斜坡的上方时,是因为发生了如下图所示的情况:
2d Platformer-07
所以为了确定是否在斜坡之上,以下条件之一必须为真:
1、如果左光线和右光线均检测到斜坡,则控制器在斜坡之上
2、如果只有一束光线检测到斜坡,且作为命中点的斜坡被另一个命中点来得高
然后,为了确定控制器是否在斜坡上,我必须确定它是否接触斜坡或在斜坡的上方。然而,我发现我需要更大的“缓冲区”来防止当控制器在斜坡上时退出它的“着陆”状态。所以,修改法的伪代码是:
If aboveSlope
Set groundCheckValue to yMinLimit + 5
Else
Set groundCheckValue to yMinLimit + 1
If controller position on y axis <= groundCheckValue
Set grounded to true
If grounded is true And is not jumping
Set speed on y axis = 0
If aboveSlope
set onSlope to true
Else
Apply gravity
Set onSlope to false
设置控制器Y位置 既然我已经知道控制器是否在斜坡上了,那么接下我只要确定它的位置就行了。
因为我希望控制器“呆在”斜坡的底部中心,从它的中心投射一束垂直向下的光线,且使用命中Y位置当作新的yMinLimit。
2d Platformer-08
然后,为了避免控制器产生弹跳下坡的现象,我抛弃了Y轴速度,直接设置控制器Y位置为yMinLimit(X轴上的速度从未改变):
If onSlope
Set controller y position to yMinLimit
Else
Set controller y position to actual y position * y speed * deltaTime
Set x position to actual x position * x speed * deltaTime
峰值问题 当所有这些棘手的小问题都似乎解决了,我又遇到一个新问题:顶点!事实上,当达到顶点时,控制器就被认为位于斜坡上,它继续用来自中心的光线确定yMinLimit。所以,只要这个中心超过顶点,控制器就会产生碰撞,yMinLimit如下图所示:
2d Platformer-09
作为开发者,我必须承认当时我不知道我是否希望我的游戏中出现这种碰撞……但我不想因为自己不能处理它们就回避它们!
事实上,我没有找到这个问题的清楚解决办法,但我所选择的做法似乎也蛮管用的……
首先,我检测控制器是否在斜坡的上方,也就是顶点在左边:左光线是否检测到斜坡,且介于左命中点与中心命中点之间的距离是否大于5(任意值,取决于对贴图大小、控制器碰撞器的大小的测试)。
最后,如果控制高于顶点,我就使用其他边的光线命中点来确定yMinLimit。
2d Platformer-10
这个办法并不完美,因为中心光线和边光线之间没有过渡,不能很精准地确定yMinLimit,且它产生一个有点儿怪异的切换。但我仍然希望有一天能找到更稳妥的解决办法。
结论 总之,在自己处理碰撞问题上,我确实遇到很严峻的考验;但结果满足了我的要求。如果某些程序员看到本文能给我一些反馈,我会很高兴的。我还要问自己是否有更复杂的、处理其他类几何形状的碰撞系统也使用了类似方法?如果你知道,就请满足我的好奇心吧!