25 KiB
25 KiB
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。 - 继承后可使用生命周期方法(
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
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] 特性
- 问题:字段设为
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
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
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() 中调用 |
运行时每帧自动执行 |
Button(UI) |
玩家通过游戏界面触发 |
💡
[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 视图画射线(仅编辑器)