第一章:梦开始的地方——一个 Unity 新手的“直觉”UI 代码

在每个 Unity 开发者的职业生涯中,几乎都有一段相似的、不堪回首却又无比宝贵的经历:第一次尝试去实现一个“有交互功能”的 UI 面板。没有架构的指引,没有模式的束缚,全凭从教程和文档中学来的零散知识,以及最原始的编程直觉。

这,就是我们故事的起点。让我们一起回到那个梦开始的地方,看看一个 Unity 新手,会如何搭建他的第一个“角色信息面板”。

1. 场景设定:一个简单的“角色信息面板”

我们的任务很简单:创建一个 UI 面板,用于显示玩家的角色信息。它需要具备以下功能:

  • 显示信息:显示角色的头像、名字、等级、经验条、攻击力和防御力。

  • 交互功能:有一个“升级”按钮,点击后,如果经验值足够,角色等级+1,经验值清零,攻击力和防御力提升。

  • 外部影响:当角色在游戏中打怪获得了经验,这个面板需要能实时更新经验条。

2. “直觉”的实现:万物皆在 CharacterPanel.cs

一个典型的新手,会如何实现这个功能呢?他的思路通常是这样的:

1.创建 UI 元素:在 Unity 编辑器里,拖拽创建好 Image(头像)、Text(名字、等级等)、Slider(经验条)、Button(升级按钮)等所有 UI 控件。

2.创建主脚本:创建一个名为 CharacterPanel.cs 的脚本,并将它挂载到 UI 面板的根物体上。

3.万物皆可 public:为了在脚本中能控制这些 UI 控件,他会把所有需要操作的 UI 控件,都在 CharacterPanel.cs 里声明为 public 字段,然后在 Inspector 面板里,把它们一个一个拖拽上去。

using UnityEngine;
using UnityEngine.UI;

public class CharacterPanel : MonoBehaviour
{
    // --- 视图引用 ---
    public Image avatarImage;
    public Text nameText;
    public Text levelText;
    public Slider expSlider;
    public Text attackText;
    public Text defenseText;
    public Button levelUpButton;

    // --- 状态数据 ---
    private int level = 1;
    private int currentExp = 0;
    private int expToNextLevel = 100;
    private int attack = 10;
    private int defense = 5;

    void Start()
    {
        // 初始化按钮点击事件
        levelUpButton.onClick.AddListener(OnLevelUpButtonClicked);
        // 初始化显示
        UpdatePanelDisplay();
    }

    // --- 核心业务逻辑  ---
    private void OnLevelUpButtonClicked()
    {
        if (currentExp >= expToNextLevel)
        {
            level++;
            currentExp = 0;
            // 每次升级,攻防+2,下一级所需经验增加 50
            attack += 2;
            defense += 2;
            expToNextLevel += 50;

            // 升级后,立即更新显示
            UpdatePanelDisplay();
            Debug.Log("升级成功!");
        }
        else
        {
            Debug.Log("经验不足,无法升级!");
        }
    }

    // --- 数据更新方法  ---
    public void AddExperience(int amount)
    {
        currentExp += amount;
        if (currentExp > expToNextLevel)
        {
            currentExp = expToNextLevel;
        }
        // 获得经验后,也立即更新显示
        UpdatePanelDisplay();
    }

    // --- 视图更新方法 ---
    private void UpdatePanelDisplay()
    {
        levelText.text = "Lv: " +level;
        nameText.text = "勇者";
        expSlider.value = (float)currentExp / expToNextLevel;
        attackText.text = "攻击力: " +attack;
        defenseText.text = "防御力: " +defense;
    }
}

4.外部调用:当玩家打怪时,另一个脚本(比如 GameManager.cs)需要找到这个面板,并调用它的 AddExperience 方法。

// 在 GameManager.cs 中
public CharacterPanel characterPanel; // 同样在 Inspector 里拖拽赋值

void PlayerKilledAMonster()
{
    characterPanel.AddExperience(20);
}

3. 初尝“成功”与“混乱”的种子

对于一个新手来说,当他运行游戏,点击按钮,看到角色成功升级时,那种喜悦是无与伦比的。代码能跑,功能实现了,这就是成功!

然而,这看似天经地义的写法,却在不经意间,埋下了未来所有混乱的种子。这,就是我们即将要解剖的“东北乱炖”的雏形。让我们冷静地审视一下这份直觉代码,它究竟有什么问题?

  • 职责的混淆(The God Class - 上帝类): CharacterPanel.cs 这个类,它承担了太多的责任。它既是 View(持有所有 UI 控件的引用),又是 Model(存储着 level, exp, attack 等核心数据),同时还是 Controller(包含了 OnLevelUpButtonClicked 这样的业务逻辑)。所有东西都搅和在一起,像一锅东北乱炖。

  • 数据的“私有化”困境: 角色数据(等级、经验等)被私有地存储在了 CharacterPanel 内部。这意味着什么?如果游戏中有另一个系统(比如一个“成就系统”)也需要知道玩家的等级,它就必须先获取到 CharacterPanel 的实例,然后再从里面读取等级。这造成了模块间不必要的强耦合。

  • 视图与逻辑的深度绑定: OnLevelUpButtonClicked 这个方法里,业务逻辑(level++)和视图更新(UpdatePanelDisplay())紧紧地绑在了一起。如果我们想测试“升级逻辑”是否正确,唯一的办法就是启动整个游戏,然后去手动点击那个按钮。我们无法将这部分逻辑抽离出来,进行独立的单元测试。

  • 可扩展性的“末日”: 现在,让我们想象一下新的需求来了:

    “我们需要在升级时,播放一个炫酷的粒子特效,并且让按钮在经验不足时变成灰色不可点击。”

    “我们需要增加‘魔法值’和‘敏捷’两个新属性。”

    “我们需要在另一个界面(比如队伍界面)也显示这个角色的头像和等级。”

面对这些需求,我们唯一的选择,就是继续往 CharacterPanel.cs 这个日益臃肿的“怪物”里,添加更多的 public 引用、更多的私有数据、更多的 if/else 逻辑。很快,这个文件将变得巨大无比,任何人想要修改它,都如同在雷区里排爆,胆战心惊。

深度洞察: 新手凭直觉写出的代码,其最大的问题在于它是一种“面向过程”而非“面向对象”的思维方式。尽管使用了类,但其本质是“当点击发生时,执行这一系列操作”。它缺乏对“职责”和“边界”的思考。 这种“万物皆在一处”的写法,在功能极其简单时,是最高效、最直观的。但它完全不具备抵抗“复杂度”侵蚀的能力。随着项目需求的增长,混乱会以指数级的速度蔓延,直至代码完全失控。 正是为了对抗这种与生俱来的“熵增”趋势,为了在复杂的软件工程中建立秩序,我们才需要引入架构。接下来的章节,我们将看到,MVC、MVP、MVVM 这些看似复杂的“理论”,其诞生的唯一目的,就是为了优雅地解决我们在这个“新手起点”上所遇到的、所有令人头疼的问题。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐