從程序角度考慮,許多 JavaScript 都基於循環和大量的 if/else 語句。在本文中,咱們可瞭解一種更聰明的作法 — 在 JavaScript 遊戲中使用面向對象來設計。本文將概述原型繼承和使用 JavaScript 實現基本的面向對象的編程 (OOP)。學習如何在 JavaScript 中使用基於經典繼承的庫從 OOP 中得到更多的好處。本文還將介紹架構式設計模式,來展現瞭如何使用遊戲循環、狀態機和事件冒泡 (event bubbling) 示例來編寫更整潔的代碼。web
在本文中,您將瞭解 JavaScript 中的 OOP,來探索原型繼承模型和經典繼承模型。舉例說明遊戲中可以從 OOP 設計的結構和可維護性中得到極大利益的模式。咱們的最終目標是讓每一塊代碼都成爲人類可讀的代碼,並表明一種想法和一個目的,這些代碼的結合超越了指令和算法的集合,成爲一個精緻的藝術品。算法
OOP 的目標就是提供數據抽象、模塊化、封裝、多態性和繼承。經過 OOP,您能夠在代碼編寫中抽象化代碼的理念,從而提供優雅、可重用和可讀的代碼,但這會消耗文件計數、行計數和性能(若是管理不善)。編程
過去,遊戲開發人員每每會避開純 OOP 方式,以便充分利用 CPU 週期的性能。不少 JavaScript 遊戲教程採用的都是非 OOP 方式,但願可以提供一個快速演示,而不是提供一種堅實的基礎。與其餘遊戲的開發人員相比,JavaScript 開發人員面臨不一樣的問題:內存是非手動管理的,且 JavaScript 文件在全局的上下文環境中執行,這樣一來,無頭緒的代碼、命名空間的衝突和迷宮式的 if/else 語句可能會致使可維護性的噩夢。爲了從 JavaScript 遊戲的開發中得到最大的益處,請遵循 OOP 的最佳實踐,顯著提升將來的可維護性、開發進度和遊戲的表現能力。canvas
與使用經典繼承的語言不一樣,在 JavaScript 中,沒有內置的類結構。函數是 JavaScript 世界的一級公民,而且,與全部用戶定義的對象相似,它們也有原型。用 new 關鍵字調用函數實際上會建立該函數的一個原型對象副本,並使用該對象做爲該函數中的關鍵字 this的上下文。清單 1 給出了一個例子。設計模式
JavaScript // constructor function function MyExample() { // property of an instance when used with the 'new' keyword this.isTrue = true; }; MyExample.prototype.getTrue = function() { return this.isTrue; } MyExample(); // here, MyExample was called in the global context, // so the window object now has an isTrue property—this is NOT a good practice MyExample.getTrue; // this is undefined—the getTrue method is a part of the MyExample prototype, // not the function itself var example = new MyExample(); // example is now an object whose prototype is MyExample.prototype example.getTrue; // evaluates to a function example.getTrue(); // evaluates to true because isTrue is a property of the // example instance
// constructor function function MyExample() { // property of an instance when used with the 'new' keyword this.isTrue = true; }; MyExample.prototype.getTrue = function() { return this.isTrue; } MyExample(); // here, MyExample was called in the global context, // so the window object now has an isTrue property—this is NOT a good practice MyExample.getTrue; // this is undefined—the getTrue method is a part of the MyExample prototype, // not the function itself var example = new MyExample(); // example is now an object whose prototype is MyExample.prototype example.getTrue; // evaluates to a function example.getTrue(); // evaluates to true because isTrue is a property of the // example instance
依照慣例,表明某個類的函數應該以大寫字母開頭,這表示它是一個構造函數。該名稱應該可以表明它所建立的數據結構。瀏覽器
建立類實例的祕訣在於綜合新的關鍵字和原型對象。原型對象能夠同時擁有方法和屬性,如 清單 2 所示。安全
JavaScript // Base class function Character() {}; Character.prototype.health = 100; Character.prototype.getHealth = function() { return this.health; } // Inherited classes function Player() { this.health = 200; } Player.prototype = new Character; function Monster() {} Monster.prototype = new Character; var player1 = new Player(); var monster1 = new Monster(); player1.getHealth(); // 200- assigned in constructor monster1.getHealth(); // 100- inherited from the prototype object
// Base class function Character() {}; Character.prototype.health = 100; Character.prototype.getHealth = function() { return this.health; } // Inherited classes function Player() { this.health = 200; } Player.prototype = new Character; function Monster() {} Monster.prototype = new Character; var player1 = new Player(); var monster1 = new Monster(); player1.getHealth(); // 200- assigned in constructor monster1.getHealth(); // 100- inherited from the prototype object
爲一個子類分配一個父類須要調用 new 並將結果分配給子類的 prototype 屬性,如 清單 3 所示。所以,明智的作法是保持構造函數儘量的簡潔和無反作用,除非您想要傳遞類定義中的默認值。服務器
若是您已經開始嘗試在 JavaScript 中定義類和繼承,那麼您可能已經意識到該語言與經典 OOP 語言的一個重要區別:若是已經覆蓋這些方法,那麼沒有 super 或 parent 屬性可用來訪問父對象的方法。對此有一個簡單的解決方案,但該解決方案違背了 「不要重複本身 (DRY)」 原則,並且頗有多是現在有不少庫試圖模仿經典繼承的最重要的緣由。數據結構
JavaScript function ParentClass() { this.color = 'red'; this.shape = 'square'; } function ChildClass() { ParentClass.call(this); // use 'call' or 'apply' and pass in the child // class's context this.shape = 'circle'; } ChildClass.prototype = new ParentClass(); // ChildClass inherits from ParentClass ChildClass.prototype.getColor = function() { return this.color; // returns "red" from the inherited property };
function ParentClass() { this.color = 'red'; this.shape = 'square'; } function ChildClass() { ParentClass.call(this); // use 'call' or 'apply' and pass in the child // class's context this.shape = 'circle'; } ChildClass.prototype = new ParentClass(); // ChildClass inherits from ParentClass ChildClass.prototype.getColor = function() { return this.color; // returns "red" from the inherited property };
在 清單 3 中, color 和 shape 屬性值都不在原型中,它們在 ParentClass 構造函數中賦值。ChildClass 的新實例將會爲其形狀屬性賦值兩次:一次做爲 ParentClass 構造函數中的 「squre」,一次做爲 ChildClass 構造函數中的 「circle」。將相似這些賦值的邏輯移動到原型將會減小反作用,讓代碼變得更容易維護。架構
在原型繼承模型中,可使用 JavaScript 的 call 或 apply 方法來運行具備不一樣上下文的函數。雖然這種作法十分有效,能夠替代其餘語言的 super 或 parent,但它帶來了新的問題。若是須要經過更改某個類的名稱、它的父類或父類的名稱來重構這個類,那麼如今您的文本文件中的不少地方都有了這個 ParentClass 。隨着您的類愈來愈複雜,這類問題也會不斷增加。更好的一個解決方案是讓您的類擴展一個基類,使代碼減小重複,尤爲在從新建立經典繼承時。
雖然原型繼承對於 OOP 是徹底可行的,但它沒法知足優秀編程的某些目標。好比以下這些問題:
● 它不是 DRY 的。類名稱和原型隨處重複,讓讀和重構變得更爲困難。
● 構造函數在原型化期間調用。一旦開始子類化,就將不能使用構造函數中的一些邏輯。
● 沒有爲強封裝提供真正的支持。
● 沒有爲靜態類成員提供真正的支持。
不少 JavaScript 庫試圖實現更經典的 OOP 語法來解決上述問題。其中一個更容易使用的庫是 Dean Edward 的 Base.js(請參閱 參考資料),它提供了下列有用特性:
● 全部原型化都是用對象組合(能夠在一條語句中定義類和子類)完成的。
● 用一個特殊的構造函數爲將在建立新的類實例時運行的邏輯提供一個安全之所。
● 它提供了靜態類成員支持。
● 它對強封裝的貢獻止步於讓類定義保持在一條語句內(精神封裝,而非代碼封裝)。
其餘庫能夠提供對公共和私有方法和屬性(封裝)的更嚴格支持,Base.js 提供了一個簡潔、易用、易記的語法。
清單 4 給出了對 Base.js 和經典繼承的簡介。該示例用一個更爲具體的 RobotEnemy 類擴展了抽象 Enemy 類的特性。
JavaScript // create an abstract, basic class for all enemies // the object used in the .extend() method is the prototype var Enemy = Base.extend({ health: 0, damage: 0, isEnemy: true, constructor: function() { // this is called every time you use "new" }, attack: function(player) { player.hit(this.damage); // "this" is your enemy! } }); // create a robot class that uses Enemy as its parent // var RobotEnemy = Enemy.extend({ health: 100, damage: 10, // because a constructor isn't listed here, // Base.js automatically uses the Enemy constructor for us attack: function(player) { // you can call methods from the parent class using this.base // by not having to refer to the parent class // or use call / apply, refactoring is easier // in this example, the player will be hit this.base(player); // even though you used the parent class's "attack" // method, you can still have logic specific to your robot class this.health += 10; } });
// create an abstract, basic class for all enemies // the object used in the .extend() method is the prototype var Enemy = Base.extend({ health: 0, damage: 0, isEnemy: true, constructor: function() { // this is called every time you use "new" }, attack: function(player) { player.hit(this.damage); // "this" is your enemy! } }); // create a robot class that uses Enemy as its parent // var RobotEnemy = Enemy.extend({ health: 100, damage: 10, // because a constructor isn't listed here, // Base.js automatically uses the Enemy constructor for us attack: function(player) { // you can call methods from the parent class using this.base // by not having to refer to the parent class // or use call / apply, refactoring is easier // in this example, the player will be hit this.base(player); // even though you used the parent class's "attack" // method, you can still have logic specific to your robot class this.health += 10; } });
基本的遊戲引擎不可避免地依賴於兩個函數:update 和 render。render 方法一般會根據 setInterval 或 polyfill 進行requestAnimationFrame,好比 Paul Irish 使用的這個(請參閱 參考資料)。使用 requestAnimationFrame 的好處是僅在須要的時候調用它。它按照客戶監視器的刷新頻率運行(對於臺式機,一般是一秒 60 次),此外,在大多數瀏覽器中,一般根本不會運行它,除非遊戲所在的選項卡是活動的。它的優點包括:
● 在用戶沒有盯着遊戲時減小客戶機上的工做量
● 節省移動設備上的用電。
● 若是更新循環與呈現循環有關聯,那麼能夠有效地暫停遊戲。
出於這些緣由,與 setInterval 相比,requestAnimationFrame 一直被認爲是 「客戶友好」 的 「好公民」。
將 update 循環與 render 循環捆綁在一塊兒會帶來新的問題:要保持遊戲動做和動畫的速度相同,而無論呈現循環的運行速度是每秒 15 幀仍是 60 幀。這裏要掌握的技巧是在遊戲中創建一個時間單位,稱爲滴答 (tick),並傳遞自上次更新後過去的時間量。而後,就能夠將這個時間量轉換成滴答數量,而模型、物理引擎和其餘依賴於時間的遊戲邏輯能夠作出相應的調整。好比,一箇中毒的玩家可能會在每一個滴答接受 10 次損害,共持續 10 個滴答。若是呈現循環運行太快,那麼玩家在某個更新調用上可能不會接受損害。可是,若是垃圾回收在最後一個致使過去 1 個半滴答的呈現循環上生效,那麼您的邏輯可能會致使 15 次損害。
另外一個方式是將模型更新從視圖循環中分離出來。在包含不少動畫或對象或是繪製佔用了大量資源的遊戲中,更新循環與 render 循環的耦合會致使遊戲徹底慢下來。在這種狀況下,update 方法可以以設置好的間隔運行(使用 setInterval),而無論requestAnimationFrame 處理程序什麼時候會觸發,以及多久會觸發一次。在這些循環中花費的時間實際上都花費在了呈現步驟中,因此,若是隻有 25 幀被繪製到屏幕上,那麼遊戲會繼續以設置好的速度運行。在這兩種狀況下,您可能都會想要計算更新週期之間的時間差;若是一秒更新 60 次,那麼完成函數更新最多有 16ms 的時間。若是運行此操做的時間更長(或若是運行了瀏覽器的垃圾回收),那麼遊戲仍是會慢下來。 清單 5 顯示了一個示例。
JavaScript // requestAnim shim layer by Paul Irish window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(/* function */ callback, /* DOMElement */ element){ window.setTimeout(callback, 1000 / 60); }; })(); var Engine = Base.extend({ stateMachine: null, // state machine that handles state transitions viewStack: null, // array collection of view layers, // perhaps including sub-view classes entities: null, // array collection of active entities within the system // characters, constructor: function() { this.viewStack = []; // don't forget that arrays shouldn't be prototype // properties as they're copied by reference this.entities = []; // set up your state machine here, along with the current state // this will be expanded upon in the next section // start rendering your views this.render(); // start updating any entities that may exist setInterval(this.update.bind(this), Engine.UPDATE_INTERVAL); }, render: function() { requestAnimFrame(this.render.bind(this)); for (var i = 0, len = this.viewStack.length; i < len; i++) { // delegate rendering logic to each view layer (this.viewStack[i]).render(); } }, update: function() { for (var i = 0, len = this.entities.length; i < len; i++) { // delegate update logic to each entity (this.entities[i]).update(); } } }, // Syntax for Class "Static" properties in Base.js. Pass in as an optional // second argument to.extend() { UPDATE_INTERVAL: 1000 / 16 });
// requestAnim shim layer by Paul Irish window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(/* function */ callback, /* DOMElement */ element){ window.setTimeout(callback, 1000 / 60); }; })(); var Engine = Base.extend({ stateMachine: null, // state machine that handles state transitions viewStack: null, // array collection of view layers, // perhaps including sub-view classes entities: null, // array collection of active entities within the system // characters, constructor: function() { this.viewStack = []; // don't forget that arrays shouldn't be prototype // properties as they're copied by reference this.entities = []; // set up your state machine here, along with the current state // this will be expanded upon in the next section // start rendering your views this.render(); // start updating any entities that may exist setInterval(this.update.bind(this), Engine.UPDATE_INTERVAL); }, render: function() { requestAnimFrame(this.render.bind(this)); for (var i = 0, len = this.viewStack.length; i < len; i++) { // delegate rendering logic to each view layer (this.viewStack[i]).render(); } }, update: function() { for (var i = 0, len = this.entities.length; i < len; i++) { // delegate update logic to each entity (this.entities[i]).update(); } } }, // Syntax for Class "Static" properties in Base.js. Pass in as an optional // second argument to.extend() { UPDATE_INTERVAL: 1000 / 16 });
若是您對 JavaScript 中 this 的上下文不是很熟悉,請注意 .bind(this) 被使用了兩次:一次是在 setInterval 調用中的匿名函數上,另外一次是在 requestAnimFrame 調用中的 this.render.bind() 上。setInterval 和 requestAnimFrame 都是函數,而非方法;它們屬於這個全局窗口對象,不屬於某個類或身份。所以,爲了讓此引擎的呈現和更新方法的 this 引用咱們的 Engine 類的實例,調用.bind(object) 會迫使此函數中的 this 與正常狀況表現不一樣。若是您支持的是 Internet Explorer 8 或其更早版本,則須要添加一個 polyfill,將它用於綁定。
狀態機模式已被普遍採用,但人們並不怎麼承認它。它是 OOP(從執行抽象代碼的概念)背後的原理的擴展。好比,一個遊戲可能具備如下狀態:
● 預加載
● 開始屏幕
● 活動遊戲
● 選項菜單
● 遊戲接受(贏、輸或繼續)
這些狀態中沒有關注其餘狀態的可執行代碼。您的預加載代碼不會知曉什麼時候打開 Options 菜單。指令式(過程式)編程可能會建議組合使用 if 或 switch 條件語句,從而得到順序正確的應用程序邏輯,但它們並不表明代碼的概念,這使得它們變得很難維護。若是增長條件狀態,好比遊戲中菜單,等級間轉變等特性,那麼會讓條件語句變得更難維護。
相反,您能夠考慮使用 清單 6 中的示例。
JavaScript // State Machine var StateMachine = Base.extend({ states: null, // this will be an array, but avoid arrays on prototypes. // as they're shared across all instances! currentState: null, // may or may not be set in constructor constructor: function(options) { options = options || {}; // optionally include states or contextual awareness this.currentState = null; this.states = {}; if (options.states) { this.states = options.states; } if (options.currentState) { this.transition(options.currentState); } }, addState: function(name, stateInstance) { this.states[name] = stateInstance; }, // This is the most important function—it allows programmatically driven // changes in state, such as calling myStateMachine.transition("gameOver") transition: function(nextState) { if (this.currentState) { // leave the current state—transition out, unload assets, views, so on this.currentState.onLeave(); } // change the reference to the desired state this.currentState = this.states[nextState]; // enter the new state, swap in views, // setup event handlers, animated transitions this.currentState.onEnter(); } }); // Abstract single state var State = Base.extend({ name: '', // unique identifier used for transitions context: null, // state identity context- determining state transition logic constructor: function(context) { this.context = context; }, onEnter: function() { // abstract // use for transition effects }, onLeave: function() { // abstract // use for transition effects and/or // memory management- call a destructor method to clean up object // references that the garbage collector might not think are ready, // such as cyclical references between objects and arrays that // contain the objects } });
// State Machine var StateMachine = Base.extend({ states: null, // this will be an array, but avoid arrays on prototypes. // as they're shared across all instances! currentState: null, // may or may not be set in constructor constructor: function(options) { options = options || {}; // optionally include states or contextual awareness this.currentState = null; this.states = {}; if (options.states) { this.states = options.states; } if (options.currentState) { this.transition(options.currentState); } }, addState: function(name, stateInstance) { this.states[name] = stateInstance; }, // This is the most important function—it allows programmatically driven // changes in state, such as calling myStateMachine.transition("gameOver") transition: function(nextState) { if (this.currentState) { // leave the current state—transition out, unload assets, views, so on this.currentState.onLeave(); } // change the reference to the desired state this.currentState = this.states[nextState]; // enter the new state, swap in views, // setup event handlers, animated transitions this.currentState.onEnter(); } }); // Abstract single state var State = Base.extend({ name: '', // unique identifier used for transitions context: null, // state identity context- determining state transition logic constructor: function(context) { this.context = context; }, onEnter: function() { // abstract // use for transition effects }, onLeave: function() { // abstract // use for transition effects and/or // memory management- call a destructor method to clean up object // references that the garbage collector might not think are ready, // such as cyclical references between objects and arrays that // contain the objects } });
您可能無需爲應用程序建立狀態機的特定子類,但確實須要爲每一個應用程序狀態建立 State 的子類。經過將轉變邏輯分離到不一樣的對象,您應該:
● 使用構造函數做爲當即開始預加載資產的機會。
● 向遊戲添加新的狀態,好比在出現遊戲結束屏幕以前出現的一個繼續屏幕,無需嘗試找出某個單片的 if/else 或 switch 結構中的哪一個條件語句中的哪一個全局變量受到了影響。
● 若是是基於從服務器加載的數據建立狀態,那麼能夠動態地定義轉換邏輯。
您的主要應用程序類不該關注狀態中的邏輯,並且您的狀態也不該太多關注主應用程序類中的內容。例如,預加載狀態可能負責基於構建在頁面標記中的資產來實例化某個視圖,並查詢某個資產管理器中的最小的遊戲資產(電影片段、圖像和聲音)。雖然該狀態初始化了預加載視圖類,但它無需考慮視圖。在本例中,此理念(此狀態所表明的對象)在責任上限於定義它對應用程序意味着處於一種預加載數據狀態。
請記住狀態機模式並不限於遊戲邏輯狀態。各視圖也會由於從其表明邏輯中刪除狀態邏輯而獲益,尤爲在管理子視圖或結合責任鏈模式處理用戶交互事件時。
能夠將 HTML5 canvas 元素視爲一個容許您操縱各像素的圖像元素。若是有一個區域,您在該區域中繪製了一些草、一些戰利品 以及站在這些上面的一我的物,那麼該畫布並不瞭解用戶在畫布上單擊了什麼。若是您繪製了一個菜單,畫布也不會知道哪一個特定的區域表明的是一個按鈕,而附加到事件的唯一 DOM 元素就是畫布自己。爲了讓遊戲變得可玩,遊戲引擎須要翻譯當用戶在畫布上單擊時會發生什麼。
責任鏈設計模式旨在將事件的發送者(DOM 元素)與接受者分離開來,以便更多的對象有機會處理事件(視圖和模型)。典型的實現,好比 Web 頁,可能會讓視圖或模型實現一個處理程序界面,而後將全部的鼠標事件 指派到某個場景圖,這有助於找到被單擊的相關的「事物」並在截取畫面時讓每個事物都有機會。更簡單的方法是讓此畫布自己託管在運行時定義的處理程序鏈,如 清單 7 所示。
JavaScript var ChainOfResponsibility = Base.extend({ context: null, // relevant context- view, application state, so on handlers: null, // array of responsibility handlers canPropagate: true, // whether or not constructor: function(context, arrHandlers) { this.context = context; if (arrHandlers) { this.handlers = arrHandlers; } else { this.handlers = []; } }, execute: function(data) for (var i = 0, len = this.handlers.length; i < len; i++) { if (this.canPropagate) { // give a handler a chance to claim responsibility (this.handlers[i]).execute(this, data); } else { // an event has claimed responsibility, no need to continue break; } } // reset state after event has been handled this.canPropagate = true; }, // this is the method a handler can call to claim responsibility // and prevent other handlers from acting on the event stopPropagation: function() { this.canPropagate = false; }, addHandler: function(handler) { this.handlers.push(handler); } }); var ResponsibilityHandler = Base.extend({ execute: function(chain, data) { // use chain to call chain.stopPropegation() if this handler claims // responsibility, or to get access to the chain's context member property // if this event handler doesn't need to claim responsibility, simply // return; and the next handler will execute } });
var ChainOfResponsibility = Base.extend({ context: null, // relevant context- view, application state, so on handlers: null, // array of responsibility handlers canPropagate: true, // whether or not constructor: function(context, arrHandlers) { this.context = context; if (arrHandlers) { this.handlers = arrHandlers; } else { this.handlers = []; } }, execute: function(data) for (var i = 0, len = this.handlers.length; i < len; i++) { if (this.canPropagate) { // give a handler a chance to claim responsibility (this.handlers[i]).execute(this, data); } else { // an event has claimed responsibility, no need to continue break; } } // reset state after event has been handled this.canPropagate = true; }, // this is the method a handler can call to claim responsibility // and prevent other handlers from acting on the event stopPropagation: function() { this.canPropagate = false; }, addHandler: function(handler) { this.handlers.push(handler); } }); var ResponsibilityHandler = Base.extend({ execute: function(chain, data) { // use chain to call chain.stopPropegation() if this handler claims // responsibility, or to get access to the chain's context member property // if this event handler doesn't need to claim responsibility, simply // return; and the next handler will execute } });
ChainOfResponsibility 類沒有子類化也能很好地工做,這是由於全部特定於應用程序的邏輯都會包含在 ResponsibilityHandler 子類中。在各實現之間唯一有所改變的是傳入了一個適當的上下文,好比它表明的視圖。例如,有一個選項菜單,在打開它時,仍會顯示處於暫停狀態的遊戲,如 清單 8 所示。若是用戶單擊菜單中的某個按鈕,背景中的人物不該對此單擊操做有任何反應。
JavaScript var OptionsMenuCloseHandler = ResponsibilityHandler.extend({ execute: function(chain, eventData) { if (chain.context.isPointInBackground(eventData)) { // the user clicked the transparent background of our menu chain.context.close(); // delegate changing state to the view chain.stopPropegation(); // the view has closed, the event has been handled } } }); // OptionMenuState // Our main view class has its own states, each of which handles // which chains of responsibility are active at any time as well // as visual transitions // Class definition... constructor: function() { // ... this.chain = new ChainOfResponsibility( this.optionsMenuView, // the chain's context for handling responsibility [ new OptionsMenuCloseHandler(), // concrete implementation of // a ResponsibilityHandler // ...other responsibility handlers... ] ); } // ... onEnter: function() { // change the view's chain of responsibility // guarantees only the relevant code can execute // other states will have different chains to handle clicks on the same view this.context.setClickHandlerChain(this.chain); } // ...
var OptionsMenuCloseHandler = ResponsibilityHandler.extend({ execute: function(chain, eventData) { if (chain.context.isPointInBackground(eventData)) { // the user clicked the transparent background of our menu chain.context.close(); // delegate changing state to the view chain.stopPropegation(); // the view has closed, the event has been handled } } }); // OptionMenuState // Our main view class has its own states, each of which handles // which chains of responsibility are active at any time as well // as visual transitions // Class definition... constructor: function() { // ... this.chain = new ChainOfResponsibility( this.optionsMenuView, // the chain's context for handling responsibility [ new OptionsMenuCloseHandler(), // concrete implementation of // a ResponsibilityHandler // ...other responsibility handlers... ] ); } // ... onEnter: function() { // change the view's chain of responsibility // guarantees only the relevant code can execute // other states will have different chains to handle clicks on the same view this.context.setClickHandlerChain(this.chain); } // ...
在 清單 8 中,view 類包含針對一組狀態的一個引用,而且每一個狀態決定了對象將會負責單擊事件的處理。這樣一來,視圖的邏輯限於此視圖身份所表明的邏輯:顯示此選項菜單。若是更新遊戲,以包含更多的按鈕、更漂亮的效果或新視圖的轉換,那麼這裏提供了一個獨立對象,它可以處理每一個新特性,無需更改、中斷或重寫現有邏輯。經過巧妙組合 mousedown、mousemove、mouseup 和 click 事件的責任鏈,並管理從菜單到人物的全部事情,可以以高度結構化、有組織的方式處理拖放庫存屏幕,不會增長代碼的複雜性。
設計模式和 OOP 自己是很中立的概念,將這兩者捆綁使用會帶來一些問題,而不是解決問題。本文提供了 JavaScript 中的 OOP 概述,探討了原型繼承模型和典型繼承模型。咱們瞭解了遊戲中一些常見模式,這些模式可以從 OOP 設計(基本的遊戲循環、狀態機和事件冒泡)的結構和易維護性模式中得到極大的利益。本文只是對常見問題的解決方案進行了簡要介紹。經過實踐,您會熟練掌握如何編寫具備表現力強的代碼,並會最終減小在編寫代碼上花費的時間,增長創做的時間。