0x0a.建造者模式
我们已经把创建型模式中的“大魔王”们(单例、工厂、抽象工厂)聊完了,现在迎来创建型模式里的最后一位高手——建造者模式(Builder Pattern)。
如果说工厂模式关注的是“选哪种产品来生产”,那么建造者模式关注的则是“如何一步一步组装一个极其复杂的产品”。
在 Unity 开发中,它是解决“构造函数爆炸”和“对象参数太多”的绝对神器!
💡 为什么要用建造者模式?
假设你正在做一款 RPG 游戏,需要动态生成各种各样的 NPC 或敌人。一个复杂的敌人可能包含以下属性:名字、血量、速度、攻击力、防御力、武器预制体、护甲类型、是否是 Boss、AI 脚本类型……
新手的灾难写法(构造函数爆炸):
你可能会在 C# 中写一个拥有十几个参数的构造函数:
public Enemy(string name, int hp, float speed, int atk, int def, GameObject weapon, bool isBoss, string aiType) { ... }
这会导致两个巨大的痛苦:
- 极难阅读: 传参时一长串数字和字符串
new Enemy("恶魔", 5000, 2.5f, 150, 80, swordPrefab, true, "Aggressive"),根本分不清哪个数字代表什么。 - 缺乏灵活性: 如果你只想创建一个普通的史莱姆,绝大多数参数都是默认值,你却不得不传一堆无意义的参数(或者写十几个不同版本的重载构造函数)。
建造者模式的优雅解法:
把复杂对象的构建过程抽离出来,允许你通过链式调用(Fluent API),像拼乐高积木一样,需要什么属性就加什么属性,最后大喊一声 Build() 搞定!
一、 🌟 Unity 中最实用的链式建造者 (Fluent Builder)
这是 C# 开发者最喜欢的写法,利用 return this; 实现丝滑的连写。
1. 复杂的产品:敌人数据类 (EnemyCharacter)
using UnityEngine;
public class EnemyCharacter
{
// 拥有大量的属性
public string Name { get; set; }
public int Health { get; set; }
public float MoveSpeed { get; set; }
public int AttackPower { get; set; }
public GameObject WeaponPrefab { get; set; }
public bool IsBoss { get; set; }
// 打印一下信息,验证组装结果
public void Introduce()
{
Debug.Log($"生成成功 -> 名字: {Name}, 血量: {Health}, 速度: {MoveSpeed}, 攻击力: {AttackPower}, 是否是Boss: {IsBoss}");
}
}
2. 核心:建造者类 (EnemyBuilder)
using UnityEngine;
public class EnemyBuilder
{
// 内部持有一个正在构建的产品实例
private EnemyCharacter _enemy = new EnemyCharacter();
// 构造函数中可以设置一些默认值
public EnemyBuilder()
{
_enemy.Health = 100;
_enemy.MoveSpeed = 3.5f;
_enemy.AttackPower = 10;
_enemy.IsBoss = false;
}
// 关键点:每个设置属性的方法都返回 Builder 自己 (this)
public EnemyBuilder SetName(string name)
{
_enemy.Name = name;
return this;
}
public EnemyBuilder SetHealth(int hp)
{
_enemy.Health = hp;
return this;
}
public EnemyBuilder SetSpeed(float speed)
{
_enemy.MoveSpeed = speed;
return this;
}
public EnemyBuilder SetAttack(int atk)
{
_enemy.AttackPower = atk;
return this;
}
public EnemyBuilder SetWeapon(GameObject weapon)
{
_enemy.WeaponPrefab = weapon;
return this;
}
public EnemyBuilder MarkAsBoss()
{
_enemy.IsBoss = true;
_enemy.Health *= 5; // Boss 血量翻 5 倍
return this;
}
// 最终的组装方法:把造好的产品交出去
public EnemyCharacter Build()
{
// 可以在这里进行最后的合法性校验
if (string.IsNullOrEmpty(_enemy.Name))
{
_enemy.Name = "未命名怪物";
}
return _enemy;
}
}
3. 在 Unity 中使用(优雅得让人感动)
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
public GameObject goldenSwordPrefab; // 在面板拖入的武器
void Start()
{
// 场景 1:我们需要造一只普通的史莱姆(很多属性用默认的就行)
EnemyCharacter slime = new EnemyBuilder()
.SetName("小史莱姆")
.SetSpeed(1.5f)
.Build(); // 完工!
slime.Introduce();
// 场景 2:我们需要造一个极其强悍的终极大 Boss
EnemyCharacter boss = new EnemyBuilder()
.SetName("不灭魔王")
.SetHealth(2000)
.SetAttack(999)
.SetWeapon(goldenSwordPrefab)
.MarkAsBoss() // 触发 Boss 逻辑
.Build(); // 完工!
boss.Introduce();
}
}
二、 进阶:引入“指挥者” (Director) —— 固定配方
在标准的 GoF 建造者模式中,还有一个可选的角色叫 指挥者(Director)。它的作用是封装常用的“组装配方”。
如果你的游戏里经常需要批量生产同一种配置的怪物(比如“标准的远程弓箭手”),你不需要每次都自己去连一串 Set,你可以把这个配方交给 Director。
// 指挥者:负责规定建造的步骤和配方
public class EnemyDirector
{
// 传入一个建造者,指挥它去按照特定配方建造
public EnemyCharacter ConstructStandardGoblin(EnemyBuilder builder)
{
return builder
.SetName("哥布林步兵")
.SetHealth(80)
.SetSpeed(4.5f)
.SetAttack(15)
.Build();
}
public EnemyCharacter ConstructEliteKnight(EnemyBuilder builder, GameObject coolSword)
{
return builder
.SetName("精英圣骑士")
.SetHealth(500)
.SetSpeed(3.0f)
.SetAttack(50)
.SetWeapon(coolSword)
.Build();
}
}
使用 Director:
EnemyBuilder builder = new EnemyBuilder();
EnemyDirector director = new EnemyDirector();
// 直接问导演要一个标准哥布林
EnemyCharacter goblin = director.ConstructStandardGoblin(builder);
💡 建造者模式在 Unity 中的其他神级应用
除了用来配置复杂的数据,建造者模式在 Unity 的以下领域简直是绝对的主角:
- 关卡 / 地图随机生成 (Procedural Generation):
当你需要用代码动态生成一个迷宫、地下城或星球表面时,由于工序繁多(先生成网格 -> 刷地形高度 -> 布置植被 -> 放置怪物 -> 烘焙灯光),使用一个MapBuilder能够把极其杂乱的工序管理得井井有条。 - 网络请求工具类封装:
在 Unity 中向服务器发请求时,我们常常需要自定义 Header、Body、Token。用建造者模式可以写出超爽的代码:
HttpBuilder.Create().SetUrl("api/login").AddParam("user", "123").SetTimeout(5f).Post();
🚨 避坑指南
- 不要为了用而用: 如果你的类只有两三个属性,且基本固定不变,直接用 C# 的大括号初始化器
new Enemy { Name = "A", Health = 10 };就足够了,没必要为了这几个变量凭空多写一个 Builder 类。 - 状态重置问题: 如果你的 Builder 实例在
Build()完之后还要重复利用,别忘了在Build()的瞬间把内部的临时对象new一个新的,防止下一个怪物污染了上一个怪物的属性。