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

(三)完整代码逐行解析

cs 复制代码
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 变量不显示在面板上但可以在代码中跨函数使用。

cs 复制代码
    // ────────────────────────────── ② Awake - 获取组件 ──────────────────────────────
    void Awake()
    {
        rb = GetComponent<Rigidbody>();
        if (rb == null)
        {
            Debug.Log("没有刚体组件");  // 提前报错,防止运行时才发现
        }
    }

    // ────────────────────────────── ③ Start - 初始化 ──────────────────────────────
    void Start()
    {
        // 锁定鼠标到窗口中心并隐藏指针
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

说明Awake 中提前检查 Rigidbody 是否存在,比运行时报错更容易定位问题。

cs 复制代码
    // ────────────────────────────── ④ 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 保证斜方向速度和直走一致。

cs 复制代码
        // ③ 鼠标视角计算 - 左右转(绕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 像你点头/仰头。两者分开控制才能实现"边走边看"的效果。

cs 复制代码
        // ⑤ 应用旋转到物体本身(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 ?? b a 不为 null 用 a,否则用 b
cs 复制代码
        // ⑦ 跳跃请求(标记模式,不在 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 是执行清单的人。好处是不会漏掉跳跃请求,也不会跳多次。

cs 复制代码
    // ────────────────────────────── ⑤ 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.GetComponent<Rigidbody>().AddForce(
            launchPoint.forward * force,  // 方向 × 力度
            ForceMode.Impulse              // 瞬间爆发力
        );
    }
}

常见错误 :忘记用变量接收 Instantiate 的返回值,导致给原始预制体施力而不是克隆体!

(四)Instantiate 返回值对比

写法 结果
Instantiate(prefab); 生成但不控制
GameObject clone = Instantiate(prefab); 生成并可控制克隆体 ✅

四、碰撞检测与积分系统 - BulletCollisoin.cs

子弹发射也写完了,那么我们当然需要来检测碰撞执行打击到物体后的效果啦,从而实现可以获取积分结束游戏

(一)OnCollisionEnter vs OnTriggerEnter

特性 OnCollisionEnter OnTriggerEnter
触发条件 两物体都有 Collider 至少一个是 Trigger
物理碰撞 有(会弹开) ✅ 无(可穿透)
穿透效果 正常阻挡 可穿墙检测
适用场景 子弹打墙、拳击 拾取金币、进入区域

(二)脚本查找 API 对比

旧版(已弃用) 新版 说明
FindObjectOfType<T>() Object.FindAnyObjectByType<T>() 找一个
FindObjectsOfType<T>() Object.FindObjectsByType<T>() 找全部

(三)数字解析方法对比

方法 文本"100" 文本"分数10" 文本""
int.Parse() 100 ✅ ❌ 抛异常 ❌ 抛异常
int.TryParse() 100 ✅ false false

(四)完整代码解析

复制代码
using UnityEngine;

public class BulletCollisoin : MonoBehaviour
{
    [Header("子弹存活时间(秒)")]
    public float lifetime = 5f;

    private Target target;  // 预制体无法拖拽,运行时查找

    void Start()
    {
        // 运行时查找场景中的 Target 脚本
        target = Object.FindAnyObjectByType<Target>();

        // 兜底保险:即使没碰撞,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:

属性 推荐值 说明
Mass 1 质量
Drag 0 空气阻力
Use Gravity true 受重力影响
Collision Detection Discrete 移动缓慢够用

子弹预制体 Rigidbody:

属性 推荐值 说明
Mass 1 质量
Drag 0 空气阻力
Use Gravity false 子弹不受重力
Collision Detection Continuous Dynamic 防止穿墙 ✅
Is Kinematic false 启用物理

七、运行成功展示:

可以看到我们成功的发射子弹击杀了目标并且加分20,但是由于完整展示无法上传,所以大家就自己动手吧!

**今天的教学就到这里!**接下来我将连续更新90天的Untiy教程从基础到一个网络部分,有兴趣的朋友们可以收藏关注,谢谢!如果有疑问,评论区见。

相关推荐
郝学胜-神的一滴2 小时前
中级OpenGL教程 001:从Main函数到相机操控的完整实现
c++·程序人生·unity·图形渲染·unreal engine·opengl
韩楚风2 小时前
PostgreSQL入门与进阶学习,体系化的SQL知识,完成终极目标高可用与容灾,性能优化与架构设计,以及安全策略
sql·学习·postgresql
亚空间仓鼠2 小时前
Python学习日志(四):实例
开发语言·python·学习
sealaugh322 小时前
react native(学习笔记第二课) 英语打卡微应用(1)-开始构建
笔记·学习·react native
夜瞬2 小时前
NLP学习笔记03:文本分类——从 TF-IDF 到 BERT
笔记·学习·自然语言处理
Fanfanaas2 小时前
Linux 系统编程 进程篇 (二)
linux·运维·服务器·c语言·开发语言·学习
RReality2 小时前
【Unity Shader URP】顶点波浪动画(Vertex Wave)实战教程
ui·unity·游戏引擎·图形渲染
克里斯蒂亚诺·罗纳尔达2 小时前
智能体学习22——智能体间通信(A2A)
人工智能·学习·ai
炽烈小老头2 小时前
【每天学习一点算法 2026/04/15】两整数之和(附带位运算总结)
学习·算法