文档章节

竟然这就是面向对象的游戏设计?!

柳猫
 柳猫
发布于 06/21 13:59
字数 6314
阅读 444
收藏 1
点赞 0
评论 3

从程序角度考虑,许多 JavaScript 都基于循环和大量的 if/else 语句。在本文中,我们可了解一种更聪明的做法 — 在 JavaScript 游戏中使用面向对象来设计。本文将概述原型继承和使用 JavaScript 实现基本的面向对象的编程 (OOP)。学习如何在 JavaScript 中使用基于经典继承的库从 OOP 中获得更多的好处。本文还将介绍架构式设计模式,来展示了如何使用游戏循环、状态机和事件冒泡 (event bubbling) 示例来编写更整洁的代码。

在本文中,您将了解 JavaScript 中的 OOP,来探索原型继承模型和经典继承模型。举例说明游戏中能够从 OOP 设计的结构和可维护性中获得极大利益的模式。我们的最终目标是让每一块代码都成为人类可读的代码,并代表一种想法和一个目的,这些代码的结合超越了指令和算法的集合,成为一个精致的艺术品。

JavaScript 中的 OPP 的概述

OOP 的目标就是提供数据抽象、模块化、封装、多态性和继承。通过 OOP,您可以在代码编写中抽象化代码的理念,从而提供优雅、可重用和可读的代码,但这会消耗文件计数、行计数和性能(如果管理不善)。

过去,游戏开发人员往往会避开纯 OOP 方式,以便充分利用 CPU 周期的性能。很多 JavaScript 游戏教程采用的都是非 OOP 方式,希望能够提供一个快速演示,而不是提供一种坚实的基础。与其他游戏的开发人员相比,JavaScript 开发人员面临不同的问题:内存是非手动管理的,且 JavaScript 文件在全局的上下文环境中执行,这样一来,无头绪的代码、命名空间的冲突和迷宫式的 if/else 语句可能会导致可维护性的噩梦。为了从 JavaScript 游戏的开发中获得最大的益处,请遵循 OOP 的最佳实践,显著提高未来的可维护性、开发进度和游戏的表现能力。

原型继承

与使用经典继承的语言不同,在 JavaScript 中,没有内置的类结构。函数是 JavaScript 世界的一级公民,并且,与所有用户定义的对象类似,它们也有原型。用 new 关键字调用函数实际上会创建该函数的一个原型对象副本,并使用该对象作为该函数中的关键字 this的上下文。清单 1 给出了一个例子。

清单 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 所示。

清单 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)” 原则,而且很有可能是如今有很多库试图模仿经典继承的最重要的原因。

清单 3. 从子类调用父方法

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 类的特性。

清单 4. 对 Base.js 和经典继承的简介

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;
    }
});

游戏设计中的 OOP 模式

基本的游戏引擎不可避免地依赖于两个函数: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 显示了一个示例。

清单 5. 带有 render 和 update 循环的基本应用程序类

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 中的示例。

清单 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 所示。

清单 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 所示。如果用户单击菜单中的某个按钮,背景中的人物不应对此单击操作有任何反应。

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

心动了吗?还不赶紧动起来,打造属于自己的游戏世界!可能有些同学因为感到学习困难,一头雾水,都知求学不易,我不希望那些想学习的同学因为看不见对岸而放弃大好前程,为方便想学习的有志之士更容易的获取知识上的交流,进一步学习,特此建立了一个C/C++学习群,里面不仅有众多学友相互勉励学习,还有很多大家发的相关资料,还请持续关注更新,更多干货和资料请直接联系我,也可以加群710520381,邀请码:柳猫,欢迎大家共同讨论。

© 著作权归作者所有

共有 人打赏支持
柳猫
粉丝 3
博文 28
码字总数 77271
作品 0
加载中

评论(3)

节节草
节节草
博主辛苦了,有些技术点我是自愧不如,不过为什么要用原生js去做游戏呢?国内不错的几款游戏引擎基本都是基于ts的
柳猫
柳猫
如何简洁实现游戏中的AIhttps://my.oschina.net/u/3875054/blog/1831913
柳猫
柳猫
手把手教你建立一个Java游戏引擎https://my.oschina.net/u/3875054/blog/1831112
大神级回答---【面向对象和面向过程的区别】

最近在学Python,于是乎又想到这个可以说是对每个计算机系的同学刚开始学习最头疼的问题:“什么是面向过程,什么是面向对象”,虽然教科书上都有讲解,网上资料也一堆,但是这么多年一直没让...

1清风揽月1
2017/08/08
0
0
转载知乎上的一篇:“ 面向对象编程的弊端是什么?”

弊端是,没有人还记得面向对象原本要解决的问题是什么。 1、面向对象原本要解决什么(或者说有什么优良特性) 似乎很简单,但实际又很不简单:面向对象三要素封装、继承、多态 (警告:事实上...

since1986
2014/10/13
0
0
【blade04】用面向对象的方法写javascript坦克大战

前言 javascript与程序的语言比如C#或者java不一样,他并没有“类”的概念,虽然最新的ECMAScript提出了Class的概念,我们却没有怎么用 就单以C#与Java来说,要到真正理解面向对象的程度也是...

范大脚脚
2017/11/16
0
0
不要用面向对象编程分散新手程序员的注意力

来源:Ackalrix博客【http://www.ackarlix.com】 编者按:原文作者James Hague是一位修复性程序员(recovering programmer),从上世纪80年代起开始设计视频游戏,属于发烧友级别,用汇编语言...

Ackarlix
2011/03/30
234
1
Silverlight游戏设计(Game Design):(五)面向对象的思想塑造游戏对象

传说,面向对象的开发模式最初是因为程序员偷懒而不小心诞生的。发展至今,人们从最初的热忠于讨论某某语言是否足够面向对象到现在开始更广泛的关注面向对象的思想而不是具体内容。面向对象的...

晨曦之光
2012/03/09
0
0
决战到底--unity3d手机游戏源码下载

源码介绍 决战到底--unity3d手机游戏源码下载 游戏名称:决战到底 开发引擎:unity3D 4.6.0 所用插件:PlayMaker、NGUI、EasyTouch、FXMarker、FT Slasher Volume等 介绍与声明(若有不当之处...

tanglin6263871
2014/10/21
1K
0
一个程序猿自己设计了一款游戏,让所有玩家都变成了程序猿;

  不知何时起,程序猿这个物种总是被在自黑与被黑中生机勃勃……               今天小高要介绍的游戏就是由一名游戏程序猿设计并开发的,它让每个玩家都变成了程序猿……    ...

万能的大白
2017/12/05
0
0
从Native到Web, NaCl学习笔记(三): 3D渲染(DX9迁移到GLES)

NaCl的3D渲染API使用的GLES2.0, 这也很好理解, 因为这已经是公认的跨平台标准了. 手机, 平板, 网页, PC都可以使用. 就算在Windows上, 也有一些基于DX9的GLES2.0实现, 比如ANGLE. 有时候我真在...

长平狐
2012/11/12
128
0
设计模式 (一)——策略模式(Strategy,行为型)

转载自:https://blog.csdn.net/k346k346/article/details/55270962 1.概述 使用设计模式可以提高代码的可复用性、可扩充性和可维护性。策略模式(Strategy Pattern)属于行为型模式,其做法...

anda0109
04/10
0
0
计算机图书封面也疯狂

计算机图书的封面向来不如《花花公子》那种杂志的封面刺激眼球,就像《哪本书是对程序员最有影响、每个程序员都该阅读的书?》列出的所有最著名的图书一样,这种图书以内容取胜。但也有例外,...

oschina
2013/05/20
9.4K
47

没有更多内容

加载失败,请刷新页面

加载更多

下一页

java 重写排序规则,用于代码层级排序

1.dataList 是个List<Map<String,Object>> 类型的数据,所以比较的时候是冲map中获取数据,并且数据不能为空。 2.dataList 类型是由自己定义的,new Comparator<Map<String,Object>> 也是对应......

轻量级赤影
9分钟前
0
0
分布式大型互联网企业架构!

摘要: 开发工具 1.Eclipse IDE:采用Maven项目管理,模块化。 2.代码生成:通过界面方式简单配置,自动生成相应代码,目前包括三种生成方式(增删改查):单表、一对多、树结构。生成后的代码...

明理萝
9分钟前
0
1
对MFC程序的一点逆向分析:定位按钮响应函数的办法

因为消息响应函数保存在AFX_MSGMAP_ENTRY数组中, 观察nMessage、nCode、nID、pfn利用IDA在rdata段中搜索即可, 在IDA中找到代码段基址0x401000,函数地址0x403140, 在WinDbg中运行!addre...

oready
9分钟前
0
0
阻抗匹配与史密斯(Smith)圆图基本原理

参考:http://bbs.eeworld.com.cn/thread-650695-1-1.html

whoisliang
15分钟前
0
0
maven配置文件分离

一、 简介 遇到很多次别人处理的项目,测试环境,本地开发和线上环境的配置不一样,每一次部署都要重新修改配置文件,提交审核代码,才能打包,非常不方便。 其实相信很多人都知道可以使用m...

trayvon
15分钟前
0
0
MacOS和Linux内核的区别

导读 有些人可能认为MacOS和Linux内核有相似之处,因为它们可以处理类似的命令和类似的软件。甚至有人认为苹果的MacOS是基于linux的。事实上,这两个内核的历史和特性是非常不同的。今天,我...

问题终结者
31分钟前
1
0
SpringBoot | 第八章:统一异常、数据校验处理

前言 在web应用中,请求处理时,出现异常是非常常见的。所以当应用出现各类异常时,进行异常的捕获或者二次处理(比如sql异常正常是不能外抛)是非常必要的,比如在开发对外api服务时,约定了响...

oKong
39分钟前
2
0
mysql高级

一、存储引擎 InnoDB MyISAM 比较 二、数据类型 整型 浮点数 字符串 时间和日期 三、索引 索引分类 索引的优点 索引优化 B-Tree 和 B+Tree 原理 四、查询性能优化 五、切分 垂直切分 水平切分...

丁典
59分钟前
1
0
rsync通过同步服务、系统日志、screen工具

rsync通过后台服务同步 在远程主机中建立一个rsync服务器,在服务器上配置好rsync的各种应用,然后将本机作为rsync的一个客户端连接远程的rsync服务器。 首先在A机器上建立并且配置rsync的配...

黄昏残影
今天
5
0
Spring Cloud Gateway 接口文档聚合实现

在微服务架构下,通常每个微服务都会使用Swagger来管理我们的接口文档,当微服务越来越多,接口查找管理无形中要浪费我们不少时间,毕竟懒是程序员的美德。 由于swagger2暂时不支持webflux 走...

冷冷gg
今天
150
2

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部