[譯]終極塔防——運用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 中國大陸許可協議進行許可。歡迎轉載、演繹、或用於商業目的,可是必須保留本文的署名(包含連接),或聯繫做者協商受權。

相關文章
相關標籤/搜索