OnOff-Unity2D游戏开发

引言
本文主要面向对Unity有基本了解,并想学习制作一个完整的2D Platformer游戏的读者。在课程中会透过重新制作《On Off》这个小游戏,来为大家讲解关于2D人物的操纵以及动画转换,也包含了开始选单、音乐音效等在各游戏中都非常泛用的知识点。
项目介绍
项目架构
- 玩家
PlayerController.cs - 游戏功能
GameManager.cs - 转换颜色
SwitchColor.cs - 地板
Ground.cs - 终点
Goal.cs
玩家功能
按左右键进行移动
侦测左右键输入
float h = Input.GetAxisRaw("Horizontal")- 不按键,
h=0;按右键,h=1;按左键,h=-1
实际移动
- 修改速度
rb.velocity - 只修改
x方向速度 rb.velocity=new Vector2(moveSpeed*h,rb.velocity.y)
- 修改速度
角色面向随行走方向改变
- 通过改变
transform.localScale.x实现
- 通过改变
角色动画
- 站立、行走、跳跃三个动画之间的转换
向上进行跳跃
- 向上施加力
- 不能在空中跳跃
- 判断是否踩在地板上
isGround
- 判断是否踩在地板上
核心功能搭建
角色控制
变量声明
我们首先声明一些变量:
moveSpeed:玩家移动速度jumpForce:玩家跳跃速度rb:玩家物理组件anim:玩家动画组件
1 | public float moveSpeed; |
声明变量之后我们先在Start()中获取这些组件,需要注意的是动画组件是用获取子物件的GetComponentInChildren<Animator>()
1 | // Start is called before the first frame update |
横向移动实现
这里使用GetAxisRaw而不是GetAxis的原因是如果我们使用GetAxis按键松手后h会慢慢过渡回0,而GetAxisRaw是瞬时变化为0。
移动的实现我们选择rb.velocity,Vector2()中的h * moveSpeed代表了方向乘以移动速度,这样如果按下左键h = -1就会向左移动;如果按下右键h = 1则向右移动;没有输入则保持不动。
1 | // Update is called once per frame |
回到Unity,在PlayerController.cs脚本中设定moveSpeed,测试角色可以正常移动。但相比其他平台跳跃类游戏,我们的角色移动时的朝向并不会改变,所以我们需要接着修改。
移动朝向
朝向问题其实不难解决,我们可以先尝试修改一下角色Transform组件中的Scale,不难发现向右时为1;向左时为-1。这时候联想一下刚才的h,赋值属性非常契合。
在代码中我们需要考虑没有输入的情况,如果依然随着h,那么角色会被压缩到无,所以需要加入判断if (h != 0),然后在判断内通过对Scale赋值为h来改变朝向。
1 | // Update is called once per frame |
回到Unity测试,角色已经可以随着移动方向改变而改变朝向。
跳跃
通常来讲一般和物理相关都需要写在FixedUpdate()里,但我们这个小游戏没有很吃性能,所以就写在Update()里面了。
如果按下上键,那么我们会给Rigidbody2D添加一个方向向上、大小为jumpForce,施力模式向ForceMode2D.Impulse是此 rigidbody2D 添加瞬时力冲击。
1 | // Update is called once per frame |
返回Unity,设置调整jumpForce数值,测试角色跳跃。
但如果多测试几次就会发现,角色可以在空中进行多次跳跃,也就是所谓的“无限跳”。对于这个问题,我们其实只需要判断 角色的脚底是否是地板 就可以了,而踩在地板这个条件则可以通过标签进行判断,如果角色脚底是地板则可以进行跳跃操作,否则则不能跳跃。
所以我们在Player上创建子物件GroundCheck移到角色脚底,作用就是检测角色是否碰到地板。

回到程式我们需要找到这个检测物件GroundCheck,所以首先在前面声明变量:
1 | public class PlayerController : MonoBehaviour |
然后回到跳跃的判断,加入判断函数isGround()来判断我们是否踩在地板上:
1 | - if (Input.GetKeyDown(KeyCode.UpArrow)) |
编写判断函数isGround(),这里Physics2D.CircleCast(transform.position,0.3f,Vector2.down,0.35f,whatIsGround)的意思是我们画一个圆在物件本身的位置上,向下0.35个单位画一个半径为0.3的圆,这个圆将只会和被我们判定成Ground的图层碰撞。如果hit碰撞到返回true,否则返回false。
1 | bool IsGround() |
这样写就不需要groundCheck了,我们删除之前设置的变量和空物件,改为获取图层LayerMask:
1 | public class PlayerController : MonoBehaviour |
回到Unity设置检测图层为Ground:

动画连接
Move部分是由Move这个Parameter判断的,0播放Player_Idle;1播放Player_Move。

可以发现动画播放和h的关联性,但因为有-1的关系需要取绝对值Mathf.Abs(),这样左右移动取值均为1。
1 | // Update is called once per frame |
回到Unity,测试站立、移动和跳跃的动画。
整理
对我们PlayerController.cs的代码稍加整理,添加一些注释:
1 | // Update is called once per frame |
颜色转换
这部分我们来实现游戏的核心功能:颜色转换的部分。首先为所有需要颜色转换功能的物件(玩家、地板等等)新建一个共同的脚本命名为SwitchColor.cs,同时因为这是游戏的核心功能,所以我们新建一个GameManager.cs来进行管理。
为了确保GameManager游戏管理者有且仅有一个,我们使用单例模式:
1 | public class GameManager : MonoBehaviour |
SwitchAllColor()函数用于抓取场景所有有SwitchColor,同时设置数组存储这些物体:
1 | private SwitchColor[] colorObjs; |
而colorObjs则在开始时的Start()中获取,保存在SwitchColor[]中:
1 | void Start() |
回到SwitchAllColor()中遍历数组呼叫里面的SwichObjColor()函数:
1 | public void SwitchAllColor() |
新建SwitchColor.cs,声明之前在GameManager.cs调用的SwichObjColor()。因为这个脚本需要同时满足玩家、地板、背景、UI等的颜色变换需求,所以我们用枚举的方式区分不同的类别,然后根据枚举的类型决定颜色转换的方式:
对于玩家而言,颜色的改变只需要将spriteRender中的颜色改变即可;而地板则需要考虑碰撞体的激活问题;背景则由相机的Background来控制;image和text之后会用于UI的颜色转换。
1 | public class SwitchColor : MonoBehaviour |
回到Unity对相应物件的组件类型ComponentType进行赋值。返回SwitchColor.cs,声明作为选项的组件类型和变换前后的两种颜色:
1 | public class SwitchColor : MonoBehaviour |
有了上面的变量,我们就可以根据不同的组件类型利用Switch对转换颜色进行分支操作:
player:获取精灵渲染器改变颜色,color == startColor ? endColor : startColor做一个颜色的翻转spriteRender:同playerimage:涉及UI部分需要引入using UnityEngine.UItext:同imagecamera:同playerground:在player的基础上对是否启用BoxCollider2D进行翻转
1 | public void SwichObjColor() |
回到PlayerController.cs,在Update()函数中调用GameManager.cs中的SwitchAllColor(),再经由此跳转到SwitchColor.cs中的SwichObjColor()方法:
1 | // Update is called once per frame |
回到Unity,对组件的起始、结束的颜色进行赋值,注意透明度调至255:
| ComponentType | startColor | endColor |
|---|---|---|
| Main Camera | 255 255 255 | 0 0 0 |
| Player | 0 0 0 | 255 255 255 |
| BlackGround | 51 51 51 | 71 71 71 |
| WhiteGround | 235 235 235 | 255 255 255 |
测试颜色转换功能:


死亡及死亡特效


1 | //Switch Color |
1 | void Update() |
1 | public void Die() |
加载关卡

为目标添加Goal.cs:
OnTriggerEnter2D(Collider2D collision):检测碰撞玩家,播放动画;为防止再次触发过关手动把CircleCollider2D关掉ToNextStage():调用GameManager中的NextStage()函数进入下一关EnableCollider():将碰撞体激活
1 | using UnityEngine; |
回到GameManager.cs编写进入关卡的逻辑,直接调用SceneManager读取我们的关卡数,+1后就是下一关。
1 | using UnityEngine; |
回到Unity将结束动画中最后帧中的Function赋值给ToNextStage()以触发:

用户界面及数据继承
搭建UI组件,放入素材中的图片,挂载颜色转换脚本SwitchColor.cs同时将类型更改为Image或Text:

为了便于管理,我们新建一个管理UI的脚本UIManager.cs。
首先像之前一样使用单例模式创建一个UIManager,再声明两个Text类型的变量(死亡数和星星数),最后声明两个 Refresh刷新函数用于接收新数据并更新UI界面的数据:
1 | using UnityEngine.UI; |
回到GameManager.cs新增统计死亡数和星星数的变量:
1 | public class GameManager : MonoBehaviour |
分别在死亡和过关的函数中计数并调用UIManager:
1 | public void PlayerDie() |
最后在Star()函数中调用刷新函数确保数据能够刷新:
1 | void Start() |
音乐与音效
1 | using UnityEngine; |
效果完善总结
开始界面

残影效果
代码逻辑
我们首先需要声明一些变量:
dashSpeed:冲刺速度dashTime:冲刺持续时间startDashTime:倒计时的计时器isDashing:冲刺状态的记录量dashObj:保存残影物体
冲刺的逻辑是:
1 | if(不是冲刺状态) |
通过上面的伪代码,我们可以轻松写出业务逻辑:
1 | using System.Collections; |
粒子效果
回到Unity编辑器,新建一个空的游戏对象并为其添加一个粒子系统组件
粒子系统(Particle System)主模块
Particle System 模块包含影响整个系统的全局属性。大多数这些属性用于控制新创建的粒子的初始状态。
Duration:系统运行的时间长度。取值与冲刺时间相近避免残影持续太长时间Start LifeTime:粒子的初始生命周期。与上面同理Start Size:每个粒子的初始大小。根据自己的大小来调整StartColor:每个粒子的初始颜色。这里我们选择由浅绿色到浅蓝色的随机渐变Simulation Space:控制粒子的运动位置是在父对象的局部空间中(因此与父对象一起移动)、在世界空间中还是相对于自定义对象(与您选择的自定义对象一起移动)。选择世界空间(World)使残影生成时留在原地,不会和角色移动有任何关系Max Particles:系统中同时允许的最多粒子数。可以适当调小一些
Emission 模块
此模块中的属性会影响粒子系统发射的速率和时间。
Rate over Distance:每个移动距离单位发射的粒子数。这样粒子系统就会随距离而不是时间生成粒子

Shape模块
此模块用于定义可发射粒子的体积或表面以及起始速度的方向。Shape 属性定义发射体积的形状,其余模块属性根据您选择的 Shape 值而变化。
Shape:发射体积的形状。选择圆形(Circle)
Color Over Lifetime 模块
此模块指定粒子的颜色和透明度在其生命周期中如何变化。
Color:粒子在其生命周期内的颜色渐变,渐变条的左侧点表示粒子寿命的开始,而渐变条的右侧表示粒子寿命的结束。我们这里设置渐变透明,透明度由高到低
Texture Sheet Animation 模块
粒子的图形不必是静止图像。此模块允许您将纹理视为可作为动画帧进行播放的一组单独子图像。
Mode:弹出菜单。2D残影选择Sprites模式,图片选择切分好的站立的精灵即可

效果
最后我们按下LeftShift就有一个由绿到蓝的渐变透明残影的效果了:

尖刺和二段跳
尖刺
创建一个三角形物体并拖拽至精灵文件夹Sprites中,命名为Triangle:

制作预制体,为其添加组件BoxCollider2D,可以在Edit Collider中变更碰撞判定。在实际游戏开发中不可能将碰撞体制作修正的和本体一模一样,大部分做法是根据感觉选择一个折中的碰撞判定区域,所以我们框选一个差不多的碰撞体即可。和之前的地板一致,在初始状态下白色的尖刺碰撞体应该是取消勾选碰撞组件的。
为其添加颜色转换组件SwitchColor.cs:
Commponent Type:选择Ground,性质与Ground一致Start Color:颜色235,235,235;透明度255End Color:颜色255,255,255;透明度255

对于尖刺的脚本只需要附载OnTriggerEnter2D(Collider2D collision),通过判断碰撞体标签是否为Player来判断是否碰到玩家,如果碰到则触发玩家控制器PlayerController.cs中的玩家死亡Die()即可。
1 | public class Trap : MonoBehaviour |
同理我们可以复制粘贴白色尖刺,通过更改变换颜色SwitchColor.cs脚本的颜色和重新勾选碰撞体来制作黑色尖刺:
Commponent Type:选择Ground,性质与Ground一致Start Color:颜色51,51,51;透明度255End Color:颜色71,71,71;透明度255

最后将黑白两个尖刺都拖至预制体文件夹Prefab中以备之后关卡搭建使用。

项目导出
分辨率
直接导出为可执行程序会导致之前设计的UI位置会发生变化,所以我们新建脚本Resolution.cs,固定分辨率为1024x768:
1 | using System.Collections; |
这里1024x768的分辨率来源是根据UI的Canvas Scaler中的分辨率:

回到Unity将其挂在到主相机Main Camera上,测试运行,UI就恢复了正常,回到原位。