今回 Lanza 将带来一些古老的游戏开发经验~
(最早的试验我想应该是这个) https://indienova.com/u/lanza/blogread/1863
注:本文章采用 Y
轴向下的屏幕直角坐标系
“3D”游戏
我们玩到一些游戏时,往往按照画面表现给出一种分类,讲它是 2D 或是 3D 的电子游戏。
实际上我们不仅可以从画面来分,更可以从游戏当中呈现给我们的活动范围来划分,比如经典的:FC《双截龙》系列。
我们可以发现,游戏对象有纵向移动、横向移动、还可以在高度轴上发生移动,
实际上,这往往已经可以说明它是一个 3D 游戏了。
物理的世界
我们知道现在有很多物理引擎—— 2D 的、3D 的都有,这些物理引擎能够制作出像《愤怒的小鸟》这样的具有典型的物理感、机关感的游戏。其实我们自己玩的时候会有一些"感觉"的,这些用专业物理引擎制作出的游戏物体运动和我们以前接触到的一些古典的游戏是有很大不同。这就是我们要去研究的点——在不使用专业物理引擎的情况下,怎么去避开这些麻烦的物理拟真,去做一个简单的、古典的具有运动物体和活动范围的空间呢?
物体
首先我们可以去设计一个物件,用它去表示物理世界内的一个可运动的物体。一般而言,在一开始设计物体时,你最先给出它的两个关键元素:坐标和速度。
如果你的编程环境中有基本的一些数学工具(比如 Vector3
这样的东西),做起来就会比较快,如果没有的话,可以自己简单地写一个——搬中学数学课本。
物体的坐标用于直接表示它在这个物理世界当中所处的位置,而速度则指示该物体将要如何去变化它的位置——你在游戏循环中需要不断根据速度来修正其位置,这样就实现了物体的运动。
当然,你还需要给物体施加一个重力(gravity
),让它无条件地产生某种速度变化。
# 可以先这样写一个简单的实现,之后我们再修改 def update gravity = Vector3.new(0, 0, -0.1) self.speed += gravity self.position += self.speed end
这样的示例代码就表示了一个简单的物体每帧受到在 z
轴为 -0.1
个单位的重力的运动模拟的过程。
物体每帧受到 z
轴负向的重力就会产生自然下落,如果你要给物体一个 z
轴正向的速度,那么物体就会出现起跳~下坠,这样的一个很自然的过程。
通常你绘制一个 2D 物体时,直接将物体的 x
y
坐标拿来加上摄像机坐标就可以拿去直接绘制精灵了——在绘制 3D 物体时,你需要体现第三轴向的变化。
屏幕是一个二维空间,体现第三轴向的变化就需要借助 x
y
这两个轴向才行,像是我举例的《双截龙》截图当中就是借助 y
轴体现 z
轴的变化的(跳跃只产生了 z
轴的变化,但是绘制时反映到了 y
轴上,你可以简单地认为绘制坐标 = 角色 Y - 角色 Z
)。
那么问题来了,y
轴和 z
轴在绘制上合并了,那么如果物体同时在 y
轴和 z
轴发生移动的话,就会造成玩家的困惑,比如下图:
Shari 在跳跃的同时还进行了上下移动,这在战斗场景等需要玩家判断位置的场合是容易带来困扰的,那么怎么解决呢?也很简单,加个影子就可以了:
有一个可以指示物体 Y
轴位置、且大小和浓度受高度影响的阴影,就可以更快使人透过屏幕而辨识出物体在空间当中的位置。
古典的舞台是一个大台阶
刚才我们的示例当中已经做出了一个可以运动的物体,那么,现在我们需要给物体一个活动范围的限制,一般来说,也就是碰撞相关的内容了。
像我们上面的代码,给出一个 gravity
,然后让速度不断添加,坐标也不断变化……那么我们看到的就是一个物体无限地朝着 z
轴负方向掉了下去。
实际上这个过程我们是可以去施加一个限度的,你可以想象为设置一个"地板",物体的 z
坐标到了这个地板就到底了,不能再降低了。
def height return 0 end def update gravity = Vector3.new(0, 0, -0.1) self.speed += gravity self.position += self.speed self.position.z = height if self.position.z < height end
以此代码为例,我们写了一个 height
方法来获取高度,这个高度我们先写死为 0
——这样一来,物体的 z
坐标到 0
这个值就不会再降低了。
当然,这样的话世界就变成了一个超大的平地,因此我们可以做点变化……
def height(x, y) return 32 if x > 100 return 0 end def update gravity = Vector3.new(0, 0, -0.1) self.speed += gravity self.position += self.speed self.position.z = height(self.position.x, self.position.y) if self.position.z < height end
这样修改的话,高度就不是一个固定的值,而是一个变化的函数:我们提供一个坐标,获取一个高度值,也即理解成:获取世界在某处的高度。
代码示例当中,x > 100
这片区域的高度是 32
,而剩下区域的高度是 0
,这样你就可以做一个实验:放置一个物体,坐标为 (180, 0, 64)
,
然后你就会观察到,它的 z
坐标会因为重力从 64
落到 32
后不再变化,这时你把它向左(x
轴的负向移动)直到其 x 坐标 < 100
,它的 z
坐标又将继续下落到 0
了。
其实用这种方式构建的世界就像一个大台阶一样,是一个高度不断变化的空间。
在实际的开发当中,你当然不需要去手写这种高度变化的表达式——你可以直接在地图编辑器当中划定某些区域的高度,然后在引擎里去解读。
限制移动
当前我们已经做到了物体运动以及舞台高度的变化,接下来的问题就是将上面的例子反过来——
我们把物体从高度较高的移动到一个高度较低的地方,那么它会继续下落,如果反之,我们将物体从高度低的地方向高度高的地方移动,那将会发生什么呢?
看上面的代码可以发现,如果物体从高度较低的地方移向高度较高的地方,那就将会直接让物体爬升——这是我们不希望看到的。
解决的方法也很简单,我们连续地做高度检测,先检测物体当前所处位置的地形高度,再检测一下物体移动后所处位置的地形高度,如果前者小于后者,说明物体面对了高度差,要"撞墙",那么就要阻止移动的发生。
当然你也可以放宽一点点,比如后者如果只比前者小一定的值(像是1~4像素),那么也允许爬升。这可以制作楼梯、爬坡等地形。
按照这个思路,可以拓展更多的内容——
按照这种思路构建的世界已经足以去制作一款古典风格的动作游戏了,当然你还需要解决更细致的问题:比如在高速运动下发生的穿透(需要用分 substep 解决)、不规则的碰撞检测等。
当然本文只是一种抛砖引玉的意图,如果大家有更多关于老式游戏中的想法,可以多分享和交流喔!
有趣
z轴位移施加到y轴,有意思!