从程序角度考虑,许多 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 设计(基本的游戏循环、状态机和事件冒泡)的结构和易维护性模式中得到极大的利益。本文只是对常见问题的解决方案进行了简要介绍。经过实践,您会熟练掌握如何编写具备表现力强的代码,并会最终减小在编写代码上花费的时间,增长创做的时间。