18.自制平台跳跃总结

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.GetAxisInput.GetAxisRaw 两个方法获取轴输入。请说明两者的区别,并解释为什么本系列的角色移动选择 GetAxisRaw

深入解析

返回值区别

  • GetAxis:返回 -1 到 1 之间的浮点数,有平滑过渡。按下按键后值从 0 渐变到 1,松开后从 1 渐变回 0
  • GetAxisRaw:返回 -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 的原因

  1. 找到”当前瞬间”的最高平台:角色跳跃轨迹是一条抛物线,可能同时处于多个平台的落点范围内。每帧重置后重新查找,能确保找到的是”当前 Y 值最高”的平台,而不是”整个跳跃轨迹中遇到的最高平台”

  2. 处理平台高度变化:如果场景中有动态移动的平台,每帧重新检测能及时响应平台位置变化

  3. 处理角色位置回退:角色可能在跳跃中改变方向,导致位置回退到之前检测过的平台范围,重新检测能正确处理这种情况

如果复用上一帧结果的问题

  • 可能”记住”了之前经过的高平台,导致角色落到更低的平台时被错误地拉回高平台
  • 无法处理动态平台的位置变化
答题示例

每帧重置为 null 是为了确保找到”当前瞬间”的最高平台。角色跳跃时可能同时处于多个平台的落点范围内,如果不重置,可能”记住”之前经过的高平台,导致落到低平台时被错误拉回。重置后重新遍历,能保证每帧都找到当前 Y 值最高的平台,落地检测才准确。

参考文章
  • 13.上平台

深度题

1. 动画状态机分层设计:跳跃层为什么要设置权重为 1 且默认为空状态?

题目

本系列的动画控制器采用了分层设计,跑步层和跳跃层独立运行。跳跃层的权重设为 1,默认状态设为空(Empty)。请解释这样设计的原因,以及如果权重设为 0 或默认状态设为其他动画会有什么问题。

深入解析

分层动画的工作原理
Unity 动画控制器的多个层会按权重混合播放。权重越高,该层动画对最终姿态的影响越大。如果两层权重都是 1,最终姿态是两层动画的混合。

为什么跳跃层权重为 1

  • 跳跃层需要完全覆盖跑步层的下半身动画(腿部),让跳跃动画独立播放
  • 如果权重为 0,跳跃层动画不会生效,角色跳跃时腿部还是跑步动画
  • 权重为 1 时,跳跃层动画会完全覆盖跑步层的对应骨骼

为什么默认为空状态

  • 空状态不播放任何动画,不会影响跑步层的动画
  • 如果默认状态是某个跳跃动画(如起跳),那么即使角色在跑步,跳跃层也会播放起跳动画,与跑步动画混合产生错误效果
  • 只有当角色真正跳跃时,才通过参数切换到起跳状态

设计总结

  • 跑步层:负责角色在地面上的移动动画
  • 跳跃层:默认空状态(不影响跑步层),跳跃时覆盖跑步动画
  • 两层独立控制,互不干扰,代码只需设置对应层的参数
答题示例

分层动画会按权重混合播放,跳跃层权重为 1 才能完全覆盖跑步层的下半身动画。默认设为空状态是因为空状态不播放任何动画,不会干扰跑步层。如果默认状态是某个跳跃动画,角色跑步时就会错误地混合播放跳跃动画。这样设计后,两层独立运行:跑步层负责地面移动,跳跃层在角色起跳时才激活,代码只需设置对应参数,逻辑清晰。

参考文章
  • 4.动画控制器制作


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏