终极塔式防御

分享于 

44分钟阅读

Web开发

  繁體

介绍

塔式防御游戏是一个非常简单的游戏。 维基百科说,一个塔防御游戏的目标是"。 to敌人from地图 slow slow slow,slow,towers,properties,properties,properties,properties,properties,properties,properties,properties units。 塔有不同的功能,必须通过花钱来购买。 玩家通过战胜敌人的敌人获得金钱。

本文将构建一个基本的塔式防御游戏,它易于扩展和开放。 我们将使用 HTML5.CSS3和JavaScript为现代网络构建这个游戏。 图形将使用 <canvas> 元素完成,但是图形是完全耦合的,并且可以很容易地改变为 <div> 元素。

项目本身不包含对 3rd 方代码( 如 jQuery。AngularJS或者任何现有游戏引擎)的任何依赖。 相反,一切都是从头开始构建的。 这比许多人认为的要容易得多,并给我们一些额外的自由。 另一个原因是避免了包含不必要函数的潜在开销。 最后但并非最不重要的是,这会给有用的教程带来极大的可能性。

背景

每年我会在艺术技术的状态。比如"使用 C# 编程"或者"。使用 HTML5,CSS3和javascript的网络应用程序"上进行几次讲讲。 通常我对那些课程非常热心。 一个原因是学习曲线相当陡峭,有天才的学生会找到一种方法来处理所呈现的材料,或者更容易地使用这些工具。 另一个原因是在最终的项目中。 每次工作两周后我真的很高兴。 从零到英雄( 够近) !

这个概念的酷事是我可以给我的学生"项目构思"。 我充分的想法,这是我个人的问题,因为我永远找不到时间去完成它们。 然而对于 final 项目的学生来说,当然可以制作一些快捷方式,并且在任意的时候停止。 这样不仅可以学习很多酷的东西,而且我可以教自己一个或者另一个有用的位置。 如果我想解决一个特定问题的想法是( 它有多好)的话,至少这是一个好的交叉检查。

大多数项目实际上是游戏。 虽然不是一个要求,但如果我们考虑它,那是有意义的。 如果我们有特定的项目,那么这个应用程序实际上可以帮助我们,因此我们可以将它作为 final 项目。 但是,大多数人不需要特定的时间点应用特定类型的应用程序。 创建一个游戏有一些好处,这很有趣,在一般情况下使用。 另外一些人也可能喜欢它,因为游戏永远不会解决问题,而是创建了一个新的问题。 这个问题只能用应用程序来解决( 例如。 游戏本身。

当然大多数学生从来没有写过游戏- 或者至少是一个图形游戏。 因此,他们需要了解游戏引擎是什么,他们可能需要它。 所以我教他们如何写一个简单的游戏引擎以及如何设计他们的游戏。 有时我给他们一些有用的算法或者实现。 另一个重要的( 但经常被忽略) 问题是在哪里获取所有这些资源,比如 声音和图形。 幸运的是,我在硬盘上存储了许多好的url和资源。

这个塔式防御游戏最初是在 C# 课程中开发的。 它使用 SDL.NET 渲染声音,使用DirectX播放声音。 大多数精灵都画在绘画中,这使得游戏看起来有些复古。 现在我决定通过将它移植到JavaScript来改进这个项目。 最后我认为这是个很好的测试: 我如何将这种特殊类型的代码聚合到网站上?

游戏引擎要点

游戏引擎是负责绘制图形。播放声音和旋转逻辑的一段代码。 这三个职责应该尽可能分开。 如果我们能够完美地分离它们,那么代码就非常灵活和易于维护。 视频循环是时间独立的,换句话说,正常执行 body,逻辑循环通常是时间依赖的换句话说,脉冲。 这非常重要:有时候游戏对于某些计算机来说可能太强。 虽然逻辑仍然以固定的速率运行,但图形看起来可能会失败。 这是架构的一种效果: 固定数量的图形步骤的固定数量的逻辑步骤。

在我们的游戏中,我们把主逻辑例程放在一个叫做 GameLogic的类中。 通过调用 start() 方法,我们将触发逻辑。 JavaScript引擎的那个点将以 constants.ticks 中定义的常量间隔调用 tick() 方法。 如果没有逻辑循环运行,我们将只启动逻辑循环。

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

逻辑类已经知道有类似于视图的东西。 但是,它不知道具体的视图,也不知道除 start()stop() 之外的任何方法。 当逻辑循环开始时,视频循环也应该开始。 另外,我们希望在逻辑暂停时结束连续图形绘制。

UI的交互将通过事件完成。 这里连接有两个方面:

  • 由用户界面元素触发的事件,如单击按钮
  • 游戏逻辑触发的事件,如wave完成

游戏逻辑所使用的事件系统是在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);
 } elsethis.events[event].splice(0, this.events[event].length);
 }
 },
}); 

派生类 register 事件( 通常在他们的init() 方法中) 使用 registerEvent() 方法。 triggerEvent() 方法用于激发事件。 使用 addEventListener()removeEventListener() 方法可以注册或者注销侦听器。 然后,它与( un - ) 将事件处理程序注册到JavaScript中的任何UI元素非常类似。

最后,我们将编写如下代码:

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

这将帮助我们将游戏逻辑和UI连接起来。

构建塔式防御游戏

塔式防御游戏不是很难构建的。 有以下几种原因:

  • 基本的塔式防御游戏可能是圆
  • 它们非常适合粗网格。
  • 只需要非常基本的物理
  • 这些规则实际上是非常简单和直接

任何塔式防御游戏的核心是( 在大多数策略游戏中) 良好的路径查找算法。 因为我们不会处理成千上万的单元,所以我们对一个非常快速的算法不感兴趣。 对于这个示例项目,我们可以使用著名的A* 算法。 几乎所有编程语言中都有几个实现。 一个例子是我的实现,它更多或者 LESS 是 C#的一个端口。 如果你对我的工作有兴趣,那就直接阅读我的网页上的文章。 本文还包含了的链接,使用了一个单一的策略。

在这种情况下,存储不同迷宫策略的枚举类似于下面的对象:

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

通常单位在迷宫中移动曼哈顿的公制。 ,指令是一个特殊的,它不允许对角线快捷方式。 在曼哈顿,从( 1,1 ) 到( 2,2 )的metric至少需要 2个步骤。 作为比较,普通的欧几里德度量允许对角线移动。 在这里我们只需一步就可以从( 1,1 ) 到( 2,2 )。

还有其他度量,在游戏( 与一个欧氏度量没有平方根的欧氏度量,这将不同于普通欧氏在某些情况下) 中使用。 在这些指标中,空气策略是最显著的。 通过知道最短距离只忽略所有的障碍,它将使任何计算过时。 这种策略由一个单一的单元使用,只能被一个特殊的防空塔攻击。

塔是通过从 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() 方法,它使用特定参数调用基构造函数。 此外,还创建了塔楼的视觉。 视觉存储整个sprite信息,如帧。方向移动和sprite图像源。

每个塔都定义了一个镜头类型,它就是一个适当的射击类的类型。 在JavaScript术语中:这是对构造函数的引用,该函数可以用于实例化正确的快照对象。

每个快照类型的基类具有以下轮廓:

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

对于 Flak 塔,我们定义 shotType 属性作为对 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');
 },
}); 

目标没有定义。 可以能的目标的List 总是由塔设置,这样实例化了快照对象。 由于 AirShot 只能被 Flak 塔使用,所以它总是只对空气单元有效。 构造器看起来相当相似。 主要区别是,镜头对象在实例化后也会播放声音。

下面的屏幕截图显示了游戏中的。

Tower Defense action

那么tower的目标是什么? 这些目标以一个单位的形式出现。 在这一点上,我们应该遵循与之前相同的策略。 我们将使用一个名为 Unit的基类作为每个派生对象的样板。

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

这个游戏中有几个单位。 平衡一切都取决于创建一些好的波形算法,这将使游戏难以实现,但不可以能。 让我们看一下各种单元类型:

  • Tower Defense Unit Mario,opponent easy
  • Tower Defense Unit RopeRope ( 更多 hitpoints )
  • Tower Defense Unit FireWizzrobeFire Wizzrobe - 相当快,但不多
  • Tower Defense Unit AirWolf游戏中唯一的空中单位
  • Tower Defense Unit DarkNutDarkNut - 速度不错,hitpoints更高
  • Tower Defense Unit Speedy快速游戏中最快的单位,退出一些 hitpoints
  • Tower Defense Unit ArmosArmos - 多数 hitpoints,但也是最慢的单位

添加一个新单元相当简单。 新单元设计中的关键问题是: 单元应什么时候出现以及( 大多数是速度,装甲) 应考虑哪些属性?

作为一个例子,我们考虑了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的实例,而第二部分只设置 static 属性,这些属性适用于所有实例。 sprite将从 createVisual() 方法中可用子画面的List 加载。

示例游戏

为了得到所提供代码的工作游戏,我们必须将所有内容捆绑在一起。 我们首先使用一个非常简单的HTML样板代码:

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

不过,这比一个简单的例子多,但是它比使用游戏提供的每一个信息都要复杂得多的一个例子。

所有这些JavaScript文件都可以打包和最小化。 Web框架如 ASP.NET MVC自动完成这一任务,或者我们编写一些执行该任务的脚本作为一次构建。 我们还有什么其他的? 最重要的元素是放置在 <div> 元素给出的框架中心的<canvas> 元素。

这三个按钮负责控制游戏。 我们可以开始一个新的波形( 我们早应该准备好了),买一个medipack或者买另一个塔。 可能的塔数量有限。 购买建筑物的权利还有一些成本会随着可以能的塔数而增长。

我们怎么才能建造塔? 这并不能直接从 上面 代码中看到。 我们将使用带有 id 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> 元素在没有任何样式的情况下效果很好。 因此,造型留给那些真正关注专业寻找他们游戏的人。

类关系图

重写整个游戏的目的之一是激励以面向对象的方式描述一切。 这将使编码更有趣,更简单。 此外,final 游戏将包含 LESS Bug。 在创建游戏之前,计划了以下类图:

Class diagram of the game

这个游戏紧跟着这个类图。 扩展游戏实际上像使用游戏作为任意塔防游戏的基本样板一样简单。 理论上它也应该非常容易地扩展与它的他类型的战斗场,如泥浆。门和更多。 这里的技巧是使用它的他的砖块,在构建时不能反映 0的重量。 但是,它已经包含,但它不在使用中。

在下一节中,我们将看到如何使用现有代码发布我们自己的塔式防御游戏。

使用代码

给定的代码不代表已经完成的游戏。 它代表了一种塔式防御游戏样板。 提供的web应用程序仅仅是一个使用代码的各个部分来表示示例游戏的示例。

资源加载器是一个非常有趣的类。 它定义了专用资源装载器的核心功能。 基本上它只是接收资源的List,可以选择用回调函数进行进度。错误和完整指示。

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 MP3和。

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;
 elseif (element.canPlayType('audio/mpeg').replace(/^no$/, ''))
 element.src = value.mp3;
 elsereturn me.progress(name);
 this._super(name, element);
 },
});

通常,将这个资源加载程序扩展到任意数量的格式是相当简单的,但是由于调整是微不足道的,所以灵活性不是一个问题。

另外,我们还引入了另一种资源加载器,它不从 ResourceLoader 派生。 相反,它只是尝试捆绑其他 ResourceLoader 实例。 原因很简单: 最后我们必须指定一组资源要使用的资源加载器。 整个加载过程将由这个加载器进行监督,它只会连续触发给定的资源装载器。

那么我们需要什么才能推出我们自己的塔式防御游戏?

  • 定义你的资源并在 manifest.js 中更改一些全局变量。
  • 通过替换/修改文件 towers.js 定义自定义塔。
  • 通过替换/修改文件 units.js 定义自定义单元。
  • 通过替换/修改文件 shots.js 定义自定义快照。
  • 你想用不同于画布的东西画画? 考虑扩展 video.js。

将所有内容合并到一个简单的启动脚本中,如下所示。 我们可以将这里脚本嵌入到文档中。 如果我们最小化所有可用的脚本,你也应该把它包装在。 这将使所有全局变量局部化,这将是一个。 但是,这种方法不能嵌入文档中的启动脚本,因为嵌入脚本不能从它的他脚本中看到局部变量。

一个非常简单的启动脚本:

(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转换。

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 被修改为生成简单的波,而在末尾的则是硬的。 首先,我们使用一个polynom来查找给定圆的对手数。 这是通过使用一些可以应用于所需值的to来计算的。 现在行为是由少数对手在前几轮和越来越多的对手在第 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 resultsvar n = ~~(1.580451 - 0.169830 * this.index + 0.071592 * this.index * this.index);
 //This is the number of opponent unit typesvar 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只是对手每个对手的对手倍数。 高斯的峰值放置在下边界和上边界之间。 上边界相对于当前标高移动。 最终我们将到达最强的对手,把高斯分布的中心放在那里。 这就是大多数对手都会强大的点,有些弱对手,只有少数对手,如果有的话。

Ultimate Tower Defense Chaos

前面的截图展示了演示游戏在一个非常晚的阶段的新设计。 一个非常乏味的迷宫已经被创建用来减慢对手的速度。 也有许多 hellgates,这是一种极好的停止强制装甲对手的方法。 finally 我们还必须处理大量堆叠对手,这将导致我们的塔的大量问题。

这个小迭代的另一个特点是能够恢复现有的游戏。 当前的游戏一旦完成就会保存一次。 如果浏览器在一开始找到一个存储的游戏,它会询问玩家是否应该恢复以前的游戏。 这使得长期播放无需担心。

有两种方法负责存储和加载游戏。 第一种是 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的上下文中。

Ultimate Tower Defense Lost

这里游戏中没有赢家。 所以唯一剩下的问题是: 你能来多远? 有些时候会失去。 这也将清除保存游戏。 另一种选择是,在新wave启动后,保存保存游戏。 但是,在当前版本中,这允许游戏刷新浏览器以避免过早丢失。

某些 final 触摸

当然,像其他软件一样,游戏永远不会结束。 尽管如此,这应该只是一个小的移植项目,我想以某种方式完成它。 如果使用浏览器提供的消息框,那么有很多缺失或者不太专业的东西。 然而,我认为,在下一轮中,选择玩家可能会处理哪种对手是关键之一。

实现这个实际上是直接向前的。 首先我需要一些方法来读下下一层的对手。 当我使用随机波时,我必须实现一种访问下一波的方法,然后才能实际创建它。

var WaveList = Class.extend({
 /* as before */ nextOpponents: function() {
 var upper = this.index * 0.3 + 1.3;
 var m = Math.min(this.unitNames.length, ~~upper);
 var units = [];
 for (var i = 0; i <this.unitNames.length && i < m; i++)
 units.push(this.unitNames[i]);
 return units;
 },
});

现在我们可以在 app.js 文件中使用这个函数,它基本上是逻辑和UI之间的glueing部分。 在这里,我们可以添加如下方法:

var updateNextWave = function() {
 nextwave.innerHTML = '';
 var names = logic.waves.nextOpponents();
 for (var i = 0; i < names.length; i++) {
 var name = names[i];
 var unit = types.units[name];
 var img = images[unit.sprite];
 var div = document.createElement('div');
 var icon = document.createElement('canvas');
 var width = img.width/unit.frames;
 icon.width = 32;
 icon.height = 32;
 var targetHeight = img.height >32? 32 : img.height;
 var targetWidth = width * targetHeight/img.height;
 var ctx = icon.getContext('2d');
 ctx.drawImage(img, 0, 0, width, img.height, 16 - targetWidth * 0.5, 16 - targetHeight * 0.5, targetWidth, targetHeight);
 div.appendChild(icon);
 var info = document.createElement('div');
 info.innerHTML = [
 '<div class=title>', unit.nickName, '</div>',
 '<div class=description>', unit.description, '</div>',
 '<div class=rating>', ~~unit.rating, '</div>',
 '<div class=speed>', unit.speed, '</div>',
 '<div class=damage>', unit.hitpoints, '</div>',
 ].join('');
 info.classList.add('info');
 div.appendChild(info);
 nextwave.appendChild(div);
 }
};

基本上我们只回绕所有给定的名称,获取类型,这是构造函数,用于创建类似于塔的预览窗格。 大多数工作实际上用来使用画布从给定的by ( 使用重缩放 等等 ) 中预览图像。 这与塔按钮使用的预览图像不同,其中一些CSS规则已经应用。

一个 final 事件是图形上下文只绘制图像( 那是那些精灵的当前帧)的约束。 如果我们想提供更多的动态信息,这并不是很。 所以我决定用几行代码来扩展它:

var CanvasView = View.extend({
 /*.. . */ drawVisual: function(element) {
 /* as before */ element.draw(ctx, dx, dy, w, h);
 },
});

现在,使用一些参数调用每个元素的draw() 方法。 因此,每个元素都可以覆盖它以提供定制的覆盖。

下面是 GameObject 提供的标准实现。

var GameObject = Base.extend({
 /*.. . */ draw: function(ctx, x, y, width, height) {
 },
});

下面是 Unit 所使用的自定义实现:

var Unit = GameObject.extend({
 /*.. . */ draw: function(ctx, x, y, width, height) {
 var maxLength = 12;
 var barLength = maxLength * this.health/this.hitpoints;
 x += (width - maxLength) * 0.5;
 ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
 ctx.fillRect(x, y - 6, maxLength, 3);
 ctx.fillStyle = '#00ff00';
 ctx.fillRect(x, y - 6, barLength, 3);
 },
});

我们 在这儿 干什么? 好吧,在读取代码行之后,我们实际上显示了单元的当前健康状况。 首先,我们绘制一个稍微不透明的rectangle,它显示了。 然后我们在上面放置一个绿色的rectangle,它表示当前的健康状态。

现在我们知道还有多少 hitpoints ! 太糟糕了,我们不能将这些信息作为每个塔的自动目标。 这可能会在未来的版本中更改,当前我认为这个项目已经关闭了。

Points of Interest

代码对"oop"javascript使用相同的方法作为Mario游戏( 可以在CodeProject上找到这篇文章:)。 基于 Pattern的编程对象是面向对象,原型和对象中心样式使继承和类中心的体系结构更加。 这是文件 oop.js 非常方便的地方。 它允许我们扩展现有定义并创建新的定义。

此外,这里还可能有某种反射。 我们可以将这些函数( 让我们把它们叫做) 添加到一些对象或者数组中。 这样我们就可以模拟类似于. NET-Framework 或者Java中的反射的东西。 我们只需要显式地执行这个加法。 但是,这样做的好处是无需再进行过滤。 我们也非常灵活,可以轻松地改变订单,价值和更多。

我们也不需要任何属性。 属性只是注释某种类型,换句话说,将 static 值放入类。 在JavaScript中,我们可以给出任意的函数属性。 由于类只是一个类型,它由JavaScript中的构造函数表示,因这里我们可以在这里函数中作为一些属性。

回答问题:它能被移植得多好和快?

正式的答案是:4个晚上。 但是,我花了一些时间来找出原始代码的一些问题,并得到原始作者的意图。 此外,在JavaScript中调试一些代码实际上比使用 Visual Studio ( 我觉得我对JavaScript很有经验)的C#。 最大的问题甚至不来自一般的算法或者实现。 大多数时候动态类型系统通过隐藏一些通常编译器来检测的小型错误引起了 Bug。 但是我承认,使用app的比如 在这方面是有益的。 由于它包含了用于使用类的关键字,它也会使OOP方法过时,这将非常适合与当前生成的运行时代码类似的代码。 但也有一个问题。 我认为,随着调试时间的增加,应用程序的开发时间会稍微长一些- 对于给定大小的项目来说。

我可以在线试试?

当然,我在网页上放置了一个版本( 与提供的演示应用程序中的代码稍有不同)。 你可以在 html5.florian-rappl。de/towerdefense 播放它。 如果你有任何评论。建议或者改进,那么感谢你在这里发布或者写入任何信息。

历史记录

  • v1.0.0 | 初始发行版|
  • v1.1.0 | 更新资源加载程序| 1
  • | typos typos typos typos typos
  • 平衡| 1的 | 更新
  • v1.2.1 | 校正丢失的图像|
  • | remarks remarks remarks remarks remarks

DEF  
相关文章