Unity学习90天 - 第 5 天 - 阶段小项目

张开发
2026/4/16 1:48:07 15 分钟阅读

分享文章

Unity学习90天 - 第 5 天 - 阶段小项目
Hey欢迎回来经过四天的基础学习相信大家都对Unity的功能已经略有了解那么 今天我们来做一个简单的射击小游戏啦一、构建思路布置场景一功能构建思路序号功能涉及知识点一第一人称角色移动Rigidbody、生命周期、Mouse X/Y二点击鼠标发射子弹Instantiate、AddForce、欧拉角三靶子自动生成补全Random.Range四碰撞检测与销毁OnCollisionEnter、CompareTag五积分系统与结束判定TextMeshProUGUI、TryParse二场景布置使用我们的平面和方形构建一个简单的靶场并且加入我们角色和UI二、角色移动控制器 - PlayerP.cs那么我们就先开始思考并编写我们的玩家移动控制器脚本一生命周期回顾在写代码之前先回顾一下 Unity 的生命周期执行顺序。函数执行时机我们用来做什么Awake()对象创建时始终最先执行获取组件Start()第一次 Update 前执行一次锁定鼠标Update()每帧执行约60次/秒读取输入、计算视角FixedUpdate()固定0.02秒执行一次执行物理操作生活类比想象做作业。Awake是准备好文具Start是翻开作业本Update是一道题一道题做FixedUpdate是做完一道题检查一次固定间隔。二为什么物理要写在 FixedUpdate 里写入位置帧率60帧率30效果Update()1秒执行60次1秒执行30次速度不稳定 ❌FixedUpdate()1秒执行50次1秒执行50次速度恒定 ✅重要原则Update只负责读取输入真正的物理操作交给FixedUpdate三完整代码逐行解析using UnityEngine; public class PlayerP : MonoBehaviour { // ────────────────────────────── ① 字段声明 ────────────────────────────── [Header(移动速度)] public float moveSpeed 10f; // 每秒移动10米 [Header(跳跃高度)] public float jumpForce 10f; // 跳跃冲量 [Header(视角参数)] public float rotateSpeed 100f; // 旋转速度度/秒 public float minPitch -45f; // 最小俯角低头限制 public float maxPitch 45f; // 最大仰角抬头限制 // 私有变量跨函数通信用 private bool jumpRequest; // 跳跃请求标记 private Vector3 inputDir; // 输入方向Update写FixedUpdate读 private float pitch; // 上下看角度绕X轴 private float yaw; // 左右看角度绕Y轴 private Rigidbody rb; // 刚体引用说明[Header]特性让 Inspector 面板分组显示private变量不显示在面板上但可以在代码中跨函数使用。// ────────────────────────────── ② Awake - 获取组件 ────────────────────────────── void Awake() { rb GetComponentRigidbody(); if (rb null) { Debug.Log(没有刚体组件); // 提前报错防止运行时才发现 } } // ────────────────────────────── ③ Start - 初始化 ────────────────────────────── void Start() { // 锁定鼠标到窗口中心并隐藏指针 Cursor.lockState CursorLockMode.Locked; Cursor.visible false; }说明Awake中提前检查Rigidbody是否存在比运行时报错更容易定位问题。// ────────────────────────────── ④ Update - 读取输入 ────────────────────────────── void Update() { // ① 读取 WASD 输入范围 -1 ~ 1 float h Input.GetAxis(Horizontal); // A/D → -1/0/1 float v Input.GetAxis(Vertical); // W/S → -1/0/1 // ② 将本地方向转为世界方向跟随角色朝向 Vector3 localDir new Vector3(h, 0, v).normalized; inputDir transform.TransformDirection(localDir);说明Input.GetAxis返回 -1~1 的连续值比GetKey的 0/1 更平滑适合角色移动。normalized保证斜方向速度和直走一致。// ③ 鼠标视角计算 - 左右转绕Y轴 float mouseX Input.GetAxis(Mouse X); yaw mouseX * rotateSpeed * Time.deltaTime; // 乘时间保证不同帧率下速度一致 // ④ 鼠标视角计算 - 上下看绕X轴 float mouseY Input.GetAxis(Mouse Y); pitch - mouseY * rotateSpeed * Time.deltaTime; pitch Mathf.Clamp(pitch, minPitch, maxPitch); // 限制角度防止相机翻跟头生活类比yaw像你左右转头pitch像你点头/仰头。两者分开控制才能实现边走边看的效果。// ⑤ 应用旋转到物体本身Y轴 左右转 transform.rotation Quaternion.Euler(0, yaw, 0); // ⑥ 应用旋转到相机子物体X轴 上下看 Transform cam Camera.main?.transform ?? transform.GetChild(0); if (cam ! null) { cam.localRotation Quaternion.Euler(pitch, 0, 0); }说明?.是空条件运算符Camera.main为 null 时不报错。??是空合并运算符左边为 null 时用右边。两者配合实现相机兼容。运算符写法含义?.obj?.method()obj 不为 null 才调用方法??a ?? ba 不为 null 用 a否则用 b// ⑦ 跳跃请求标记模式不在 Update 里直接跳跃 if (Input.GetKeyDown(KeyCode.Space)) { jumpRequest true; // 记录请求FixedUpdate 统一处理 } // ⑧ Esc 切换鼠标锁定状态 if (Input.GetKeyDown(KeyCode.Escape)) { if (Cursor.lockState CursorLockMode.Locked) // 当前锁定中 { Cursor.lockState CursorLockMode.None; // 释放锁定 Cursor.visible true; // 显示鼠标 } else // 当前自由状态 { Cursor.lockState CursorLockMode.Locked; // 重新锁定 Cursor.visible false; // 隐藏鼠标 } } }生活类比jumpRequest true就像你在清单上画个圈说待会要跳FixedUpdate是执行清单的人。好处是不会漏掉跳跃请求也不会跳多次。// ────────────────────────────── ⑤ FixedUpdate - 物理执行 ────────────────────────────── void FixedUpdate() { // ① 物理驱动移动比直接改 position 更稳定 Vector3 nextPos rb.position inputDir * moveSpeed * Time.fixedDeltaTime; rb.MovePosition(nextPos); // ② 执行跳跃 if (jumpRequest) { // ForceMode.Impulse 瞬间冲量适合跳跃这种瞬时动作 rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); jumpRequest false; // 重置标记 } } }ForceMode效果适合场景Force持续施力受质量影响推车、火箭Impulse瞬间冲量受质量影响跳跃、爆炸 ✅Acceleration持续加速不受质量影响统一加速VelocityChange直接改变速度不受质量影响直接设定速度三、子弹发射系统 - Bullet.cs玩家移动做完之后来思考我们的子弹发射应该如何制作以及需要些什么一Instantiate 参数详解参数类型说明示例第1个GameObject要克隆的预制体bulletPrefab第2个Vector3生成位置transform.position第3个Quaternion生成旋转Quaternion.identity常用 Quaternion含义Quaternion.identity无旋转默认姿态Quaternion.Euler(x, y, z)按欧拉角旋转二欧拉角三轴含义轴旋转名称效果X俯仰角点头/仰头Y偏航角左右转头Z翻滚角侧翻三完整代码解析using UnityEngine; public class Bullet : MonoBehaviour { [Header(子弹预制体)] public GameObject bulletPrefab; // 要克隆的预制体 [Header(发射口)] public Transform launchPoint; // 发射口Transform [Header(作用力)] public float force 20f; // 发射力度 void Update() { // 按下鼠标左键发射 if (Input.GetMouseButtonDown(0)) { Fire(); } } void Fire() { // ① 获取发射位置 Vector3 pos launchPoint.position; // ② 计算旋转 // 获取发射口的欧拉角X轴固定为90度预制体模型要求Y/Z跟随发射口 Vector3 euler launchPoint.eulerAngles; euler.x 90f; // ③ 生成克隆体必须用变量接收 GameObject clone Instantiate(bulletPrefab, pos, Quaternion.Euler(euler)); // ④ 给克隆体施力一定是 clone不是 bulletPrefab clone.GetComponentRigidbody().AddForce( launchPoint.forward * force, // 方向 × 力度 ForceMode.Impulse // 瞬间爆发力 ); } }常见错误忘记用变量接收Instantiate的返回值导致给原始预制体施力而不是克隆体四Instantiate 返回值对比写法结果Instantiate(prefab);生成但不控制GameObject clone Instantiate(prefab);生成并可控制克隆体 ✅四、碰撞检测与积分系统 - BulletCollisoin.cs子弹发射也写完了那么我们当然需要来检测碰撞执行打击到物体后的效果啦从而实现可以获取积分结束游戏一OnCollisionEnter vs OnTriggerEnter特性OnCollisionEnterOnTriggerEnter触发条件两物体都有 Collider至少一个是 Trigger物理碰撞有会弹开 ✅无可穿透穿透效果正常阻挡可穿墙检测适用场景子弹打墙、拳击拾取金币、进入区域二脚本查找 API 对比旧版已弃用新版说明FindObjectOfTypeT()Object.FindAnyObjectByTypeT()✅找一个FindObjectsOfTypeT()Object.FindObjectsByTypeT()找全部三数字解析方法对比方法文本100文本分数10文本int.Parse()100 ✅❌ 抛异常❌ 抛异常int.TryParse()100 ✅falsefalse四完整代码解析using UnityEngine; public class BulletCollisoin : MonoBehaviour { [Header(子弹存活时间秒)] public float lifetime 5f; private Target target; // 预制体无法拖拽运行时查找 void Start() { // 运行时查找场景中的 Target 脚本 target Object.FindAnyObjectByTypeTarget(); // 兜底保险即使没碰撞5秒后也销毁 Destroy(gameObject, lifetime); } private void OnCollisionEnter(Collision collision) { // 只处理带有 NPC 标签的物体 if (collision.gameObject.CompareTag(NPC)) { // ① 销毁被击中的目标 Destroy(collision.gameObject); // ② 更新分数 if (target ! null target.textM ! null) { target.num--; // 读取当前分数空文本当作0处理 int score 0; if (!string.IsNullOrEmpty(target.textM.text)) { int.TryParse(target.textM.text, out score); } score 10; target.textM.text score.ToString(); // ③ 分数达到100时结束游戏 if (score 100) { Application.Quit(); #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying false; #endif } } } // ④ 无论打什么都销毁子弹 Destroy(gameObject); } }五、靶子自动生成系统 - Target.cs重要的功能全部实现后就是我们的随机生成靶子啦这样才有趣味性一Random.Range 版本区别版本调用方式返回值范围整型版Random.Range(1, 10)1 ~ 9不含最大值浮点版Random.Range(1f, 10f)1f ~ 10f含最大值二完整代码解析using TMPro; using UnityEngine; public class Target : MonoBehaviour { [Header(预制体)] public GameObject target; // 靶子预制体 [Header(生成范围)] public float maxR 10f; // 最大随机坐标 public float minR -10f; // 最小随机坐标 [Header(当前靶子数)] public int num; // 初始值设为0 [Header(分数显示)] public TextMeshProUGUI textM; // UI分数文本 void Update() { // 靶子少于20个时自动生成补全 if (num 20) { SpawnEnemy(); num; } } public void SpawnEnemy() { // 随机生成位置X和Z随机Y固定在地面 Vector3 pos new Vector3( Random.Range(minR, maxR), // X轴随机 0, // Y轴固定 Random.Range(minR, maxR) // Z轴随机 ); // 生成靶子Y轴旋转180度面对玩家 Instantiate(target, pos, Quaternion.Euler(0f, 180f, 0f)); } }六、组件配置一脚本挂载总览脚本挂载位置职责PlayerP.cs玩家物体移动、跳跃、视角Bullet.cs发射器物体发射子弹BulletCollisoin.cs子弹预制体碰撞检测、计分Target.cs场景空物体生成靶子二Recommended 组件配置玩家物体 Rigidbody属性推荐值说明Mass1质量Drag0空气阻力Use Gravitytrue受重力影响Collision DetectionDiscrete移动缓慢够用子弹预制体 Rigidbody属性推荐值说明Mass1质量Drag0空气阻力Use Gravityfalse子弹不受重力Collision DetectionContinuous Dynamic防止穿墙 ✅Is Kinematicfalse启用物理七、运行成功展示可以看到我们成功的发射子弹击杀了目标并且加分20但是由于完整展示无法上传所以大家就自己动手吧今天的教学就到这里接下来我将连续更新90天的Untiy教程从基础到一个网络部分有兴趣的朋友们可以收藏关注谢谢如果有疑问评论区见。

更多文章