18.总结
18.1 知识点

主要讲解内容

完成目标

主要使用的知识点

思考题

18.2 核心要点速览
动画控制器
动画控制器采用分层结构,跑步层和跳跃层独立运行。
| 层级 | 默认状态 | 触发参数 | 关键设置 |
|---|---|---|---|
| 跑步层 | 默认状态 | isRun |
连线取消退出时间,即时响应 |
| 跳跃层 | 空状态(Empty) | isJump / isFall |
权重为 1,不影响跑步层 |
跳跃层状态流转:起跳 → 滞空 → 下落
- 起跳到滞空:无参数、有切换时间,让起跳动画自然过渡
- 滞空到下落:通过
isFall参数触发 - 其他连线:取消退出时间,保证即时切换
特殊转换:跑步掉落时直接切换下落状态;下落途中二段跳切换回起跳状态。
角色移动
移动逻辑的核心是用 Input.GetAxisRaw("Horizontal") 获取水平输入,返回 -1、0、1 三个值。
horizontalMove = Input.GetAxisRaw("Horizontal");
if (horizontalMove < 0)
transform.rotation = Quaternion.Euler(0, -90, 0);
else if (horizontalMove > 0)
transform.rotation = Quaternion.Euler(0, 90, 0);
if (horizontalMove != 0)
transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
roleAnimator.SetBool("isRun", horizontalMove != 0);
关键点:先旋转面朝向,再沿 Vector3.forward(自身前方)移动,不用单独处理左右方向。
角色跳跃
跳跃采用竖直上抛运动模拟,核心公式 v = v0 - g*t,每帧更新速度并位移。
核心变量:
| 变量 | 作用 |
|---|---|
initYSpeed |
跳跃初始速度,决定跳跃高度 |
G |
重力加速度,控制下落快慢 |
nowYSpeed |
当前 Y 方向速度,每帧递减 |
nowPlatformY |
当前平台 Y 值,用于落地检测 |
jumpIndex |
二段跳计数,落地时重置为 0 |
跳跃流程:
if (Input.GetKeyDown(KeyCode.Space) && jumpIndex != 2)
{
roleAnimator.SetBool("isJump", true);
nowYSpeed = initYSpeed;
++jumpIndex;
}
if (roleAnimator.GetBool("isJump"))
{
transform.Translate(Vector3.up * nowYSpeed * Time.deltaTime);
nowYSpeed -= G * Time.deltaTime;
roleAnimator.SetBool("isFall", nowYSpeed <= 0);
if (transform.position.y <= nowPlatformY)
{
roleAnimator.SetBool("isJump", false);
roleAnimator.SetBool("isFall", false);
transform.position = new Vector3(transform.position.x, nowPlatformY, transform.position.z);
jumpIndex = 0;
}
}
落地时要把 Y 值拉回平台高度,避免因帧率问题”掉进平台里”。
影子系统
影子是独立的 2D Sprite 对象,不作为角色子物体,否则会跟着角色一起跳跃。
位置跟随:X 跟随角色,Y 固定在当前平台高度。
缩放公式:(极限高度 - 当前跳跃高度) / 极限高度
shadowPos.x = transform.position.x;
shadowPos.y = nowPlatformY;
shadowObj.transform.position = shadowPos;
float jumpHeight = transform.position.y - nowPlatformY;
shadowObj.transform.localScale = 1.5f * Vector3.one * Mathf.Max(0, (jumpMaxH - jumpHeight)) / jumpMaxH;
Mathf.Max(0, ...) 保证缩放值不会为负。
摄像机跟随
使用 Vector3.Lerp 插值移动,实现平滑跟随。
public Transform target;
public float moveSpeed = 8;
public float offsetY = 5;
void Update()
{
Vector3 targetPos = target.position;
targetPos.y += offsetY;
targetPos.z = transform.position.z;
transform.position = Vector3.Lerp(transform.position, targetPos, moveSpeed * Time.deltaTime);
}
Z 轴保持不变,offsetY 让摄像机在角色上方,视野更合理。
平台数据类
平台类 Platform 挂载到场景中的平台对象上,负责存储平台属性和提供落点检测。
核心属性:
| 属性 | 类型 | 作用 |
|---|---|---|
width |
float | 平台宽度,用于计算左右边界 |
canShowShadow |
bool | 是否显示角色影子 |
canFall |
bool | 是否允许主动下平台 |
Y |
float | 平台 Y 坐标(表达式属性) |
Left / Right |
float | 左右边界(表达式属性) |
边界计算:左边界 = 中心 X - 宽度/2,右边界 = 中心 X + 宽度/2。
落点检测:
public bool CheckObjFallOnMe(Vector3 pos)
{
if (pos.y >= Y && pos.x <= Right && pos.x >= Left)
return true;
return false;
}
场景可视化:用 OnDrawGizmos 绘制红色线段表示平台范围,方便编辑器中调试。
平台数据管理类
PlatformDataMgr 采用单例模式,统一管理所有平台数据,避免每帧重复查找。
public class PlatformDataMgr
{
private static PlatformDataMgr instance;
public static PlatformDataMgr Instance { get { ... } }
public List<Platform> platformList = new List<Platform>();
public void AddPlatform(Platform data) { platformList.Add(data); }
public void RemovePlatform(Platform data) { ... }
public void ClearData() { platformList.Clear(); }
}
平台类在 Start 中自动注册到管理器:PlatformDataMgr.Instance.AddPlatform(this);
平台逻辑处理类
PlatformLogic 负责处理角色的上下平台逻辑,每个角色拥有独立的逻辑处理实例。
核心结构:
public class PlatformLogic
{
private RoleObj obj;
private Platform nowPlatform;
private List<Platform> platformData;
public PlatformLogic(RoleObj obj) { ... }
public void UpdateCheck() { ... }
}
上平台逻辑:跳跃时遍历所有平台,找到能落到且 Y 值最高的平台:
if (obj.isJump || obj.isFall)
{
nowPlatform = null;
for (int i = 0; i < platformData.Count; i++)
{
if (platformData[i].CheckObjFallOnMe(obj.transform.position) &&
(nowPlatform == null || nowPlatform.Y < platformData[i].Y))
{
nowPlatform = platformData[i];
obj.ChangePlatformData(nowPlatform.Y, nowPlatform.canShowShadow, nowPlatform.canFall);
}
}
}
被动下平台:移动状态下,检测是否走出了当前平台边界:
if (!obj.isJump && !obj.isFall &&
nowPlatform != null &&
!nowPlatform.CheckObjFallOnMe(obj.transform.position))
{
obj.Fall();
}
主动下平台:站立状态下,按 S + 空格组合键触发:
if (canFall && !isJump && !isFall &&
Input.GetKey(KeyCode.S) && Input.GetKeyDown(KeyCode.Space))
{
Fall();
}
下落处理:Fall() 方法让角色进入自由落体状态:
public void Fall()
{
roleAnimator.SetBool("isFall", true);
nowPlatformY = -9999;
nowYSpeed = 0;
jumpIndex = 1;
}
关键细节:跳跃/下落时,先计算速度再位移,否则第一帧位置没变,平台检测会误判为仍在当前平台。
双摄像机渲染
场景采用双摄像机实现 2D 与 3D 混合渲染,让背景建筑呈现透视效果,而角色和平台保持正交视角。
摄像机配置:
| 摄像机 | 类型 | 渲染层级 | 作用 |
|---|---|---|---|
| 主摄像机 | 正交 | 2D、Role | 渲染角色、影子、2D 背景 |
| 透视摄像机 | 透视 | 3D | 渲染 3D 建筑、天空盒 |
层级设置:
- Role 层:角色、影子
- 2D 层:拖车、背景楼等 2D 贴图
- 3D 层:立方体等 3D 物体
实现效果:房子后面显示 3D 物体,房子前面是 2D 贴图,通过层级分离实现前后景深。
18.3 面试题精选
基础题
1. GetAxisRaw 和 GetAxis 有什么区别?为什么角色移动用 GetAxisRaw?
题目
Unity 输入系统提供了 Input.GetAxis 和 Input.GetAxisRaw 两个方法获取轴输入。请说明两者的区别,并解释为什么本系列的角色移动选择 GetAxisRaw。
深入解析
返回值区别:
GetAxis:返回 -1 到 1 之间的浮点数,有平滑过渡。按下按键后值从 0 渐变到 1,松开后从 1 渐变回 0GetAxisRaw:返回 -1、0、1 三个整数,没有平滑过渡。按下立即返回 1 或 -1,松开立即返回 0
为什么角色移动用 GetAxisRaw:
- 平台跳跃游戏需要即时响应,按键按下角色立即移动,松开立即停止
GetAxis的平滑过渡会让角色有”惯性”感,手感拖沓GetAxisRaw的即时响应更适合需要精确控制的游戏
GetAxis 的适用场景:
- 需要平滑加速/减速的移动,如赛车游戏的方向盘输入
- 第一人称射击游戏的视角旋转,平滑过渡更自然
答题示例
GetAxis返回 -1 到 1 的浮点数,有平滑过渡;GetAxisRaw返回 -1、0、1 三个整数,即时响应。平台跳跃游戏需要精确控制,按键按下立即移动、松开立即停止,所以用GetAxisRaw。如果用GetAxis,平滑过渡会让角色有”惯性”感,手感拖沓。
参考文章
- 5.角色移动
进阶题
1. 跳跃物理模拟:为什么用代码模拟竖直上抛运动而不是 Unity 刚体?
题目
在平台跳跃游戏中,角色的跳跃可以通过 Unity 的 Rigidbody + 重力实现,也可以像本系列一样用代码手动模拟竖直上抛运动。请分析两种方案的优缺点,以及什么场景下适合用代码模拟。
深入解析
Rigidbody 方案:
- 优点:物理引擎自动处理碰撞、重力、反弹等,开箱即用;与其他物理对象交互自然
- 缺点:物理行为难以精确控制,跳跃高度、滞空时间受质量、阻力等参数影响;物理引擎有不确定性,同一输入可能产生略微不同的结果
代码模拟方案:
- 优点:跳跃高度、滞空时间完全可控,参数直观(初始速度、重力加速度);行为确定,便于调试和调优;性能开销小
- 缺点:需要手动处理落地检测、碰撞等逻辑;与其他物理对象交互需要额外处理
适用场景:
- 代码模拟适合:平台跳跃、动作游戏等对跳跃手感要求精确的游戏
- Rigidbody 适合:物理模拟类游戏、需要与其他物理对象交互的场景
本系列选择代码模拟,核心原因是平台跳跃游戏对跳跃手感要求高,需要精确控制跳跃高度和滞空时间,代码模拟更容易调出”爽快”的跳跃手感。
答题示例
两种方案各有适用场景。Rigidbody 适合需要物理交互的游戏,但跳跃手感难以精确控制。代码模拟通过
v = v0 - g*t公式每帧更新速度,跳跃高度和滞空时间完全由参数决定,行为确定、易于调优。平台跳跃游戏对跳跃手感要求高,代码模拟更容易调出”爽快”的感觉,所以选择手动模拟。
参考文章
- 6.角色跳跃
2. 平台检测逻辑:为什么每帧要重置 nowPlatform 为 null?
题目
在 PlatformLogic.UpdateCheck() 中,每次检测平台时都会先把 nowPlatform 设为 null,然后再遍历查找。为什么不直接复用上一帧的结果,而要每帧重新查找?
深入解析
重置为 null 的原因:
找到”当前瞬间”的最高平台:角色跳跃轨迹是一条抛物线,可能同时处于多个平台的落点范围内。每帧重置后重新查找,能确保找到的是”当前 Y 值最高”的平台,而不是”整个跳跃轨迹中遇到的最高平台”
处理平台高度变化:如果场景中有动态移动的平台,每帧重新检测能及时响应平台位置变化
处理角色位置回退:角色可能在跳跃中改变方向,导致位置回退到之前检测过的平台范围,重新检测能正确处理这种情况
如果复用上一帧结果的问题:
- 可能”记住”了之前经过的高平台,导致角色落到更低的平台时被错误地拉回高平台
- 无法处理动态平台的位置变化
答题示例
每帧重置为 null 是为了确保找到”当前瞬间”的最高平台。角色跳跃时可能同时处于多个平台的落点范围内,如果不重置,可能”记住”之前经过的高平台,导致落到低平台时被错误拉回。重置后重新遍历,能保证每帧都找到当前 Y 值最高的平台,落地检测才准确。
参考文章
- 13.上平台
深度题
1. 动画状态机分层设计:跳跃层为什么要设置权重为 1 且默认为空状态?
题目
本系列的动画控制器采用了分层设计,跑步层和跳跃层独立运行。跳跃层的权重设为 1,默认状态设为空(Empty)。请解释这样设计的原因,以及如果权重设为 0 或默认状态设为其他动画会有什么问题。
深入解析
分层动画的工作原理:
Unity 动画控制器的多个层会按权重混合播放。权重越高,该层动画对最终姿态的影响越大。如果两层权重都是 1,最终姿态是两层动画的混合。
为什么跳跃层权重为 1:
- 跳跃层需要完全覆盖跑步层的下半身动画(腿部),让跳跃动画独立播放
- 如果权重为 0,跳跃层动画不会生效,角色跳跃时腿部还是跑步动画
- 权重为 1 时,跳跃层动画会完全覆盖跑步层的对应骨骼
为什么默认为空状态:
- 空状态不播放任何动画,不会影响跑步层的动画
- 如果默认状态是某个跳跃动画(如起跳),那么即使角色在跑步,跳跃层也会播放起跳动画,与跑步动画混合产生错误效果
- 只有当角色真正跳跃时,才通过参数切换到起跳状态
设计总结:
- 跑步层:负责角色在地面上的移动动画
- 跳跃层:默认空状态(不影响跑步层),跳跃时覆盖跑步动画
- 两层独立控制,互不干扰,代码只需设置对应层的参数
答题示例
分层动画会按权重混合播放,跳跃层权重为 1 才能完全覆盖跑步层的下半身动画。默认设为空状态是因为空状态不播放任何动画,不会干扰跑步层。如果默认状态是某个跳跃动画,角色跑步时就会错误地混合播放跳跃动画。这样设计后,两层独立运行:跑步层负责地面移动,跳跃层在角色起跳时才激活,代码只需设置对应参数,逻辑清晰。
参考文章
- 4.动画控制器制作
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com