引言
本文内容包括俯视角 2D 人物移动控制、2.5D 风格的实现一机使用协程实现相机绕玩家旋转效果。
通常游戏按照镜头角度可以分为 2D 游戏和 3D 游戏,但还有一种独树一帜的游戏风格,将二维与三维的特点结合起来,在 2D 图像上展现出了 3D 的效果,我们称之为 2.5D,也就是伪 3D。有很多非常受欢迎的游戏都采用了这种风格,而这种风格也将它们从同类的其他游戏中凸显出来,例如《饥荒》和《八方旅人》。
今天我们就来一起使用 Unity 制作出类似《饥荒》 的俯视角 2.5D 风格。
场景搭建
首先我们来快速制作一个 2D 的俯视角场景和人物控制。首先从资源商店里导入我们需要的游戏素材:
这个素材是免费的,你可以很容易的从资源商店找到。
接着更改素材的尺寸,然后把需要的素材的轴心设置为底部,保证 sprite 在旋转后位于地面上层:
在场景中创建一个瓦片地图,然后打开平铺调色板创建瓦片资源:
导入完成后就可以开始绘制地图了:
为了让场景物体和玩家有碰撞,我们给场景物体添加碰撞器。
新建一个空物体 FaceObject
,将所有需要朝向摄像机的物体都作为这个物体的子物体,便于管理:
角色
然后就是角色。我们首先创建一个 sprite,指定它的默认精灵并重命名为 Player
:
为 Player
设置标签 Player
,方便之后查找。
我们需要 player 移动,与场景中的物体碰撞和播放动画,所以我们给 player 添加上刚体、碰撞体和动画控制器:
角色动画
选中 player 并按下 ctrl
+6
快捷键打开动画窗口,将序列帧素材拖到窗口中创建新动画:
然后我们打开动画器,可以看到动画已经导入了。这里我们用 blendtree 快速实现动画的过渡,右键 create stat,选择 blendtree 重命名为 walk
:
双击这个节点打开新混合树。目前这个混合树只接受一个输入参数,我们需要两个输入参数来表示二维平面上的移动。选择类型为 2D simple directional,这样我们就有两个可接受的参数了:
所以我们创建两个 float
类型的参数,分别取名为 InputX
和 InputY
:
然后在混合树中就可以选择接受的两个参数了。我们选择好刚创建的两个参数,点击加号添加要混合的动画:
设置完之后我们可以拖动屏幕中心的红点来模拟输入,混合树会自动的对我们输入的值进行判断播放哪个动画。然后我们还需要静止的画面,回到上一层同样创建一个混合树用来播放静止动画,命名为 idle
,再创建一个 bool
变量 isMoving
来切换两组动画:
设置好两个混合树的切换条件:
设置 idle
混合树的混合动画:
角色控制
接下来我们来编写一个简单的程序控制 Player
的移动。新建 C# 脚本起名为 PlayerMovement
,然后打开它,首先设置好需要的变量:
public float speed; |
其中 speed
控制移动速度,rigidbody
和 animator
获取刚体和控制体,inputX
和 inputY
获取键盘上的方向输入。在 Start()
函数中通过变量获取 Player
相应组件方便后续使用:
void Start() |
然后在 Update()
函数中实时获取到玩家的输入信息,使用 GetAxisRaw()
获取玩家的输入,这个函数会根据参数字符串获取到不同的玩家输入:
inputX = Input.GetAxisRaw("Horizontal"); |
例如我这里输入 Horizontal 也就是水平的英文单词,这个参数会自动获取我的左右和 AD 这两组按键并返回一个 -1 到 1 的值,可以用这种方法非常快速的获取到玩家输入。按键对应的字符串可在 Edit - Project Setting 中的 Input Manager 中找到:
我们现在就可以找到垂直输入的参数是 Vertical,它对应的按键是上下和 WS。回到脚本中获取到 Y 轴的输入,用一个 Vector2
向量接受输入并进行 normalized 标准化:
Vector2 input = (transform.right * inputX + transform.up * inputY).normalized; |
然后使用这个变量乘以速度赋值给刚体的速度:
rigidbody.velocity = input * speed; |
那么为什么需要将向量标准化呢?我们先取消标准化然后运行游戏,人物现在可以在场景中自由移动了,但当同时按下 X 轴和 Y 轴的按键时角色的移动会比正常情况下更快,这是因为 X 轴的速度和 Y 轴的速度叠加了。事实上我们只需要速度的方向,因此这个向量的长度必须是 1,而标准化后的向量方向不会变,而大小被限制成了 1。现在加上标准化再次运行,可以看到速度没有异常了。
接下来来控制动画的播放。当存在输入的时候,也就是 inputX
或 inputY
不为零的时候,修改动画控制器为 true
,否则为 false
:
运行游戏,可以看到移动时的动画在正常播放,但不会停在原来的方向。我们查看动画器可以发现,一旦我们停止输入,动画器的参数就会归零,而混合树则会默认播放第一个动画。
我们的解决办法是传给动画器有效的输入,新增两个变量 stopX
和 stopY
记录输入不为零的部分,当存在输入的时候才更新这两个值,然后将这两个值传给动画器:
if (input != Vector2.zero) |
再次运行,可以看到我们的动画已经完美运行了。
视角
接下来就是重头部分了,我们将把这个场景从 2D 扩展到 2.5D。事实上 Unity 的 2D 是基于 3D 的,我们只需要点击这个 2D 按钮取消 2D 模式,Unity 就会回到 3D视图:
透视投影
在这个视图下我们可以调整相机的角度俯视地图,不过首先要修改相机的投影模式,修改为透视。这个模式通常用于 3D,有近大远小的感觉:
你可以随意更改相机的角度,不过我推荐 45 度,这是最常用的视角,能最大程度的平衡玩家和视野场景:
然后编写简单脚本来控制场景中的物体都朝向摄像机。新建 C# 脚本,起名为 FacingCamera
,将脚本添加到场景副物体上,进入脚本。
首先我们需要一个数组变量来获取这个物体的所有子物体,在 Start()
方法中循环遍历所有子物体并存储下来:
Transform[] childs; |
在 Update()
方法中遍历子物体,并让子物体的旋转值设置成和 camera 一样:
void Update() |
这样场景中的所有 sprite 就会始终朝向摄像机了,再在 PlayerMovement
脚本中加上简单的相机跟随 Player 位置代码:
...... |
最后在项目设置 - 图形设置透明度排序模式为透视,这将让 sprite 根据距离相机远近渲染先后顺序:
运行场景,这样一个还不错的 2.5D 场景就制作完成了。
旋转视角
但在最后我们要再实现一个《饥荒》中的小功能:按下 q
和 e
键旋转摄像机,这让我们可以更全面的观察整个场景。
《饥荒》中的摄像机是围绕着玩家旋转的,每次旋转 45 度,那么如何让摄像机围绕玩家旋转呢?我们可以使用 RotateAround()
方法,但经过我的测试,这种方法并不好用。然后我发现,其实没必要让摄像机主动转起来,只需要让摄像机作为子物体,旋转父物体就会让摄像机被动地绕父物体旋转。所以我们先让摄像机作为玩家的子物体,再旋转玩家,就可以看到绕玩家旋转的效果。
但我们没办法就这样简单的旋转玩家控制摄像机,还记得刚刚的 FacingCamera
脚本吗?Player
的旋转角度实际上是受摄像机的旋转角度控制的,所以我们新建一个空物体,起名为 CameraPosition
并把摄像机作为子物体,这样我们就可以通过旋转或移动这个物体控制摄像机的位置和旋转。
为这个物体新建脚本 RotatingCamera
,进入脚本。首先我们要删掉之前在 PlayerMovement
脚本中的相机跟随脚本。
我们需要一个公有变量控制一次旋转需要的时间 rotateTime
,然后需要一个变量 player
来获取 Player
的位置,还需要一个 bool
类型的变量 isRotating
来防止多次旋转冲突:
public float rotateTime = 0.2f; |
在 Start()
函数中获取到 Player
的 Transform
,在 Update()
函数中让摄像机的位置一直跟随玩家,然后编写控制旋转的函数 Rotate()
和进行旋转的协程函数:
void Start() |
这里简单解释一下协程函数的概念。在普通的函数中,无论内部有多少代码,进行多少次循环,都将在 1 帧内执行完,着就导致我们无法在普通函数中使用循环来做一个持续进行的效果。如果不幸遇到了死循环,还会导致 Unity 失去响应,这时候就需要用到协程函数,这种函数内部在执行时可以先暂停执行,等到固定的时间后继续执行,加上循环后给人的感觉就像是一个可以设置更新频率,自动停止的 Update()
函数,可以很方便的制作程序性动画或一个循序渐进的效果。
协程函数没有返回值但可以有参数,在声明的时候必须使用 IEnumerator
关键字。我们创建一个协程,取名为 RotateAround()
,传入两个参数,分别为旋转的角度和旋转的时间。
然后首先计算出需要旋转多少次,我这里是用 FixUpdate()
的帧率更新的,1 秒执行 60次。接着根据次数和总角度计算每次需要旋转多少度。
接下来开始循环计算出的次数,每次旋转一个较小的度数,然后使用 yield return new WaitForFixedUpdate
暂停执行,等到下一帧时继续执行下个循环:
IEnumerator RotateAround(float angel, float time) |
开启协程需要使用 StartCoroutine()
函数,传入 -45 角度和时间,按照同样的方法写出 e
的旋转:
void Rotate() |
回到 Unity 开始运行,可以看到我们的旋转效果已经成功运行了:
源码
PlayerMovement
using System.Collections; |
FacingCamera
using System.Collections; |
RotatingCamera
using System.Collections; |