[译]终极塔防——运用HTML5从头建立一个塔防游戏

翻译共享一篇CodeProject的高星力做,原文地址:http://www.codeproject.com/Articles/737238/Ultimate-Tower-Defensejavascript

article

 

介绍

塔防游戏是一种很是简单的游戏。维基百科对它的解释是,一个塔防游戏的目标在于“试图阻止敌人经过地图:经过各类陷阱来延缓它们的行进、同时搭建各类炮塔来射击消灭它们...”。敌对单元有各类基本属性(如速度、生命值)。防护塔则各自具备不一样的能力,不过无例外地都须要消耗金钱来购买,金钱是由玩家经过击败入侵的敌人来取得的。css

本文会带您构建一款基础级的塔防游戏,它易于扩展而且已经开源。咱们将运用网间流行的HTML五、CSS三、和JavaScript来搭建。图形部分会用到<canvas>来实现,不过图形和其余部分之间实际上是松耦合的、彻底能够用其余技术(例如<div>加一些CSS)来替代实现。html

本项目并不依赖于任何第三方代码例如jQuery、AngularJS、或任何游戏引擎。彻底是从零基础创建起来。这其实要比不少人想象的容易得多,并且能给咱们额外的自由度。另外一个考量就是能够免去因包含进一些非必须的函数而带来的负担。最后但并不是最不重要的是,这样作也使得编写出一篇简单易上手的指南成为了可能。html5

 

背景

我每一年都会开几回培训课,讲授当下流行的技术,如C#开发、HTML5/CSS3/JavaScript网页应用开发。我每每会投入至关多的热情来对待这些课程。缘由之一就是它们的学习曲线一般都是并不平坦的,尽管也的确有有天赋的学生能单单靠讲义就融会贯通的。另外一个缘由就在于结业项目会很是棒。每次我都惊喜于仅仅两周多时间所能创造出的成果,真的是从“新手”到“高手”!java

在这方面一件我感受很酷的事就是能把“项目构想”灌输给个人学生。要知道我是一个充满了想法的人,甚至我都感受这成了一个麻烦了,由于我实在都找不到时间(至少是不能以我所能接受的高效方式)来实现它们!然而做为一个结业项目,学生们固然就能够走走捷径,而且把进度停在某个点上、只要这个点自己有价值。这样一来,不只个人学生们能学到不少劲酷的内容,我本身也能收获到很多有价值的东西。这种方式至少能验证我脑中所想的到底是否有助于解决问题、以及能解决到何种程度。算法

多数的项目,其实就是游戏~。这并不是是源自客户需求,但却的确是有道理的。当咱们构思某个课题来做结业项目时,它每每应该对咱们能派得上用场才最好。但是,大多数人在某个时间点上并不会都特别须要某种应用。而打造一个游戏在这方面就颇有优点,由于它能带给咱们不少欢乐。并且别人也可能会喜欢它,游戏并不能解决真正的问题、但它能创造出新的问题(一种任务),这种任务也只有在这个游戏中才能得到解决。数据库

固然,大多数学生并不曾写过游戏——至少是有画面的游戏。所以他们会面对“游戏引擎”的入门学习以便于去用到它。而我会教授他们如何写一个简单的游戏引擎、以及如何设计他们的游戏。有时我还能给他们一些有用的算法或实现。另外一个关键但容易被忽略的地方是去哪里搜寻那些资源,例如声音、图像,所幸的是个人硬盘里存有大量的优质连接和资源文件。编程

这个塔防游戏最先是在C#培训中被开发的。用到了SDL.NET作渲染、以及DirectX来播放声音。多数的角色是用手工画出来的,这使得游戏有些"像素"怀旧风格(...)。后来我考虑把这个项目进一步修改为JavaScript版,最终我以为这必定会是一次不错的实验:究竟我能多快多好地、把这些有趣的C#游戏代码转成网页版?(请拭目以待吧)canvas

 

游戏引擎的要素

游戏引擎是一段用来负责游戏的图形绘画、声音播放、以及逻辑演绎的代码。这三项职责应该尽量地被分隔开。若是咱们可以精确地解耦它们,那么代码就会是真正具备可扩展性、而且易维护的。尽管视频循环是“时间无关”的(例如以尽量高的频率来执行画面更新,只要硬件容许),但逻辑循环倒是“时间相关”的(例如每隔一个预设的时间间隔作一次处理)。记住这一点很是重要:一个游戏有时对某台设备来讲多是画面负荷太重的。而此时逻辑循环仍旧是试图以它的“节奏”来运行,表如今画面上就会变“卡”。此类结果源自于这样一种架构:一套步数固定的逻辑步骤,要匹配到一套(视硬件配置而定)步数可变的绘画步骤上去。浏览器

在游戏中咱们把主要的逻辑处理放在了一个叫作GameLogic的类中。经过调用start()方法能触发该逻辑处理。从那一时点上JavaScript引擎就能开始以固定间隔调用tick()函数(这一间隔被定义在了contants.ticks中)。只有当前一个逻辑循环已经再也不运行时新的逻辑循环才能被触发。

var GameLogic = Base.extend({
    /* ... */
    start: function() {        
        /* ... */
        if (!this.gameLoop) {
            var me = this;
            me.view.start();
            me.gameLoop = setInterval(function() {
                me.tick();
            }, constants.ticks);    
        }
    },
    tick: function() {
        /* ... */
    },
    pause: function() {
        if (this.gameLoop) {
            this.view.pause();
            clearInterval(this.gameLoop);
            this.gameLoop = undefined;    
        }
    },
);

逻辑类预先就知道会有一个View存在。但它并不知道具体的View类、也不知道start(),stop()以外的任何方法。当逻辑循环开始时,视频循环也该被同时开始。此外当逻辑循环暂停时咱们也将中止绘画操做。

UI之间的交互经过事件来完成,包括两个方向上的事件:

  • 来自UI元素的事件,例如点击了一个按钮
  • 来自游戏逻辑的事件,例如一波攻击已经结束

游戏逻辑层所用到的事件系统是用JavaScript来实现的。咱们使用一个对象来负责管理已被注册的事件、以及相关事件的侦听者。每一个事件都能有任意多的侦听者。

var Base = Class.extend({
    init: function() {
        this.events = {};
    },
    registerEvent: function(event) {
        if (!this.events[event])
            this.events[event] = [];
    },
    unregisterEvent: function(event) {
        if (this.events[event])
            delete this.events[event];
    },
    triggerEvent: function(event, args) {
        if (this.events[event]) {
            var e = this.events[event];
            for (var i = e.length; i--; )
                e[i].apply(this, [args || {}]);
        }
    },
    addEventListener: function(event, handler) {
        if (this.events[event] && handler && typeof(handler) === 'function')
            this.events[event].push(handler);
    },
    removeEventListener: function(event, handler) {
        if (this.events[event]) {
            if (handler && typeof(handler) === 'function') {
                var index = this.events[event].indexOf(handler);
                this.events[event].splice(index, 1);
            } else
                this.events[event].splice(0, this.events[event].length);
        }
    },
});

派生类经过registerEvent()来注册事件(一般在它们的init()中)。triggerEvent()被用于触发一个事件。侦听者能经过addEventListenerremoveEventListener来分别注册、注销到一个事件。其它就和一般在JavaScript中注册/注销到某个UI控件的事件处理器同样了。

最后咱们能够这样来写:

logic.addEventListener('moneyChanged', function(evt) {
    moneyInfo.textContent = evt.money;
});

这样就能把游戏逻辑和UI联系到了一块儿。

 

构建一个塔防游戏

塔防游戏并不很难搭建。由于有这么几个缘由:

  • 一个基础级的塔防游戏每每是回合制的
  • 很适合用粗糙的网格就能表现出来
  • 只需用到很基本的物理原理
  • 规则很是简单直接

任何塔防游戏的核心(就和不少策略游戏中同样)就是路径搜寻算法。咱们没必要去应对成千上万个游戏单元,所以也无需去寻求一个快速算法。在这个范例项目中咱们能够就采用著名的A*算法,它在各类语言中几乎都有多种版本的实现。其中之一就是个人实现~,移植自它的C#版本。若是你关心它是如何实现的,能够阅读个人相关文章。文中也包括了使用单一(固定)策略的一段简短演示的连接

在此,用于存储不一样迷宫策略的枚举型对象将是这个样子的:

var MazeStrategy = {
    manhattan        : 1,
    maxDXDY         : 2,
    diagonalShortCut : 3,
    euclidean        : 4,
    euclideanNoSQR : 5,
    custom         : 6,
    air             : 7
};

一般游戏单元会以Manhattan计量方式来走过迷宫。Manhattan是一种较为特殊的计量方式,它不容许走对角线捷径。在Manhattan方式中,从(1,1)走到(2,2)被算做至少须要2步。相对而言在更为普通的Euclidean计量方式中,从(1,1)走到(2,2)会被只算做1步。

还有其余的计算方式会被用在游戏中(好比不对平方距离计算平方根的Euclidean算法的变体,在某些状况下它的结果是不一样于标准Euclidean算法的)。固然,在全部计算方式中,air策略堪称是最“了不得”的:它会令一切优秀的路径算法黯然失色,由于它熟知忽略掉一切障碍物直取目标的方式才是“最短路径”;这种策略只能被用在一种游戏单元上,而这种游戏单元也只能由一种塔防单元来击落——那就是、防空塔。

一个塔是经过继承Tower类来实现的。这个类的代码概要以下:

var Tower = GameObject.extend({
    init: function(speed, animationDelay, range, shotType) {
        /* ... */
    },
    targetFilter: function(target) {
        return target.strategy !== MazeStrategy.air;
    },
    update: function() {
        this._super();
        /* ... */
    },
    shoot: function() {
        /* ... */
    },
});

targetFilter()用来过滤塔防的攻击目标。全部的塔,除了防空塔,只会用一种标准过滤器,就是过滤掉空军单位。防空塔的代码只须要覆盖掉缺省方法就行。

var Flak = Tower.extend({
    init: function() {
        this._super(Flak.speed, 200, Flak.range, Flak.shotType);
        this.createVisual(Flak.sprite, [1, 1, 1, 1]);
    },
    targetFilter: function(target) {
        return target.strategy === MazeStrategy.air;
    },
});

构造函数init(),只需带着一些特定参数调用基类的构造函数便可。此外就是建立塔的视觉效果。一个视觉效果类中包含了完整的动画对象的信息,例如全部的帧、带方向的移动、以及动画对象的图像源。

每一个塔都定义了一种发射类型,也就是特定的shot类的类别。用JavaScript的语言来讲,就是一个指向能用来实例化特定shot对象的构造函数的引用。

全部发射类型的基类都以下:

var Shot = GameObject.extend({
    init: function(speed, animationDelay, damage, impactRadius) {
        /* ... */
    },
    update: function() {
        /* ... */
    },
});

Flak塔(防空塔)中咱们定义的发射类型指向的就是AirShot。它的构造函数很是简单,以下:

var AirShot = Shot.extend({
    init: function() {
        this._super(AirShot.speed, 10, AirShot.damage, AirShot.impactRadius);
        this.createVisual(AirShot.sprite, [1, 1, 1, 1], 0.2);
        this.playSound('flak');
    }, });

这里并无定义发射目标,而是应该由实例化发射对象的塔来配置一个列表、管理全部可能的发射目标。由于AirShot只被Flak塔(防空塔)用到,它也就只能把空军单位做为目标。(发射类的)构造函数看上去都很近似,主要区别也就在于被实例化以后的那一声“炮响”(会用到不一样的音效)。

下图展现了在通过了若干行动以后的游戏画面:

action

那么什么能被防护塔做为发射目标呢?很好,这样的目标就来自于“游戏单元”。显然的,在此咱们能够遵循以前的策略,咱们将使用一个Unit类来做为全部相关派生对象的基类。

var Unit = GameObject.extend({
     init: function(speed, animationDelay, mazeStrategy, hitpoints) {
         /* ... */
     },
     playInitSound: function() {
         /* ... */
     },
     playDeathSound: function() {
         /* ... */
     },
     playVictorySound: function() {
         /* ... */
     },
     update: function() {
         /* ... */
     },
     hit: function(shot) {
         /* ... */
     },
});

游戏里有几种单位。游戏的平衡性主要就依赖于建立一些好的攻击波算法,从而使得游戏有难度,但又并不是不可能完成。让咱们看看各类单元类型:

  • mario马里奥(Mario) - 一种很是好对付的小怪
  • rope草蛇(Rope) - 只增长了一点点难度(更多的生命值)
  • wizzrobe火法师(Fire Wizzrobe) - 很是快速,但没有多少生命值
  • airwolf空中战狼(Air Wolf) - 游戏中惟一的飞行单位
  • darknut黑骑士(DarkNut) - 速度还能够,可是生命值很高
  • speedy极速精灵(Speedy) - 游戏中最快速的单位,并且颇有些生命值
  • armos重装者(Armos) - 最高生命值单位,但也是速度最慢的

添加一种新的单元很是简单(并且实际上也颇有趣!)。设计一个新游戏单元的关键问题在于:这一单元应该在社么时候出现,以及具备什么属性(主要是速度、装甲)。

做为一个例子,咱们看一下马里奥(Mario)单元的实现。以下代码将把Mario单元加入全部单元的集合中。

var Mario = Unit.extend({
    init: function() {
        this._super(Mario.speed, 100, MazeStrategy.manhattan, Mario.hitpoints);
        this.createVisual(Mario.sprite, [8,8,8,8]);
    },
}, function(enemy) {
    enemy.speed = 2.0;
    enemy.hitpoints = 10;
    enemy.description = 'You have to be careful with that plumber.';
    enemy.nickName = 'Mario';
    enemy.sprite = 'mario';
    enemy.rating = enemy.speed * enemy.hitpoints;
    types.units['Mario'] = enemy;
});

第一部分控制了Mario实例,第二部分则只是设置了静态属性(会被应用到全部实例)。在createVisual()中,会从一个可用动画对象的列表中加载其动画对象。

 

游戏范例

要能从上述各段代码升级到一个能运行的游戏,咱们得把各样东西捆绑起来。让咱们用一份很简单的HTML模板来开头:

<!doctype html>
<html>
<head>
<meta charset=utf-8 />
<title>Tower Defense Demo</title>
<link href="Content/style.css" rel="stylesheet" />
</head>
<body>
<div id="frame" class="hidden">
    <div id="info">
        <div id="money-info" title="Money left"></div>
        <div id="tower-info" title="Towers built"></div>
        <div id="health-info" title="Health left"></div>
    </div>
    <canvas id="game" width=900 height=450>
        <p class="error">Your browser does not support the canvas element.</p>
    </canvas>
    <div id="towers"></div>
    <div id="buttons">
        <button id="startWave">Start Wave</button>
        <button id="buyMedipack">Buy Medipack</button>
        <button id="buyTowerbuild">Buy Towerbuild</button>
    </div>
</div>
<script src="Scripts/manifest.js"></script>
<script src="Scripts/oop.js"></script>
<script src="Scripts/utilities.js"></script>
<script src="Scripts/path.js"></script>
<script src="Scripts/resources.js"></script>
<script src="Scripts/video.js"></script>
<script src="Scripts/sound.js"></script>
<script src="Scripts/main.js"></script>
<script src="Scripts/logic.js"></script>
<script src="Scripts/units.js"></script>
<script src="Scripts/shots.js"></script>
<script src="Scripts/towers.js"></script>
<script src="Scripts/app.js"></script>
</body>
</html>

好吧,这可能有点超出了一个最低限度的游戏范例的要求,不过这比起一个很是考究的、要用到全部游戏所提供的信息的范例而言仍是要简单得多了。

全部JavaScript文件都能被捆绑并最小化。网页开发框架例如ASP.Net MVC会自动作这些,或者咱们能够写一些脚原本把这做为构建任务来执行。那么除此以外咱们还有什么? 最重要的元素就是<canvas>,它被放在一个由<div>来标记的frame框的正中。

有3个按钮被用来控制游戏。咱们能让新的一波攻击开始(在此以前请布置好防护)、购买一个医疗包、或是购买一个额外的塔防建造权。可建造的塔防的数量是受限制的。构建额外的塔防所需的开销是会随着已容许建造的塔防的数量而递增的。

咱们能怎么建造塔防?好吧,这个没法直接从上面的代码看出。咱们会用到一个带标识符towers<div>。这会被做为一个容器,里面装载着相关的防护塔类型。已有的JavaScript以下:

var towerPanel = document.querySelector('#towers');
var towerButtons = [];
var addTower = function(tower) {
    var div = document.createElement('div');
    div.innerHTML = [
        '<div class=title>', tower.nickName, '</div>',
        '<div class=description>', tower.description, '</div>',
        '<div class=rating>', ~~tower.rating, '</div>',
        '<div class=speed>', tower.speed, '</div>',
        '<div class=damage>', tower.shotType.damage, '</div>',
        '<div class=range>', tower.range, '</div>',
        '<div class=cost>', tower.cost, '</div>',
    ].join('');
    towerButtons.push(div);
    div.addEventListener(events.click, function() {
        towerType = tower;
        for (var i = towerButtons.length; i--; )
            towerButtons[i].classList.remove('selected-tower');
        this.classList.add('selected-tower');
    });
    towerPanel.appendChild(div);
};
var addTowers = function() {
    for (var key in types.towers)
        addTower(types.towers[key]);
};

因而咱们只需触发addTowers()方法,它会对全部的塔作循环、为每一种塔建立并添加一个按钮。

CSS文件并不容易看懂,好在<canvas>控件也并不须要用到任何风格。因此风格的改善就留待但愿拥有更专业游戏外观的的开发者来作吧。

 

类图

重写整个游戏的另外一个目的,是源自于想用面向对象的方式把全部东西从新描述一番。这会使得编程更为有趣和简单。并且最终的游戏也会更少有Bug。下面这张类图就是在建立这个游戏之初所作的筹划:

clsdiagram-preview

游戏严格遵循着这份类图。扩展这个游戏实际上简单到只需把它做为一个模板、就基本上能扩展到任何塔防游戏。理论上也很容易把战场扩展成其余类型,例如泥沼(译者:仍是MUD游戏?)、传送门等等。这里的一个技巧就是改用其余的、在构建时不会反射出0权重的方格(译者:此处不甚理解)。这已经被包括在代码内了,可是尚未被正式用。

下一节咱们将看到怎样运用现有的代码来发行咱们本身的塔防游戏。

 

运用代码

我所给出的代码并不表明一个游戏的完成态。相反,它表明的是一系列塔防游戏的模板。我所提供的网页应用,只是运用到了代码的各个不一样部分来合成一个简单游戏的范例。

资源加载器(resource loader)是一个颇为有趣的类。它定义了一个特定的资源加载器所需的核心功能。基本上它只是接收一个资源列表,而加载任务的进度、错误、完成事件、则可经过设置回调函数来取得。

var ResourceLoader = Class.extend({
    init: function(target) {
        this.keys = target || {};
        this.loaded = 0;
        this.loading = 0;
        this.errors = 0;
        this.finished = false;
        this.oncompleted = undefined;
        this.onprogress = undefined;
        this.onerror = undefined;
    },
    completed: function() {
        this.finished = true;
        if (this.oncompleted &&typeof(this.oncompleted) === 'function') {
            this.oncompleted.apply(this, [{
                loaded : this.loaded,
            }]);
        }
    },
    progress: function(name) {
        this.loading--;
        this.loaded++;
        var total = this.loaded + this.loading + this.errors;
        if (this.onprogress && typeof(this.onprogress) === 'function') {
            this.onprogress.apply(this, [{
                recent : name,
                total : total,
                progress: this.loaded / total,
            }]);
        }
        if (this.loading === 0)
            this.completed();
    },
    error: function(name) {
        this.loading--;
        this.errors++;
        var total = this.loaded + this.loading + this.errors;
        if (this.onerror && typeof(this.onerror) === 'function') {
            this.onerror.apply(this, [{
                error : name,
                total : total,
                progress: this.loaded / total,
            }]);
        }
    },
    load: function(keys, completed, progress, error) {
        this.loading += keys.length;
        if (completed && typeof(completed) === 'function')
            this.oncompleted = completed;
        if (progress && typeof(progress) === 'function')
            this.onprogress = progress;
        if (error && typeof(error) === 'function')
            this.onerror = error;
        for (var i = keys.length; i--; ) {
            var key = keys[i];
            this.loadResource(key.name, key.value);
        }
    },
    loadResource: function(name, value) {
        this.keys[name] = value;
    },
});

这个资源加载器有两种实现。一个是为图像而作的,另外一个是为声音。二者加载资源的方式并不相同,由于图像的加载能够很容易地经过以下代码完成

var ImageLoader = ResourceLoader.extend({
    init: function(target) {
        this._super(target);
    },
    loadResource: function(name, value) {
        var me = this;
        var img = document.createElement('img');
        img.addEventListener('error', function() {
            me.error(name);
        }, false);
        img.addEventListener('load', function() {
            me.progress(name);
        }, false);
        img.src = value;
        this._super(name, img);
    },
});

不过,对声音来讲可能就不那么简单了。主要的问题在于,不一样的浏览器支持不一样的声音格式。所以就有必要用到以下的代码了。它会检测浏览器支持何种声音格式(若是有支持的话)、并选择被检测到的格式。这里有个范例,声音格式被限定在MP3和OGG上。

var SoundLoader = ResourceLoader.extend({
    init: function(target) {
        this._super(target);
    },
    loadResource: function(name, value) {
        var me = this;
        var element = document.createElement('audio');
        element.addEventListener('loadedmetadata', function() {
            me.progress(name);
        }, false);
        element.addEventListener('error', function() {
            me.error(name);
        }, false);
        if (element.canPlayType('audio/ogg').replace(/^no$/, ''))
            element.src = value.ogg;
        else if (element.canPlayType('audio/mpeg').replace(/^no$/, ''))
            element.src = value.mp3;
        else
            return me.progress(name);
        this._super(name, element);
    },
});

把这个资源加载器扩展到能支持任意格式其实也很简单,不过,这方面的修改就较为琐碎了、灵活性在这里也并不是大问题。

在这段代码中咱们额外介绍了另一种资源加载器,它并不从ResourceLoader类派生,而是试图捆绑其余的ResourceLoader实例来实现。缘由很简单:最终咱们只须要针对一组资源、指定所需的资源加载器的类型,而加载器会逐一激活相应的加载器、监督整个加载过程。

那么哪些是咱们开发本身的塔防游戏所需作的呢?

  • 定义你本身的资源,并在manifest.js中修改相关的全局变量
  • 定制防护塔,替换/修改tower.js
  • 定制游戏单元,替换/修改units.js
  • 定制发射类,替换/修改shots.js
  • 你想用不一样于<canvas>的东西来作绘图么?能够考虑扩展video.js

用后面的一个简单的启动脚原本组装全部东西。咱们能把这个启动脚本嵌入到一般的文档(html)中。若是咱们想要最小化全部的可执行脚本,你就还须要把它封装在一个IIFE(Immediately-Invoked-Function-Expression)表达式中。它会使得全部的全局变量变成局部可用,这是个很棒的选择。不过这个方法有个问题,就是咱们就不能把启动脚本嵌入到文档中了,由于被嵌入的脚本、将没法从其余脚本文件的一些方法中看到局部变量。

一个很是简单的启动脚本:

(function() {
    "use strict";
    var canvas = document.querySelector('#game');
    var towerType = undefined;
    var getMousePosition = function(evt) {
        var rect = canvas.getBoundingClientRect();
        return {
            x: evt.clientX - rect.left,
            y: evt.clientY - rect.top
        };
    };
    var addHandlers = function() {
        logic.addEventListener(events.playerDefeated, function() {
            timeInfo.textContent = 'Game over ...';
        });
        startWaveButton.addEventListener(events.click, function() {
            logic.beginWave();
        });
        canvas.addEventListener(events.click, function(evt) {
            var mousePos = getMousePosition(evt);
            var pos = logic.transformCoordinates(mousePos.x, mousePos.y);
            evt.preventDefault();
            if (towerType) logic.buildTower(pos, towerType);
            else logic.destroyTower(pos);
        });
    };
    var completed = function(e) {
        addHandlers();
        view.background = images.background;
        logic.start();
    };
    var view = new CanvasView(canvas);
    var logic = new GameLogic(view, 30, 15);
    var loader = new Loader(completed);
    loader.set('Images', ImageLoader, images, resources.images);
    loader.set('Sounds', SoundLoader, sounds, resources.sounds);
    loader.start();
})();

它定义了除了什么防护塔该被建立以外的全部事情。另一个更高级的版本已经被包括在我提供的范例代码中。

 

游戏平衡性

最初的游戏演示版本过于简单。最大的问题在于,小怪的分布被平均化了,以致于即便在高级关卡中一些很弱的怪也会被大量生产出来。并且一些强力怪物、会以和弱怪同样的出现几率出现。

须要选择一个更优的分布来解决这一问题。高斯分布看来是解决这个怪物生产问题的最佳选择。惟一的问题是,咱们该把高斯分布的峰值设置在哪里。峰值决定了咱们但愿那种怪物出现得最多,这将随着关卡变化而变。

咱们须要以代码形式写出一个较简单的高斯随机数生成算法。这并不难,由于咱们能作一个十分简单的Box-Muller转换。

var randu = function(max, min) {
    min = min || 0;
    return (Math.random() * (max - min) + min);
}
var randg = function(sigma, mu) {
    var s, u, v;
    sigma = sigma === undefined ? 1 : sigma;
    mu = mu || 0;
    do
    {
        u = randu(1.0, -1.0);
        v = randu(1.0, -1.0);
        s = u * u + v * v;
    } while (s == 0.0 || s >= 1.0);
    return mu + sigma * u * Math.sqrt(-2.0 * Math.log(s) / s);
}

在这里咱们丢弃了另外一个基于v值来计算得出的值。一般咱们能够保存该值供一下次randg()被调用时用。在这个简单游戏里咱们就不这么节省了。

WaveList也被修改为能在早期产生容易应对的攻击波、而在后期产生更难的。首先咱们使用一个多项式来得出某一轮所应有的怪物数量,这里会用到一些魔法数字,这些魔法是经过将一个多项式应用于某个指定值来活的的。目前产生的行为结果就是,最初几轮只会有少许怪物出现,而从第20关开始会遇到大量怪物。等到第50关时咱们已经要面对同150个怪物的战斗了。

var WaveList = Class.extend({
    /* ... */
    random: function() {
        var wave = new Wave(this.index);
        //The equation is a polynomfit (done with Sumerics) to yield the desired results
        var n = ~~(1.580451 - 0.169830 * this.index + 0.071592 * this.index * this.index);
        //This is the number of opponent unit types
        var upper = this.index * 0.3 + 1;
        var m = Math.min(this.unitNames.length, ~~upper);
        var maxtime = 1000 * this.index;
        wave.prizeMoney = n;
        for (var i = 0; i < n; ++i) {
            var j = Math.max(Math.min(m - 1, ~~randg(1.0, 0.5 * upper)), 0);
            var name = this.unitNames[j];
            var unit = new (types.units[name])();
            wave.add(unit, i === 0 ? 0 : randd(maxtime));
        }
        return wave;
    },
});

怪物选择的最高值是经过upper变量标识的。maxtime只是对每种怪物都把怪物数量乘以1秒钟(?)。高斯的峰值被放在怪物选择的最高值与最低值的正中位置。最高值会随着当前关卡而迁移,最终咱们会到达最强怪物、并把咱们的高斯分布的峰值(中心值)放在那里。这时大多数的怪物都真心很强力,伴随着一些弱一些的怪,而真正的弱怪将少到几乎不可能出现。

chaos

这幅截图展示了从新设计以后的游戏直到很是后面的关卡中的盛况。一个颇为繁琐的迷宫被建立来拖慢怪物们。同时造了不少地狱门,它们能使得即便最强装甲的怪物也停下脚步。最后咱们还要对付不少成群出现的怪物,不然它们会对咱们的塔群形成问题。

在这一开发回合中实现的另外一个特性就是游戏的保存和加载。每当一轮攻击波结束当前的游戏进度会被自动保存。当浏览器发现有已被保存的游戏进度时会提示玩家是否恢复该进度。这使得游戏能被玩得再久都不要紧。

有两个方法被用来实现游戏的保存和加载。第一个是saveState(),它把当前的GameLogic实例转换成了一个可移植对象。在该对象中不存在任何外部引用、而是一个"原子"型数据对象。

var GameLogic = Base.extend({
    /* ... */
    saveState: function() {
        var towers = [];
        for (var i = 0; i < this.towers.length; i++) {
            var tower = this.towers[i];
            towers.push({
                point : { x : tower.mazeCoordinates.x , y : tower.mazeCoordinates.y },
                type : tower.typeName,
            });
        }
        return {
            mediPackCost : this.mediPackCost,
            mediPackFactor : this.mediPackFactor,
            towerBuildCost : this.towerBuildCost,
            towerBuildFactor : this.towerBuildFactor,
            towerBuildNumber : this.maxTowerNumber,
            hitpoints : this.player.hitpoints,
            money : this.player.money,
            points : this.player.points,
            playerName : this.player.name,
            towers : towers,
            wave : this.waves.index,
            state : this.state,
        };
    },
    loadState: function(state) {
        this.towers = [];
        for (var i = 0; i < state.towers.length; i++) {
            var type = types.towers[state.towers[i].type];
            var tower = new type();
            var point = state.towers[i].point;
            var pt = new Point(point.x, point.y);
            
            if (this.maze.tryBuild(pt, tower.mazeWeight)) {
                tower.mazeCoordinates = pt;
                tower.cost = type.cost;
                this.addTower(tower);
            }
        }
        this.mediPackFactor = state.mediPackFactor;
        this.towerBuildFactor = state.towerBuildFactor;
        this.player.points = state.points;
        this.player.name = state.playerName;
        this.setMediPackCost(state.mediPackCost);
        this.setTowerBuildCost(state.towerBuildCost);
        this.setMaxTowerNumber(state.towerBuildNumber);
        this.player.setHitpoints(state.hitpoints);
        this.player.setMoney(state.money);
        this.waves.index = state.wave;
        this.state = state.state;
    },
    /* ... */
});

代码中的第二个方法是loadState()。当获得一个原子型数据对象时,咱们能产生全部的塔防实例并设置好全部的属性。这样(?)咱们就能对原子型数据对象作任何所需的处理了。一个简单天然的方法就是把该对象字符串化(或反之,字符串解析)而后保存到本地存储中。

另外一种可行的方法会涉及一些异步访问,例如将数据对象保存到位于服务器或本地的数据库中、或是cookie中。

lost

这个游戏并不会有“赢家”。所剩惟一的问题就在于:你能走得多远?在某些时间点上你可能会输、这会致使进度被删除重来。而有一个方法就是,在一波新的攻击开始后选择删除进度。在当前版本中,刷新浏览器能够删除当前进度、做为挽回你败局的一种另类手段。

 

有趣之处

(TODO)

回答那个问题: 这样的移植能完成得多快多好?

官方回答是:4个晚上。不过,我也花了一些时间解决原有代码的一些问题、以及正确理解原做者的意图。在JavaScript中作调试要比在C#中可贵多(尽管我自认很是熟悉JavaScript了)。最大的问题并不在于通常的算法或实现。主要的时间消耗源自于动态类型系统(dynamic type system)使得那些琐碎的本来能被浏览器自动定位到的类型错误全都被隐藏了起来(那个难找啊..)。我认可,若是使用TypeScript的话本来会有助于解决这个问题。TypeScript也会使得如今的OOP方式再也不须要,由于它本身包含了使用类的关键词、能产生出和如今的运行时代码一样优美的代码。不过TypeScript也有本身的问题——在我感受中、对同等项目开发而言、若是用TypeScript来作开发周期会被拖得更长。

这游戏能在线玩么?

固然,我把它放在了个人网页上(版本与个人范例有少量不一样)。你能在html5.florian-rappl.de/towerdefense访问到。若是你有评价、建议、改进,你能够在下方(CodeProject文章原址上)以任何形式发给我(原做者)反馈。

 

编辑履历

(请参照原文)

 

版权

本文及全部相关代码,遵循CPOL(The Code Project Open License)版权规定

 

关于做者

(请参照原文)

 

知识共享许可协议

本做品由楚天译做,采用知识共享署名 3.0 中国大陆许可协议进行许可。欢迎转载、演绎、或用于商业目的,可是必须保留本文的署名(包含连接),或联系做者协商受权。

相关文章
相关标签/搜索