UltimateVolley

分享于 

48分钟阅读

Web开发

  繁體

Ultimate Volley

简介

在 22和 29之间的每个德语都应该熟悉 Volley的游戏。 我不太确定是否那么大,但是你可以相信我几年前这个游戏是非常流行的。 在这场比赛中,两种外星粘性熊互相。 那些外星粘液熊就是所谓的斑点。 这就是为什么游戏被称为 Volley的原因。 游戏本身是 MS-DOS 游戏街机 Volley的高级版本,具有类似的规则集。 原始的blob Volley 可以在 Sourceforge找到。

可以在下面找到原始版本的屏幕截图:

The original version

现在这个游戏不再那么流行了。 我认为其中的一个原因是 static的游戏。 游戏本身可以在第二版本中使用,这个也被移植到web中。 然而,可用的版本在旧的static 感觉中仍然是兼容的。 因这里,如果你知道我的意思,就需要一些新的空气来让游戏看起来更重要。 我的版本是基于我的一个学生写的旋转算法。

我想改变那个自旋算法,但是游戏( 那个学生) 直接两周,我发现( 有时 buggy ) 自旋算法引入了最有趣的。 游戏本身可能是一个挑战,在这里我们已经有了一个竞赛并确定了一个"世界冠军"。

背景

在本文中,我们将介绍基本的代码细节和最重要的算法。 我将解释集成和分割游戏到时间片的概念。 我还将尝试为 跨浏览器 键盘实现提供一个很好的解决方案。 这里提供的代码将是第一个阶段代码。 这是实验性的,所以谁可以看到它并用它来玩。 下一步我将把游戏包装成一个好的网站,这给用户提供了配置的可以能性。

最后一个阶段将涉及到与SignalR的连接,这样实时通信和网络游戏是可以能的。 我们将在本文中讨论第 1阶段,第 2步和 3将在下一篇文章中发布,或者只有一天。 考虑到英特尔,竞争和我的工作负载,在下个月我可以花费多少时间在这里。

在线实时版本

你可以观看在线演示的YouTube视频:

YouTube video

如果你想自己尝试一下,那就去我的主页上看看吧。 在那里你可以使用与上面的源代码包下载相同的版本。

特性

这里版本将具有以下方面的( * = 未包含在阶段 1中):

  • 额外的( 不 unrealistic ) 自旋物理- 给游戏增加一个额外的技能因素
  • 随机位置( 每一个都有不同的background 图像) ;当前对球物理没有影响
  • 随机第一台服务器
  • 减小运动能力的脉冲测量( 如果脉冲太高)
  • 网络实际上可以震动( 当它被球击中时) 并且具有圆形的顶部。
  • 每次集会的即时重播和回放
  • 弱计算机敌人
  • *Good 计算机敌人
  • *Online 多人聊天和观察者
  • *Online 游说和排名系统

这些特性中的大多数已经包含在第一阶段。 现在电脑的敌人已经被加入了娱乐。 当前的实现可以根据随机数进行移动,而不需要考虑实际游戏的任何知识/计算。 这一定会在某些时候变化- 但是现在我们对这个弱的计算机敌人很满意。

游戏的规则

从原理上来说,它接近 Volley 球。 要更具体地说,它可以能是 Volley 球,如果它只是每个团队的1个球员。 每个玩家都有一个最大的。 3个连续球接触。 如果球击中地板,它的上方。

每个玩家在服务时也有 3个连续的联系人。 点击边界和网是允许尽可能频繁的。 每个集合都播放到 25点,每个集都需要 2个集合才能获得 MATCH。 一旦一位玩家 made,他就有权发球。

所有这些规则都可以改变。 其中一些可以在 constants.js 文件中找到,而另一些则必须在指定的例程中更改。 最大点数和最大值。 集合在 Game 类( 更多关于那个) 中设置。 这样做可以包括更改默认值( 从HTML应用程序)的简单可能性。

赢得一组需要两个球员之间的两个点的最小。 因此没有任何集合会以 25 -24结尾。 这里 26 -24可以是最终得分,也可以是 29 -27或者 27 -29. 这一切都取决于哪个玩家第一个拥有 2点领先者。

HTML和 CSS

让我们从一些HTML开始。 大多数 below 代码很明显,如适当的文档类型,设置正确的字符集或者包含一些外部样式表。 在 body 标签的末尾,我们以正确的顺序包含所有需要的脚本。 这就是魔术的用武之地。

<!doctypehtml><htmllang="en"><head><metacharset="utf-8"/><title>Ultimate Volley</title><linkrel="stylesheet"href="Content/style.css"/></head><body><divid="beach"></div><canvasid="canvas"width="1000"height="600"> Your browser is too old. Please consider upgrading your browser.
 </canvas><scriptsrc="Scripts/oop.js"charset="utf-8"></script><scriptsrc="Scripts/constants.js"charset="utf-8"></script><scriptsrc="Scripts/variables.js"charset="utf-8"></script><scriptsrc="Scripts/enums.js"charset="utf-8"></script><scriptsrc="Scripts/game.js"charset="utf-8"></script><script>/* We'll discuss this later! */</script></body></html>

应当注意,在生产站点上不需要包含所有这些脚本。 这里我们将所有文件打包成一个并最小化它。 因此只需要对web服务器请求一个请求,传输大小就减少了。 所以我们在页面上只看到了两个有趣的东西:

  • 带有 ID beachdiv -element
  • 带后备提示的canvas -element

让我们看一下样式表,以获得页面外观的感觉。 首先我们要注意,页面使用一种特殊的字体,称为合并。 实际上这个字体不是特别特殊的( 通常,我们将包括所有类型的special/weird/cool 字体,因为游戏是从版式和图形中的一般),但它看起来很不错,会给游戏一个独特的外观。

@font-face { /* Define a special font */font-family: 'Merge';src:url('fonts/merge_light.eot');src:local('☺'),/* To prevent old IEs from reading this */
 url('fonts/merge_light.woff') format('woff'),
 url('fonts/merge_light.otf') format('opentype'),
 url('fonts/merge_light.ttf') format('truetype'),
 url('fonts/merge_light.svg') format('svg');font-weight: normal;font-style: normal;}html, body {
 margin: 0;padding: 0;width: 100%;height: 100%;/* nice background tiles */background: url('background.png');}
#canvas {
 display: block;position: absolute;top: 50%;left: 50%;margin-left: -500px;/* Center it */margin-top: -300px;}
#beach {
 display: block;position: absolute;top: 50%;left: 50%;margin-left: -500px;/* Center 上面 canvas */margin-top: -380px;height: 80px;width: 1000px;font: 36pt Merge;color: white;text-align: center;}

我们也为元素选择一个绝对定位。 canvas 元素将被放置在屏幕的中间,而 div 元素( 这是 beach的has ) 则位于右边的上面。 我们还设置了一些字号和文本对齐语句,并调整了网页的background。

这并不是什么特殊的事情 ! 所以,让我们到JavaScript端,让我们看一下所有的魔术。

基本代码设计

我已经阅读过我关于Mario5游戏( 它在这里可用,在CodeProject的窗口上。)的文章,知道我已经沉迷于JavaScript类。 要不混淆任何一个: JavaScript是面向对象的,但面向对象的对象不等于类。 JavaScript作为( 脚本) 语言是 Prototype,换句话说,没有类( 尽管对于新的ECMAScript有一些建议( 是的,那是JavaScript的官方 NAME ) ) 版本,要包含一个 class 关键字和所有的物品。

但是基于 prototype的编程可以用来给程序员( 我们) 提供类似我们实际上有类的能力。 这种想法实际上在两种情况下有效:

  • 我们可以使用熟悉的( 或者合理) 关键字,这给我们提供了使用类的印象
  • 我们可以直接使用基本功能,替代和继承

由于Mario5文章提供的小 helper 满足所有需求,我们将在这个游戏中再次使用它。

一个简单的helper

JavaScript中面向OOP的helper 被放在文件 oop.js 中。 它包括以下代码:

(function(){
 var initializing = false, 
 fnTest =/xyz/.test(function(){xyz;})?/b_superb/:/.*/;
 // The base Class implementation (does nothing)this.Class = function(){ };
 // Create a new Class that inherits from this class Class.extend = function(prop) {
 var _super = this.prototype;
 // Instantiate a base class (but only create the instance, don't run the init constructor) initializing = true;
 var prototype = newthis();
 initializing = false;
 // Copy the properties over onto the new prototypefor (var name in prop) {
 // Check if we're overwriting an existing function prototype[name] = 
 typeof prop[name] == "function" && 
 typeof _super[name] == "function" && 
 fnTest.test(prop[name])?
 (function(name, fn) {
 returnfunction() {
 var tmp = this._super;
 // Add a new. _super() method that is the same method// but on the super-classthis._super = _super[name];
 // The method only need to be bound temporarily, so we// remove it when we're done executingvar ret = fn.apply(this, arguments); 
 this._super = tmp;
 return ret;
 };
 })(name, prop[name]) : prop[name];
 }
 // The dummy class constructorfunction Class() {
 // All construction is actually done in the init methodif (!initializing && this.init)
 this.init.apply(this, arguments);
 }
 // Populate our constructed prototype object Class.prototype = prototype;
 // Enforce the constructor to be what we expect Class.prototype.constructor = Class;
 // And make this class extensible Class.extend = arguments.callee;
 return Class;
 };
})();

这里我们将一个名为 class的对象附加到 window 元素( 提供,this 指向该对象;但是由于我们直接包含它,上下文将是当前 window的一个。)。 这里对象可以与 extend() 函数一起使用。 下面的代码Fragment将创建一些类:

var SomeClass = Class.extend({
 init: function() {
 //The is the constructor of the class"SomeClass" },
 //Other methods to follow the same pattern/* Always call base function with this._super() within the corresponding method
});

我们将大量使用这个构造来构建所有的类。

类关系图

我们直接从 class 对象生成三个继承树。 这三个分支是:

  • 一个 Resources 类,其中所有资源管理器都将从
  • 一个 VirtualObject 类,它是没有 paint() 方法的所有对象的主要起始点
  • 一个 DrawObject 类,它是所有具有 paint() 方法的类的基

第一个分支对于显示是相当不。 目前只创建了一个资源管理器- 它负责映像。 在第 2阶段的项目声音效果和音乐将被添加- 有一或者两个额外的资源管理器需要。

第二个分支看起来像下面的图。

VirtualObject inheritance

基本上,没有实现 paint() 方法( 以及 widthheight 等等 等相关属性)的每个对象都是从 VirtualObject 类派生的。 此类提供任何具有基本字符串和数字操作方法的继承类。

我们将在后面讨论 Control 类的实现。 我们还将深入讨论 Keyboard 类实现。

最后一个分支具有以下图示表达式:

DrawObject inheritance

在这里我们看到基本上有三个重要的类。 第一个是 ViewPort。 这一条捆绑所有图形对象并调用它们的paint() 方法。 它还有一个名为 ReplayViewPort的特殊派生,它用于显示重播。

下一个子分支是 Figure 类。 一个 Figure 是任何对象,可以在游戏( 通过与另一个图形交互,或者通过键盘控制它) 中移动。 当前( 可能永远) 中,有三种类型的图形: 一个 PlayerBallNet

Player 始终由 Control 类型的实例控制。 因此 Player的另外两个衍生的衍生物。 这两个类由/已经发出的输入控制。

最后一个子分支是 Field。 这里我们有真实的BigField,它基本上是完整的ViewPort,只是它的一部分,一个 SubField。 一个 Field 有责任检查它的子元素的边界条件。 在 SubField 实例中,Player 实例将位于实例中,而 Ball 始终位于 BigField 中。

最后,所有东西都在 Game 实例中。 这里的关系如下:

DrawObject inheritance

所以基本上一切都依赖于这个实例。 这里的玩家和观察者是例外,他们不是直接通过游戏创建的,只是添加了。 理论上可以能有很多玩家- 但现在我们只是坚持 2. stage stage可能包含特殊 2 vs 2和/或者 vs 邪恶 1模式。

游戏算法

一个 Game 对象有很多方法。 最重要的两个叫做 play()pause()。 第一个启动无限游戏循环并绑定特定的控制事件处理程序,而第二个则停止游戏循环并取消事件处理程序绑定。

var Game = VirtualObject.extend({
 /*.. . */ play: function() {
 var me = this;
 if(!me.running) {
 me.running = true;
 for(var i = this.players.length; i--; )
 this.players[i].input.bind();
 me.loop = setInterval(function() {
 me.tick();
 }, LOGIC_STEP); 
 }
 },
 pause: function() {
 if(this.running) {
 this.running = false;
 for(var i = this.players.length; i--; )
 this.players[i].input.unbind();
 clearInterval(this.loop);
 }
 },
 /*.. . */});

但是,如果我们不能指定无限游戏循环应该做什么,那么这两个函数根本不可以见。 在我们的例子中,我们在每个调用中调用 tick() 函数。

这里函数首先查看是否应该暂停某些帧的逻辑。 这是在玩家a 后的情况。 为了不直接启动( 也许为玩家做一些意想不到的事情),逻辑应该等待一秒钟的一秒。

A game in orlando

这个函数所执行的第二件事是对任何玩家的控制进行更新。 所以我们告诉所有 Player的实例,位于 players array 中,以调用 steer() 函数。

在这些强制更新之后,我们还更新当前重播记录的数据。 然后( 最终) 我们执行逻辑步骤。

每个逻辑步骤由以下几点组成:

  • 执行球的逻辑( 碰撞检测与边界,运动)
  • 执行网络的逻辑( 移动)
  • 执行每个玩家的运动( 碰撞检测与边界,运动)
  • 对每个玩家进行球检测的碰撞
  • 利用网络实现球检测的碰撞

这些步骤不断地执行。 所以我们要做一个无穷小的积分。 我们这样做是为了防止球碰到球员的( 如果速度比玩家大)。

var Game = VirtualObject.extend({
 /*.. . */ tick: function() {
 if(!this.wait) {
 for(var i = this.players.length; i--; )
 this.players[i].steer();
 this.instantReplay.addData(this.players);
 for(var t = TIME_SLICES; t--; ) {
 this.ball.logic();
 this.net.logic();
 for(var i = this.players.length; i--; ) {
 this.players[i].logic(); 
 this.ball.collision(this.players[i]);
 }
 this.ball.collision(this.net);
 }
 } else {
 this.wait--;
 }
 this.viewPort.paint(this.ball, this.players);
 },
 /*.. . */});

最后我们通过调用附加的ViewPort 实例来绘制当前场景,并使用球和播放机来绘制。 然后,ViewPort 对象执行其余操作。 它的paint() 方法类似于:

var ViewPort = DrawObject.extend({
 /*.. . */ paint: function() {
 context.clearRect(0, 0, width, height);
 context.drawImage(this.background, 0, 0);
 this.field.paint();
 this.net.paint();
 this.ball.paint();
 for(var i = this.players.length; i--; )
 this.players[i].paint();
 if(this.ball.getBottom()> height)
 this.paintCursor(this.ball.x);
 this.paintScore();
 for(var i = this.players.length; i--; )
 this.players[i].paintPulse();
 this.paintMessage();
 },
 /*.. . */});

我们清理当前的场景并在上面画一个新的。 所以首先我们绘画 background,然后我们一般地绘制这个场,网和 finally 球和球员。 然后我们把这些表。

在这里,我们首先从一个指示器开始,如果球太高了。 之后我们绘制每个玩家的当前得分和玩家的脉冲。 我们还必须绘制任何消息,比如关于玩家获胜的信息。

控制游戏

任何电脑游戏中最重要的一个方面就是输入。 如果程序员使输入功能变得粗糙,或者总体游戏设计不涉及足够的交互,游戏本身将失败。 使用浏览器提供正确的控件基本上是相当容易的。 更详细地看一下我们将会发现的一些细节,这些思想仍然需要放入设计和实现中。

在这种情况下,我们的实现以基类 Control 开始。 如果不指定任何关于任何输入设备( 鼠标键盘触摸屏。)的特定绑定,我们就能够描述输入设备应该实际做什么。 在我们的情况下,只能执行( 或者不执行) 3操作:

  • 跳跃,换句话说,上升
  • 向左移动/向左移动
  • 向右行走/向右移动

我们为这些 3操作提供所有属性和方法。 我们的总体设计规定了以下行为: 任何输入只写入缓冲区,只有当 update() 调用时才会更新真实值。 这里方法还保存以前的状态,以便始终可以将当前值与以前的值进行比较。

我们为什么要这样做呢? 那么,update() 方法将在播放器方法的steer() 中被调用。 这里方法在任何逻辑循环的开始处调用,但不在 inside的时间片集成中调用。 这种方法的意思是:用户只能改变他的blob在所有的毫秒,而整体游戏分辨率远低于那些 40毫秒的时间。

var Control = VirtualObject.extend({
 init: function() {
 this.reset();
 this._super();
 },
 update: function() {
 this.previousUp = this.up;
 this.previousLeft = this.left;
 this.previousRight = this.right;
 this.left = this.bufferLeft;
 this.up = this.bufferUp;
 this.right = this.bufferRight;
 },
 reset: function() {
 this.up = false;
 this.left = false;
 this.right = false;
 this.bufferUp = false;
 this.bufferLeft = false;
 this.bufferRight = false;
 this.previousUp = false;
 this.previousLeft = false;
 this.previousRight = false;
 },
 setUp: function(on) {
 this.bufferUp = on;
 },
 setLeft: function(on) {
 this.bufferLeft = on;
 },
 setRight: function(on) {
 this.bufferRight = on;
 },
 bind: function() { },
 unbind: function() { },
 cancelBubble: function(e) {
 var evt = e || event;
 if(evt.preventDefault)
 evt.preventDefault();
 else evt.returnValue = false;
 if (evt.stopPropagation)
 evt.stopPropagation();
 else evt.cancelBubble = true;
 },
 copy: function() {
 if(this.previousUp === this.up && this.previousRight === this.right && this.previousLeft === this.left)
 return0;
 return {
 l: this.left? 1 : 0,
 u: this.up? 1 : 0,
 r: this.right? 1 : 0 };
 }
});

为什么我们要保存以前的输入数据? 我们可以看一下 copy() 方法。 这个仅用于向重播提供数据。 我们稍后将讨论重播,但是现在我们可以说这将节省大量内存( 可能是磁盘空间),因这里获得性能。 这个原因是,如果键没有变化,我们可以引入一个特殊的符号。 否则,我们将只提供键的当前状态。

这里提到的另一个方法是 cancelBubble()。 这是一个在所有浏览器中停止事件传播的小 helper。 这个应该确保键盘输入不会冒泡到浏览器,并且没有未定义的行为,例如重载页面或者打开书签或者执行手势操作。 所有这些动作都是对游戏体验的威胁,这就是我们必须消除它们的原因。

让我们看一下 Control 类的具体实现:

var Keyboard = Control.extend({
 init: function(codeArray) {
 var me = this;
 me._super();
 me.codes = {};
 me.codes[codeArray[0]] = me.setUp;
 me.codes[codeArray[1]] = me.setLeft;
 me.codes[codeArray[2]] = me.setRight;
 var handleEvent = false;
 this.downhandler = function(event) { 
 handleEvent = me.handler(event, true);
 return handleEvent;
 };
 this.uphandler = function(event) { 
 handleEvent = me.handler(event, false);
 return handleEvent;
 };
 this.presshandler = function(event) { 
 if (!handleEvent)
 me.cancelBubble(event);
 return handleEvent;
 };
 }, 
 bind: function() {
 document.addEventListener('keydown', this.downhandler, false);
 document.addEventListener('keyup', this.uphandler, false);
 document.addEventListener('keypress', this.presshandler, false);
 //The last one is required to cancel bubble event in Opera! },
 unbind: function() {
 document.removeEventListener('keydown', this.downhandler, false);
 document.removeEventListener('keyup', this.uphandler, false);
 document.removeEventListener('keypress', this.presshandler, false);
 },
 handler: function(e, status) {
 if(this.codes[e.keyCode]) {
 (this.codes[e.keyCode]).apply(this, [status]);
 this.cancelBubble(e);
 returnfalse;
 }
 returntrue;
 },
});

当然,我们需要实现 bind()unbind() 方法。 ,我们只需要使用 keydown和event事件,Opera 中的Bug () 就会强制我们绑定键事件。 如果我们不这样做,事件仍然会传播。

Keyboard 类的构造函数中,我们将对象的键设置为传递的键代码。 最后我们只需要看看当前输入的键是否是对象中的键并执行函数- 这是键后面的对象的值,关键代码是。 需要 handleEvent 闭包变量的技巧才能使工作正常工作。 否则我们要么总是停止传播要么永远不会。 这允许我们确定是否应该停止它。

物理方面

在物理方面,我们必须用弹性/非弹性碰撞来处理整个能源业务。 一旦我们发现了碰撞,真正的问题就开始了。 因为我们只需要处理圆的( 我们的边界,但也可以很快地解决),所以检测碰撞是非常容易的。 检测碰撞只是在添加两个x 值( 第一个是来自的;或者通常来自检测球的对象,第二个是球。),并将它们添加到添加两个y 值的方块中。

这可以与两个半径之和的平方进行比较。 如果这个值是 GREATER的平方,那么我们没有检测到碰撞,否则我们就会发现球面上的球。 现在棘手的部分开始了 !

让我们来看看一个简短草图中涉及的所有变量。 草图显示在实际碰撞发生之前游戏的帧。

Involved physical variables

我们可以看到( 球和目标) 有一定的速度。 在这里,我们有一些速度矢量,换句话说,的一部分总速度在x direction 中,剩下的部分是 direction。 我们还看到球和靶之间的位置在彼此之间有某种角度的( 称为 α )。 如果在碰撞中 α = 0,那么我们有一个纯水平碰撞。 如果 α = Π/2 或者 90 °,那么我们有一个完全垂直的碰撞。

我们还看到球有一些自旋特性。 精确的物理是复杂的,并没有被移植到 1到 1. 因此我们将不再关注这个。 下面给出以下两个语句:

  • 自旋物理部分已经实现,为游戏带来了一些动作
  • 这个游戏没有那部分就能正常工作,但是球永远不会开始旋转。

因此,排除自旋物理部分或者改进它是由你自己决定的。 spin物理有一些已知问题,但是对于个人的原因我喜欢当前的实现,它在我看来是游戏的一个有趣的部分。

自旋函数包含在 Ball 类中,如下所示:

var Ball = Figure.extend({
 /*.. . */ changeSpin: function(vx, vy, dx, dy, sign) {
 var distance = dx * dx + dy * dy;
 var scalar = (dx * vx + dy * vy)/distance;
 var svx = vx + sign * dx * scalar;
 var svy = vy + sign * dy * scalar;
 this.omega += (svx * dy - svy * dx)/distance;
 },
 spin: function(vx, vy, dx, dy) {
 this.changeSpin(this.vx, this.vy, dx, dy, 1);
 this.changeSpin(vx, vy, dx, dy, -1);
 },
 /*.. . */});

如果我们将代码修改为如下代码 Fragment ( 例如。 去掉 changeSpin() 函数,去掉 spin() 函数的body,我们就成功地从游戏中移除了自旋物理。

var Ball = Figure.extend({
 /*.. . */// No more changeSpin  spin: function(vx, vy, dx, dy) {
 //No body! },
 /*.. . */}); 

剩下的魔术在哪里发生? 让我们简要看一下 Figure 基类。 我们有三个重要功能:

var Figure = DrawObject.extend({
 /*.. . */ checkField: function() {
 if(this.y <this.hh) {
 this.y = this.hh;
 this.vy = 0;
 }
 if(this.getLeft() <this.container.x) {
 this.vx = 0;
 this.x = this.container.x + this.wh;
 } elseif(this.getRight()> this.container.x + this.container.width) {
 this.vx = 0;
 this.x = this.container.x + this.container.width - this.wh;
 }
 },
 logic: function() {
 this.vy -= ACCELERATION;
 this.x += this.vx;
 this.y += this.vy;
 this.checkField();
 },
 hit: function(dx, dy, ball) {
 var distance = dx * dx + dy * dy;
 var angle = Math.atan2(dy, dx);
 var v = ball.getTotalVelocity();
 var ballVx = Math.cos(angle) * this.friction * v;
 var ballVy = Math.sin(angle) * this.friction * v;
 ballVx += BALL_RESISTANCE * ball.omega * dy/distance;
 ballVy -= BALL_RESISTANCE * ball.omega * dx/distance;
 ball.setVelocity(ballVx, ballVy);
 },
 /*.. . */});

实际上,我们并没有讨论这三种方法中的哪一种被。 我们看到了 logic() 方法的调用;inside的游戏循环基本上由 Game 类的tick() 方法表示。

logic() 方法根据重力定律和玩家输入( 改变了速度变量) 改变当前位置。 也可能是玩家越界了。 因此我们还需要检查这里的逻辑,这意味着调用 checkField() 函数。 这里我们只是比较一些变量,并决定是否应该反映一些速度。

那么,hit() 方法在哪里被调用? 我们已经看到了对 collision() 方法的调用。 这个实际上是基本的。 Figure 类已经包含这里方法,但它还没有实现( 在JavaScript中,表示它存在,但它有一个空体)。 每个孩子都有( 很明显它没有出现但也没有发生什么) 来实现自己的collision() 方法。

方法基本上看是否发生了一些碰撞( 用球),它的中所有的规则都是实现的。 如果发生冲突,将使用参数调用 hit() 函数。 参数包括:

  • 什么是水平距离
  • 什么是垂直距离
  • 把球给我 !

最后,命中函数获取当前对象和球的属性,并更改球的特定属性。

一个小把戏: 重播

如果我们能一次又一次地观看最好的节目会不会很好? 这就像观看最佳目标最佳得分或者最佳动作。 因此,包括录制和播放重播的可能性是必须的。

节省重播可以完成,因为我们没有任何随机数( 否则我们需要把它们储存起来) 在游戏中。 对于任何集会,我们只需要以下信息:

  • 球在开头的位置( 哪个球员在哪里服务)?
  • 开始的球员在哪里?
  • 目前的得分是多少?
  • 球员脉冲的电流是什么?
  • 玩家的名字是什么?
  • 玩家的颜色是什么?

另外,我们只需要存储任何键盘输入。 重播文件的第一次尝试看起来像这样:

{"beach":"Mauritius","ballx":247.5,"bally":250,"data":[[{"left":true,"up":false,"right":false},{"left":false,"up":false,"right":false}],[{"left":true,"up":false,"right":false},{"left":false,"up":false,"right":false}],[{"left":true,"up":false,"right":false},{"left":false,"up":false,"right":false}],/* many many more */,"players":[{"color":"#0000FF","name":"Lefty","points":2,"sets":0},{"color":"#FF0000","name":"Righto","points":1,"sets":0}],"count":580}

只有 580帧的总( 我把你从阅读中) 47209个字符 ! 经过一点点优化后,重播文件如下所示:

"{"beach":"Mauritius","ballx":752.5,"bally":250,"data":[[0,{"l":0,"u":0,"r":1}],[0,0],[0,0],[0,0],[0,0],[0,0],[0,{"l":0,"u":0,"r":0}],
/* many many more */,
"players":[{"color":"#0000FF","name":"Florian","points":22,"sets":1},{"color":"#FF0000","name":"Christian","points":19,"sets":0}],"count":442,"contacts":14}"

在这个文件中,我有 4487个字符和 442个联系人。 因这里,如果我们推断并取得比率,我们将看到这些变化导致了一个只包含 1/7 到 1/8的文件。 我还添加了一个名为 contacts ( 来确定它是否有价值)的新属性。 还有什么改变了?

  • 键盘的属性有更短的名称- 左边已经变成了l,右边已经成为 R 了。
  • true/false 已经被更改为 1/0 ( 仍然在JavaScript中计算相同)
  • 如果输入还没有更改,则添加 0而不是对象

在这里,我们滥用对象的( 即使空) 计算为 true,而 0计算为 false。 返回零或者不返回的魔术 behind 已经在 Control 类中实现。 现在让我们看一下表示 Replay的特定类。

var Replay = VirtualObject.extend({
 init: function(beach, ball, players) {
 /* Initialize data for a new replay */ },
 addData: function(players) {
 var inputs = [];
 this.contacts = 0;
 for(var i = 0; i <players.length; i++) {
 inputs.push(players[i].input.copy());
 this.contacts += players[i].totalContacts;
 }
 //Before starting to add data - look if the data is worth saving (no action = not worth)if(this.count === 0) {
 var containsData = false;
 for(var i = inputs.length; i--; ) {
 if(inputs[i]) {
 containsData = true;
 break;
 }
 }
 if(!containsData)
 return;
 }
 this.data.push(inputs);
 this.count++;
 },
 play: function(game, continuation) {
 game.pause();
 var frames = this.count;
 var index = 0;
 /* Create view for replays and fill with objects */var data = this.data;
 for(var i = 0; i <this.players.length; i++) {
 var bot = new ReplayBot(game, game.players[i].container);
 bot.setIdentity(this.players[i].name, this.players[i].color);
 replayBots.push(bot);
 }
 var iv = setInterval(function() {
 if(index === frames) {
 //Return to the game clearInterval(iv);
 if(continuation)
 continuation.apply(game);
 game.play();
 return;
 }
 /* Logic and paint methods being called */ }, LOGIC_STEP/2);
 }
});

将从 Game 实例调用 addData() 方法,该实例包含包含所有播放机的array。 然后,每个玩家的当前数据将被接受,并建立一个带有这些数据点的临时 array。 然后,如果没有添加数据,则将这里 array 添加到不变的数组中。 这将排除所有操作在任何一个玩家开始在一个新的集合开始之前。 这可以防止保存没有意义的数据。

play() 方法开始播放重播。 因此它使用另一个 ViewPort。 因为游戏循环( 所以画) 暂停了,所以它必须执行自己的绘画,同时观看重播。 一个重要的部分是,这里的循环也可以是递归定时器。 这将允许调整重播的速度。 现在回放的速度设置为 LOGIC_STEP/2,换句话说,的速度是真实游戏的两倍。 这个值在物理上没有什么区别,因为实际的游戏评价是在 TIME_SLICES 集成中执行的。 逻辑本身看起来与游戏非常相似。

那么,现在哪个viewport真正执行绘画? 这是派生的版本,执行这里任务:

var ReplayViewPort = ViewPort.extend({
 init: function(players, container, net, ball) {
 var pseudo = {
 players: players,
 field: container,
 net: net,
 ball: ball
 };
 this._super(pseudo);
 this.setMessage(Messages.Replay);
 },
 setup: function() {},
 paintScore: function() {}
});

基本上它和普通的ViewPort 一样,但它不会画成分,而且它没有 setup() 例程。 另外它还有一个不同的构造函数,它创建一个新对象,包含所有回放机器人。字段。网络和球( 所有这些事情都被改变为用于重播的东西)。 然后将这里对象传递给类的基构造函数。 我们将使用 app,并且我们会告诉构造函数的构造函数只接受游戏实例,这样我们就会在这里得到编译器错误。 但幸运的是我们仍然在动态仙境中这个 pseudo 游戏帮助我们。

Replay

这是最终版本- 我们只包含了文本重播,这样大家都能立即实现: 嘿这是重播 ! 也可以考虑它的他要包括的东西;像淡色的色彩覆盖或者一些有趣的漫画图形。 但现在这一切都可以了 ! 我也不显示分数( 如上所述如上所述) - 但它们保存在重播文件中,换句话说,可以在它的他位置使用。

要在它的周围播放的常量常量

下面是来自文件 constants.js的代码。 在这里,游戏使用的所有常量都被定义。 继续和那些人一起玩。 其中一些不应该改变,比如 TWOPI 或者 MIN_PLAYERSMAX_PLAYERS。 其他的动作会产生滑稽的动作或者行为。

// Defines the width of the netvar NET_WIDTH = 10;// Defines the height of the netvar NET_HEIGHT = 290;// Converts grad to radvar GRAD_TO_RAD = Math.PI/180;// Just saves one operationvar TWOPI = 2 * Math.PI;// Time of 1 logic step in msvar LOGIC_STEP = 40;// Number of time slices per iterationvar TIME_SLICES = 40;// Frames per second (inverse of LOGIC_STEP)var FRAMES = 1000/LOGIC_STEP;// Sets the g factorvar ACCELERATION = 0.001875;// The minimum amount of playersvar MIN_PLAYERS = 2;// The maximum amount of playersvar MAX_PLAYERS = 4;// The maximum amount of contacts per movevar MAX_CONTACTS = 3;// The maximum (horizontal) speed of a playervar MAX_SPEED = 0.4;// The maximum (vertical) speed of a playervar MAX_JUMP = 1.05;// The (default) maximum number of points per setvar DEFAULT_MAX_POINTS = 25;// The (default) maximum number of sets per matchvar DEFAULT_MAX_SETS = 2;// The time between two points in msvar POINT_BREAK_TIME = 450;// The start height of the ball in pxvar BALL_START_HEIGHT = 250;// Sets the acceleration of the ball through the playervar BALL_SPEEDUP = 0.4;// Sets the strength of the reflection of the ball while servingvar BALL_LAUNCH = 1.5;// Sets strength of the reflection of the ballvar BALL_REFLECTION = 0.8;// Sets the air resistancy of the ballvar BALL_RESISTANCE = 1;// Sets the drag coefficient of the ballvar BALL_DRAG = 0.005;// Sets the increase per iteration of pulsevar PULSE_RECOVERY = 0.0004;// Sets the decrease per iteration of pulse while runningvar PULSE_RUN_DECREASE = 0.0005;// Sets the decrease per iteration of pulse for jumpingvar PULSE_JUMP_DECREASE = 0.17;// Sets the size of the circles in the won-sets-displayvar SETS_WON_RADIUS = 10;

如果你想改变游戏的规则,那么只需改变 DEFAULT_MAX_POINTS 或者 DEFAULT_MAX_SETS。 改变 MAX_CONTACTS 或者任何物理常量也很有趣。

使用代码

游戏需要所有的JavaScript文件。 然而,在这些文件中,找不到正确的命令来实际创建 Game 和插件的插件。 这里代码也必须创建,( 别担心,已经完成了,但随时可以改变它)。 这是一个简单游戏需要两个玩家的代码,( 在左边) 和 Righto ( 在右边),用于工作。

键盘可以手动设置。 为了做到这一点,你需要特定的关键代码。 通过使用Google编写几行代码( 在HTML文件中,直接在浏览器中或者在任何JavaScript控制台中) 或者通过下列列表,可以找到关键代码的列表:

KeyCodeKeyCodeKeyCode
退格键8制表符9输入13
左移16ctrl17alt18
暂停/中断19大写锁20escape27
向上翻页33向下翻页34结尾35
主页36左箭头37向上箭头38
右箭头键39向下箭头40插入45
删除46048149
250351452
553654755
856957a65
b66c67d68
e69f70g71
h72i73j74
k75l76m77
n78o79p80
q81r82s83
t84u85v86
w87x88y89
z90°旋转速度左窗口键91右窗口键92
选择密钥93数字小键盘 096数字小键盘 197
数字小键盘 298数字小键盘 399数字小键盘 4100
数字小键盘 5101数字小键盘 6102数字小键盘 7103
数字小键盘 8104数字小键盘 9105相乘106
添加107相减109小数点110
除法111f1112f2113
f3114f4115f5116
f6117f7118f8119
f9120f10121f11122
f12123num锁144滚动锁定145
半冒号186等号187逗号188
破折号189期间190正斜杠191
抑音符192打开括号219反斜杠220
关闭 braket221单引号222

必须按以下顺序给出参数: 向上。向左。向下。向下对齐。 因此默认的设置是左玩家有A-W-D键盘( left-up-right ),而右边的玩家使用箭头键 ←-↑-→ ( left-up-right )。

(function() {
 // Create new gamevar game = new Game();
 // Create the left boundaryvar fieldLeft = new SubField(0, 2);
 // Set up the keyboard for the left playervar keyboardLeft = new Keyboard([87, 65, 68]);
 // Create and add left playervar playerLeft = new Player(game, fieldLeft, keyboardLeft);
 playerLeft.setIdentity('Lefty', '#0000FF');
 game.addPlayer(playerLeft);
 // Create the right boundaryvar fieldRight = new SubField(1, 2);
 // Set up the keyboard for the right playervar keyboardRight = new Keyboard([38, 37, 39]);
 // Create and add right playervar playerRight = new Player(game, fieldRight, keyboardRight);
 playerRight.setIdentity('Righto', '#FF0000');
 game.addPlayer(playerRight);
 // Start game game.beginMatch();
})();

让我们讨论一下你会发现那些JavaScript文件。

  • oop.js 包含 class Prototype的定义和OOP魔术
  • constants.js 包含游戏使用的常量- 常量总是用纯大写写
  • variables.js 包含游戏使用的全局变量- 这只是画布和一些属性
  • enums.js 包含诸如海滩类型和可用信息的枚举
  • game.js 包含所有类,基本上包含所有代码

因此,game.js 文件可能是最有趣的。 我只能推荐使用一些常量。 这会产生很有趣的效果 !

兴趣点

就像我所写的,"只是只是"阶段 1是三阶段开发过程。 但是,我希望你能喜欢这个结果。 我已经在本地多人对战中与我的同事们 enjoyed。 随着( buggy,或者 weird ) 旋转物理,脉冲和即时重播我们有很多乐趣。 当重放显示出与真实游戏不同的结果时,甚至发生了。 这让我想到了:为什么? 但是,它仍然有很多乐趣,因为我的对手只是笑了一下,并且一直在讨论"视频证明"。 这个视频证据很有趣 !

游戏的最终版本将具有与SpaceShoot游戏具有类似功能的多人游戏。 尽管如此,它很可能用的编写,因为这也使用长轮询AJAX请求处理旧的webbrowsers,或者那些禁用了web服务的。

在 final 版本中,你可以在本地保存最佳重播,在 localStorage 或者下载中保存 换句话说,。 你可以在网上或者单独观看他们。 包含全球游戏系统和排名的聊天大厅将包括在内。 唯一剩下的问题是:

谁将成为世界冠军?

历史

  • v1.0.0 | 初始发行版|
  • v1.1.0 | 添加节"游戏规则"| 13.10.2012
  • v1.1.1 | 次要代码修复| 1
  • v1.1.2 | 一些输入固定的| 14.10.2012
  • | contents contents contents contents 14.10.2012
  • v1.2.0 | 添加了YouTube视频| 15.10.2012
  • v1.3.0 | 添加了关于常量| 16.10.2012的部分
  • 清单v1.3.1中添加了 |的条目

相关文章