游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

业余程序员如何制作2D平台游戏的控制器

发布时间:2013-10-16 11:32:04 Tags:,,,,

作者:Yoann Pignole

在“业余程序员”系列,我想分享一下作为一名业余程序员的经验。我的分享有两个主要目的:一是证明非专业的程序员也能制作出原型;二是向专业程序员展示设计师是怎么做程序的,也许能得到一些反馈和建议吧!最后,本系列也是对我本人工作的一些反思。这次我先介绍我如何使用Unity为个人项目制作自定义操作器:

为什么使用自定义操作器,而不使用Unity默认的那个?

你可能会问自己:“但为什么不使用Unity自带的角色控制器呢?”事实上,我试过了,至少在一开始的时候。但后来我遇到了一些问题:

1、Unity的默认角色控制器是基于Unity的物理引擎的:如果你想制作一款物理平台游戏(游戏邦注:如《Little Big Planet》、《Trine》等),这也许是个不错的解决方案。然而,我需要一种“敏锐”而准确的操作,像老式2D平台游戏(《马里奥》、《索尼克》、《超级食肉男孩》等)那样的,所以Unity的物理引擎太难调整了。

2、Unity默认的角色操作器是基于胶囊状碰撞器:所以,玩家能够轻易地在斜坡上行走,但当你要做的是朝右移动的平台游戏时,胶囊就会卡在边缘上,这是不可行的:

2d Platformer-01(from gamasutra)

2d Platformer-01(from gamasutra)

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(from gamasutra)

2d Platformer-02(from gamasutra)

对一边的光线不止一束的情况,我还必须比较它们的命中点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(from gamasutra)

2d Platformer-03(from gamasutra)

光源的重要性

这不是一个大问题,我认为对专业的程序员来说是小菜一碟。但我自己花了一些时间,我真的想在这里分享一下我是怎么理解它的:

一开始,我的光线是由控制器碰撞器的角产生的。问题是,控制器不能正确地处理右边的碰撞。现在我知道它是一个代码执行顺序的问题,用下图更容易解释:

2d Platformer-04(from gamasutra)

2d Platformer-04(from gamasutra)

后来我发现了一个解决办法:光线从更远处投射,以产生“缓冲区”:

2d Platformer-05(from gamasutra)

2d Platformer-05(from gamasutra)

斜坡问题

斜坡……当想到碰撞系统时,我总是很怕斜坡。我怕到不得不去寻找“无斜坡”的做法!但是,我最终克服了斜坡障碍!

令人吃惊的是,在我整合基本的碰撞系统后,控制器居然能够应付斜坡了。好吧,虽然处理得不是很漂亮,但至少不会卡在斜坡上了,这极大地鼓励了我。事实上爬坡很好,因为X轴上的极限位置总是“推回”。然而,下坡很成问题,因为当玩家跑得非常快时,他会先在X轴上移动,然后受重力作用在斜坡上下落,这就产生了“弹跳”下坡现象!

另外,在老式2D平台游戏中,玩家碰到斜坡的底部中心。但当我的基本碰撞系统运作时,控制器碰到的是斜坡地面的最接近底部角度的地方。

2d Platformer-06(from gamasutra)

2d Platformer-06(from gamasutra)

为了让控制器固定在斜坡地面上,我尝试了多种解决办法,从各种教程到自己购买方案,我最终决定使用比较简单的一个。基本上就是:

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(from gamasutra)

2d Platformer-07(from gamasutra)

所以为了确定是否在斜坡之上,以下条件之一必须为真:

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(from gamasutra)

2d Platformer-08(from gamasutra)

然后,为了避免控制器产生弹跳下坡的现象,我抛弃了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(from gamasutra)

2d Platformer-09(from gamasutra)

作为开发者,我必须承认当时我不知道我是否希望我的游戏中出现这种碰撞……但我不想因为自己不能处理它们就回避它们!

事实上,我没有找到这个问题的清楚解决办法,但我所选择的做法似乎也蛮管用的……

首先,我检测控制器是否在斜坡的上方,也就是顶点在左边:左光线是否检测到斜坡,且介于左命中点与中心命中点之间的距离是否大于5(任意值,取决于对贴图大小、控制器碰撞器的大小的测试)。

最后,如果控制高于顶点,我就使用其他边的光线命中点来确定yMinLimit。

2d Platformer-10(from gamasutra)

2d Platformer-10(from gamasutra)

这个办法并不完美,因为中心光线和边光线之间没有过渡,不能很精准地确定yMinLimit,且它产生一个有点儿怪异的切换。但我仍然希望有一天能找到更稳妥的解决办法。

结论

总之,在自己处理碰撞问题上,我确实遇到很严峻的考验;但结果满足了我的要求。如果某些程序员看到本文能给我一些反馈,我会很高兴的。我还要问自己是否有更复杂的、处理其他类几何形状的碰撞系统也使用了类似方法?如果你知道,就请满足我的好奇心吧!感谢阅读,敬请期待“业余程序员”系列的下一篇文章!(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

The hobbyist coder #1: 2D platformer controller

by Yoann Pignole

Hi everybody! With this “hobbyist coder” series, I want to share my experience as a…well…hobbyist coder. This sharing has two main goals: showing to non professional programmers making prototypes is not inaccessible and showing to professional programmers the way a designer takes on programming, and maybe have some feedbacks/advices about perfectible parts! Finally, it’s some kind of personal post-mortems… So, let’s start with the custom 2D platformer controller I made in Unity for a personal project:

Why using a custom controller and not the Unity’s default one?

You may ask yourself “but why not using the unity character controller from Unity?” Actually, I did. At least, at start! Then, I encountered some problems:

The Unity’s default character controller is based on the Unity’s physics engine: it’s probably a good solution if you want a physics-based platformer feeling (like Little Big Planet, Trine, etc.). However, I wanted a “sharp” and precise control, like old school 2D platformers (mario, sonic, meat boy, etc.), so the physics engine way turned out to be very difficult to tweak in this way.

The Unity’s default character controller is based on a capsule shape collider : so, the player is able to walk on slopes easily but, when you have to handle platforms with right-angled edges, the capsule gets stuck on the edges and it’s just unacceptable:

Finally, I decided to do my own controller when thinking about future IA pathfinding : I think it will be much easier to control the IA moves along the path with simple function like Run(speed) and Jump(height) on a generic platformer controller, dealing itself with the world constraints (collisions and gravity).
Basic principle

Nothing extraordinary here, there is a lot of tutorials about collisions on 2D games on the internet and the way I took is just a mix among different things I read and some of my own (unsuccessful) experiences.

As my prototype is a tile-based platformer, I could have used a simple system to check if the tile I want to move on is a wall or not. For several reasons, mainly because I previously had difficulties to handle slopes with this kind of approach, I chose a more versatile system, using raycasting to detect collision.

So, a basic collision concept is very simple: the controller moves with… a speed value, except if this speed brings its collider into another collider. So, we just need to detect these potential collisions !

To detect the collisions, as I said above, I use a raycast: when there is a speed value, some rays search for a collision in the direction of this speed, for each axis x and y. If the ray hits some collision, the hit point will define the maximum value the controller will be able to reach on the axis.

In pseudocode, it looks like this for each ray of the four directions:

If speed on x axis > 0
If raycast to the right hits a collision on point (Px,Py)
Set xMaxLimit = Px

Depending of the size of my controller collider and the size of my tiles, I had to cast only two rays per side, for each corner. But if one of your two is bigger than your tiles size, you may encounter some issues with only two rays:

With more than one ray per side, I also have to compare their hit points P1 and P2 and keep the closest one:

Set xMaxLimit = Minimum value between P1x and P2x

Finally I check if the controller is blocked on each side to set the speed value to 0 in case of. If not, the controller will still be blocked but will keep a speed value against the wall. So, if you want to suddenly go the other way, you have to wait the opposite acceleration compensate the actual speed to finally increase speed in the other direction: it results a “stuck” feeling for the player.

The solution was pretty simple:

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

So, very simple and traditional way I guess…

Let’s bring Newton!

Gravity is just a constant acceleration applied on speed on y axis each frame when in the air :

speed on y axis = speed on y axis + gravity acceleration * delta time

(I won’t talk about delta time here, as coders now exactly what it is and non-coders can find much better explanation on the internet, for example here : http://alexreidy.me/game-programming-an-explanation-of-delta-time/ )

I won’t explain the jump system I chose in details now (maybe another post…). The important thing here is that when a jump input is called, an impulsion speed is applied on the controller on y axis and then the gravity makes its job to step-by-step compensate the elevation and bring back the controller to a fall state.

Then I used the same system as above to determinate move limits and grounded flag:

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)

You can see some variations with the previous code:

No need to check if there’s a speed in the bottom direction to raycast as there is always one (generated by the continuous gravity acceleration).

The same way, no need to check it when grounded.

I thought a new check if the controller is jumping was needed before set speed on y axis to 0: if not, when the jump impulsion is applied on y speed, it directly returns to 0 as the controller will be probably still considerate grounded at the next frame.

The importance of rays origins

It’s not a big problem and I guess not very hard to solve for a professional coder. But I took some time for me to and I really want to share the hard time I had to understand it

At start, my rays were generated from the exact corners of the controller collider. The problem is the controller was not able to handle correctly right-edged collisions. Now I understand it’s a question of code execution order that is much easier to explain with a little scheme:

So I found a solution casting rays from further to have a “buffer”:

The slopes problem

Slopes… it always frightens me when thinking about a collision system. It frightened me enough to really considerate the “no slopes” options! But, let’s see how I did to finally handle them!

Surprisingly, after I integrated the basic collision system, the controller was able to take slopes. Ok, really roughly, but it wasn’t stuck on them and it was encouraging for me  Actually, the climbing of slopes was pretty good, as the limit position on x axis was always “pushed back”. However, the descent was problematic because when the player was running very fast, he moved on x axis just before falling on the slope with gravity, resulting in a “bouncing” descent!

In addition, on old school 2D platform games, the player touches the slope ground with its bottom center. But when my collision basic system does the job, the controller touches the slope ground with the closest bottom corner.

After trying a lot of solution to keep the controller on the ground on slopes, from different tutorial or by my own, I finally decided to use a simple one. Basically:

Detect when controller is grounded on a slope

If it is, directly set the y position relating to the y position of the slope collision

Detect if grounded on slope

First, to detect if the controller is grounded on a slope, I detect if it is “above a slope”. To do that, when one bottom rays hit a collision, I just check the difference between this collision normal vector and a unit vector to the right:

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

Only one of the two bottom rays can’t determinates if the controller is really above the slope because of this case:

So to determinate if above a slope, one of the following conditions must be true:

If there a slope detected with the left AND the right ray, the controller is above a slope.

If there a slope detected only with one ray AND the hit point were the slope is detected is higher than the other hit point

Then, to determinates if the controller is ON the slope, I just have to determinates if it is grounded AND above a slope. However, I found that I need a bigger “buffer” to prevent the controller to exit its “grounded” state when on a slope. So, the modifications in pseudo-code:

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

Set the controller y position

Now I know if the controller is on a slope or not. I just have to determinate its position.
As I want the controller to “stay” on slope on its bottom center, I cast a vertical down ray from its center and use the hit y position as the new yMinLimit.

Then, to avoid the controller to make bounces descending the slopes, I discard the classic speed application on y axis and directly set the controller y position to the yMinLimit (the speed application on x axis never changes):

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

And now the peaks problem!!!

As soon as all these tricky stuff seemed to work, I encountered a new problem: the *$@! peaks!! The fact is, as the controller is considerate on a slope when on a peak, it continues to define the yMinLimit with the ray from the center. So, as soon as the center goes over the peak, the controller goes through the collision as the yMinLimit is defined below:

As a designer, I have to admit that I didn’t know at this time if I wanted to have such collisions in my game… But I didn’t want to refuse them just because I wasn’t able to handle them!

Actually, I didn’t found a clean solution to handle this case, but the one I chose doesn’t work so bad…

First, I check if my controller is above a peak which means, for a peak on the left: if slope detected with left ray AND distance between left hit point and center hit point > 5 (arbitrary value determinate with tests depends of tiles size, controller collider size, etc…).

Finally, if the controller is above a peak, I use the other side ray hit point to define the yMinLimit.

It’s not perfect because there’s no transition between the center ray and the side ray to determinate the yMinLimit and it results a little strange shift. But I haven’t lost hope in finding a cleaner solution someday!

Conclusion

In conclusion that was a really cool challenge for me to handle collisions myself and the result works pretty good for what I wanted to do. If some coders read this, I would be glad to have some feedbacks about this solution. I’m also asking myself if more complex systems of collisions with physics engines, working with different kind of geometrical shapes, are using similar approaches ? If you have some answers or reading about that, feel free to share and satisfy my curiosity ! Thanks for your reading and maybe see you later for another episode of the “hobbyist coder”!(source:gamasutra)


上一篇:

下一篇: