《游戏编程模式》五、原型模式(Unity 实现)

279 阅读5分钟

使用特定原型实例来创建特定各类的对象,并且通过拷贝原型来创建新的对象

我们通常谈论的“原型”,并非 GoF 的原型模式

概要

优点

  • 提高性能:避免了重复的对象初始化过程,尤其是对于复杂对象,能显著提高对象创建的速度,减少系统开销,提升游戏的运行性能。
  • 灵活性和可扩展性:可以在运行时动态地克隆对象,根据不同的需求对克隆对象进行定制化修改,方便添加新的游戏元素或调整现有元素的属性。
  • 代码复用:通过复用原型对象的代码和属性,减少了代码的重复编写,提高了代码的可维护性和可读性。

缺点

  • 深拷贝复杂性:当对象包含复杂的引用类型成员(如嵌套对象、集合等)时,实现深拷贝可能会比较复杂,并且可能会带来一定的性能开销。
  • 克隆对象的管理:如果克隆对象过多,需要对这些对象进行有效的管理,否则可能会导致内存占用过高或对象状态管理混乱的问题。

适用场景

  • 游戏角色创建
  • 道具生成
  • 关卡元素复制

5.1 原型设计模式

需求:

  • 主角旁有各种怪物,怪物都想要杀掉主角
  • 每种怪物都有不同的特性

我们为游戏里 3 种怪物类型——幽灵、恶魔和术士分别设计了三个类:

class Monster {...}
class Ghost : Monster {}
class Demon : Monster {}
class Sorcerer : Monster {}

一个怪物生成器可以构造特定类型的怪物实例,我们为游戏里所有怪物都设计一个生成器

class Spawner
{
    public virtual Monster Spawn() { return null; }
}

class GhostSpawner : Spawner
{
    public override Monster Spawn() { return new Ghost(); }
}
class DemonSpawner : Spawner
{
    public override Monster Spawn() { return new Demon(); }
}
//...

显然这不是个好设计,太多的类,太多样板,太多冗余

而原型模式提供了一种解决方案。核心思想是一个对象可以生成与自身相似的其他对象。为此我们给 Monster 类加个抽象方法

class Monster
{
    public virtual Monster Clone() { return null; }
}

每个 Monster 的子类都提供一个特定的实现,返回一个与自身类型和状态相同的对象

class Ghost : Monster
{
    private int health;
    private int speed;

    public Ghost(int health, int speed)
    {
        this.health = health;
        this.speed = speed;
    }

    public override Monster Clone()
    {
        return new Ghost(health, speed);
    }
}

这样我们就不再需要为每个怪物定义一个 Spawner 类了,我们只需要一个

class Spawner
{
    private Monster prototype;

    public Spawner(Monster prototype)
    {
        this.prototype = prototype;
    }

    public Monster Spawn()
    {
        return prototype.Clone();
    }
}

那要创建幽灵生成器就简单了

Monster ghostPrototype = new Ghost(100, 50);
Spawner ghostSpawner = new Spawner(ghostPrototype);

我们还可以创造出各种各样的生成器,如极速幽灵、虚弱幽灵、龟速幽灵。只需要再创建一个相应的类再把它的实例当作模板创建生成器即可

class FastGhost : Ghost
{
    // 移动速度加快
    public FastGhost() : base(100, 100) { }
}

FastGhost fastGhostPrototype = new FastGhost();
Spawner fastGhostSpawner = new Spawner(fastGhostPrototype);

5.1.1 原型模式效果如何

想要实现一个正确的 clone 方法也非常不容易,比如深拷贝和浅拷贝问题,打个比方,若一个魔鬼正拿着叉子,那么克隆出来的也要拿着叉子吗?

当类结构很复杂时,我们就需要用到后面会讲到的组件模式类型对象模式,这样就能避免为每一种实体都编写一个类

5.1.2 生成器函数

另外一种实现方案就是定义孵化函数,而不再是为每种怪物定义生成器类(即不用为上面的“快速幽灵”定义一个类)

// 创建快速幽灵的方法
Monster SpawnFastGhost()
{
    return new Ghost(100, 100);
}

这样生成器只要包含孵化函数即可

class Spawner
{
    private Func<Monster> spawnMonster;

    public Spawner(Func<Monster> spawnMonster)
    {
        this.spawnMonster = spawnMonster;
    }

    public Monster Spawn()
    {
        return spawnMonster();
    }
}

创造幽灵生成器时,可以这样写

// 快速幽灵生成器
Spawner fastGhostSpawner = new Spawner(SpawnFastGhost);

5.1.3 模板

我们的生成器类需要构建一些对象实例,但我们并不想硬编码每个怪物类。用泛型就可以很自然地引入类型参数来解决

class Spawner<T> where T : Monster, new()
{
    public Monster Spawn()
    {
        return new T();
    }
}

使用方法

Spawner<Ghost> ghostSpawner = new Spawner<Ghost>();

5.1.4 头等公民类型

若你使用像 JS、Python 和 Ruby 这样的把 Class 当作头等公民的动态语言时,Class 可以当作函数参数进行传递,那么解决方案将更优雅

5.2 原型语言范式

(略)

5.3 原型数据建模

游戏里只有代码和数据,而数据的占比在稳步增加。今天的大部分游戏,代码只是游戏的引擎,玩法全部定义在数据中。

但简单地将内容都放到数据文件中并不能解决大项目难于组织的问题。当你的游戏数据到达一定规模时,你会开始想要一些“可复用”特性,可以试试原型和委托来实现

游戏里的哥布林,可能会被定义为这样

{
    "name": "goblin grunt",
    "minHeight": 20,
    "maxHeight": 30,
    "resists": ["cold", "poison"],
    "weaknesses": ["fire", "light"]
}

更多类型的哥布林

{
    "name": "goblin wizard",
    "minHeight": 20,
    "maxHeight": 30,
    "resists": ["cold", "poison"],
    "weaknesses": ["fire", "light"],
    "spells": ["fire ball", "lightning bolt"]
}
{
    "name": "goblin archer",
    "minHeight": 20,
    "maxHeight": 30,
    "resists": ["cold", "poison"],
    "weaknesses": ["fire", "light"],
    "attacks": ["short bow"]
}

如果我们想要将所有的哥布林加强一下,那我们得逐个更新数据表,这怎么行

如果这些数据是代码的话,则我们可以为“哥布林”创建一个抽象,然后在 3 个不同的哥布林类型之间重用

我们给对象声明一个“prototype”属性,该属性指定另外一个对象。若访问的属性不在此对象中,则去它的原型对象中找

{
    "name": "goblin wizard",
    "prototype": "goblin grunt", // 其他属性委托给原型对象
    "spells": ["fire ball", "lightning bolt"]
}
{
    "name": "goblin archer",
    "prototype": "goblin grunt", // 其他属性委托给原型对象
    "attacks": ["short bow"]
}

你只需要一点额外的努力就可以在你的游戏引擎里建立数据建模系统了,有了它,游戏设计者们就可以更加方便地添加更多好玩的武器和怪物