Files
rpg_start/StudyNotes.md

25 KiB
Raw 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 视图画射线(仅编辑器)