2D开发
1. 内容简介
- 使用2D资源
- 2D RPG游戏开发流程
- 创建并控制角色(脚本开发)
- 设置动态精灵
- 学习简单特效(粒子效果)
2. 内容详情
2.1 创建角色
Sprite:Unity中2D素材的默认存在形式,是Unity中的2D图形对象
PNG ---> Sprite ---> GameObject
2.2 移动角色
2D游戏,X、Y定位元素位置,X增大元素向右移动,Y增大元素向上移动
对象的X、Y的信息存放在Transform组件的Position属性中
// 每帧向右移动0.01米
void Update()
{
Vector2 pos = this.transform.position;
pos.x += 0.01f;
this.transform.position = pos;
}
注:transform.position的类型为Vector3,结构体类型,该值仅为其内存的拷贝,无法对其进行直接修改
2.3 控制角色
控制角色,最基础的用户交互
2.3.1 控制方式
- 鼠标键盘
- 手柄
- 手机触屏、陀螺仪
- 体感
- 可穿戴设备(VR、AR、瞳孔捕捉)
- 声控
2.3.2 键盘控制
Input类:交互接口类,用以获取交互信息
// 获取水平输入
float horizontal = Input.GetAxis("Horizontal");
// 获取垂直输入
float vertical = Input.GetAxis("Vertical");
按左键,获取-1.0f,按右键,获取1.0f
按上键,获取1.0f,按下键,获取-1.0f
// 根据输入修改坐标
pos.x = pos.x + horizontal * step;
pos.y = pos.y + vertical * step;
Unity中默认使用Input Manager中设置项目的输入管理,可进行配置或自定义
负值键触发后设置值为-1,正值键触发后设置值为1
注:也可使用GetKey等方法获取用户输入,但局限性强不推荐
2.3.3 时间与帧率
Update事件按帧率触发,该触发次数受硬件配置的影响,会造成不同用户间的体验效果有所不同
解决方案Ⅰ:锁帧(垂直同步)
// 开启垂直同步
QualitySettings.vSyncCount = 0;
// 锁帧为60帧
Application.targetFrameRate = 60;
锁帧会降低画面效果(帧数降低)
解决方案Ⅱ:时间计算
以单位/秒为单位计算位移,而非单位/帧
Time.deltaTime:每帧的时间间隔,浮点型
将移动速度乘以每帧的时间,则移动速度会以秒表示
// 根据输入修改坐标
pos.x = pos.x + horizontal * step * Time.deltaTime;
pos.y = pos.y + vertical * step * Time.deltaTime;
2.4 瓦片地图
2.4.1 概念
Tilemap是2D游戏中构建世界的工具
该系统可存储和操作Tile资源,并将信息传输给其他组件(如Tilemap Renderer和Tilemap Collider 2D)
创建瓦片地图时,会默认创建一个Grid组件,作为瓦片地图的父级,并在瓦片布置时作参考
相关概念:
- Sprite:纹理的容器
- Tile:包含一个Sprite,以及两个属性,颜色和碰撞体类型
- Tile Pattle:瓦片调色板
- Brush:调色板的笔刷
- Tilemap:瓦片地图,可以在Tilemap上增加Tile
- Grid:Tilemap的父对象
- Tilemap Renderer:Tilemap的一部分,控制Tile在Tilemap上的渲染
2.4.2 Tilemap设置
Hierarchy --> Scene ---> GameObject ---> 2D Object ---> Tilemap
Tilemap设置后默认生成Tile Palette,Tile Palette内可创建Palette,在调色板中可进行Tile的相关操作
2.4.3 Tile设置
Sprite拖拽至Palettle可自动创建Tile
注:Sprite原文件设置Pixels Per Unit(单元格绘制大小)
可进入Sprite Editor中切割精灵图
注:切割前将精灵图的SpriteMode修改为Mulitple
2.4.4 场景地图排序
渲染器的Order in Layer属性设置先后关系
伪透视图:仿照3D视图的深度效果,实现2D元素间的遮罩关系
设置伪透视图:在y轴(y值小遮盖,y值大被遮盖)基于精灵的位置来绘制精灵(Unity的Project Manager的Graphic中设置 )
同时修改精灵图的锚点位置为底层,并将游戏对象的SpriteSortPoint属性修改为Pivot,以实现效果(Unity基于中心点判断对象的位置)
2.5 2D物理系统
Unity提供了一套完整的物理系统,以计算物体间的移动与碰撞,为对象添加Rigidbody2D后对象可进行物理计算
2.5.1 碰撞
-
添加Rigidbody2D组件和Collider2D组件
注:碰撞是相互的,故碰撞双方均需添加;游戏中的大型不可移动物体无需添加刚体组件,因为它们不实现移动逻辑
-
调整碰撞体边界
注:注意考虑透视效果
-
锁定z轴旋转
-
碰撞抖动bug
产生原因:Update函数中transform更新角色移动至碰撞体内,物理引擎检测生效并将角色设置到碰撞体外,进而产生抖动的现象
解决方法:利用刚体属性的transform值进行移动
// 获取刚体组件 rigidbody2d = GetComponent<Rigidbody2D>();
// 获取当前位置 Vector2 pos = this.transform.position; // 根据输入修改坐标 pos.x = pos.x + horizontal * step * Time.deltaTime; pos.y = pos.y + vertical * step * Time.deltaTime; // 赋值新坐标 rigidbody2d.position = pos;
注:物理引擎相关操作需放在FixedUpdate函数中
-
为Tilemap添加Tilemap碰撞体,并根据需求修改tile的碰撞关系
-
为Tilemap添加整合碰撞体,以优化计算性能
注:在Tilemap碰撞体内勾选使用整合选项,并修改刚体为静态,以提升性能
2.5.2 触发
- 可收集对象
-
添加生命统计功能
// 声明生命值 public int maxHp = 10; private int curHp; void Start() { // 初始化当前生命值 curHp = maxHp; } // 修改血量函数 private void ChangeHp(int amount) { curHp = Mathf.Clamp(curHp + amount, 0, maxHp); }
注:Clamp方法用以限定取值范围
-
添加触发器
触发器:一种特殊的碰撞体,不会阻止物体移动,但会检查碰撞,并触发事件(void OnTriggerEnter2D(Collider2D other))
-
添加触发逻辑
private void OnTriggerEnter2D(Collider2D collision) { // 获取Ruby对象的脚本对象 RubyController rubyController = collision.GetComponent<RubyController>(); if (rubyController != null) { rubyController.ChangeHp(amount); Destroy(this.gameObject); } }
-
优化逻辑
满血不吃血瓶
else if (rubyController.GetCurHp() == rubyController.maxHp) Debug.Log("full hp");
封装curHp为属性
// 当前生命值 private int _curHp; public int CurHp { get => _curHp; }
- 伤害区域
-
添加触发器
-
添加触发逻辑(OnTriggerStay2D回调)
-
设置Sleeping Mode
定义游戏对象如何在处于静止状态时“睡眠”以节省处理器时间
NeverSleep:永不睡眠
StartAwake:最初处于唤醒
StartAsleep:最初处于睡眠,碰撞以唤醒
-
设置无敌时间
// 无敌时间 public float InvincibleTime = 2.0f; // 无敌状态 private bool IsInvincible = false; // 无敌时间计时器 private float InvincibleTimer; void Update() { // 无敌状态,计时器倒数 if (IsInvincible) { InvincibleTimer -= Time.deltaTime; // 倒计时结束,取消无敌状态 if (InvincibleTimer <= 0) { IsInvincible = false; InvincibleTimer = InvincibleTime; } } } public void ChangeHp(int amount) { // 无敌状态不伤血 if (amount < 0 && IsInvincible) return; _curHp = Mathf.Clamp(CurHp + amount, 0, maxHp); // 限制方法 // 受伤,进入无敌状态 if (amount < 0) IsInvincible = true; Debug.Log($"hp = {CurHp}/{maxHp}"); }
-
平铺区域
- scale复原
- DrawMode改为Tile
- 素材的MeshType改为Full Rect
2.6 敌人
2.6.1 敌人游荡
敌人按一定速率、在一定范围内直线往返移动
[System.Serializable]
public enum Dirction
{
HORIZONTAL,
VERTICAL,
}
public class BotController : MonoBehaviour
{
// 速度
public float speed = 3;
// 位移距离
public float distance = 4;
// 移动方向
public Dirction moveDirction = Dirction.VERTICAL;
// 记录移动距离
private float moveRecord = 0;
// 方向转换
private int dirction = 1;
// 刚体
private new Rigidbody2D rigidbody;
void Start()
{
rigidbody = this.GetComponent<Rigidbody2D>();
}
void FixedUpdate()
{
// 不在移动范围内
if (moveRecord < 0 || moveRecord > distance)
dirction = -dirction;
// 获取当前位置
Vector2 pos = this.transform.position;
// 移动
float moveDistance = speed * dirction * Time.deltaTime;
if (moveDirction == Dirction.HORIZONTAL)
pos.x += moveDistance;
else
pos.y += moveDistance;
rigidbody.position = pos;
// 更新位置记录
moveRecord += moveDistance;
}
}
2.6.2 敌人伤害
OnCollisionEnter2D:当刚体和某对象碰撞后调用,参数提供碰撞信息
void OnCollisionEnter2D(Collision2D collision)
{
// 获取ruby的脚本
var rubyController = collision.gameObject.GetComponent<RubyController>();
if (rubyController != null)
rubyController.ChangeHp(damage);
}
2.7 动画
2.7.1 Unity动画工作流程
动画剪辑(Animation Clip):动画的基本组成部分,包含游戏对象的位置变化、旋转或其他相关信息
动画剪辑一般和不同的状态相挂接,比如“静止状态”挂接站立不动的动画、“奔跑状态”挂接跑动的动画......
Animator Controller用以调度动画的播放及切换
Unity中通过Animator组件进行操作
2.7.2 Animator挂接
游戏对象添加Animator组件 >>> 创建Animator Controller >>> Controller挂接到Animator组件
2.7.3 动画剪辑录制
选取待录制的游戏对象 >>> 选取Animation面板(Ctrl+6) >>> 创建Animation >>> 选取素材片段并拖入动画时间窗中 >>> 添加相应属性调整动画
Unity默认动画每秒渲染60次
2.7.4 构建动画关系
-
进入Animator面板
动画状态机(state machine):每一个动画剪辑,以及一段动画剪辑与另一段动画剪辑的连接
Layers:将动画用于角色的不同位置,可用用于3D
Parameters:由脚本向Controller提供信息
混合树(Blend Tree):根据参数来混合多段动画
-
添加并设置Blend Tree
添加参数 >>> 添加Motion
- 用脚本为Animator Controller传值
// 动画控制器
private Animator animator;
animator = this.GetComponent<Animator>();
// 设置移动动画
animator.SetFloat("MoveX", dircX);
animator.SetFloat("MoveY", dircY);
2.7.5 基础动画总结
相关术语
术语 | 定义 |
---|---|
动画剪辑(Animation Clip) | 角色的简单动画数据,是控制动画的素材 |
Animator组件 | 用以管理动画素材,对模型动画化 |
Animator Controller | 通过控制动画状态机和混合树来控制动画 |
状态机 | 一种用于控制动画状态交互情况的图 |
混合树 | 根据浮点动画参数在类似动画剪辑之间进行连续混合 |
动画工作流程
Animation创建动画剪辑 => Animator控制动画的播放与转换 => 脚本控制Animator Controller的参数
2.8 飞弹
// 子弹预制件脚本
public class BulletController : MonoBehaviour
{
// 刚体
private new Rigidbody2D rigidbody;
void Start()
{
rigidbody = GetComponent<Rigidbody2D>();
}
// 发生碰撞
void OnCollisionEnter2D(Collision2D collision)
{
Debug.Log($"Get Object => {collision.gameObject}");
Destroy(this.gameObject);
}
/// <summary>
/// 子弹移动
/// </summary>
/// <param name="dirc">发射方向</param>
/// <param name="force">发射力</param>
public void Launch(Vector2 dirc, float force)
{
// 对当前子弹施加一个力
rigidbody.AddForce(dirc * force);
}
}
// Ruby脚本
public class RubyController : MonoBehaviour
{
// 子弹预制件
public GameObject bulletPrefeb;
void Update()
{
// 获取发射子弹输入 J键发射
if (Input.GetKeyDown(KeyCode.J))
Launch();
}
/// <summary>
/// Ruby发射子弹
/// </summary>
private void Launch()
{
// 在指定位置创建子弹对象
GameObject bullet = Instantiate(bulletPrefeb,
rigidbody2d.position + Vector2.up * 0.5f,
Quaternion.identity);
// 获取子弹脚本
BulletController bulletController = bullet.GetComponent<BulletController>();
// 发射子弹
bulletController.Launch(staticDirction);
// 发射子弹动画
animator.SetTrigger("Launch");
}
}
相应问题:
- 通过Instantiate函数创建的游戏对象不会执行Start函数,故rigidbody组件获取异常,应将获取组件的代码写入Awake函数
- 子弹直接与玩家碰撞产生逻辑错误,应通过调整图层解决问题
- 未触发的子弹无法销毁,会影响游戏性能,可用计时器逻辑进行控制
2.9 玩家攻击
// Bot脚本
public class BotController : MonoBehaviour
{
/// <summary>
/// 修复机器人
/// </summary>
public void Fix()
{
// 转换修复状态
isFixed = true;
// 机器人无伤害
damage = 0;
// 播放修复动画
animator.SetBool("IsFix", true);
}
}
// 子弹脚本
public class BulletController : MonoBehaviour
{
// 发生碰撞
void OnTriggerEnter2D(Collider2D collision)
{
// 获取Bot脚本
BotController botController = collision.GetComponent<BotController>();
if (botController != null)
{
botController.Fix();
}
Destroy(this.gameObject);
}
}
2.10 摄像头移动
-
脚本控制Camera组件移动
-
第三方组件Cinemachine
- 安装Cinemachine插件(已集成至Unity)
- 添加2D Cinemachine
- 更改虚拟摄像机的设置(实际摄像机会复制虚拟摄像机的设置)
- 添加跟随
- 添加Confiner组件以限定边界
2.11 Unity视觉特效
2.11.1 粒子系统
模拟渲染许多称为粒子的小图像或网格以产生视觉效果。系统中的每个粒子代表效果中的单个图形元素。系统共同模拟每个粒子产生的完整效果的印象。
如:火、烟、液体等,通过网格(3D)或精灵(2D)素材很难描绘这些对象,而粒子系统则可实现。
Unity的粒子系统的选择:
- 内置粒子系统 => CPU渲染
- Visual Effect Graph => GPU渲染
2.11.2 粒子系统的构建
-
准备粒子的精灵图素材
-
创建particle effect游戏对象
-
将精灵图绑定至粒子对象上
-
配置粒子系统
- Texture Sheet Animation渲染粒子播放的相关逻辑,如播放精灵图、播放次序等
- Shape设定粒子的生成形状,如生成半径、角度等
- 基础设置控制粒子的播放速度、生命周期、尺寸等
- Cover over time设定粒子结束播放的颜色和透明度变化
- Size over time设定粒子结束播放的大小变化
-
将粒子预制件与游戏对象预制件绑定为一个新预制件,即粒子作为游戏对象的子预制件
-
脚本控制烟雾
// 清除冒烟特效 if (smokeEffect != null) smokeEffect.Stop();
-
脚本控制子弹碰撞特效
// 子弹脚本 public class BulletController : MonoBehaviour { void OnTriggerEnter2D(Collider2D collision) { // 生成子弹击中特效 Instantiate(hitEffectPrefeb, this.transform.position, Quaternion.identity); } } // 子弹特效脚本 public class HitEffect : MonoBehaviour { private float destroyTime = 1.0f; private void Update() { // 子弹特效的销毁 if (destroyTime < 0) Destroy(this.gameObject); else destroyTime -= Time.deltaTime; } }
2.12 UI系统
User Interface,用于用户与游戏的信息交互与反馈
2.12.1 Unity UI分类
- UI toolkit:Unity最新的UI系统,优化跨平台性能,基于标准Web技术,但暂时缺少部分功能
- Unity UI(UGUI):大多数商业软件沿用的UI技术,基于游戏对象开发
- NGUI:第三方UI系统,UGUI的前身
- IMGUI:纯代码驱动的UI工具包,一般用于构建Unity编辑器内的UI界面
2.12.2 UGUI
-
添加Canvas
UGUI的UI组件均存放在**画布(Canvas)**上,可设置画布的渲染模式
-
添加Image
锚点与枢轴的相对距离不变,通过设置锚点位置保证UI的位置在不同分辨率下保持一致
-
添加遮罩
遮罩用于控制图像的显示范围
一般先建立遮罩层,再将Image作为遮罩层的子元素进行控制
注:调节锚点位置以保证
-
脚本控制遮罩
// ImageExtension internal static class ImageExtension { // 获取遮罩长度 public static float GetRectWidth(this Image image) => image.rectTransform.rect.width; } // 生命条UI脚本 public class HealthBarController : MonoBehaviour { // 静态生命条实例 public static HealthBarController Instance { get; private set; } // UI图形对象 public Image mask; // 初始遮罩层长度 private float originLength; void Awake() { // 获得唯一实例 Instance = this; } void Start() { // 获取遮罩层图像的宽度值 originLength = mask.GetRectWidth(); } /// <summary> /// 设置遮罩长度 /// </summary> /// <param name="percentage">血量修改百分比</param> public void SetRectWidth(float percentage) => mask.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, originLength * percentage); } // Ruby脚本 public class RubyController : MonoBehaviour { public void ChangeHp(int amount) { // UI显示 Debug.Log($"hp = {CurHp}/{maxHp}"); HealthBarController.Instance.SetRectWidth(CurHp / (float)maxHp); } }
2.13 剧情系统
2.13.1 创建NPC
- 创建游戏对象
- 创建并调整动画的播放,调整透视关系
- 创建碰撞体
- 设定图层为NPC图层
2.13.2 射线投射
将射线投射到场景中并检查该射线是否与碰撞体相交的行为,用于游戏对象的检测
var hit = Physics2D.Raycast(OffsetPoint(0.2f), staticDirction, 1.5f, LayerMask.GetMask("NPC"));
// 检测碰撞
if (hit.collider != null)
{
Debug.Log($"raycast get collider {hit.collider.gameObject}");
// 检测交互 "E"键
if (Input.GetKeyDown(KeyCode.E))
{
Debug.Log("Text here...");
}
}
设置Layer可以限制投射的生效层
2.13.3 对话系统
-
为NPC添加Canvas并设置
-
为Canvas添加外框(Image)
-
为外框添加Text文本
第一次使用TMP组件需要导入组件至项目中
-
NPC脚本控制Text的显示
void Update()
{
// 开始计时
if (displayTimer >= 0)
{
displayTimer -= Time.deltaTime;
}
// 超时,文本框隐藏
else
{
dialogBox.SetActive(false);
}
}
/// <summary>
/// 显示文本框
/// </summary>
public void ShowDialogBox()
{
displayTimer = displayTime;
dialogBox.SetActive(true);
}
2.13.4 Text Mash Pro
- 中文字体问题
- 动态字体
下载中文开源字体,直接再project中将字体转换为字体资源即可使用
- 静态字体
window >>> TextMeshPro >>> Font Asset Creator >>> 将已有的常用字体套用字体文件的字体
注:静态字体性能高于动态字体,针对文本量大的游戏(AVG游戏)应优先使用静态字体
- 多页显示
- Overflow设置为Page
- 设置脚本控制翻页逻辑
// 获取总页数(start函数无法获取) totalPage = tmp.textInfo.pageCount; // 开始计时 if (hintBtnRange.isInRange) { // 文本对话翻页 "空格"翻页 if (Input.GetKeyUp(KeyCode.Space)) { // 翻到最后一页回到第一页,否则继续翻页 curPage = (curPage == totalPage) ? 1 : curPage + 1; // 根据当前页tmp显示 tmp.pageToDisplay = curPage; } }
2.14 音频系统
2.14.1 音频基础关系
Unity中通过Audio Source(音源)和Audio Linsener(音频接收者)构建基础的音频系统
Audio Source的资源来自于Audio Clip(音频剪辑),Unity可接受多数音频格式文件(如WAV、MP3、Ogg等)
2.14.2 音频系统定义
- Audio Clip是Unity的实体发声文件,可导入.wav, .mp3, .ogg等音频文件,也可导入.mod, .xm等音轨模块
- Audio Source用以控制音频剪辑的播放,类似于动画系统的Animator
- Audio Lisener用以接收Audio Source发出的声音
2.14.3 工作流程
- 游戏对象绑定音频
创建固定的游戏对象,绑定固定的音源播放音频
- 代码控制音频
/// <summary>
/// 播放音频
/// </summary>
/// <param name="audioClip">待播放的音频剪辑</param>
public void PlayAudio(AudioClip audioClip, float volume = 1.0f)
{
audioSource.PlayOneShot(audioClip, volume);
}
2.14.4 空间音频
根据Audio Lisener的位置与Audio Source的位置播放音频,在Spatial Blend和3D Sound Setting中调整
2.15 打包发布
- 游戏信息配置:在Project Setting的Player中设置开发者信息
- 发布配置:在Build Setting设置发布信息