Files
rpg_start/StudyNotes.md

55 KiB
Raw Permalink Blame History

Unity RPG 学习笔记

📅 创建于 2026-05-23 🎯 按时间顺序记录学习过程


2026-05-23 · 第一次更新

文件: Assets/Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    public float xInput;

    private void Update()
        // 旧的移动方式
    {
        xInput = Input.GetAxisRaw("Horizontal");
    }
}

知识点

1. MonoBehaviour 继承

  • Unity 中挂载到 GameObject 的脚本必须继承 MonoBehaviour
  • 继承后可使用生命周期方法(UpdateStartAwake 等)。

2. public 字段暴露到 Inspector

  • public float xInput; 会在 Inspector 面板中可见、可编辑。
  • 公开字段会被序列化,保存到场景/预制体数据中。

3. Update() 生命周期

  • 每帧调用一次,帧率越高调用越频繁。
  • 适合处理输入检测持续移动等实时逻辑。

4. Input.GetAxisRaw("Horizontal") vs GetAxis("Horizontal")

方法 返回值变化 手感
GetAxis 0→1 平滑过渡(加减速曲线) 柔和,有惯性
GetAxisRaw 瞬间跳变 -1/0/1 灵敏,即时响应
  • "Horizontal" 默认绑定A/D、←/→、手柄左摇杆 X 轴。

5. Input 类常用方法

  • GetAxis() / GetAxisRaw() — 轴向输入
  • GetKey() / GetKeyDown() / GetKeyUp() — 按键检测
  • GetMouseButton() — 鼠标检测

2026-05-23 · 第二次更新

文件: Assets/Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    public Rigidbody2D rb;

    public float moveSpeed = 3.5f;
    public float xInput;

    private void Update()
        // 旧的移动方式
    {
        xInput = Input.GetAxisRaw("Horizontal");

        rb.linearVelocity = new Vector2 (xInput * moveSpeed, rb.linearVelocity.y);
    }
}

新增知识点

1. Rigidbody2D 组件

  • Unity 2D 物理引擎的核心组件,赋予物体物理属性(质量、重力、速度等)。
  • 通过 public Rigidbody2D rb; 声明后,需在 Inspector 中拖入对应的 Rigidbody2D 组件进行绑定。
  • 凡是需要物理模拟(重力、碰撞、速度)的 2D 物体都应挂载此组件。

2. linearVelocity 属性(物理移动)

  • Rigidbody2D.linearVelocity 表示刚体的线性速度Vector2 类型)。
  • 直接赋值可以瞬间改变物体速度,实现物理驱动的移动。
  • 这是旧版 velocity 属性的新命名Unity 2023+ 推荐使用 linearVelocity)。
  • 相比直接改 Transform.position,物理移动能正确参与碰撞检测。

3. Vector2 构造函数

  • new Vector2(x, y) 创建一个二维向量。
  • x 控制水平移动:xInput * moveSpeed,按方向键时值为 ±3.5f,松开时为 0。
  • y 保持当前垂直速度:rb.linearVelocity.y,避免干扰重力/跳跃。
  • ⚠️ 注意:代码中 new Vector2 (...) 括号前多了一个空格,虽不影响编译但建议去掉。

4. 物理移动 vs Transform 移动(对比)

方式 原理 碰撞检测 适用场景
rb.linearVelocity 修改刚体速度 物理正确 平台跳跃、推箱子等
transform.Translate 直接改位置 可能穿模 纯位移、无物理需求

5. 移动逻辑解析

按下 A / ←  →  xInput = -1  →  velocity.x = -3.5  →  向左移动
松开按键    →  xInput =  0  →  velocity.x =  0    →  停止
按下 D / →  →  xInput =  1  →  velocity.x =  3.5  →  向右移动

2026-05-24 · 第三次更新

文件: Assets/Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    private Rigidbody2D rb;
    [SerializeField] private float moveSpeed = 3.5f;
    private float xInput;


    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    private void Update()
        // 旧的移动方式
    {
        xInput = Input.GetAxisRaw("Horizontal");

        rb.linearVelocity = new Vector2 (xInput * moveSpeed, rb.linearVelocity.y);
    }
}

本次改动 · 新增知识点

1. private 字段与封装

  • 之前字段全用 public,现在改为 private
  • 原则: 不需要外部访问的字段应该设为私有,遵循封装原则,防止意外修改。
  • xInput 改为 private 很合理——它只是内部计算用的临时值Inspector 中没必要看到。

2. [SerializeField] 特性

  • 问题:字段设为 privateInspector 面板中就看不到了。
  • 解决:加 [SerializeField] 可以让私有字段也显示在 Inspector 中
  • 这样就做到了「Inspector 可调,代码不可外部访问」的最佳实践。
  • moveSpeed 用了这个特性——可以在编辑器里调速度,但其他脚本不能随便改。

3. Awake() 生命周期方法

  • Awake() 在脚本实例加载时调用,Start() 更早,且只调用一次。
  • Unity 生命周期顺序:Awake()OnEnable()Start()Update() → ...
  • 适合做组件引用的初始化(获取自己身上的组件、初始化变量等)。
  • Start() 的区别:Awake 在所有脚本的 Start 之前执行完,适合组件间的依赖初始化。

4. GetComponent<T>() 方法

  • 从当前 GameObject 上获取指定类型的组件引用。
  • rb = GetComponent<Rigidbody2D>(); — 自动找到同物体上的 Rigidbody2D 组件并赋值。
  • 这是比手动在 Inspector 中拖拽更优雅的方式——减少人为忘记绑定的风险
  • 前提:该组件必须挂载在同一个 GameObject 上。

5. 对比:手动拖拽 vs GetComponent

方式 字段类型 优点 缺点
Inspector 拖拽 public 可以绑定其他物体上的组件 容易忘记拖,运行时 NullReference
GetComponent private 不会忘,代码自动获取 只能获取同一物体上的组件

💡 当前代码中 rb 用的是 GetComponent,因为 Rigidbody2D 肯定在 Player 自己身上,很合适。

使用建议

  • 自己身上的组件 → GetComponent + Awake
  • 需要引用其他物体的组件 → public(或 [SerializeField] private+ Inspector 拖拽

2026-05-24 · 第四次更新

文件: Assets/Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    private Rigidbody2D rb;
    [SerializeField] private float moveSpeed = 3.5f;
    private float xInput;


    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    private void Update()
        // 旧的移动方式
    {
        xInput = Input.GetAxisRaw("Horizontal");

        rb.linearVelocity = new Vector2 (xInput * moveSpeed, rb.linearVelocity.y);
        
        // 按K
        if (Input.GetKey(KeyCode.K))
        {
            Debug.Log("holding K");
        }
        // 按下K
        if (Input.GetKeyDown(KeyCode.K))
        {
            Debug.Log("pressed K");
        }
    }
}

本次改动 · 新增知识点

1. 按键检测三剑客:GetKey / GetKeyDown / GetKeyUp

方法 触发时机 返回值 典型用途
Input.GetKey(KeyCode) 按键按住期间 每帧 true 持续开火、加速跑
Input.GetKeyDown(KeyCode) 按键按下的那一帧 true 一次 跳跃、菜单确认
Input.GetKeyUp(KeyCode) 按键松开的那一帧 true 一次 蓄力释放、弹起

记忆口诀:GetKey = 按住不放,GetKeyDown = 点一下,GetKeyUp = 松开那一刻。

2. KeyCode 枚举

  • KeyCode.K 代替字符串 "k"避免拼写错误IDE 有自动补全。
  • 常用按键:KeyCode.Space(空格)、KeyCode.EscapeKeyCode.W/A/S/DKeyCode.Mouse0(鼠标左键)。

3. Debug.Log() 调试输出

  • 向 Unity 控制台Console输出消息调试必备。
  • Debug.Log("xxx") — 普通信息
  • Debug.LogWarning("xxx") — 警告(黄色)
  • Debug.LogError("xxx") — 错误(红色,会暂停编辑器播放)

4. 代码验证思路

当前代码跑起来后的效果:

  • 按住 K → 控制台疯狂刷 "holding K"(每帧一条)
  • 敲一下 K → 只出现一条 "pressed K"
  • 通过这个对比就能直观理解 GetKeyGetKeyDown 的区别

2026-05-24 · 第五次更新

文件: Assets/Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    private Rigidbody2D rb;
    [SerializeField] private float moveSpeed = 3.5f;
    [SerializeField] private float jumpForce = 8;
    private float xInput;


    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    private void Update()
        // 旧的移动方式
    {
        xInput = Input.GetAxisRaw("Horizontal");

        rb.linearVelocity = new Vector2 (xInput * moveSpeed, rb.linearVelocity.y);
        
       
        if (Input.GetKeyDown(KeyCode.K))
        {
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
        }
    }
}

本次改动 · 新增知识点

1. 跳跃实现原理

  • 跳跃 = 给刚体的 Y 轴速度一个瞬间向上的值。
  • rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
    • X 保持不变:角色在空中仍能左右移动
    • Y 覆盖为 jumpForce(正数 = 向上)
  • 之后重力会自然把 Y 速度拉回负值,形成"上升→下落"的弧线。

2. 为什么跳跃用 GetKeyDown 而不是 GetKey

  • GetKeyDown — 只在按下那一帧触发 → 按一次跳一次
  • GetKey — 按住期间每帧都触发 → 角色会一直往上飞
  • 跳跃是一次性动作,必须用 GetKeyDown

3. jumpForce 参数调优

  • jumpForce = 8 是目前的值,可以根据手感调整:
    • 太小 → 跳不起来
    • 太大 → 飞出屏幕
  • 配合 Rigidbody2D 的 Gravity Scale(重力缩放)一起调,效果更好。
    • Gravity Scale 越大 → 下落越快 → 跳跃手感更"重"
    • Gravity Scale 越小 → 轻飘飘(适合太空/月球感)

4. 当前完整移动逻辑图

┌─────────────────────────────────────────┐
│                 Update()                 │
├─────────────────────────────────────────┤
│  xInput = GetAxisRaw("Horizontal")      │
│         ↓                               │
│  velocity.x = xInput × moveSpeed        │  ← 左右移动(每帧)
│  velocity.y = 保持原来的 y               │
│         ↓                               │
│  if GetKeyDown(K)                       │
│      velocity.y = jumpForce             │  ← 跳跃(按下瞬间)
└─────────────────────────────────────────┘

⚠️ 当前跳跃没有接地检测,角色可以在空中无限跳跃。后续可以加 isGrounded 判断来限制。


2026-05-24 · 第六次更新

文件: Assets/Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    private Animator anim;
    private Rigidbody2D rb;
    [SerializeField] private float moveSpeed = 3.5f;
    [SerializeField] private float jumpForce = 8;
    private float xInput;


    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        anim = GetComponentInChildren<Animator>();
    }

    private void Update()
    {
        HandleInput();
        HandleMovement();
        HandleAnimations();
    }

    private void HandleInput()
    {
        xInput = Input.GetAxisRaw("Horizontal");

        if (Input.GetKeyDown(KeyCode.K))
        {
            jump();
        }
    }

    private void HandleMovement()
    {
        rb.linearVelocity = new Vector2(xInput * moveSpeed, rb.linearVelocity.y);
    }

    private void HandleAnimations()
    {
        bool isMoving = rb.linearVelocity.x != 0;
        anim.SetBool("isMoving", isMoving);
    }

    private void jump()
    {
        rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
    }
}

本次改动 · 新增知识点

1. 代码重构:拆分 Update()

  • 之前 Update() 里什么都混在一起,现在拆成三个职责明确的方法:
方法 职责
HandleInput() 读取玩家输入,触发相应动作
HandleMovement() 根据输入计算并应用速度
HandleAnimations() 根据状态更新动画参数
  • 好处:易读、易改、易扩展。比如以后加攻击,加一个 HandleCombat() 即可。

2. Animator 组件与动画控制

  • Animator 是 Unity 的动画状态机组件,控制角色动画的播放。
  • GetComponentInChildren<Animator>() — 从子物体上获取 Animator。
    • 因为 Animator 通常挂在角色模型的子物体上,而不是 Player 根物体。
  • 控制动画的方式:通过 SetBool / SetFloat / SetTrigger 等方法修改 Animator 参数。

3. anim.SetBool("isMoving", value)

  • 在 Animator 窗口中预先创建 isMoving 参数Bool 类型)。
  • 代码中设置这个参数 → Animator 根据状态切换动画Idle ↔ Run
  • isMoving = rb.linearVelocity.x != 0:水平速度不为 0 就是移动中。
isMoving = false  →  Idle 动画
isMoving = true   →  Run 动画

4. Animator 参数类型对比

类型 方法 用途
Bool SetBool("name", true/false) 二元状态:跑/不跑、地面/空中
Float SetFloat("name", 值) 连续值:速度、方向
Trigger SetTrigger("name") 一次性触发:攻击、受伤、死亡
Int SetInteger("name", 值) 整数状态:武器序号、连击段数

5. GetComponent vs GetComponentInChildren

方法 搜索范围 示例
GetComponent<T>() 当前物体 获取 Player 上的 Rigidbody2D
GetComponentInChildren<T>() 当前物体 + 所有子物体 获取模型子物体上的 Animator
GetComponentInParent<T>() 当前物体 + 所有父物体 少用,偶尔反向查找

6. 当前架构图

Player (MonoBehaviour)
├── Awake()
│   ├── rb = GetComponent<Rigidbody2D>()
│   └── anim = GetComponentInChildren<Animator>()
│
└── Update()
    ├── HandleInput()      → 读取按键,调用 jump()
    ├── HandleMovement()   → 设置 velocity
    └── HandleAnimations() → 设置 isMoving 驱动动画

2026-05-24 · 第七次更新

文件: Assets/Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    private Animator anim;
    private Rigidbody2D rb;
    [SerializeField] private float moveSpeed = 3.5f;
    [SerializeField] private float jumpForce = 8;
    private float xInput;
    [SerializeField] private bool facingRight = true;

    // ... Awake、Update、HandleInput、HandleMovement、HandleAnimations、jump 同上 ...

    [ContextMenu("Flip")]
    private void Flip()
    {
        transform.Rotate(0, 180, 0);
        facingRight = !facingRight;
    }
}

本次改动 · 新增知识点

1. [ContextMenu] 特性 —— 测试利器

  • 给方法加上 [ContextMenu("显示名称")] 后,可以在 Inspector 中右键组件直接调用。
  • 非常适合在编辑器中手动测试某个功能,不需要写额外的测试代码。
用法:
1. 选中挂载 Player 脚本的 GameObject
2. 在 Inspector 中找到 Player 组件,右键
3. 菜单中出现 "Flip" → 点击即可执行 Flip() 方法

2. transform.Rotate(x, y, z) —— 旋转物体

  • transform.Rotate(0, 180, 0) → 绕 Y 轴旋转 180°水平翻转
  • 三个参数分别对应 X / Y / Z 轴旋转(欧拉角,单位:度)。
  • 绕 Y 轴旋转 180° 是 2D 游戏中最常用的翻转方式(让角色面朝另一个方向)。
旋转 效果
Rotate(0, 0, 0) 面朝右
Rotate(0, 180, 0) 面朝左

3. facingRight 方向追踪

  • private bool facingRight = true; 记录当前朝向。
  • [SerializeField] 让它在 Inspector 中可见,方便调试时观察。
  • facingRight = !facingRight; 每次翻转时取反,保持状态同步。
  • 后续可根据 facingRight 决定子弹发射方向、攻击判定范围等。

4. ContextMenu vs 其他调用方式

方式 何时用
[ContextMenu] 编辑器里手动测试某个功能
Update() 中调用 运行时每帧自动执行
ButtonUI 玩家通过游戏界面触发

💡 [ContextMenu] 即使方法是 private 也能调用,非常适合调试。


2026-05-25 · 第八次更新

文件: Assets/Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    private Animator anim;
    private Rigidbody2D rb;
    [SerializeField] private float moveSpeed = 4.58f;
    [SerializeField] private float jumpForce = 8;
    private float xInput;
    [SerializeField] private bool facingRight = true;

    // ... Awake 同上 ...

    private void Update()
    {
        HandleInput();
        HandleMovement();
        HandleAnimations();
        HandleFlip();         // ← 新增
    }

    // ... HandleInput、HandleMovement、HandleAnimations、jump 同上 ...

    private void HandleFlip()
    {
        if(rb.linearVelocity.x > 0 && facingRight == false)
        {
            Flip();
        }
        else if (rb.linearVelocity.x < 0 && facingRight == true)
        {
            Flip();
        }
    }

    private void Flip()
    {
        transform.Rotate(0, 180, 0);
        facingRight = !facingRight;
    }
}

本次改动 · 新增知识点

1. HandleFlip() —— 自动转向逻辑

  • 之前 Flip() 只能通过 [ContextMenu] 手动调用,现在改成运行时自动翻转
  • 核心思路:当角色移动方向当前朝向不一致时,执行翻转。

2. 翻转判断逻辑拆解

向右移动 (velocity.x > 0)  且  面朝左 (facingRight == false)  →  翻转
向左移动 (velocity.x < 0)  且  面朝右 (facingRight == true)   →  翻转
其他情况(静止、方向已一致)                                   →  不翻转
速度方向 facingRight 需要翻转?
向右 (>0) true面朝右 方向一致
向右 (>0) false面朝左 翻过来
向左 (<0) true面朝右 翻过来
向左 (<0) false面朝左 方向一致
静止 (=0) 任意 不动不翻

3. 为什么用 facingRight 而不是直接每帧翻转

  • 如果每帧都根据速度方向直接翻转(if velocity.x > 0 → 右else → 左),会导致:
    • 静止时角色朝向不确定velocity.x = 0 时往哪边?)
    • 可能每帧都在调用 Rotate,浪费性能
  • facingRight脏标记dirty flag:只在需要翻转时才翻,静止时保持上次朝向。

4. [ContextMenu] 的移除

  • 上一次 Flip() 上挂了 [ContextMenu("Flip")] 用于手动测试。
  • 现在翻转逻辑已集成到 Update 中自动运行,不再需要手动触发,所以去掉了。
  • 这也是开发中的常见节奏:先手动测试 → 确认 OK → 集成到自动化流程

5. 当前完整流程图

Update()
├── HandleInput()        → 读取 A/D/K 输入
├── HandleMovement()     → xInput → velocity
├── HandleAnimations()   → velocity.x != 0 → 跑/停 动画
└── HandleFlip()         → 方向不一致时 Flip()
                             └── Flip()
                                   ├── Rotate(0, 180, 0)
                                   └── facingRight = !facingRight

2026-05-25 · 第九次更新

文件: Assets/Player.cs

using Unity.VisualScripting;
using UnityEngine;

public class Player : MonoBehaviour
{
    private Animator anim;
    private Rigidbody2D rb;

    [Header("Movement details")]
    [SerializeField] private float moveSpeed = 4.58f;
    [SerializeField] private float jumpForce = 12;
    private bool facingRight = true;
    private float xInput;

    [Header("Collision details")]
    [SerializeField] private float groundCheckDistance;
    [SerializeField] private LayerMask whatIsGround;
    private bool isGrounded;

    // ... Awake 同上 ...

    private void Update()
    {
        HandleCollision();     // ← 新增,放在最前面
        HandleInput();
        HandleMovement();
        HandleAnimations();
        HandleFlip();
    }

    // ... HandleInput、HandleMovement、HandleAnimations、HandleFlip、Flip 同上 ...

    private void jump()
    {
        if(isGrounded)         // ← 加了接地判断
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
    }

    private void HandleCollision()
    {
        isGrounded = Physics2D.Raycast(transform.position, Vector2.down, groundCheckDistance, whatIsGround);
    }

    private void OnDrawGizmos()
    {
        Gizmos.DrawLine(transform.position, transform.position + new Vector3(0, -groundCheckDistance));
    }
}

本次改动 · 新增知识点

1. 地面检测 (isGrounded) 解决无限跳跃

  • 之前跳跃没有限制,空中也能一直跳。
  • 现在 jump() 加了 if(isGrounded) 判断,只有站在地面上才能跳。

2. Physics2D.Raycast() —— 2D 射线检测

  • 从指定位置向指定方向发射一条不可见的射线,检测是否碰到碰撞体。
  • 参数:Physics2D.Raycast(起点, 方向, 距离, 层级掩码)
  • 当前用法:
    起点transform.position角色脚底位置
    方向Vector2.down向下
    距离groundCheckDistance
    过滤whatIsGround只检测地面层
    
        角色
         │
    ─────┼─────  transform.position
         │
         ↓  射线 (Vector2.down, 长度 = groundCheckDistance)
    ═════╪═════  地面碰撞体 → 射线命中 → isGrounded = true
         
    ······│······  没碰到地面 → isGrounded = false在空中

3. [Header("...")] —— Inspector 分组标签

  • Unity 会把 Inspector 字段按 [Header] 分组,加上加粗标题。
  • 让 Inspector 面板更有条理:
┌─ Player (Script) ──────────────┐
│  Script          Player        │
│                                │
│  ── Movement details ──        │  ← [Header("Movement details")]
│  Move Speed      4.58          │
│  Jump Force      12            │
│                                │
│  ── Collision details ──       │  ← [Header("Collision details")]
│  Ground Check Distance  1.5    │
│  What Is Ground   Ground ▾     │
└────────────────────────────────┘

4. LayerMask —— 层级过滤

  • LayerMask whatIsGround 在 Inspector 中显示为下拉菜单,可以选择哪些 Layer 算作"地面"。
  • 射线只会检测被选中的层,忽略其他层(玩家自身、敌人等)。
  • 必须设置: 给地面物体分配一个 Layer如"Ground"),然后在 Player 的 whatIsGround 中勾选它。

5. OnDrawGizmos() —— Scene 视图可视化调试

  • Gizmos.DrawLine(起点, 终点) 在 Scene 视图中画线,方便调试射线位置。
  • 只在编辑器 Scene 视图可见,游戏运行时不会显示。
  • 当前画了一条向下的红线,直观看到 groundCheckDistance 的长度是否合适。

6. ⚠️ 注意事项

  • using Unity.VisualScripting; 是多余的引用,当前代码并未使用 Visual Scripting 功能。可以安全删除,不影响编译但保持代码整洁。
  • jump()if(isGrounded) 后面没有花括号 —— 只有一行代码时可以省略 {},但建议养成始终加括号的习惯,避免后续扩展时出错。

7. Vector2.down 快捷向量

写法 等价于 含义
Vector2.up (0, 1)
Vector2.down (0, -1)
Vector2.left (-1, 0)
Vector2.right (1, 0)
Vector2.zero (0, 0) 零向量
Vector2.one (1, 1) 单位向量

8. 当前架构图(更新版)

Player (MonoBehaviour)
├── Awake()
│   ├── rb = GetComponent<Rigidbody2D>()
│   └── anim = GetComponentInChildren<Animator>()
│
├── Update()
│   ├── HandleCollision()    → Raycast 向下 → isGrounded
│   ├── HandleInput()        → 读取按键
│   ├── HandleMovement()     → 设置 velocity
│   ├── HandleAnimations()   → 驱动动画
│   └── HandleFlip()         → 自动转向
│
└── OnDrawGizmos()           → Scene 视图画射线(仅编辑器)

2026-05-26 · 第十次更新

文件: Assets/Player.cs + Assets/Animations/Player.controller

Player.cs 当前代码

using UnityEngine;

public class Player : MonoBehaviour
{
    private Animator anim;
    private Rigidbody2D rb;

    [Header("Movement details")]
    [SerializeField] private float moveSpeed = 8f;
    [SerializeField] private float jumpForce = 12;
    private bool facingRight = true;
    private float xInput;

    [Header("Collision details")]
    [SerializeField] private float groundCheckDistance;
    [SerializeField] private LayerMask whatIsGround;
    private bool isGrounded;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        anim = GetComponentInChildren<Animator>();
    }

    private void Update()
    {
        HandleCollision();
        HandleInput();
        HandleMovement();
        HandleAnimations();
        HandleFlip();
    }

    private void HandleInput()
    {
        xInput = Input.GetAxisRaw("Horizontal");

        if (Input.GetKeyDown(KeyCode.K))
        {
            jump();
        }
    }

    private void HandleAnimations()
    {
        anim.SetFloat("xVelocity", rb.linearVelocity.x);
        anim.SetFloat("yVelocity", rb.linearVelocity.y);
        anim.SetBool("isGrounded", isGrounded);
    }

    private void HandleMovement()
    {
        rb.linearVelocity = new Vector2(xInput * moveSpeed, rb.linearVelocity.y);
    }

    private void jump()
    {
        if (isGrounded)
        {
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
        }
    }

    private void HandleCollision()
    {
        isGrounded = Physics2D.Raycast(transform.position, Vector2.down, groundCheckDistance, whatIsGround);
    }

    private void HandleFlip()
    {
        if (rb.linearVelocity.x > 0 && facingRight == false)
        {
            Flip();
        }
        else if (rb.linearVelocity.x < 0 && facingRight == true)
        {
            Flip();
        }
    }

    private void Flip()
    {
        transform.Rotate(0, 180, 0);
        facingRight = !facingRight;
    }

    private void OnDrawGizmos()
    {
        Gizmos.DrawLine(transform.position, transform.position + new Vector3(0, -groundCheckDistance));
    }
}

本次改动 · 新增知识点

1. HandleAnimations 大改Bool → Float 驱动

  • 旧方案: 只传一个 isMoving 布尔值 → 动画只能"跑"或"不跑",很僵硬。
  • 新方案: 传入三个参数,让 Blend Tree 做平滑混合:
anim.SetFloat("xVelocity", rb.linearVelocity.x);  // 水平速度 → 驱动 跑/停 混合
anim.SetFloat("yVelocity", rb.linearVelocity.y);  // 垂直速度 → 驱动 上升/下落 混合
anim.SetBool("isGrounded", isGrounded);            // 是否着地 → 切换 地面/空中 状态

2. Blend Tree混合树概念

  • Blend Tree 是 Animator 中的一种动画混合器,根据一个或多个浮点参数,在多个动画之间平滑过渡。
  • 类比:调音台上的推子,推到哪里就播对应位置的动画。

当前配置了两个 Blend Tree

Animator 状态机
├── idle/move (地面状态) ── 1D Blend Tree参数: xVelocity
│   ├── Threshold = -1  →  向左跑动画
│   ├── Threshold =  0  →  Idle 动画
│   └── Threshold =  1  →  向右跑动画
│
└── jump/fall (空中状态) ── 1D Blend Tree参数: yVelocity
    ├── Threshold = -1  →  下落动画
    ├── Threshold =  0  →  空中顶点动画
    └── Threshold =  1  →  上升跳跃动画

3. 1D Blend Tree 工作原理

  • m_BlendType: 0 = 1D 简单混合。
  • 只有一个驱动参数(如 xVelocity),沿着一条轴混合动画。
  • 参数值在 threshold 之间时,自动插值混合两个相邻动画。
xVelocity = 0.0  →  100% Idle
xVelocity = 0.3  →  70% Idle + 30% 跑
xVelocity = 0.7  →  30% Idle + 70% 跑
xVelocity = 1.0  →  100% 跑

4. 状态切换:isGrounded 驱动的 Transition

从 Animator Controller 文件中可以看到两条 Transition

条件 效果
idle/move → jump/fall isGrounded == false 离开地面 → 切到空中动画
jump/fall → idle/move isGrounded == true 落地 → 切回地面动画
  • m_ConditionMode: 1 = If为 true 时触发)
  • m_ConditionMode: 2 = IfNot为 false 时触发)
  • m_HasExitTime: 0 = 不等待动画播完,条件满足立即切换(关键!跳跃需要即时响应)

5. Animator 参数类型YAML 对照)

从 Controller 文件中可看到三种参数类型的序列化 ID

类型 YAML m_Type C# 方法
Float 1 SetFloat()
Int 3 SetInteger()
Bool 4 SetBool()
Trigger 9 SetTrigger()

6. 新旧动画方案对比

旧方案 新方案Blend Tree
参数 1 个 Bool (isMoving) 2 个 Float + 1 个 Bool
动画数量 2 个Idle / Run 6 个Idle、左跑、右跑、上升、顶点、下落
过渡效果 硬切 平滑混合
空中动画 有(跳跃/下落独立)
表现力 基础 流畅自然

7. moveSpeed 调整4.58 → 8

  • 速度翻倍,角色移动更快。
  • 配合 Blend TreexVelocity 参数值范围变大,动画混合范围需要与之匹配。

8. 当前 Animator 架构图

                    ┌──────────────────────┐
                    │   Animator Controller │
                    │   参数:                │
                    │   · xVelocity (Float) │
                    │   · yVelocity (Float) │
                    │   · isGrounded (Bool) │
                    └──────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
    ┌─────────────────┐             ┌─────────────────┐
    │   idle/move      │             │   jump/fall      │
    │   (默认状态)      │  isGrounded │   (空中状态)      │
    │                  │◄────────────│                  │
    │  Blend Tree 1D   │────────────►│  Blend Tree 1D   │
    │  参数: xVelocity  │  !isGrounded│  参数: yVelocity  │
    │                  │             │                  │
    │  -1 → 左跑       │             │  -1 → 下落       │
    │   0 → Idle       │             │   0 → 顶点       │
    │   1 → 右跑       │             │   1 → 上升       │
    └─────────────────┘             └─────────────────┘

2026-05-26 · 第十一次更新

文件: Assets/Player.cs + Assets/PlayerAnimationEvents.cs(新文件)

Player.cs 当前代码

using UnityEngine;

public class Player : MonoBehaviour
{
    private Animator anim;
    private Rigidbody2D rb;

    [Header("Movement details")]
    [SerializeField] private float moveSpeed = 8f;
    [SerializeField] private float jumpForce = 12;
    private bool facingRight = true;
    private float xInput;
    private bool canMove = true;       // ← 新增
    private bool canJump = true;       // ← 新增

    [Header("Collision details")]
    [SerializeField] private float groundCheckDistance;
    [SerializeField] private LayerMask whatIsGround;
    private bool isGrounded;

    // ... Awake、Update 同上 ...

    public void EnableMovementAndJump(bool enable)    // ← 新增
    {
        canMove = enable;
        canJump = enable;
    }

    private void HandleInput()
    {
        xInput = Input.GetAxisRaw("Horizontal");

        if (Input.GetKeyDown(KeyCode.K))
        {
            TryToJump();                // ← 改名
        }
        if (Input.GetKeyDown(KeyCode.J)) // ← 新增攻击
        {
            TryToAttack();
        }
    }

    private void TryToAttack()          // ← 新增方法
    {
        if(isGrounded)
        {
            anim.SetTrigger("attack");
            rb.linearVelocity = new Vector2(0, rb.linearVelocity.y);
        }
    }

    private void HandleMovement()
    {
        if(canMove)                     // ← 加了 canMove 判断
        {
            rb.linearVelocity = new Vector2(xInput * moveSpeed, rb.linearVelocity.y);
        }
    }

    private void TryToJump()            // ← 改名 + 加 canJump 判断
    {
        if (isGrounded && canJump)
        {
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
        }
    }

    // ... HandleCollision、HandleFlip、Flip、OnDrawGizmos 同上 ...
}

PlayerAnimationEvents.cs新文件

using UnityEngine;

public class PlayAnimationEvents : MonoBehaviour
{
    private Player player;

    private void Awake()
    {
        player = GetComponentInParent<Player>();
    }

    private void DisableMovementAndJump() => player.EnableMovementAndJump(false);
    private void EnableMovementAndJump() => player.EnableMovementAndJump(true);
}

本次改动 · 新增知识点

1. 攻击系统(初版)

  • 按 J 键触发攻击:Input.GetKeyDown(KeyCode.J)
  • anim.SetTrigger("attack") — 播放一次性攻击动画
  • 攻击时将水平速度归零:new Vector2(0, rb.linearVelocity.y),阻止攻击时滑行
  • 只能在地面攻击:if(isGrounded)

2. canMove / canJump 控制标志Lock 模式)

  • 两个 bool 变量控制角色能否移动和跳跃。
  • 攻击动画播放期间设为 false,动画结束恢复 true
  • 这是一种常见的"动作锁定"设计,防止在特定动画期间执行不该执行的动作。
攻击开始  canMove = false, canJump = false  不能移动/跳跃
攻击结束  canMove = true,  canJump = true   恢复正常

3. EnableMovementAndJump(bool) 公共接口

  • public 方法,供其他脚本调用(这里是 PlayAnimationEvents)。
  • 封装了两个状态一起控制,避免外部重复设置。

4. GetComponentInParent<T>() —— 向上查找组件

方法 搜索方向 场景
GetComponent<T>() 当前物体 Rigidbody2D 在自己身上
GetComponentInChildren<T>() 向下(自身+子物体) Animator 在模型子物体上
GetComponentInParent<T>() 向上(自身+父物体) Player 脚本在父物体上
  • PlayAnimationEvents 挂载在角色模型子物体上(和 Animator 同级),需要通过 GetComponentInParent 找到父物体上的 Player 脚本。

5. Animation Events动画事件

  • 在攻击动画的时间轴上插入事件,指定时间点调用 PlayAnimationEvents 的方法:
    • 动画开始帧 → 调用 DisableMovementAndJump() → 锁定移动
    • 动画结束帧 → 调用 EnableMovementAndJump() → 解锁移动
  • 挂载位置:PlayAnimationEvents 脚本挂在和 Animator 同一个 GameObject 上。
攻击动画时间轴
├── 第0帧  ── DisableMovementAndJump()  锁定
├── ... 动画播放中 ...   (canMove=false, canJump=false)
└── 最后帧 ── EnableMovementAndJump()   解锁

6. 表达式体方法Expression-bodied

private void DisableMovementAndJump() => player.EnableMovementAndJump(false);
//                                     ↑ 等价于 { player.EnableMovementAndJump(false); }
  • => 是 C# 的表达式体语法糖,单行方法更简洁。

7. ⚠️ 已知 Bug攻击时仍能移动

  • 当前在 TryToAttack() 中虽然把速度归零了(new Vector2(0, ...)),但 HandleMovement()同一帧后面又用 canMove 重新设置了速度。
  • 如果 DisableMovementAndJump 没有被及时调用(存在一帧延迟),玩家在攻击第一帧仍能移动
  • 这是用户提到的 bug预计下次更新修复。

8. 当前 Update 执行顺序 & 控制流

Update()
├── HandleCollision()       → isGrounded
├── HandleInput()
│   ├── xInput = GetAxisRaw("Horizontal")
│   ├── K → TryToJump()     → isGrounded && canJump?
│   └── J → TryToAttack()   → isGrounded? → SetTrigger("attack"), velocity.x=0
├── HandleMovement()        → canMove? → 设置 velocity
├── HandleAnimations()      → 更新 xVelocity/yVelocity/isGrounded
└── HandleFlip()            → 自动转向

2026-05-26 · 第十二次更新

文件: Assets/Player.csPlayAnimationEvents.cs 未变)

改动对比

位置 旧代码 新代码
TryToAttack() anim.SetTrigger("attack"); + velocity.x=0 只剩 anim.SetTrigger("attack");
HandleMovement() if(canMove) { 设置速度 } if(canMove) { 设置速度 } else { velocity.x=0 }
// TryToAttack — 去掉了速度归零
private void TryToAttack()
{
    if(isGrounded)
    {
        anim.SetTrigger("attack");
    }
}

// HandleMovement — 新增 else 分支
private void HandleMovement()
{
    if(canMove)
    {
        rb.linearVelocity = new Vector2(xInput * moveSpeed, rb.linearVelocity.y);
    }
    else
    {
        rb.linearVelocity = new Vector2(0, rb.linearVelocity.y);  // ← 攻击时停住
    }
}

知识点

职责分离:为什么把速度归零从 TryToAttack 移到 HandleMovement

  • 旧方案: TryToAttack 里直接操作速度 → 攻击逻辑和移动逻辑混在一起。
  • 新方案: HandleMovement 统一管理"能不能移动"canMove=false 时自动归零。
  • 好处:不需要每个动作单独处理速度归零,后续加技能/受伤等,只要设 canMove=false 就行。

🐛 Bug 分析:快速双击 J 攻击两次

现象: 快速按两次 J第二次在第一次攻击动画期间攻击动画播放了两次。

根因: TryToAttack() 只检查了 isGrounded,没有检查是否正在攻击中

时间线:
帧1  按下J → TryToAttack() → isGrounded=true ✓ → SetTrigger("attack") → 攻击开始
      → Animation Event → canMove=false
帧2  再次按下J → TryToAttack() → isGrounded=true ✓ → SetTrigger("attack") ⚠️ 又设了一次!
帧N  第一次攻击动画结束 → Animator 发现还有一次 trigger 没消费 → 再播一次攻击 ❌

核心问题: Unity 的 Trigger 会累积。第一次攻击动画还没播完,第二次 SetTrigger 就被攒着,等第一次结束自动触发第二次。

修复方案(下次更新):TryToAttack() 中加 canMove 判断:

private void TryToAttack()
{
    if(isGrounded && canMove)    // ← 攻击期间 canMove=false阻止第二次触发
    {
        anim.SetTrigger("attack");
    }
}

💡 canMove 就是天然的"是否正在攻击"标志——攻击时已被 Animation Event 设为 false。

当前控制流(含 Bug 标注)

Update()
├── HandleCollision()
├── HandleInput()
│   └── J → TryToAttack()
│           └── isGrounded? → SetTrigger("attack")  ⚠️ 没有防重入
├── HandleMovement()
│   ├── canMove=true  → velocity = (xInput*speed, vy)
│   └── canMove=false → velocity = (0, vy)           ✅ 好的设计
├── HandleAnimations()
└── HandleFlip()

2026-05-26 · 第十三次更新Bug 修复)

文件: Assets/Player.cs(仅一行改动)

改动

// 修复前
private void TryToAttack()
{
    if(isGrounded)                       // ← 只检查地面
    {
        anim.SetTrigger("attack");
    }
}

// 修复后
private void TryToAttack()
{
    if(isGrounded && canMove)           // ← 加上 canMove 防重入
    {
        anim.SetTrigger("attack");
    }
}

Bug 修复原理

  • 攻击动画开始时Animation Event 调用 DisableMovementAndJump()canMove = false
  • 此时再按 J → TryToAttack() 检查 canMovefalse → 不执行 SetTrigger
  • 动画结束后 canMove 恢复 true,才能再次攻击

一句话: canMove 既是移动开关,也是"是否正在攻击"的天然标志,一行改动解决双触 Bug。


2026-05-27 · 第十四次更新

文件: Assets/Player.csAssets/PlayerAnimationEvents.csAssets/Enemy.cs(新文件)

Player.cs 新增

// 新增字段
[Header("Attack details")]
[SerializeField] private float attackRadius;
[SerializeField] private Transform attackPoint;
[SerializeField] private LayerMask whatIsEnemy;

// 新增方法
public void DamageEnemies()
{
    Collider2D[] enemyColliders = Physics2D.OverlapCircleAll(
        attackPoint.position, attackRadius, whatIsEnemy);
    foreach (Collider2D enemy in enemyColliders)
    {
        enemy.GetComponent<Enemy>().TakeDamage();
    }
}

// OnDrawGizmos 新增
Gizmos.DrawWireSphere(attackPoint.position, attackRadius);

PlayerAnimationEvents.cs 新增

public void DamageEnemies() => player.DamageEnemies();

Enemy.cs新文件

using UnityEngine;

public class Enemy : MonoBehaviour
{
    private SpriteRenderer sr;
    [SerializeField] private float redColorDuration = 1;

    private void Awake()
    {
        sr = GetComponent<SpriteRenderer>();
    }

    public void TakeDamage()
    {
        sr.color = Color.red;
        Invoke(nameof(TurnWhite), redColorDuration);
    }

    private void TurnWhite()
    {
        sr.color = Color.white;
    }
}

本次改动 · 新增知识点

1. 攻击判定:Physics2D.OverlapCircleAll()

  • 以某点为中心画一个圆,返回圆内所有碰撞体。
  • 用于攻击范围判定(圆形 AOE 伤害)。
参数Physics2D.OverlapCircleAll(中心点, 半径, 层级过滤)

        ╭─────────────────╮
        │    attackRadius  │
        │     ┌─────┐      │
        │     │ 玩家 │      │  ← attackPoint (子空物体)
        │     └──┬──┘      │
        │        │         │
        │   ╔════╧════╗    │  ← 敌人碰撞体在圆内 → 被检测到
        │   ║  敌人   ║    │
        │   ╚═════════╝    │
        ╰─────────────────╯
  • 返回 Collider2D[] 数组,可能有 0~N 个敌人被击中。

2. Transform 作为位置标记

  • attackPoint 是一个空 GameObject 的 Transform拖到攻击判定位置。
  • 这是一种常见做法:在角色子物体下放一个空物体标记攻击中心点,代码通过 attackPoint.position 获取世界坐标。
Player (父物体)
├── Graphics (模型)
└── AttackPoint (空物体) ← Transform attackPoint放在拳头/武器位置

3. 攻击流程全链路

按下 J
  → TryToAttack()
    → anim.SetTrigger("attack")     // 播放攻击动画
      → 动画播放到"伤害帧"
        → Animation Event: DamageEnemies()
          → Physics2D.OverlapCircleAll()  // 搜敌人
            → foreach enemy
              → GetComponent<Enemy>().TakeDamage()  // 扣血/反馈

🔑 关键设计:伤害判定不在代码里固定时间触发,而是通过动画事件在挥刀命中的那一帧触发。

4. SpriteRenderer 组件

  • 2D 精灵渲染器,控制角色/物体的显示。
  • sr.color = Color.red; — 直接改颜色,用于受伤闪烁效果。
  • sr.color 类型是 ColorRGBA

5. Invoke() 延迟调用

Invoke(nameof(TurnWhite), redColorDuration);
参数 含义
nameof(TurnWhite) 要调用的方法名(字符串安全版本)
redColorDuration 延迟秒数(这里是 1 秒)
  • 1 秒后自动调用 TurnWhite(),颜色恢复白色。
  • nameof() 是 C# 关键字,编译时检查方法名是否存在,比写死字符串 "TurnWhite" 更安全。

6. nameof() vs 硬编码字符串

Invoke("TurnWhite", 1f);           // ❌ 拼错编译不报错,运行时才崩
Invoke(nameof(TurnWhite), 1f);     // ✅ 拼错直接编译报错

7. Gizmos.DrawWireSphere() —— 攻击范围可视化

Gizmos.DrawWireSphere(attackPoint.position, attackRadius);
  • 在 Scene 视图中画一个线框球体。
  • 方便调整 attackRadiusattackPoint 的位置,确保攻击范围覆盖敌人。

8. Color 常用预设

代码 颜色
Color.white 白色(默认)
Color.red 红色(受伤)
Color.green 绿色(回血)
Color.yellow 黄色(警告)
Color.clear 完全透明

9. foreach 循环

foreach (Collider2D enemy in enemyColliders)
{
    enemy.GetComponent<Enemy>().TakeDamage();
}
  • 遍历数组中每个被击中的敌人,分别调用 TakeDamage()
  • 如果圆内没有敌人(数组为空),循环直接跳过,不会报错。

10. 当前架构图(完整版)

Player.cs
├── 字段
│   ├── [Movement] moveSpeed, jumpForce, facingRight, xInput, canMove, canJump
│   ├── [Collision] groundCheckDistance, whatIsGround, isGrounded
│   └── [Attack] attackRadius, attackPoint, whatIsEnemy  ← 新增
│
├── Update()
│   ├── HandleCollision()    → Raycast 地检
│   ├── HandleInput()        → J攻击/K跳跃
│   ├── HandleMovement()     → canMove控制速度
│   ├── HandleAnimations()   → 传参给Animator
│   └── HandleFlip()         → 自动转向
│
├── DamageEnemies()          → OverlapCircleAll → Enemy.TakeDamage()
└── OnDrawGizmos()           → 地检线 + 攻击范围球

--------------------

PlayAnimationEvents.cs (挂在模型子物体)
├── DamageEnemies()           → 动画事件调用 → player.DamageEnemies()
├── DisableMovementAndJump()  → 动画事件调用 → 锁定
└── EnableMovementAndJump()   → 动画事件调用 → 解锁

--------------------

Enemy.cs (挂在敌人物体)
├── sr = GetComponent<SpriteRenderer>()
├── TakeDamage()              → 变红 → Invoke(nameof(TurnWhite), 1s)
└── TurnWhite()               → 恢复白色

2026-05-27 · 第十五次更新(计时器专题)

文件: Assets/Enemy.cs

🎯 本次围绕同一功能(受伤红色闪烁后恢复白色),演示了三种计时器实现方式。


方式一:Invoke() 延迟调用

public class Enemy : MonoBehaviour
{
    [SerializeField] private float redColorDuration = 1;

    public void TakeDamage()
    {
        sr.color = Color.red;
        Invoke(nameof(TurnWhite), redColorDuration);
    }

    private void TurnWhite()
    {
        sr.color = Color.white;
    }
}

特点:

优点 缺点
代码极简,一行搞定 再次受伤不会重置计时
不依赖 Update 无法暂停/取消(除非用 CancelInvoke
Unity 原生支持 不适合需要精确中途控制的场景

方式二:Time.deltaTime 倒计时

public float timer;

private void Update()
{
    timer -= Time.deltaTime;
    if(timer < 0 && sr.color != Color.white)
    {
        sr.color = Color.white;
    }
}

public void TakeDamage()
{
    sr.color = Color.red;
    timer = redColorDuration;    // 每次受伤重置倒计时
}

特点:

优点 缺点
再次受伤自动重置计时 需要额外一个 timer 字段
可以暂停(不执行 timer -= Update 中多了一行减法
可随时查看剩余时间 不适合大量计时器Update 会臃肿)

方式三:Time.time 时间戳对比(最终版)

public float currentTimeInGame;
public float lastTimeWasDamaged;

private void Update()
{
    currentTimeInGame = Time.time;
    if(currentTimeInGame > lastTimeWasDamaged + redColorDuration)
    {
        if (sr.color != Color.white)
        {
            TurnWhite();
        }
    }
}

public void TakeDamage()
{
    sr.color = Color.red;
    lastTimeWasDamaged = Time.time;    // 记录受伤时刻
}

特点:

优点 缺点
再次受伤自动延长红色时间 需要两个字段
不依赖逐帧递减(无浮点累积误差) 代码稍多
逻辑清晰:当前时间 vs 受伤时刻

三种计时器对比总结

Invoke() deltaTime 递减 Time.time 对比
代码量 最少 中等 较多
重置计时 需手动 CancelInvoke 天然支持 天然支持
浮点精度 ⚠️ 累积误差 无累积
Update 占用 不占 ⚠️ 每帧减法 ⚠️ 每帧比较
适用场景 一次性延迟 简单倒计时 精确时间判断、冷却

Time.deltaTime vs Time.time

属性 含义 用途
Time.deltaTime 上一帧到当前帧的耗时(秒) 帧率无关的增量计算
Time.time 游戏开始到现在的总时间(秒) 时间戳、计时基准

💡 deltaTime 容易累积浮点误差,长时间运行后可能不准。Time.time 直接用绝对时间对比,更精确。

使用建议

一次性动作(音效、粒子消失)      → Invoke()
简单冷却、Buff 持续时间           → deltaTime 递减
攻击间隔、精确冷却                → Time.time 对比
大量计时器                        → 用协程(Coroutine) 或 Timer 管理器

🏁 第一阶段总结2026-05-23 ~ 2026-05-27

15 次更新3 个 C# 脚本,从零搭建了一个 2D 横版动作角色的核心系统。


📁 文件清单

文件 职责
Assets/Player.cs 角色主逻辑:移动、跳跃、攻击、翻转、动画控制
Assets/PlayerAnimationEvents.cs 动画事件桥接:攻击判定、移动锁定
Assets/Enemy.cs 敌人逻辑:受伤反馈、计时器

🧱 技能树总览

一、Unity 基础

知识点 首次出现
MonoBehaviour 继承
Update() / Awake() 生命周期 ① ③
public 字段与 Inspector 序列化
[SerializeField] 私有字段可见
[Header("...")] Inspector 分组
[ContextMenu] 右键测试

二、输入系统

知识点 首次出现
Input.GetAxisRaw("Horizontal")
GetAxis vs GetAxisRaw 区别
Input.GetKey(KeyCode)
Input.GetKeyDown(KeyCode)
KeyCode 枚举
Debug.Log() 调试输出

三、物理与移动

知识点 首次出现
Rigidbody2D 组件
rb.linearVelocity 物理移动
Vector2 构造函数
物理移动 vs Transform 移动
rb.linearVelocity.y 保持垂直速度
Physics2D.Raycast() 射线检测
LayerMask 层级过滤
Vector2.down 等快捷向量
transform.Rotate() 旋转

四、代码架构

知识点 首次出现
private 字段封装
GetComponent<T>() 获取组件
GetComponentInChildren<T>()
GetComponentInParent<T>()
Update 拆分多方法
canMove/canJump 控制标志
职责分离(移动逻辑归位)
表达式体方法 =>

五、动画系统

知识点 首次出现
Animator 组件
anim.SetBool()
anim.SetFloat()
anim.SetTrigger()
Animator 参数四类型
Blend Tree 1D 混合树
动画状态切换 Transition
Animation Events 动画事件

六、战斗系统

知识点 首次出现
Physics2D.OverlapCircleAll()
Transform 作位置标记
攻击流程全链路
SpriteRenderer 组件
sr.color 颜色修改
foreach 遍历
攻击防重入(canMove 天然锁)

七、计时器

知识点 首次出现
Invoke() 延迟调用
Time.deltaTime 倒计时
Time.time 时间戳对比
nameof() 编译安全
三种计时器对比 & 选型

八、调试与可视化

知识点 首次出现
Debug.Log()
OnDrawGizmos()
Gizmos.DrawLine()
Gizmos.DrawWireSphere()
[ContextMenu]

🏗️ 最终架构

                    ┌──────────────────────┐
                    │      Player.cs        │
                    │  ──────────────────   │
                    │  [Movement] 移动/跳跃  │
                    │  [Collision] 地面检测  │
                    │  [Attack]    攻击判定  │
                    │                      │
                    │  Update()             │
                    │  ├─ HandleCollision   │
                    │  ├─ HandleInput       │
                    │  ├─ HandleMovement    │
                    │  ├─ HandleAnimations  │
                    │  └─ HandleFlip        │
                    └──────┬───────────────┘
                           │ Animation Events
                    ┌──────▼───────────────┐
                    │ PlayAnimationEvents  │
                    │  ├─ DamageEnemies()  │
                    │  ├─ DisableMove()    │
                    │  └─ EnableMove()     │
                    └──────┬───────────────┘
                           │ OverlapCircleAll
                    ┌──────▼───────────────┐
                    │      Enemy.cs        │
                    │  └─ TakeDamage()     │
                    │     └─ 变红 → 计时恢复│
                    └──────────────────────┘

📊 数据统计

  • 更新次数: 15 次
  • C# 脚本: 3 个
  • 掌握 API 40+ 个
  • 学习天数: 5 天5/23 ~ 5/27
  • 从零实现: 移动 → 跳跃 → 翻转 → 动画 → 攻击 → 敌人 → 计时

🚀 第一阶段完成,准备进入第二阶段!