引言
本文主要面向对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
:玩家动画组件
public float moveSpeed; |
声明变量之后我们先在Start()
中获取这些组件,需要注意的是动画组件是用获取子物件的GetComponentInChildren<Animator>()
// Start is called before the first frame update |
横向移动实现
这里使用GetAxisRaw
而不是GetAxis
的原因是如果我们使用GetAxis
按键松手后h会慢慢过渡回0,而GetAxisRaw
是瞬时变化为0。
移动的实现我们选择rb.velocity
,Vector2()
中的h * moveSpeed
代表了方向乘以移动速度,这样如果按下左键h = -1
就会向左移动;如果按下右键h = 1
则向右移动;没有输入则保持不动。
// Update is called once per frame |
回到Unity,在PlayerController.cs
脚本中设定moveSpeed
,测试角色可以正常移动。但相比其他平台跳跃类游戏,我们的角色移动时的朝向并不会改变,所以我们需要接着修改。
移动朝向
朝向问题其实不难解决,我们可以先尝试修改一下角色Transform
组件中的Scale
,不难发现向右时为1;向左时为-1。这时候联想一下刚才的h
,赋值属性非常契合。
在代码中我们需要考虑没有输入的情况,如果依然随着h
,那么角色会被压缩到无,所以需要加入判断if (h != 0)
,然后在判断内通过对Scale
赋值为h
来改变朝向。
// Update is called once per frame |
回到Unity测试,角色已经可以随着移动方向改变而改变朝向。
跳跃
通常来讲一般和物理相关都需要写在FixedUpdate()
里,但我们这个小游戏没有很吃性能,所以就写在Update()
里面了。
如果按下上键,那么我们会给Rigidbody2D
添加一个方向向上、大小为jumpForce
,施力模式向ForceMode2D.Impulse
是此 rigidbody2D
添加瞬时力冲击。
// Update is called once per frame |
返回Unity,设置调整jumpForce
数值,测试角色跳跃。
但如果多测试几次就会发现,角色可以在空中进行多次跳跃,也就是所谓的“无限跳”。对于这个问题,我们其实只需要判断 角色的脚底是否是地板 就可以了,而踩在地板这个条件则可以通过标签进行判断,如果角色脚底是地板则可以进行跳跃操作,否则则不能跳跃。
所以我们在Player
上创建子物件GroundCheck
移到角色脚底,作用就是检测角色是否碰到地板。
回到程式我们需要找到这个检测物件GroundCheck
,所以首先在前面声明变量:
public class PlayerController : MonoBehaviour |
然后回到跳跃的判断,加入判断函数isGround()
来判断我们是否踩在地板上:
- 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
。
bool IsGround() |
这样写就不需要groundCheck
了,我们删除之前设置的变量和空物件,改为获取图层LayerMask
:
public class PlayerController : MonoBehaviour |
回到Unity设置检测图层为Ground
:
动画连接
Move部分是由Move
这个Parameter判断的,0播放Player_Idle;1播放Player_Move。
可以发现动画播放和h
的关联性,但因为有-1的关系需要取绝对值Mathf.Abs()
,这样左右移动取值均为1。
// Update is called once per frame |
回到Unity,测试站立、移动和跳跃的动画。
整理
对我们PlayerController.cs
的代码稍加整理,添加一些注释:
// Update is called once per frame |
颜色转换
这部分我们来实现游戏的核心功能:颜色转换的部分。首先为所有需要颜色转换功能的物件(玩家、地板等等)新建一个共同的脚本命名为SwitchColor.cs
,同时因为这是游戏的核心功能,所以我们新建一个GameManager.cs
来进行管理。
为了确保GameManager
游戏管理者有且仅有一个,我们使用单例模式:
public class GameManager : MonoBehaviour |
SwitchAllColor()
函数用于抓取场景所有有SwitchColor
,同时设置数组存储这些物体:
private SwitchColor[] colorObjs; |
而colorObjs
则在开始时的Start()
中获取,保存在SwitchColor[]
中:
void Start() |
回到SwitchAllColor()
中遍历数组呼叫里面的SwichObjColor()
函数:
public void SwitchAllColor() |
新建SwitchColor.cs
,声明之前在GameManager.cs
调用的SwichObjColor()
。因为这个脚本需要同时满足玩家、地板、背景、UI等的颜色变换需求,所以我们用枚举的方式区分不同的类别,然后根据枚举的类型决定颜色转换的方式:
对于玩家而言,颜色的改变只需要将spriteRender
中的颜色改变即可;而地板则需要考虑碰撞体的激活问题;背景则由相机的Background
来控制;image
和text
之后会用于UI的颜色转换。
public class SwitchColor : MonoBehaviour |
回到Unity对相应物件的组件类型ComponentType
进行赋值。返回SwitchColor.cs
,声明作为选项的组件类型和变换前后的两种颜色:
public class SwitchColor : MonoBehaviour |
有了上面的变量,我们就可以根据不同的组件类型利用Switch
对转换颜色进行分支操作:
player
:获取精灵渲染器改变颜色,color == startColor ? endColor : startColor
做一个颜色的翻转spriteRender
:同player
image
:涉及UI部分需要引入using UnityEngine.UI
text
:同image
camera
:同player
ground
:在player
的基础上对是否启用BoxCollider2D
进行翻转
public void SwichObjColor() |
回到PlayerController.cs
,在Update()
函数中调用GameManager.cs
中的SwitchAllColor()
,再经由此跳转到SwitchColor.cs
中的SwichObjColor()
方法:
// 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 |
测试颜色转换功能:
死亡及死亡特效
//Switch Color |
void Update() |
public void Die() |
加载关卡
为目标添加Goal.cs
:
OnTriggerEnter2D(Collider2D collision)
:检测碰撞玩家,播放动画;为防止再次触发过关手动把CircleCollider2D
关掉ToNextStage()
:调用GameManager
中的NextStage()
函数进入下一关EnableCollider()
:将碰撞体激活
using UnityEngine; |
回到GameManager.cs
编写进入关卡的逻辑,直接调用SceneManager
读取我们的关卡数,+1后就是下一关。
using UnityEngine; |
回到Unity将结束动画中最后帧中的Function
赋值给ToNextStage()
以触发:
用户界面及数据继承
搭建UI组件,放入素材中的图片,挂载颜色转换脚本SwitchColor.cs
同时将类型更改为Image
或Text
:
为了便于管理,我们新建一个管理UI的脚本UIManager.cs
。
首先像之前一样使用单例模式创建一个UIManager,再声明两个Text
类型的变量(死亡数和星星数),最后声明两个Refresh
刷新函数用于接收新数据并更新UI界面的数据:
using UnityEngine.UI; |
回到GameManager.cs
新增统计死亡数和星星数的变量:
public class GameManager : MonoBehaviour |
分别在死亡和过关的函数中计数并调用UIManager:
public void PlayerDie() |
最后在Star()
函数中调用刷新函数确保数据能够刷新:
void Start() |
音乐与音效
using UnityEngine; |
效果完善总结
开始界面
残影效果
代码逻辑
我们首先需要声明一些变量:
dashSpeed
:冲刺速度dashTime
:冲刺持续时间startDashTime
:倒计时的计时器isDashing
:冲刺状态的记录量dashObj
:保存残影物体
冲刺的逻辑是:
if(不是冲刺状态) |
通过上面的伪代码,我们可以轻松写出业务逻辑:
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
;透明度255
End Color
:颜色255
,255
,255
;透明度255
对于尖刺的脚本只需要附载OnTriggerEnter2D(Collider2D collision)
,通过判断碰撞体标签是否为Player
来判断是否碰到玩家,如果碰到则触发玩家控制器PlayerController.cs
中的玩家死亡Die()
即可。
public class Trap : MonoBehaviour |
同理我们可以复制粘贴白色尖刺,通过更改变换颜色SwitchColor.cs
脚本的颜色和重新勾选碰撞体来制作黑色尖刺:
Commponent Type
:选择Ground
,性质与Ground
一致Start Color
:颜色51
,51
,51
;透明度255
End Color
:颜色71
,71
,71
;透明度255
最后将黑白两个尖刺都拖至预制体文件夹Prefab
中以备之后关卡搭建使用。
项目导出
分辨率
直接导出为可执行程序会导致之前设计的UI位置会发生变化,所以我们新建脚本Resolution.cs
,固定分辨率为1024x768:
using System.Collections; |
这里1024x768的分辨率来源是根据UI的Canvas Scaler
中的分辨率:
回到Unity将其挂在到主相机Main Camera
上,测试运行,UI就恢复了正常,回到原位。