32.总结

32.1 知识点
学习的主要内容

总结目的

Unity做游戏的套路


如何学好Unity



达到目的

练习

32.2 核心要点速览
场景切换与退出
| 要点 | 说明 |
|---|---|
| 入口 | SceneManager.LoadScene(场景名或 Build 索引),using UnityEngine.SceneManagement; |
| 常见坑 | 场景未加入 Build Settings 列表则运行态切换失败 |
| 旧 API | Application.LoadLevel 已弃用,新代码统一 SceneManager |
| 退出 | Application.Quit() 仅在已发布包体生效,编辑器 Play 无效 |
SceneManager.LoadScene("场景2");
Application.Quit();
鼠标光标与 Cursor API
| API / 成员 | 作用 | 注意 |
|---|---|---|
Cursor.visible |
是否显示硬件指针 | false 在 Game 视图内隐藏 |
Cursor.lockState |
None / Locked / Confined |
Locked 常配合视角锁定;编辑器 Esc 可解除 |
Cursor.SetCursor |
自定义纹理与热点 | 热点相对贴图左上角;第三参 CursorMode |
随机数与委托
**C# Random**:Next(min, max) 左闭右开。
**Unity Random.Range**:
| 重载 | 区间 |
|---|---|
int |
左闭右开,如 Random.Range(0, 100) → 0~99 |
float |
双闭区间 |
委托:Action / Func 与命名空间 UnityEngine.Events 下的 UnityAction —— 常配合 UnityEvent、Inspector 绑定,语义接近无参 Action。
模型资源导入
| 项 | 要点 |
|---|---|
| 构成 | 网格 — 形,必须;贴图 — 色,必须;骨骼 — 有动画才要 |
| 格式 | 首选 FBX;亦支持 .dae、.3ds、.dxf、.obj |
| 对接美术 | 前向对齐模型空间 +Z;单位与缩放与工程一致 |
| 素材来源 | Asset Store、成套资源包等 |
坦克迷宫:项目概述与需求
类型:俯视角坦克迷宫;拆解方式:按 OOP 把需求划到 UI、存档、核心玩法三条线,再落到脚本与数据类。
前置知识
| 块 | 内容 |
|---|---|
| Unity 基础 | 场景、物体、组件、预制体 |
| 持久化 | PlayerPrefs + 封装管理器做序列化读写 |
| GUI | 自定义 CustomGUI 控件与事件委托 |
功能需求
| 线 | 内容 |
|---|---|
| UI | 约 8 脚本:1 个面板基类,管显隐与单例;7 个面板 — 开始、设置、排行榜、游戏 HUD、胜利、失败、退出确认等;按钮监听里切场景或叠面板;防穿透:先藏当前面板再显目标 |
| 存档 | 音效:BGM/SFX 开关与音量;排行榜:玩家名、分数、通关时间;统一由 数据管理单例 持有 MusicData、RankList,改完即 Save |
| 核心玩法 | 坦克基类 → 玩家坦克 / 移动敌坦 / 固定炮塔;武器与子弹;可摧毁箱子掉 属性或武器奖励;小地图相机跟目标——与 UI、存档并行实现 |
实现主线
- 工程:导入 CustomGUI、PlayerPrefsDataMgr、TDSTK 等
.unitypackage,用预制体搭 BeginScene / GameScene 展示场景与关卡围合。 - UI 骨架:
BasePanel<T>+ 开始/设置/排行榜等;开始场景CursorLockMode.Confined限制指针在窗口内。 - 设置与音效数据:
MusicData+GameDataMgr构造里 Load,首次用notFirst写默认「全开、音量满」再 Save;设置面板 Show 时刷新控件、**Hide 时timeScale = 1**。 - 排行榜:
RankInfo/RankList,Add后按 通关时间升序 排序、只留前 10 条;面板用Find("父/子名")路径 批量取标签,Show 时把秒数格式化成 时/分/秒 字符串。 - BGM:
BKMusic单例 +AudioSource,volume/mute与GameDataMgr同步。 - 游戏 HUD:
GamePanel用deltaTime累加计时、改血条 宽度比例;打开设置或退出确认时Time.timeScale = 0;设置 / 退出面板 Hide 里恢复timeScale = 1;设置 复制到游戏场景后,每场景各一份单例,靠 **Awake 重绑Instance**。
工程依赖包
| 包 | 作用 |
|---|---|
CustomGUI.unitypackage |
按钮、滑条、标签等封装与点击/值变更事件 |
PlayerPrefsDataMgr.unitypackage |
用 反射 把自定义类型读写进 PlayerPrefs |
TDSTK.unitypackage |
坦克迷宫相关模型、预制体等 |
开始场景装饰
思路:双场景建好 → 预制体铺 地板、墙、展示坦克 → 摄像机当菜单背景 → 定向光 氛围 → 装饰物挂 慢速绕 Y 轴旋转。
void Update()
{
transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime);
}
面板基类与开始 / 设置
BasePanel<T> 要点
| 项 | 做法 |
|---|---|
| 约束 | where T : class,Awake 里 instance = this as T |
| 显隐 | SetActive(true/false) |
| 注意 | 同场景 同脚本只挂一处,否则静态单例被覆盖 |
开始面板:开始游戏 → LoadScene("GameScene");设置/排行榜 → 目标 **ShowMe + 自己 HideMe**;退出 → Application.Quit()。
设置面板:初始 HideMe;关面板时若当前是 BeginScene 再 BeginPanel.ShowMe();滑条/开关 += 到 GameDataMgr;ShowMe 里 UpdatePanelInfo() 同步磁盘数据到控件。
// 典型:打开子面板并防穿透
SettingPanel.Instance.ShowMe();
HideMe();
音效数据与 GameDataMgr
MusicData 字段:isOpenBK、isOpenSound、bkValue、soundValue、notFirst。
构造里首次进入
musicData = PlayerPrefsDataMgr.Instance.LoadData(typeof(MusicData), "Music") as MusicData;
if (!musicData.notFirst)
{
musicData.notFirst = true;
musicData.isOpenBK = musicData.isOpenSound = true;
musicData.bkValue = musicData.soundValue = 1f;
PlayerPrefsDataMgr.Instance.SaveData(musicData, "Music");
}
对外方法:改音量/开关 → 写回 musicData → SaveData;BGM 实际听感由 BKMusic 改 AudioSource。
排行榜数据与面板
| 类型 | 作用 |
|---|---|
RankInfo |
name、score、time;无参构造 方便序列化 |
RankList |
List<RankInfo> |
AddRankInfo 流程:Add → Sort,时间短者靠前 → 从尾部删到 只保留 10 条 → SaveData。
面板刷新:循环 Find("Name/labName" + i) 取 CustomGUILabel;ShowMe → UpdatePanelInfo,秒内整数拆 时 / 分 / 秒 拼字符串 — 除法拆段与 HUD 计时相同。
背景音乐 BKMusic
| 方法 | 作用 |
|---|---|
ChangeValue(float) |
audioSource.volume = value |
ChangeOpen(bool) |
audioSource.mute = !isOpen |
Awake:单例、GetComponent<AudioSource>(),用 GameDataMgr.Instance.musicData 做一次初始化同步。
游戏场景:地形、HUD、暂停
地形:地板 + 四面围墙 预制体拼出可玩区域。
GamePanel
| 职责 | 做法 |
|---|---|
| 分数 | 外部调 AddScore,改 labScore 文本 |
| 时间 | nowTime += Time.deltaTime,整数秒后格式化为 时/分/秒 |
| 血条 | texHP.guiPos.width = (float)HP / maxHP * hpW |
| 暂停 | 点设置或退出确认 → Time.timeScale = 0 |
跨场景的设置面板:进新场景 重新 Awake,SettingPanel.Instance 永远指向 当前场景 里那一个;**HideMe 统一 timeScale = 1**,Begin 场景关设置时再多一步 显示开始面板。
QuitPanel:确认 → LoadScene("BeginScene");继续/关窗 → HideMe(),且在 **HideMe 末尾 timeScale = 1**。
坦克基类 TankBaseObj
| 成员 / 方法 | 作用 |
|---|---|
atk def maxHp hp |
攻防与血量 |
tankHead |
炮台 Transform,供子类旋转瞄准 |
moveSpeed roundSpeed headRoundSpeed |
移动与旋转参数 |
deadEff |
死亡时 Instantiate 的特效预制体 |
abstract void Fire() |
子类实现具体开火 |
virtual Wound(TankBaseObj other) |
dmg = other.atk - def,dmg <= 0 不扣血;hp <= 0 调 Dead |
virtual Dead() |
Destroy(gameObject);有特效则实例化并按 GameDataMgr.musicData 设 AudioSource 音量与 mute 后 Play |
玩家坦克 PlayerObj
| 块 | 做法 |
|---|---|
| 移动 | Translate 竖轴、Rotate 横轴;炮台跟 Mouse X |
| 开火 | GetMouseButtonDown(0) → Fire() → nowWeapon.Fire() |
| 受伤 | override Wound:base 后 GamePanel.Instance.UpdateHP(maxHp, hp) |
| 死亡 | 不调用 base.Dead(),避免销毁带主摄像机的物体;timeScale = 0,LosePanel.ShowMe() |
| 换武器 | 销毁旧 WeaponObj,在 weaponPos 下 Instantiate(..., false),取 WeaponObj、SetFather(this) |
摄像机:主相机挂在 炮台 子级,转炮台即转视角。
小地图摄像机
| 步骤 | 要点 |
|---|---|
| 专用相机 | 去掉 Audio Listener,避免双监听器 |
| RenderTexture | 建 RT,赋给相机 Target Texture |
| UI | 将 RT 赋给 HUD 上的小地图图控件 |
| 俯视 | 相机拉高、俯视场景 |
| 跟随 | CameraMove:LateUpdate 里 position.x/z 跟玩家,y = H 常量 |
武器与子弹
WeaponObj:bullet 预制体、shootPos[]、fatherObj;Fire 循环实例化子弹并 BulletObj.SetFather。
BulletObj:Update 沿自身 forward 移动;触发器 OnTriggerEnter:Tag 为 Cube,或 Player/Monster 与发射方阵营交叉 时,对 TankBaseObj 调 Wound(fatherObj),生成爆炸特效并按全局音效数据播声音,**Destroy 自身**。
| 工程注意 |
|---|
| 玩家坦克 Rigidbody 建议 冻结旋转、锁定 Y,避免与场景体碰撞产生翻转 |
武器奖励与自动销毁
WeaponReward:触发 Player → Random.Range 选武器预制体 → PlayerObj.ChangeWeapon → 实例化 getEff 音效 → Destroy 自身。
AutoDestroy:Start 里 Destroy(gameObject, time),用于子弹、特效寿命。
属性奖励 PropReward
| 项 | 要点 |
|---|---|
E_PropType |
Atk / Def / MaxHp / Hp |
| 触发 | Player 进入触发器,switch 改 PlayerObj 对应字段 |
| 血条 | MaxHp 或 Hp 变化后 GamePanel.UpdateHP;当前血 封顶 maxHp |
| 收尾 | 播放获取特效、Destroy 自身 |
可击毁箱子 CubeObj
| 流程 | 要点 |
|---|---|
| 概率 | Random.Range(0,100) < 50 时在原位 Instantiate 奖励预制体数组中随机一项 |
| 表现 | 实例化 deadEff,按全局音效设置播声 |
| 收尾 | Destroy 自身 |
课内 CubeObj 全码在触发进入时即执行上述逻辑;正式项目宜再用 Tag / Layer 限定触发源,避免误触。
固定炮塔 MonsterTower
| 点 | 做法 |
|---|---|
| 炮台旋转 | 子物体挂旋转脚本,本类只管开火节奏 |
| 开火 | Update 累加 nowTime,超 fireOffsetTime 则 Fire,置零计时 |
Fire |
多发射点 Instantiate 子弹并 SetFather(this) |
| 免伤 | override Wound 空实现,不扣血不死亡 |
移动敌人 MonsterObj
| 块 | 做法 |
|---|---|
| 巡逻 | RandomPos 从 randomPos[] 取目标;LookAt + Translate forward;距目标 小于 0.05 再换点 |
| 瞄准 | tankHead.LookAt(lookAtTarget),一般绑玩家 |
| 近战开火 | 与目标距离 ≤ fireDis 时按 fireOffsetTime 间隔 Fire |
| 死亡 | base.Dead() 后 GamePanel.AddScore(10) |
| 血条 | OnGUI:showTime > 0 时递减;WorldToScreenPoint,y = Screen.height - y 转 GUI 坐标;GUI.DrawTexture 底条 + 按 hp/maxHp 缩宽血条;**Wound 后 showTime = 3** |
通关与胜败界面
EndPoint:Player 进入触发器 → timeScale = 0 → WinPanel.ShowMe()。
WinPanel:初始 HideMe;确定:timeScale = 1,AddRankInfo(昵称, GamePanel.nowScore, GamePanel.nowTime),LoadScene("BeginScene")。
LosePanel:回菜单 → BeginScene;再来一局 → LoadScene("GameScene") 重载整关;两按钮均先 timeScale = 1。
失败流程在 PlayerObj.Dead 里与胜利对称:**暂停 + LosePanel.ShowMe()**。
构建与 Player 设置
Build Settings
| 项 | 含义摘要 |
|---|---|
| Target Platform | Windows / macOS / Linux 等 |
| Architecture | x86 / x86_64 等,视目标 CPU |
| Development Build | 开发包,可开 Profiler、脚本调试等子项 |
| Compression | Default / LZ4 / LZ4HC,权衡包体与解压速度 |
Player Settings:Company / Product Name、Version、默认 Icon / Cursor;Resolution 里 Fullscreen Mode、默认宽高、Run In background 等。
产出:参数设好 → Build,选输出目录生成可执行包。
32.3 面试题精选
进阶题
1. SceneManager.LoadScene 运行时失败常见原因?
题目
代码里调用 LoadScene 抛错或进不了目标场景,你会先查哪几项?
深入解析
- 目标场景是否已加入 Build Settings 列表、名称/索引是否与代码一致。
- 是否缺少
using UnityEngine.SceneManagement;。 - 单例/加载流程是否在合法时机调用(如同一帧多次加载需留意异步 API 等,入门篇以同步
LoadScene为主)。
答题示例
先看 Build Settings 里有没有该场景、名字写没写对,再看命名空间和调用时机。
参考文章
- 1.必备知识点-场景切换和游戏退出
2. CursorLockMode.Locked 与 Confined 各解决什么问题?
题目
第一人称视角常用哪种锁鼠标?只想把指针限制在 Game 窗口内用哪种?
深入解析
- Locked:指针锁到视图中心并通常隐藏,适合 FPS 视角控制;编辑器下 Esc 可解除。
- Confined:指针限制在窗口内,不强制居中隐藏,适合不需要「中心锁定」的窗口内操作。
答题示例
FPS 用 Locked;只要不出窗口用 Confined。
参考文章
- 2.必备知识点-鼠标隐藏锁定相关
3. Random.Range 的 int 与 float 重载在区间边界上有何不同?
题目
同一方法名,为何 int 与 float 版本「含不含上界」不一样?
深入解析
- int:上界不包含(离散整数惯例)。
- float:双闭区间(连续浮点插值)。
- 与
System.Random.Next的左闭右开需对比记忆,避免 off-by-one。
答题示例
int 版上界取不到;float 版上下界都能取到。
参考文章
- 3.必备知识点-随机数和Unity自带委托相关
4. BasePanel<T> 为何要 where T : class,单例在 Awake 里怎么拿到具体面板类型?
题目
泛型面板基类里 instance = this as T 的前提是什么?为何不用 new 做单例?
深入解析
where T : class:this as T要求 T 为引用类型,否则值类型与as规则不兼容。- MonoBehaviour:由引擎在场景里挂载实例化,**禁止
new**;在Awake(该物体已存在、脚本唯一)里把 当前实例 赋给静态instance,T为各面板子类如BeginPanel。 - 代价:每场景若重复放两份同脚本会互相覆盖
Instance,工程上保证 每场景每面板脚本只挂一处。
答题示例
T 要约束成引用类型才能 as 成具体面板;Mono 不能 new,Awake 里把自己赋给静态单例。
参考文章
- 8.开始场景-开始界面
5. RankPanel 里用 transform.Find("Name/labName" + i) 取控件,路径里斜杠表示什么?
题目
为何不直接 Find("labName1")?Find 默认能搜多深?
深入解析
/:表示 父子层级,即从当前transform的下一层Name再找到labName1;等价于多级子节点路径。- **默认
Find**:一般只找 直接子物体(Unity 版本行为以文档为准);跨层时常用 路径字符串 或改在父节点上Find。 - 工程取舍:控件多时用循环 + 约定命名 减少 Inspector 拖拽。
答题示例
斜杠是父子路径,从 Name 子物体下取 labName1;比每层单独 Find 省事。
参考文章
- 11.开始场景-排行榜界面
6. GameDataMgr 构造里为何用 notFirst 判断并写回默认音量?
题目
第一次装游戏时 LoadData 可能得到什么?为何要立刻 SaveData?
深入解析
- 反序列化/首次无键时,布尔与数值可能是 默认 false / 0,不符合「默认音乐音效全开」的产品预期。
notFirst为 false 视为首次:拉满音量、开开关,标notFirst = true并 持久化,下次启动走正常读取。- 与
PlayerPrefsDataMgr的 反射存取 配套,保证 内存模型与磁盘一致。
答题示例
首次存档可能是全关静音,用 notFirst 识别后设成默认全开并 Save,以后就读用户设的了。
参考文章
- 10.开始场景-音效数据逻辑
7. TankBaseObj.Wound 里 dmg = atk - def,dmg <= 0 时为何直接 return?
题目
想表达什么设计?若允许 0 伤仍要触发受击表现该怎么扩展?
深入解析
- 不破防:防御不低于对方攻击时不扣血,避免负数伤害或无效结算。
- 若要做「格挡特效」等,可与血量分支分离,在
dmg <= 0时另调表现而不改hp。
答题示例
攻击减防御小于等于 0 就不造成伤害;要受击反馈可以单独做特效分支。
参考文章
- 18.游戏场景-坦克基类
8. 子弹 OnTriggerEnter 如何用 Player / Monster Tag 做阵营过滤?
题目
为何用 fatherObj.CompareTag 和 other.CompareTag 组合判断?
深入解析
- Cube 作为场景可破坏体,双方子弹都可引爆。
- 玩家子弹只应伤 Monster,怪子弹只应伤 Player:用
other是谁 与fatherObj是谁发射 交叉判断,避免自伤或同阵营互打。
答题示例
看被撞的是 Player 还是 Monster,再和发射方 Tag 交叉判断,Cube 单独一条。
参考文章
- 21.玩家-武器对象和子弹对象
- 25.敌人-固定不动的敌人
9. PlayerObj 重写 Dead 时为何不调用 base.Dead()?
题目
主摄像机挂在玩家子层级时,直接 Destroy 玩家会怎样?
深入解析
base.Dead会Destroy整个玩家物体,子物体主相机一并销毁,场景失去渲染与后续 UI 逻辑载体。- 课内改为 暂停 + 失败面板,保留场景结构,由
LosePanel再决定回菜单或重载关卡。
答题示例
父类会 Destroy 自己,相机跟着没了;所以玩家死亡只暂停并弹失败 UI。
参考文章
- 19.玩家-基础移动旋转摄像机跟随等
- 30.失败界面
深度题
1. 为何项目里除了 System.Action 还会用到 UnityAction?
题目
二者都能表示无返回委托,Unity 工程里何时更偏向 UnityAction?
深入解析
UnityAction位于UnityEngine.Events,与 UnityEvent、Inspector 持久化、UI Button 的 onClick 等管线一致,序列化与编辑器绑定更顺。Action/Func是纯 .NET,在任意 C# 逻辑中通用;与 Unity 可视化事件绑定时用UnityAction更贴合工具链。
答题示例
要跟 UnityEvent、UI 或 Inspector 里拖方法衔接时用 UnityAction;纯代码里 Action 一样写。
参考文章
- 3.必备知识点-随机数和Unity自带委托相关
2. 与美术约定导出模型时,你会强调哪两条「硬规则」?
题目
结合本课导图,口述对接 3D 资源时最容易踩坑的两点。
深入解析
- 朝向:模型前向对齐 +Z(与 Unity 常用前向一致),减少后续逻辑里额外旋转补偿。
- 单位与缩放:DCC 与 Unity 单位统一(米/厘米等),避免导入后整体缩放错误连带物理、动画比例问题。
- 格式上优先 FBX,减少非主流格式带来的法线、材质拆分问题。
答题示例
前向朝 Z、单位缩放统一,再加一句优先 FBX。
参考文章
- 4.必备知识点-模型资源的导入
3. 打开设置把 Time.timeScale 设为 0,关面板时哪些路径必须把缩放设回 1?
题目
SettingPanel.HideMe 与 QuitPanel.HideMe 都做了什么事?漏一处会怎样?
深入解析
GamePanel打开设置或退出确认时Time.timeScale = 0暂停Update/FixedUpdate(除不受缩放影响的少数系统)。SettingPanel.HideMe重写里Time.timeScale = 1,开始场景下关设置还会BeginPanel.ShowMe();游戏场景关设置同样走HideMe,时间缩放会恢复。QuitPanel.HideMe末尾也timeScale = 1,用于从「退出确认」返回游戏。若某条分支只SetActive(false)却未走上述HideMe,才可能卡在暂停态。
答题示例
暂停靠 timeScale=0,继续玩必须在关设置或关退出面板时设回 1,否则 Update 全停。
参考文章
- 9.开始场景-设置界面
- 15.游戏场景-游戏主界面
- 17.游戏场景-游戏退出界面
4. 同名的 SettingPanel 放到 BeginScene 与 GameScene,单例会冲突吗?
题目
两个场景各有一份设置面板预制体时,Instance 指向谁?
深入解析
- 静态
Instance跟进程走:进入某场景、该场景里SettingPanel的Awake执行,instance被设为 当前场景那一颗。 - 切场景:旧场景卸载后旧实例销毁,新场景 **
Awake再赋新Instance**,因此 不会两个场景共用一个物体,但 代码写的必须是「当前场景那一份」。 - 注意:DontDestroyOnLoad 若误用会把旧实例带去新场景,与本课「复制预制到另一场景」的教法不同,需区分。
答题示例
不冲突,谁 Awake 谁就是当前 Instance;切场景会换新的。别和 DontDestroy 混用搞混。
参考文章
- 16.游戏场景-游戏设置界面
5. 小地图相机为何常用 LateUpdate 跟随,且要关掉 Audio Listener?
题目
和主相机同帧跟随时容易出现什么问题?双 Listener 会怎样?
深入解析
- LateUpdate:在玩家
Update位移之后再抄位置,减少一帧抖动或不同步。 - 仅一个 Audio Listener:多 Listener 行为未定义 / 报错风险,辅机位只出画面不参与收听。
答题示例
晚一阶段跟玩家位置更稳;副相机不要第二个 Listener。
参考文章
- 20.玩家-小地图制作
6. WinPanel 点确定时先 timeScale = 1 再 LoadScene,必要性在哪?
题目
若保持 0 直接切场景会有何隐患?
深入解析
- 部分流程在时间缩放为 0 时
Update不推进,与排行榜写入、输入收尾等组合时易出现状态不一致或体感卡顿;先恢复 1 再切场景更干净。 - 切 BeginScene 后本场景卸载,但显式恢复仍是好习惯,与设置/退出面板写法一致。
答题示例
先恢复时间流速再切场景,避免暂停态残留影响逻辑。
参考文章
- 29.胜利界面
7. Development Build 与正式发包选项如何取舍?
题目
给测试同学的包和给玩家的包应差在哪几项?
深入解析
- Development Build:开 Profiler 连接、脚本调试、Deep Profiling 等,包体更大、性能更差,适合内测定位问题。
- 正式包:关 Development,按需选 LZ4 / LZ4HC 压缩;Player Settings 里产品名、版本、图标、全屏策略与 Run in background 按发行平台调整。
答题示例
内测开 Development 方便连 Profiler;上架包关掉,压缩用 LZ4 系,Player 里把产品信息和分辨率模式配好。
参考文章
- 31.项目打包
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com