1808 lines
55 KiB
Markdown
1808 lines
55 KiB
Markdown
# Unity RPG 学习笔记
|
||
|
||
> 📅 创建于 2026-05-23
|
||
> 🎯 按时间顺序记录学习过程
|
||
|
||
---
|
||
|
||
## 2026-05-23 · 第一次更新
|
||
|
||
**文件:** `Assets/Player.cs`
|
||
|
||
```csharp
|
||
using UnityEngine;
|
||
|
||
public class Player : MonoBehaviour
|
||
{
|
||
public float xInput;
|
||
|
||
private void Update()
|
||
// 旧的移动方式
|
||
{
|
||
xInput = Input.GetAxisRaw("Horizontal");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 知识点
|
||
|
||
#### 1. `MonoBehaviour` 继承
|
||
- Unity 中挂载到 GameObject 的脚本必须继承 `MonoBehaviour`。
|
||
- 继承后可使用生命周期方法(`Update`、`Start`、`Awake` 等)。
|
||
|
||
#### 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`
|
||
|
||
```csharp
|
||
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`
|
||
|
||
```csharp
|
||
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]` 特性
|
||
|
||
- 问题:字段设为 `private` 后,Inspector 面板中就看不到了。
|
||
- 解决:加 `[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`
|
||
|
||
```csharp
|
||
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.Escape`、`KeyCode.W/A/S/D`、`KeyCode.Mouse0`(鼠标左键)。
|
||
|
||
#### 3. `Debug.Log()` 调试输出
|
||
|
||
- 向 Unity 控制台(Console)输出消息,调试必备。
|
||
- `Debug.Log("xxx")` — 普通信息
|
||
- `Debug.LogWarning("xxx")` — 警告(黄色)
|
||
- `Debug.LogError("xxx")` — 错误(红色,会暂停编辑器播放)
|
||
|
||
#### 4. 代码验证思路
|
||
|
||
当前代码跑起来后的效果:
|
||
- 按住 K → 控制台疯狂刷 "holding K"(每帧一条)
|
||
- 敲一下 K → 只出现一条 "pressed K"
|
||
- 通过这个对比就能直观理解 `GetKey` 和 `GetKeyDown` 的区别 ✅
|
||
|
||
---
|
||
|
||
## 2026-05-24 · 第五次更新
|
||
|
||
**文件:** `Assets/Player.cs`
|
||
|
||
```csharp
|
||
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`
|
||
|
||
```csharp
|
||
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`
|
||
|
||
```csharp
|
||
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()` 中调用 | 运行时每帧自动执行 |
|
||
| `Button`(UI)| 玩家通过游戏界面触发 |
|
||
|
||
> 💡 `[ContextMenu]` 即使方法是 `private` 也能调用,非常适合调试。
|
||
|
||
---
|
||
|
||
## 2026-05-25 · 第八次更新
|
||
|
||
**文件:** `Assets/Player.cs`
|
||
|
||
```csharp
|
||
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`
|
||
|
||
```csharp
|
||
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 当前代码
|
||
|
||
```csharp
|
||
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 做平滑混合:
|
||
|
||
```csharp
|
||
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 Tree,`xVelocity` 参数值范围变大,动画混合范围需要与之匹配。
|
||
|
||
#### 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 当前代码
|
||
|
||
```csharp
|
||
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(新文件)
|
||
|
||
```csharp
|
||
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`。
|
||
- 这是一种常见的"动作锁定"设计,防止在特定动画期间执行不该执行的动作。
|
||
|
||
```csharp
|
||
攻击开始 → 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)
|
||
|
||
```csharp
|
||
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.cs`(`PlayAnimationEvents.cs` 未变)
|
||
|
||
### 改动对比
|
||
|
||
| 位置 | 旧代码 | 新代码 |
|
||
|---|---|---|
|
||
| `TryToAttack()` | `anim.SetTrigger("attack");` + `velocity.x=0` | 只剩 `anim.SetTrigger("attack");` |
|
||
| `HandleMovement()` | `if(canMove) { 设置速度 }` | `if(canMove) { 设置速度 } else { velocity.x=0 }` |
|
||
|
||
```csharp
|
||
// 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` 判断:
|
||
|
||
```csharp
|
||
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`(仅一行改动)
|
||
|
||
### 改动
|
||
|
||
```csharp
|
||
// 修复前
|
||
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()` 检查 `canMove` → `false` → 不执行 `SetTrigger`
|
||
- 动画结束后 `canMove` 恢复 `true`,才能再次攻击 ✅
|
||
|
||
**一句话:** `canMove` 既是移动开关,也是"是否正在攻击"的天然标志,一行改动解决双触 Bug。
|
||
|
||
---
|
||
|
||
## 2026-05-27 · 第十四次更新
|
||
|
||
**文件:** `Assets/Player.cs`、`Assets/PlayerAnimationEvents.cs`、`Assets/Enemy.cs`(新文件)
|
||
|
||
### Player.cs 新增
|
||
|
||
```csharp
|
||
// 新增字段
|
||
[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 新增
|
||
|
||
```csharp
|
||
public void DamageEnemies() => player.DamageEnemies();
|
||
```
|
||
|
||
### Enemy.cs(新文件)
|
||
|
||
```csharp
|
||
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` 类型是 `Color`(RGBA)。
|
||
|
||
#### 5. `Invoke()` 延迟调用
|
||
|
||
```csharp
|
||
Invoke(nameof(TurnWhite), redColorDuration);
|
||
```
|
||
|
||
| 参数 | 含义 |
|
||
|---|---|
|
||
| `nameof(TurnWhite)` | 要调用的方法名(字符串安全版本) |
|
||
| `redColorDuration` | 延迟秒数(这里是 1 秒) |
|
||
|
||
- 1 秒后自动调用 `TurnWhite()`,颜色恢复白色。
|
||
- `nameof()` 是 C# 关键字,编译时检查方法名是否存在,比写死字符串 `"TurnWhite"` 更安全。
|
||
|
||
#### 6. `nameof()` vs 硬编码字符串
|
||
|
||
```csharp
|
||
Invoke("TurnWhite", 1f); // ❌ 拼错编译不报错,运行时才崩
|
||
Invoke(nameof(TurnWhite), 1f); // ✅ 拼错直接编译报错
|
||
```
|
||
|
||
#### 7. `Gizmos.DrawWireSphere()` —— 攻击范围可视化
|
||
|
||
```csharp
|
||
Gizmos.DrawWireSphere(attackPoint.position, attackRadius);
|
||
```
|
||
|
||
- 在 Scene 视图中画一个线框球体。
|
||
- 方便调整 `attackRadius` 和 `attackPoint` 的位置,确保攻击范围覆盖敌人。
|
||
|
||
#### 8. Color 常用预设
|
||
|
||
| 代码 | 颜色 |
|
||
|---|---|
|
||
| `Color.white` | 白色(默认) |
|
||
| `Color.red` | 红色(受伤) |
|
||
| `Color.green` | 绿色(回血) |
|
||
| `Color.yellow` | 黄色(警告) |
|
||
| `Color.clear` | 完全透明 |
|
||
|
||
#### 9. `foreach` 循环
|
||
|
||
```csharp
|
||
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()` 延迟调用
|
||
|
||
```csharp
|
||
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` 倒计时
|
||
|
||
```csharp
|
||
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` 时间戳对比(最终版)
|
||
|
||
```csharp
|
||
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)
|
||
- **从零实现:** 移动 → 跳跃 → 翻转 → 动画 → 攻击 → 敌人 → 计时
|
||
|
||
🚀 第一阶段完成,准备进入第二阶段!
|