首頁 web前端 H5教程 JavaScript 游戏中的面向对象的设计

JavaScript 游戏中的面向对象的设计

May 17, 2016 am 09:08 AM
html5

       简介

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

       JavaScript 中的 OPP 的概述

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

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

       原型继承

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

       清单 1. 用原型构建一个对象


// 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. 通过原型化的简单继承

// 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. 从子类调用父方法

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 和经典继承的简介

// 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 循环的基本应用程序类

// 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 // delegate rendering logic to each view layer
(this.viewStack[i]).render();
}
},

update: function() {
for (var i = 0, len = this.entities.length; 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,将它用于绑定。


12下一页
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

HTML 中的表格邊框 HTML 中的表格邊框 Sep 04, 2024 pm 04:49 PM

HTML 表格邊框指南。在這裡,我們以 HTML 中的表格邊框為例,討論定義表格邊框的多種方法。

HTML 中的巢狀表 HTML 中的巢狀表 Sep 04, 2024 pm 04:49 PM

這是 HTML 中巢狀表的指南。這裡我們討論如何在表中建立表格以及對應的範例。

HTML 左邊距 HTML 左邊距 Sep 04, 2024 pm 04:48 PM

HTML 左邊距指南。在這裡,我們討論 HTML margin-left 的簡要概述及其範例及其程式碼實作。

HTML 表格佈局 HTML 表格佈局 Sep 04, 2024 pm 04:54 PM

HTML 表格佈局指南。在這裡,我們詳細討論 HTML 表格佈局的值以及範例和輸出。

HTML 輸入佔位符 HTML 輸入佔位符 Sep 04, 2024 pm 04:54 PM

HTML 輸入佔位符指南。在這裡,我們討論 HTML 輸入佔位符的範例以及程式碼和輸出。

HTML 有序列表 HTML 有序列表 Sep 04, 2024 pm 04:43 PM

HTML 有序列表指南。在這裡我們也分別討論了 HTML 有序列表和類型的介紹以及它們的範例

HTML onclick 按鈕 HTML onclick 按鈕 Sep 04, 2024 pm 04:49 PM

HTML onclick 按鈕指南。這裡我們分別討論它們的介紹、工作原理、範例以及各個事件中的onclick事件。

在 HTML 中移動文字 在 HTML 中移動文字 Sep 04, 2024 pm 04:45 PM

HTML 中的文字移動指南。在這裡我們討論一下marquee標籤如何使用語法和實作範例。

See all articles