模块 3:读懂并二开他人 Unity 工程

Day 2 ⏱ 预计阅读 3~3.5 小时

💡 本模块目标

拿到一个陌生的 Unity 工程后,能在 30 分钟内找到入口、理解架构、定位核心逻辑,并能安全地进行二次开发。这是你实际接手客户农场游戏的关键能力。

3.1 陌生工程完整排查流程

拿到客户给的 Unity 工程后,按以下步骤排查:

第 1 步:打开工程,观察编译状态

  1. 用 Unity Hub 打开项目文件夹(选择正确的 Unity 版本!版本不匹配会有升级提示)
  2. 等待编译完成,观察 Console 窗口是否有红色 Error
  3. 有 Error:通常是缺少依赖包或版本不兼容。记录错误信息,搜索解决
  4. 无 Error:进入下一步
⚠️ Unity 版本不匹配处理

如果项目用旧版 Unity(如 2021/2022)创建,你用 Unity 6 打开,Unity 会提示升级。升级是单向的,无法降级!建议先备份项目,再点确认升级。升级后可能需要重新导入资源,Shader 和某些插件可能需要更新。

第 2 步:找到入口场景

  1. File → Build Settings(Ctrl+Shift+B)
  2. 查看 Scenes In Build 列表 — 排在最前面(Index 0)的就是入口场景
  3. 如果没有注册场景,到 Assets/Scenes/ 文件夹寻找 Main.unityGame.unityStart.unity 等明显是入口的场景文件
  4. 双击场景文件打开它

第 3 步:找到全局管理器(Manager)

一个游戏通常有几个「全局管理器」,控制核心逻辑。它们一般有以下特征:

如何快速找到它们:

  1. 在 Project 窗口搜索 t:Script,然后按名称排序,找 *Manager、*Controller、*System 结尾的脚本
  2. 在 Hierarchy 窗口搜索空的 GameObject(没有 MeshRenderer、没有视觉表现的物体)
  3. 在 Hierarchy 搜索 "GameManager"、"AppManager"、"Main" 等关键词
  4. 查看哪个脚本用了 DontDestroyOnLoad(搜索这个关键词)

第 4 步:追数据流

理解游戏的数据如何流动是二开的关键:

配置数据 JSON / ScriptableObject 管理器 GameManager 游戏逻辑 FarmController 存档系统 PlayerPrefs / JSON 典型数据流:配置 → 管理器 → 游戏逻辑 ↔ 存档

追数据流的方法:

  1. 搜索存档相关代码PlayerPrefsSaveSystemJsonUtilityFile.WriteAllTextBinaryFormatter
  2. 搜索配置文件:在 Project 搜索 *.json*.asset(ScriptableObject)、*.csv
  3. 搜索 ScriptableObjectt:ScriptableObject 在 Project 窗口搜索
  4. 顺着 Inspector 字段追:打开 Manager 的 Inspector,看它引用了哪些 Prefab/脚本/数据

第 5 步:读 Inspector 配置

  1. 在 Hierarchy 选中关键物体,看 Inspector 中挂了哪些组件
  2. 注意 [SerializeField] 和 public 字段的拖拽赋值 — 这些是运行时数据的来源
  3. 双击 Prefab 进入 Prefab 编辑模式,看 Prefab 内部结构
  4. 记录下关键数值(如初始金币、生长速度等),可能后续需要修改

第 6 步:梳理场景流程

  1. 查看 Build Settings 中所有已注册的场景
  2. 搜索 SceneManager.LoadScene,记录所有场景切换关系
  3. 画出场景流程图:启动 → 主菜单 → 加载 → 游戏 → 结算

3.2 调试工具完整用法

Debug.Log 系列

// 基本日志(Console 窗口白色文字)
Debug.Log("金币: " + gold);
Debug.Log($"玩家位置: {player.transform.position}");

// 警告(黄色)
Debug.LogWarning("金币不足!");

// 错误(红色)
Debug.LogError("存档文件损坏!");

// 带上下文的日志(点击日志会选中对应物体)
Debug.Log("点击了地块", gameObject);

// 画调试线(Scene View 中可见)
Debug.DrawLine(transform.position, target.position, Color.red, 2f);
Debug.DrawRay(transform.position, transform.forward * 5f, Color.green);

// 断言(条件为 false 时打印错误)
Debug.Assert(gold >= 0, "金币不能为负!");

IDE 断点调试

Unity 支持使用 Visual Studio 或 Rider 进行断点调试:

  1. 确保安装了 Unity 对应的 IDE 插件(VS: "Visual Studio Tools for Unity";Rider 自带)
  2. 在 IDE 中打开脚本,在行号左侧点击添加断点(红色圆点)
  3. 回到 Unity,点 Play 运行游戏
  4. 在 IDE 中 Attach to Unity(VS: Debug → Attach Unity Debugger;Rider: 自动连接)
  5. 当执行到断点时,游戏暂停,可以查看变量值、调用栈、单步执行

Profiler — 性能分析

  1. Window → Analysis → Profiler
  2. Play Mode 下运行游戏,Profiler 实时显示各项性能数据
  3. CPU Usage:每帧各阶段耗时(Update、Rendering、Physics)
  4. Memory:内存使用量、GC 回收频率
  5. Rendering:Draw Call 数量、三角面数、SetPass 调用
  6. Physics:物理计算耗时
💡 Profiler 常用操作

Frame Debugger — 逐 Draw Call 查看渲染

  1. Window → Analysis → Frame Debugger
  2. 点击 Enable,冻结当前帧
  3. 左侧显示本帧所有 Draw Call,点击任意一个可看到该步骤的渲染结果
  4. 用于排查「为什么某物体没显示」「为什么 Draw Call 太多」

运行时修改 Inspector

Play Mode 下,在 Inspector 中修改数值可以即时看到效果。这是最快的调试方式:

3.3 常见架构模式

Unity 项目中常见的架构模式。理解这些模式后,你能更快地读懂工程代码。

模式 1:单例 Manager

// 最常见的全局管理器模式
public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    public int gold = 100;
    public int day = 1;

    void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject); // 防止重复
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public void AddGold(int amount)
    {
        gold += amount;
        // 触发 UI 更新
    }
}

// 在任何地方访问
GameManager.Instance.AddGold(50);

模式 2:事件总线(Event Bus)

// 解耦的通信方式:发布者不知道订阅者是谁
public static class EventBus
{
    // 定义各种事件
    public static event System.Action<string, int> OnCropHarvested;
    public static event System.Action<int> OnGoldChanged;
    public static event System.Action OnDayEnded;

    // 触发方法
    public static void CropHarvested(string name, int amount)
    {
        OnCropHarvested?.Invoke(name, amount);
    }
    public static void GoldChanged(int newGold)
    {
        OnGoldChanged?.Invoke(newGold);
    }
    public static void DayEnded()
    {
        OnDayEnded?.Invoke();
    }
}

// 发布者(CropPlot.cs)
public class CropPlot : MonoBehaviour
{
    public void Harvest()
    {
        EventBus.CropHarvested("Wheat", 3);
    }
}

// 订阅者(UIManager.cs)
public class UIManager : MonoBehaviour
{
    void OnEnable()
    {
        EventBus.OnCropHarvested += ShowHarvestUI;
        EventBus.OnGoldChanged += UpdateGoldDisplay;
    }

    void OnDisable()
    {
        EventBus.OnCropHarvested -= ShowHarvestUI; // 必须取消!
        EventBus.OnGoldChanged -= UpdateGoldDisplay;
    }

    void ShowHarvestUI(string name, int amount) { /* ... */ }
    void UpdateGoldDisplay(int gold) { /* ... */ }
}

模式 3:ScriptableObject 配置

// ScriptableObject = 数据容器,不依附于场景/Prefab
[CreateAssetMenu(fileName = "NewCropData", menuName = "Farm/CropData")]
public class CropDataSO : ScriptableObject
{
    public string cropName;
    public int buyPrice;
    public int sellPrice;
    public int growDays;
    public Sprite icon;
    public GameObject prefab;
}

// 创建方式:Project 窗口右键 → Create → Farm → CropData

// 使用方式:在脚本中引用
public class CropShop : MonoBehaviour
{
    public CropDataSO[] availableCrops; // Inspector 拖拽多个配置

    public void BuyCrop(int index)
    {
        CropDataSO crop = availableCrops[index];
        if (GameManager.Instance.gold >= crop.buyPrice)
        {
            GameManager.Instance.AddGold(-crop.buyPrice);
            Instantiate(crop.prefab);
        }
    }
}

模式 4:状态机(State Machine)

// 农作物生长状态机
public enum CropState { Seed, Growing, Ready, Withered }

public class CropStateMachine : MonoBehaviour
{
    public CropState CurrentState { get; private set; }
    public CropDataSO cropData;
    private float _growTimer;

    void Start()
    {
        SetState(CropState.Seed);
    }

    void Update()
    {
        switch (CurrentState)
        {
            case CropState.Seed:
                // 等待浇水
                break;
            case CropState.Growing:
                _growTimer += Time.deltaTime;
                if (_growTimer >= cropData.growDays)
                    SetState(CropState.Ready);
                break;
            case CropState.Ready:
                // 等待收获
                break;
            case CropState.Withered:
                // 可以铲除
                break;
        }
    }

    void SetState(CropState newState)
    {
        CurrentState = newState;
        _growTimer = 0f;
        UpdateVisuals();
    }

    void UpdateVisuals()
    {
        // 根据状态切换模型/动画
    }

    public void Water()
    {
        if (CurrentState == CropState.Seed)
            SetState(CropState.Growing);
    }

    public void Harvest()
    {
        if (CurrentState == CropState.Ready)
        {
            EventBus.CropHarvested(cropData.cropName, 1);
            Destroy(gameObject);
        }
    }
}

模式 5:对象池(Object Pool)

// 避免频繁 Instantiate/Destroy 的 GC 开销
public class SimplePool<T> where T : Component
{
    private Queue<T> _pool = new Queue<T>();
    private T _prefab;
    private Transform _parent;

    public SimplePool(T prefab, int size, Transform parent = null)
    {
        _prefab = prefab;
        _parent = parent;
        for (int i = 0; i < size; i++)
        {
            T obj = Object.Instantiate(prefab, parent);
            obj.gameObject.SetActive(false);
            _pool.Enqueue(obj);
        }
    }

    public T Get(Vector3 pos = default, Quaternion rot = default)
    {
        T obj = _pool.Count > 0 ? _pool.Dequeue()
            : Object.Instantiate(_prefab, _parent);
        obj.transform.SetPositionAndRotation(pos, rot);
        obj.gameObject.SetActive(true);
        return obj;
    }

    public void Return(T obj)
    {
        obj.gameObject.SetActive(false);
        _pool.Enqueue(obj);
    }
}

3.4 农场游戏典型架构走读

一个典型的 3D 农场游戏通常包含以下模块:

3D 农场游戏典型架构 GameManager (单例) FarmManager InventoryManager TimeManager AudioManager CropPlot (地块) CropController DayNightCycle UIManager SaveSystem (JSON) CropDataSO (配置) EventBus (事件) Addressables 数据层:存档系统 | 配置数据 | 事件通信 | 资源加载

排查清单(拿到工程后逐项检查)

检查项如何查找记录位置
入口场景Build Settings → Scenes In Build[0]______
GameManager搜索 "Instance" 单例______
存档系统搜索 PlayerPrefs / JsonUtility / File______
作物配置搜索 ScriptableObject / JSON 配置文件______
UI 管理搜索 Canvas / UIManager______
输入处理搜索 Input.Get / EventSystem______
物理/碰撞搜索 Collider / Rigidbody / Raycast______
动画系统搜索 Animator / Animation______
音频搜索 AudioSource / AudioManager______
场景切换搜索 LoadScene______