1. 程式人生 > >Prototype原型模式在遊戲中的應用

Prototype原型模式在遊戲中的應用

筆者介紹:姜雪偉,IT公司技術合夥人,IT高階講師,CSDN社群專家,特邀編輯,暢銷書作者,已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。

本篇部落格給讀者介紹一下關於Prototype原型設計模式在遊戲中的應用,先舉個例子,假設我們為遊戲中的每一種怪物都有不同的類 - 鬼,惡魔,巫師等,如:

class Monster
{
  // Stuff...
};

class Ghost : public Monster {};
class Demon : public Monster {};
class Sorcerer : public Monster {};
一個spawner構造一個特定怪物型別的例項。 為了滿足遊戲中的每一個怪物,我們可以通過為每個怪物類提供一個spawner類來管理它,從而可以並行類層次結構如下所示:

實現它將如下所示:

class Spawner
{
public:
  virtual ~Spawner() {}
  virtual Monster* spawnMonster() = 0;
};

class GhostSpawner : public Spawner
{
public:
  virtual Monster* spawnMonster()
  {
    return new Ghost();
  }
};

class DemonSpawner : public Spawner
{
public:
  virtual Monster* spawnMonster()
  {
    return new Demon();
  }
};
這顯然不是一個好設計方法。。。。。。

Prototype模式提供了一個解決方案 關鍵的問題是,一個物件可以產生類似於自己的其他物件 如果你有一個幽靈,你可以從中製造更多的鬼魂 如果你有一個惡魔,你可以製造其他惡魔 任何怪物都可以被視為用於生成其他版本的原型怪物。

為了實現這一點,我們給我們的基類Monster,一個抽象的clone()方法:

class Monster
{
public:
  virtual ~Monster() {}
  virtual Monster* clone() = 0;

  // Other stuff...
};
每個怪物子類提供了一個實現,它返回一個在類和狀態中與自己相同的新物件。 例如:
class Ghost : public Monster {
public:
  Ghost(int health, int speed)
  : health_(health),
    speed_(speed)
  {}

  virtual Monster* clone()
  {
    return new Ghost(health_, speed_);
  }

private:
  int health_;
  int speed_;
};
一旦我們所有的怪物都支援,我們不再需要每個怪物類的spawner類 相反,我們定義了一個:
class Spawner
{
public:
  Spawner(Monster* prototype)
  : prototype_(prototype)
  {}

  Monster* spawnMonster()
  {
    return prototype_->clone();
  }

private:
  Monster* prototype_;
};

效果如下所示:


接著上面的程式碼,要建立一個ghost spawner,我們建立一個原型的ghost例項,然後建立一個持有該原型的spawner:

Monster* ghostPrototype = new Ghost(15, 3);
Spawner* ghostSpawner = new Spawner(ghostPrototype);

關於這種模式的一個簡單的部分是它不只是克隆原型的類,它也克隆了它的狀態,我發現這種模式既優雅又令人驚訝, 我無法想象自己能想出來。

下面介紹它們如何應用:

那麼,我們不必為每個怪物建立一個單獨的spawner類,所以這很好。 但是我們必須在每個怪物類中實現clone()。

即使我們每個怪物都有不同的, 而不是為每個怪物製作單獨的spawner類,我們可以做出生成函式,像這樣:

Monster* spawnGhost()
{
  return new Ghost();
}
一個spawner類可以簡單地儲存一個函式指標:
typedef Monster* (*SpawnCallback)();

class Spawner
{
public:
  Spawner(SpawnCallback spawn)
  : spawn_(spawn)
  {}

  Monster* spawnMonster()
  {
    return spawn_();
  }

private:
  SpawnCallback spawn_;
};
為了創造一個Ghosts,你可以做:
Spawner* ghostSpawner = new Spawner(spawnGhost);
現在,大多數C ++開發人員都熟悉模板。 我們的spawner類需要構造一些型別的例項,但是我們不想硬編碼一些特定的怪物類。 然後,自然的解決方案是使它成為一個型別引數,這些模板讓我們做:
class Spawner
{
public:
  virtual ~Spawner() {}
  virtual Monster* spawnMonster() = 0;
};

template <class T>
class SpawnerFor : public Spawner
{
public:
  virtual Monster* spawnMonster() { return new T(); }
};
使用它看起來像:
Spawner* ghostSpawner = new SpawnerFor<Ghost>();
前兩個解決方案解決了需要一個類,Spawner,它由一個型別引數化。 在C ++中,型別通常不是類的首先, 如果您使用JavaScript,Python或Ruby等動態型別的語言,那麼類可以傳遞給常規物件,您可以更直接地解決這些問題。

當你做一個spawner時,只需傳遞它應該構造的怪物類 - 代表怪物類的實際執行時物件,這很容易的。

有了所有這些選項,我真的不能說我發現一個案例,我覺得原型設計模式是最好的答案。 也許你的經驗會有所不同,但現在讓我們把它放在一邊,談論別的東西:原型作為一種語言正規化。

許多人認為“面向物件程式設計”是“類”的代名詞,但是一個相當不爭議的做法是,OOP允許您定義將資料和程式碼捆綁在一起的“物件”。 與C和C語言等結構語言相比,OOP的定義特徵是將狀態和行為緊密結合在一起。

在純粹的意義上,自己比面向物件的語言更加面向物件。 我們認為OOP是結婚的狀態和行為,但語言與類實際上有一線分隔。考慮您最喜歡的基於類的語言的語義 要訪問物件上的某些狀態,請檢視例項本身的記憶體 狀態包含在例項中。

要呼叫一個方法,你可以檢視例項的類,然後查詢方法。 行為包含在類中 總是有一定程度的間接訪問方法,這意味著欄位和方法是不同的。

要查詢任何東西,你只要看物件 一個例項可以包含狀態和行為 您可以擁有一個完全獨特的方法的單個物件。

如果這是自己所做的,那就很難使用了 基於類的語言的繼承儘管有其錯誤,但為您提供了重用多型程式碼並避免重複的有用機制。

要找到某個欄位或呼叫某個物件的方法,我們首先檢視物件本身 如果有的話,我們完成了 如果沒有,我們看物件的父物件 這只是一些其他物件的引用 當我們未能在第一個物件上找到一個屬性時,我們嘗試其父物件及其父物件等等。 換句話說,失敗的查詢被委託給物件的父

父物件讓我們重複使用多個物件的行為(和狀態!),所以我們已經介紹了類的實用程式的一部分。 類的其他關鍵事項是給我們建立例項的方法。 當你需要一個新的東西,你可以做新的Thingamabob(),或任何你喜歡的語言的語法,一個類是一個工廠的例項。

我非常興奮地玩純粹的基於原型的語言,但一旦開始執行,我發現了一個令人不快的事實:它只是沒有那麼有趣的程式。

也許這是因為我以前的經驗是基於類的語言,所以我的思想已經被這個範例所影響。 但我的希望是,大多數人都喜歡定義好的“種類”。
看看有多少遊戲具有明確的角色類別,以及精確的不同型別的敵人,物品和技能的名單,每個都有整齊的標籤。
雖然原型是一個非常酷的範例,而我希望更多的人知道,我們大多數人並不是每天都在使用它們。 我看到的完全擁抱原型的程式碼對我來說很困難

好的,如果基於原型的語言是如此不友好,我該如何解釋JavaScript? 這是每天有數百萬人使用原型的語言 更多的電腦執行JavaScript比地球上的任何其他語言。
JavaScript的創始人Brendan Eich直接獲得靈感,許多JavaScript的語義都是基於原型的。 每個物件都可以有任意的屬性集合,包括欄位和“方法”(這些都只是儲存為欄位的函式)。 一個物件也可以有一個稱為它的“prototype”的物件,如果一個欄位訪問失敗,它就被委派給它。

但是,儘管如此,我相信JavaScript在實踐中與基於類的語言比與原型語言更相似基於原型的語言,克隆的核心操作是無處可見的。
在JavaScript中沒有克隆物件的方法。 它最接近的是Object.create(),它允許您建立一個新物件,該物件將委託給現有物件。 即使沒有新增到ECMAScript 5之前,JavaScript出現十四年之後
而不是克隆,讓我來介紹你定義型別的典型方式,並用JavaScript建立物件 從一個建構函式開始:

function Weapon(range, damage) {
  this.range = range;
  this.damage = damage;
}
這將建立一個新物件並初始化其欄位。 呼叫方式如下所示:
var sword = new Weapon(10, 16);
這裡使用這個繫結到一個新的空物件來呼叫Weapon()函式,它 為其添加了一堆欄位,然後自動返回填寫的物件。
也為你做
另一件事 當它建立該空白物件時,將其連線起來以委託給一個原型物件您可以直接使用Weapon.prototype獲取該物件。
當狀態被新增到建構函式體中時,為了定義行為,你通常會向原型物件新增方法
,如下所示:
Weapon.prototype.attack = function(target) {
  if (distanceTo(target) > this.range) {
    console.log("Out of range!");
  } else {
    target.health -= this.damage;
  }
}

這將攻擊屬性新增到武器原型,其值是一個函式。 由於新的Weapon()返回的每個物件都會委託給Weapon.prototype,所以現在可以呼叫sword.attack()並呼叫該函式。 看起來有點像這樣:

   我們回顧一下:
建立物件的方式是通過使用表示型別的物件(建構函式)呼叫的“新”操作狀態儲存在例項本身上。
行為通過一個間接級別 - 委託給原型 - 並存儲在一個單獨的物件上,該物件表示某種型別的所有物件共享的一組方法。
您可以使用JavaScript編寫原型樣式的程式碼(無需克隆),但語言的語法和習語鼓勵了基於類的方法。
就個人而言,我認為這是件好事
就像我說的那樣,我發現原型的加倍使得程式碼更難處理,所以我喜歡這種JavaScript將核心語義包含在更加優雅的東西中。

當你的遊戲資料達到一定的大小時,你真的開始想要類似的功能。 資料建模是一個深刻的問題,我不能希望在這裡做,但我想丟擲一個功能,讓您在自己的遊戲中考慮:使用原型和委託來重用資料。

在遊戲中可能會定義如下:

{
  "name": "goblin grunt",
  "minHealth": 20,
  "maxHealth": 30,
  "resists": ["cold", "poison"],
  "weaknesses": ["fire", "light"]
}
再看下面的表內容如下所示:
{
  "name": "goblin wizard",
  "minHealth": 20,
  "maxHealth": 30,
  "resists": ["cold", "poison"],
  "weaknesses": ["fire", "light"],
  "spells": ["fire ball", "lightning bolt"]
}

{
  "name": "goblin archer",
  "minHealth": 20,
  "maxHealth": 30,
  "resists": ["cold", "poison"],
  "weaknesses": ["fire", "light"],
  "attacks": ["short bow"]
}
現在,如果這是程式碼, 這些實體之間有很多重複, 它浪費空間,花費更多的時間, 你必須仔細閱讀,以確定資料是否相同。
如果這是程式碼,我們將為它建立一個抽象,並在三個型別中重用它, 但是,JSON不知道什麼, 所以我們讓它變得更聰明。
我們將宣告,如果一個物件有一個“原型”欄位,那麼它定義了這個代表的另一個物件的名稱, 第一個物件上不存在的任何屬性都可以回溯到原型上。

為此,我們可以簡化JSON配置檔案如下

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

{
  "name": "goblin wizard",
  "prototype": "goblin grunt",
  "spells": ["fire ball", "lightning bolt"]
}

{
  "name": "goblin archer",
  "prototype": "goblin grunt",
  "attacks": ["short bow"]
}
在基於原型的系統中,任何物件都可以用作克隆來建立新的物件, 在遊戲中經常有一次性的特殊實體的遊戲中,資料特別適合。 這些通常是遊戲中更常見的物件的改進,原型代理是很適合定義的。如下所示:
{
  "name": "Sword of Head-Detaching",
  "prototype": "longsword",
  "damageBonus": "20"
}

作為遊戲引擎的資料建模系統有一點額外的功能可以使設計人員更輕鬆地為裝備遊戲世界的武器和野獸新增許多小的變化,豐富性正是讓玩家感到高興的,這也是作為遊戲引擎必不可少的部分。