簡介
毫無疑問,我們已經目睹了HTML5背後的那場偉大的Web開發革命。經過那麼多年HTML4的統治,一場全新的運動即將完全改變現在的Web世界。正是他釋放出來的現代化氣息和豐富的用戶體驗,讓它很快地成為了一個獨特的插件運行在類似Flash和Silverlight的框架之上。
如果你是一個非常年輕的開發者,也許你是剛在開始學習HTML5,所以可能你並沒有註意到他有太大的變化。在任何時候,我希望這篇文章能幫助你,當然,也希望像我一樣的老手能從中學到一些新的花樣。
你的評論對我來說非常重要,所以我很期待你的來信。當然能讓我更興奮的是當你在那個遊戲畫面上右鍵時暗暗地說一句「Hey,這居然不是Flash!也不是Silverlight!」
系統需求
想要使用本文提供的HTML5桌球應用,你必須安裝下面的這些瀏覽器:Chrome 12, Internet Explorer 9 or Fire Fox 5
遊戲規則
也許你已經知道這是什麼樣的遊戲了,是的,這是“英式斯諾克”,實際上更確切的說是“簡易版英式斯諾克”,因為沒有實現所有的斯諾克遊戲規則。你的目標是按順序將目標球灌入袋中,從而比其他選手得到更多的分數。輪到你的時候,你就要出桿了:根據提示,你必須先打進一個紅色球得到1分,如果打進了,你就可以繼續打其他的球- 但是這次你只能打彩色球了(也就是紅色球以外的球)。如果成功打進,你將會得到各自彩球對應的分數。然後被打進的彩球會回到球桌上,你可以繼續擊打其他的紅球。這樣周而復始,直到你失敗為止。當你把所有的紅球都打完以後,球桌上就只剩下6個彩球了,你的目標是將這6個彩球按以下順序依次打入袋中:黃(2 分)、綠(3分)、棕(4分)、藍(5分)、粉(6分)、黑(7分)。如果一個球不是按上面順序打進的,那它將會回到球桌上,否則,它最終會留在袋裡。當所有球都打完後,遊戲結束,得分最多的人勝出。
犯規處理
為了處罰你的犯規,其他選手將會得到你的罰分:
白球掉入袋中罰4分
白球第一次擊中的球是錯誤的話罰第一球的分數
#第一個錯誤的球掉入袋中罰第一球的分數
#處罰的分數至少是4
下面的這段程式碼展示了我是如何來計算犯規的:
Code var strokenBallsCount = 0; console.log('strokenBalls.length: ' + strokenBalls.length); for (var i = 0; i < strokenBalls.length; i++) { var ball = strokenBalls[i]; //causing the cue ball to first hit a ball other than the ball on if (strokenBallsCount == 0) { if (ball.Points != teams[playingTeamID - 1].BallOn.Points) { if (ball.Points == 1 || teams[playingTeamID - 1].BallOn.Points == 1 || fallenRedCount == redCount) { if (teams[playingTeamID - 1].BallOn.Points < 4) { teams[playingTeamID - 1].FoulList[teams[playingTeamID - 1] .FoulList.length] = 4; $('#gameEvents').append(' Foul 4 points : Expected ' + teams[playingTeamID - 1].BallOn.Points + ', but hit ' + ball.Points); } else { teams[playingTeamID - 1].FoulList[teams[playingTeamID - 1] .FoulList.length] = teams[playingTeamID - 1].BallOn.Points; $('#gameEvents').append(' Foul ' + teams[playingTeamID - 1] .BallOn.Points + ' points : Expected ' + teams[playingTeamID - 1] .BallOn.Points + ', but hit ' + ball.Points); } break; } } } strokenBallsCount++; } //Foul: causing the cue ball to miss all object balls if (strokenBallsCount == 0) { teams[playingTeamID - 1].FoulList[teams[playingTeamID - 1].FoulList.length] = 4; $('#gameEvents').append(' Foul 4 points : causing the cue ball to miss all object balls'); } for (var i = 0; i < pottedBalls.length; i++) { var ball = pottedBalls[i]; //causing the cue ball to enter a pocket if (ball.Points == 0) { teams[playingTeamID - 1].FoulList[teams[playingTeamID - 1].FoulList.length] = 4; $('#gameEvents').append(' Foul 4 points : causing the cue ball to enter a pocket'); } else { //causing a ball different than the target ball to enter a pocket if (ball.Points != teams[playingTeamID - 1].BallOn.Points) { if (ball.Points == 1 || teams[playingTeamID - 1].BallOn.Points == 1 || fallenRedCount == redCount) { if (teams[playingTeamID - 1].BallOn.Points < 4) { teams[playingTeamID - 1].FoulList[teams[playingTeamID - 1] .FoulList.length] = 4; $('#gameEvents').append(' Foul 4 points : ' + ball.Points + ' was potted, while ' + teams[playingTeamID - 1] .BallOn.Points + ' was expected'); $('#gameEvents').append(' ball.Points: ' + ball.Points); $('#gameEvents').append(' teams[playingTeamID - 1] .BallOn.Points: ' + teams[playingTeamID - 1].BallOn.Points); $('#gameEvents').append(' fallenRedCount: ' + fallenRedCount); $('#gameEvents').append(' redCount: ' + redCount); } else { teams[playingTeamID - 1].FoulList[teams[playingTeamID - 1] .FoulList.length] = teams[playingTeamID - 1].BallOn.Points; $('#gameEvents').append(' Foul ' + teams[playingTeamID - 1] .BallOn.Points + ' points : ' + ball.Points + ' was potted, while ' + teams[playingTeamID - 1].BallOn.Points + ' was expected'); } } } } }
#分數
我們根據下面的規則來計算得分:紅(1分)、黃(2分)、綠(3分)、棕(4分)、藍(5分)、粉紅(6分)、黑(7分)。程式碼如下:
Code if (teams[playingTeamID - 1].FoulList.length == 0) { for (var i = 0; i < pottedBalls.length; i++) { var ball = pottedBalls[i]; //legally potting reds or colors wonPoints += ball.Points; $('#gameEvents').append(' Potted +' + ball.Points + ' points.'); } } else { teams[playingTeamID - 1].FoulList.sort(); lostPoints = teams[playingTeamID - 1].FoulList[teams[playingTeamID - 1].FoulList.length - 1]; $('#gameEvents').append(' Lost ' + lostPoints + ' points.'); } teams[playingTeamID - 1].Points += wonPoints; teams[awaitingTeamID - 1].Points += lostPoints;
選手的閃爍動畫頭像
##遊戲是有兩位選手參與的,每一位選手都有自己的暱稱和頭像,選手的暱稱我們就簡單地以“player 1”和“player 2”來命名了(也許讓用戶自己輸入會更漂亮) 。每位選手的頭像是一隻正在打桌球的可愛小狗。輪到其中一位選手時,他的頭像就會有一閃一閃的動畫效果,同時對手的頭像會停止閃爍。 這個效果我們是透過改變img元素的CSS3屬性opacity的值來實現的:我們使用jquery的animatio
函數讓opacity的值在0-1.0之間變化。
Codefunction animateCurrentPlayerImage() { var otherPlayerImageId = 0; if (playingTeamID == 1) otherPlayerImageId = 'player2Image'; else otherPlayerImageId = 'player1Image'; var playerImageId = 'player' + playingTeamID + 'Image'; $('#' + playerImageId).animate({ opacity: 1.0 }, 500, function () { $('#' + playerImageId).animate({ opacity: 0.0 }, 500, function () { $('#' + playerImageId).animate({ opacity: 1.0 }, 500, function () { }); }); }); $('#' + otherPlayerImageId).animate({ opacity: 0.25 }, 1500, function () { }); }
<span style="color:#f79646;font-size:18px">力量控制條<strong></strong></span>
一个优秀的斯诺克选手都能很好地把握住每一杆的力度.不同的技巧需要不同的击球方式:直接的,间接的,或者利用边角的等等。不同方向和不同力度的组合可以构造成千上万种可能的路径。幸运的是,这个游戏提供了一个非常漂亮的力度控制条,可以帮助选手在击球前调整他们的球杆。
为了达到这一点,我们使用了HTML5的meter元素标签,它可以完成测量距离的工作。meter标签最好在知道这次测量的最小值和最大值的情况下使用。在我们的这个例子中,这个值在0到100之间,因为IE9不支持meter,所以我用了一张背景图来替代,这样效果也是一样的。
Code#strengthBar { position: absolute; margin:375px 0 0 139px; width: 150px; color: lime; background-color: orange; z-index: 5;}
当你点击了力度条后,你实际上是选择了一个新的力度。一开始你可能不是很熟练,但在真实世界中,这是需要时间来训练自己的能力的。点击力度条的代码如下:
Code$('#strengthBar').click(function (e) { var left = $('#strengthBar').css('margin-left').replace('px', ''); var x = e.pageX - left; strength = (x / 150.0); $('#strengthBar').val(strength * 100); });
在当前选手的头像框里面,你会注意到有一个小球,我叫他“ball on”,就是当前选手在规定时间内应该要击打的那个球。如果这个球消失了,那选手将失去4分。同样如果选手第一次击中的球不是框内显示的球,那他也将失去4分。
这个“ball on”是直接将canvas元素覆盖在用户头像上的,所以你在头像上看到的那个球,他看起来像是在标准的p上盖了一个img元素,但是这个球并不是 img实现的。当然我们也不能直接在p上画圆弧和直线,这就是为什么我要将canvas覆盖到头像上的原因了。看看代码吧:
Code<canvas id="player1BallOn" class="player1BallOn"> </canvas> <canvas id="player2BallOn" class="player2BallOn"> </canvas>
Codevar player1BallOnContext = player1BallOnCanvas.getContext('2d'); var player2BallOnContext = player2BallOnCanvas.getContext('2d'); . . . function renderBallOn() { player1BallOnContext.clearRect(0, 0, 500, 500); player2BallOnContext.clearRect(0, 0, 500, 500); if (playingTeamID == 1) { if (teams[0].BallOn != null) drawBall(player1BallOnContext, teams[0].BallOn, new Vector2D(30, 120), 20); } else { if (teams[1].BallOn != null) drawBall(player2BallOnContext, teams[1].BallOn, new Vector2D(30, 120), 20); player1BallOnContext.clearRect(0, 0, 133, 70); } }
旋转屋顶上的电风扇
在这个游戏中这把电风扇纯属拿来玩玩有趣一把的。那为什么这里要放一把电风扇?是这样的,这个游戏的名字叫HTML5斯诺克俱乐部,放一把电风扇就有俱乐部的气氛了,当然,我也是为了说明如何实现CSS3的旋转。
实现这个非常简单:首先我们需要一张PNG格式的电扇图片。只是我们并没有用电扇本身的图片,我们用他的投影。通过显示风扇在球桌上的投影,让我们觉得它在屋顶上旋转,这样就达到了我们目的:
Code#roofFan { position:absolute; left: 600px; top: -100px; width: 500px; height: 500px; border: 2px solid transparent; background-HTML5實現斯諾克桌球俱樂部的範例程式碼(圖): url('/Content/Images/roofFan.png'); background-size: 100%; opacity: 0.3; z-index: 2;} . . . <p id="roofFan"> </p>
为了获得更为逼真的气氛,我用Paint.Net软件将电扇图片平滑化了,现在你再也看不到电扇的边缘了。我觉得这是达到如此酷的效果最为简单的办法。
除了用了这图像处理的把戏,我们仅仅使用了一个带背景图的普通的p元素,这并没有什么特别。既然我们已经得到了电扇图片,我们就要让它开始旋转了。这里我们使用CSS3的rotate属性来实现这一切。
Codevar srotate = "rotate(" + renderStep * 10 + "deg)"; $("#roofFan").css({ "-moz-transform": srotate, "-webkit-transform": srotate, msTransform: srotate });
球杆动画
球杆的动画对于这个游戏也不是必需的,但是这的确为此添加了不少乐趣。当你开始用鼠标在球桌上移动时,你会注意到球杆的确是跟着你的鼠标在转动。这就是说球杆会一直保持跟随鼠标的移动,就像你身临其境一般真实。因为选手只能用他的眼睛来瞄准,所以这个效果也会对选手有所帮助。
球杆是单独一张PNG图片,图片本身不直接以img的形式展现,也不以背景的形式展现,相反,它是直接展现在一个专门的canvas上的。当然我们也可以用p和css3来达到同样的效果,但我觉得这样能更好的说明如何在canvas上展现图片。
首先,canvas元素会占据几乎整个页面的宽度。请注意这个特别的canvas有一个很大的z-index值,这样球杆就可以一直在每个球的上方而不会被球遮盖。当你在球桌上移动鼠标时,目标点会实时更新,这时候球杆图片会进行2次转换:首先,通过计算得到母球的位置,其次翻转母球周围的球杆,通过这2步我们就得到了鼠标所在点和母球的中心点。
Code#cue { position:absolute; } . . .if (drawingtopCanvas.getContext) { var cueContext = drawingtopCanvas.getContext('2d'); } . . .var cueCenter = [15, -4];var cue = new Image; cue.src = '<%: Url.Content("../Content/Images/cue.PNG") %>'; var shadowCue = new Image; shadowCue.src = '<%: Url.Content("../Content/Images/shadowCue.PNG") %>'; cueContext.clearRect(0, 0, topCanvasWidth, topCanvasHeight); if (isReady) { cueContext.save(); cueContext.translate(cueBall.position.x + 351, cueBall.position.y + 145); cueContext.rotate(shadowRotationAngle - Math.PI / 2); cueContext.drawImage(shadowCue, cueCenter[0] + cueDistance, cueCenter[1]); cueContext.restore(); cueContext.save(); cueContext.translate(cueBall.position.x + 351, cueBall.position.y + 140); cueContext.rotate(angle - Math.PI / 2); cueContext.drawImage(cue, cueCenter[0] + cueDistance, cueCenter[1]); cueContext.restore(); }
为了让球杆变得更真实我们为球杆添加了投影,并且我们故意让球杆投影的旋转角度和球杆的角度不一样,我们这样做是为了让球杆有3D的效果。最终的效果实在是太酷了。
推拉球杆
这个球杆动画模仿了真实人类的特征:你是否看到过斯诺克选手在瞄准的时候会推拉球杆?我们通过HTML5改变母球和球杆的距离实现了这一效果。当达到一个极点是球杆会被拉回来,然后到达另一个极点时又会被向前推。这样周而复始,知道选手停止移动鼠标。
Codevar cueDistance = 0;var cuePulling = true; . . . function render() { . . . if (cuePulling) { if (lastMouseX == mouseX || lastMouseY == mouseY) { cueDistance += 1; } else { cuePulling = false; getMouseXY(); } } else { cueDistance -= 1; } if (cueDistance > 40) { cueDistance = 40; cuePulling = false; } else if (cueDistance < 0) { cueDistance = 0; cuePulling = true; } . . .
显示目标路径
当选手移动鼠标时,我们会在母球和当前鼠标点之间画一条虚线。这对选手们长距离瞄准相当的便利。
这条目标路径只有在等待用户击球时才会显示:
Codeif (!cueBall.pocketIndex) { context.strokeStyle = '#888'; context.lineWidth = 4; context.lineCap = 'round'; context.beginPath(); //here we draw the line context.dashedLine(cueBall.position.x, cueBall.position.y, targetX, targetY); context.closePath(); context.stroke(); }
需要注意的是在HTML5 canvas中并没有内置函数来画虚线。幸运的是有一个叫phrogz的家伙在StackOverflow网站上发布了一个关于这个画虚线的帖子:
Code//function kindly provided by phrogz at://http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvasvar CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;if (CP && CP.lineTo) { CP.dashedLine = function (x, y, x2, y2, dashArray) { if (!dashArray) dashArray = [10, 5]; var dashCount = dashArray.length; this.moveTo(x, y); var dx = (x2 - x), dy = (y2 - y); var slope = dy / dx; var distRemaining = Math.sqrt(dx * dx + dy * dy); var dashIndex = 0, draw = true; while (distRemaining >= 0.1) { var dashLength = dashArray[dashIndex++ % dashCount]; if (dashLength > distRemaining) dashLength = distRemaining; var xStep = Math.sqrt(dashLength * dashLength / (1 + slope * slope)); var signal = (x2 > x ? 1 : -1); x += xStep * signal; y += slope * xStep * signal; this[draw ? 'lineTo' : 'moveTo'](x, y); distRemaining -= dashLength; draw = !draw; } } }
显示跟踪路径
当选手击打母球后,母球会在球桌上留下一条跟踪线,用来标明其上一个点的位置。
创建这个跟踪路径比前面提到的目标路径复杂一点。首先我必须去实例化一个Queue对象,这个项目中的Queue对象原型由Stephen Morley提供。
Codevar tracingQueue = new Queue();
一旦球开始运动,我们就将母球的实时位置压入这个Queue中:
Codeif (renderStep % 2 == 0) { draw(); enqueuePosition(new Vector2D(cueBall.position.x, cueBall.position.y)); }
enqueuePosition
函数确保了我们只保存前20个点的位置,这也就是为什么我们只让显示最近的母球的运动路径的原因。
Codefunction enqueuePosition(position) { tracingQueue.enqueue(position); var len = tracingQueue.getLength(); if (len > 20) { tracingQueue.dequeue(); } }
接下来,我们要遍历Queue中的数据,从而来创建这条跟踪路径:
Code//drawing the tracing linevar lastPosX = cueBall.position.x;var lastPosY = cueBall.position.y; var arr = tracingQueue.getArray(); if (!cueBall.pocketIndex) { context.strokeStyle = '#363'; context.lineWidth = 8; context.lineCap = 'round'; context.beginPath(); var i = arr.length; while (--i > -1) { var posX = arr[i].x; var posY = arr[i].y; context.dashedLine(lastPosX, lastPosY, posX, posY, [10,200,10,20]); lastPosX = posX; lastPosY = posY; } context.closePath(); context.stroke(); }
绘制小球
小球和他们的投影都是呈现在一个特殊的canvas上(在球杆canvas下方)。
在呈现小球时,我们先要呈现其投影,这样做主要是为了模拟3D的环境。每一个小球必须有投影,我们对每个小球的投影位置都会有一点细微的不同,这些细微差别表明了小球是在不同方向被投射的,也说明了光源所在的位置。
每个小球是由一个公共函数来画的,函数有两个参数:1)canvas context;2)小球对象。函数先画出一个完整的圆弧然后根据小球对象提供的颜色将这个圆弧线性填充。
每一个小球对象有3中颜色:光亮色、中色和暗色,这些颜色就是用来创建线性渐变颜色的,3D效果也是这样做出来的。
Codefunction drawBall(context, ball, newPosition, newSize) { var position = ball.position; var size = ball.size; if (newPosition != null) position = newPosition; if (newSize != null) size = newSize; //main circle context.beginPath(); context.fillStyle = ball.color; context.arc(position.x, position.y, size, 0, Math.PI * 2, true); var gradient = context.createRadialGradient( position.x - size / 2, position.y - size / 2, 0, position.x, position.y, size ); //bright spot gradient.addColorStop(0, ball.color); gradient.addColorStop(1, ball.darkColor); context.fillStyle = gradient; context.fill(); context.closePath(); context.beginPath(); context.arc(position.x, position.y, size * 0.85, (Math.PI / 180) * 270, (Math.PI / 180) * 200, true); context.lineTo(ball.x, ball.y); var gradient = context.createRadialGradient( position.x - size * .5, position.y - size * .5, 0, position.x, position.y, size); gradient.addColorStop(0, ball.lightColor); gradient.addColorStop(0.5, 'transparent'); context.fillStyle = gradient; context.fill(); } function drawBallShadow(context, ball) { //main circle context.beginPath(); context.arc(ball.position.x + ball.size * .25, ball.position.y + ball.size * .25, ball.size * 2, 0, Math.PI * 2, true); try { var gradient = context.createRadialGradient( ball.position.x + ball.size * .25, ball.position.y + ball.size * .25, 0, ball.position.x + ball.size * .25, ball.position.y + ball.size * .25, ball.size * 1.5 ); } catch (err) { alert(err); alert(ball.position.x + ',' + ball.position.y); } gradient.addColorStop(0, '#000000'); gradient.addColorStop(1, 'transparent'); context.fillStyle = gradient; context.fill(); context.closePath(); }
检测小球之间的碰撞
小球以快速和连续的方式呈现在canvas上:首先,我们清空canvas,然后在上面绘制投影,再绘制小球,最后更新小球的位置坐标,这样周而复始。在这个期间,我们需要检查小球是否与另一个小球发生了碰撞,我们通过对小球的碰撞检测来完成这些的。
Codefunction isColliding(ball1, ball2) { if (ball1.pocketIndex == null && ball2.pocketIndex == null) { var xd = (ball1.position.x - ball2.position.x); var yd = (ball1.position.y - ball2.position.y); var sumRadius = ball1.size + ball2.size; var sqrRadius = sumRadius * sumRadius; var distSqr = (xd * xd) + (yd * yd); if (Math.round(distSqr) <= Math.round(sqrRadius)) { if (ball1.Points == 0) { strokenBalls[strokenBalls.length] = ball2; } else if (ball2.Points == 0) { strokenBalls[strokenBalls.length] = ball1; } return true; } } return false; }
解析小球之间的碰撞
上图来自维基百科
我觉得解析小球间的碰撞问题是这个项目的核心,首先我们需要比较2个小球的组合(ball 1和ball 2)。然后我们找到一个“碰撞口”,也就是在碰撞的那一刻将它们移动到准确的位置。要完成这些我们需要做一些矢量运算。下一步就是要计算最终碰撞的冲力,最后就是要改变两个小球的冲量,也就是用它的冲力去加上或减去其速度向量得到的结果。当碰撞结束后,它们的位置和速度都将发生变化。
Codefunction resolveCollision(ball1, ball2) { // get the mtd (minimum translation distance) var delta = ball1.position.subtract(ball2.position); var r = ball1.size + ball2.size; var dist2 = delta.dot(delta); var d = delta.length(); var mtd = delta.multiply(((ball1.size + ball2.size + 0.1) - d) / d); // resolve intersection -- // inverse mass quantities var mass = 0.5; var im1 = 1.0 / mass; var im2 = 1.0 / mass; // push-pull them apart based off their mass if (!ball1.isFixed) ball1.position = ball1.position.add((mtd.multiply(im1 / (im1 + im2)))); if (!ball2.isFixed) ball2.position = ball2.position.subtract(mtd.multiply(im2 / (im1 + im2))); // impact speed var v = ball1.velocity.subtract(ball2.velocity); var vn = v.dot(mtd.normalize()); // sphere intersecting but moving away from each other already // if (vn > 0) // return; // collision impulse var i = (-(0.0 + 0.08) * vn) / (im1 + im2); var impulse = mtd.multiply(0.5); var totalImpulse = Math.abs(impulse.x) + Math.abs(impulse.y); //Do some collision audio effects here... // change in momentum if (!ball1.isFixed) ball1.velocity = ball1.velocity.add(impulse.multiply(im1)); if (!ball2.isFixed) ball2.velocity = ball2.velocity.subtract(impulse.multiply(im2)); }
检测小球与转角间的碰撞
咋眼看,要检测小球与转角之间的碰撞似乎有点复杂,但幸运的是有一个非常简单却有效的方法来解决这个问题:由于转角也是圆形元素,我们可以把它们想象成固定的小球,如果我们能正确的确定固定小球的大小和位置,那么我们就像处理小球之间的碰撞那样解决小球和转角的碰撞问题。事实上,我们可以用同一个函数来完成这件事情,唯一的区别是这些转角是固定不动的。
下图是假设转角都是一些小球,那就会这样子:
分析小球与转角之间的碰撞
如上面说到的那样,小球之间的碰撞和小球与转角的碰撞唯一不同的是后者我们要确保他保持固定不动,代码如下:
Codefunction resolveCollision(ball1, ball2) { . . . // push-pull them apart based off their mass if (!ball1.isFixed) ball1.position = ball1.position.add((mtd.multiply(im1 / (im1 + im2)))); if (!ball2.isFixed) ball2.position = ball2.position.subtract(mtd.multiply(im2 / (im1 + im2))); . . . // change in momentum if (!ball1.isFixed) ball1.velocity = ball1.velocity.add(impulse.multiply(im1)); if (!ball2.isFixed) ball2.velocity = ball2.velocity.subtract(impulse.multiply(im2)); }
检测小球与矩形边缘的碰撞
我们通过小球与矩形边缘的碰撞检测来知道小球是否到达了球桌的上下左右边缘。检测的方式非常简单:每个小球需要检测4个点:我们通过对小球的x、y坐标的加减来计算出这些点。然后将它们和我们定义的球桌矩形范围进行对比,看它们是否在这个范围内。
分析小球与矩形边缘的碰撞
上图来自维基百科
处理小球与矩形边缘的碰撞比处理小球之间的碰撞简单很多。我们需要在矩形边界上找到离小球中心点最近的点,如果这个点在小球的半径范围内,那就说明碰撞了。
播放音频
没有一个游戏是没有声音的,不同的平台处理音频的方式不同。幸运的是HTML5给我们提供了一个audio标签,这简化了我们定义音频文件,加载音频和调节音量的工作。
一般的HTML5例子都是给大家看audio的标准用法,就是展现一个播放控制条。在这个游戏中,我们使用了不同的方法,并隐藏了音频播放控制条。这样做是有道理的,因为音频的播放不是直接由用户控制的,而是由游戏中的事件触发的。
页面上一共有8个audio标签,其中6个小球碰撞的声音,一个是击打的声音,一个则是小球掉入袋中的声音。这些声音可以同时播放,所以我们不用考虑并发的情况。
当选手射击母球时,我们就根据用户选择的力度来播放对应音量的击球声音频。
Code$('#topCanvas').click(function (e) { . . . audioShot.volume = strength / 100.0; audioShot.play(); . . . });
当一个小球碰到了另一个小球,我们就计算出碰撞的强度,然后选择合适音量的audio标签播放。
Codefunction resolveCollision(ball1, ball2) { . . . var totalImpulse = Math.abs(impulse.x) + Math.abs(impulse.y); var audioHit; var volume = 1.0; if (totalImpulse > 5) { audioHit = audioHit06; volume = totalImpulse / 60.0; } else if (totalImpulse > 4) { audioHit = audioHit05; volume = totalImpulse / 12.0; } else if (totalImpulse > 3) { audioHit = audioHit04; volume = totalImpulse / 8.0; } else if (totalImpulse > 2) { audioHit = audioHit03; volume = totalImpulse / 5.0; } else { audioHit = audioHit02; volume = totalImpulse / 5.0; } if (audioHit != null) { if (volume > 1) volume = 1.0; //audioHit.volume = volume; audioHit.play(); } . . . }
最后,当小球掉入袋中,我们就播放“fall.mp3”这个文件:
Codefunction pocketCheck() { for (var ballIndex = 0; ballIndex < balls.length; ballIndex++) { var ball = balls[ballIndex]; for (var pocketIndex = 0; pocketIndex < pockets.length; pocketIndex++) { . . some code here... . if (Math.round(distSqr) < Math.round(sqrRadius)) { if (ball.pocketIndex == null) { ball.velocity = new Vector2D(0, 0); ball.pocketIndex = pocketIndex; pottedBalls[pottedBalls.length] = ball; if (audioFall != null) audioFall.play(); } } } } }
本地存储游戏状态
有时候我们叫它web存储或者DOM存储,本地存储HTML5定义的一种机制,用来保持本地数据。文章开头提到的那几种浏览器原生就支持本地存储,所以我们不需要使用额外的js框架。
我们使用本地存储主要用来保存用户的游戏状态。简而言之,我们是要允许用户在开始游戏一段时间后,关闭浏览器,第二天打开还能继续往下玩。
当游戏开始后,我们需要检索在本地是否有数据存储着,有的话就加载它们:
CodejQuery(document).ready(function () { ... retrieveGameState(); ...
另一方面,游戏开始后我们需要对每一次射击的数据进行保存。
Codefunction render() { ... processFallenBalls(); saveGameState(); ... }
本地存储是由一个字符串字典实现的。这个简单的结构体接受传入的字符串和数字。我们只需要用setItem来将数据存储到本地。下面的代码说明了我们是如存储时间数据,小球位置坐标数据,选手数据和当前击球选手与等待击球选手的id:
Codefunction saveGameState() { //we use this to check whether the browser supports local storage if (Modernizr.localstorage) { localStorage["lastGameSaveDate"] = new Date(); lastGameSaveDate = localStorage["lastGameSaveDate"]; localStorage.setItem("balls", $.toJSON(balls)); localStorage.setItem("teams", $.toJSON(teams)); localStorage.setItem("playingTeamID", playingTeamID); localStorage.setItem("awaitingTeamID", awaitingTeamID); } }
我觉得除了下面的部分,上面的代码都已经解释了自己的作用了:
CodelocalStorage.setItem("balls", $.toJSON(balls)); localStorage.setItem("teams", $.toJSON(teams));
目前为止,本地存储还不能工作,我们需要将它们字符化,上面的2行代码是利用了jquery的toJSON方法将复杂的对象转换成了json字符串。
Code[{"isFixed":false,"color":"#ff0000","lightColor":"#ffffff","darkColor":"#400000","bounce":0.5, "velocity":{"x":0,"y":0},"size":10,"position":{"x":190,"y":150},"pocketIndex":null,"points":1, "initPosition":{"x":190,"y":150},"id":0},{"isFixed":false,"color":"#ff0000","lightColor":"#ffffff", "darkColor":"#400000","bounce":0.5,"velocity":{"x":0,"y":0},"size":10,"position":{"x":172,"y":138}, "pocketIndex":null,"points":1,"initPosition":{"x":172,"y":138},"id":1},........
一旦我们将这些对象序列化到本地存储后,我们就可以用类似的方法将它们检索出来,我们现在就是用getItem方法来检索他们。
Codefunction retrieveGameState() { //we use this to check whether the browser supports local storage if (Modernizr.localstorage) { lastGameSaveDate = localStorage["lastGameSaveDate"]; if (lastGameSaveDate) { var jsonBalls = $.evalJSON(localStorage.getItem("balls")); balls = []; var ballsOnTable = 0; for (var i = 0; i < jsonBalls.length; i++) { var jsonBall = jsonBalls[i]; var ball = {}; ball.position = new Vector2D(jsonBall.position.x, jsonBall.position.y); ball.velocity = new Vector2D(0, 0); ball.isFixed = jsonBall.isFixed; ball.color = jsonBall.color; ball.lightColor = jsonBall.lightColor; ball.darkColor = jsonBall.darkColor; ball.bounce = jsonBall.bounce; ball.size = jsonBall.size; ball.pocketIndex = jsonBall.pocketIndex; ball.points = jsonBall.points; ball.initPosition = jsonBall.initPosition; ball.id = jsonBall.id; balls[balls.length] = ball; if (ball.points > 0 && ball.pocketIndex == null) { ballsOnTable++; } } //if there is no more balls on the table, clear local storage //and reload the game if (ballsOnTable == 0) { localStorage.clear(); window.location.reload(); } var jsonTeams = $.evalJSON(localStorage.getItem("teams")); teams = jsonTeams; if (jsonTeams[0].BallOn) teams[0].BallOn = balls[jsonTeams[0].BallOn.id]; if (jsonTeams[1].BallOn) teams[1].BallOn = balls[jsonTeams[1].BallOn.id]; playingTeamID = localStorage.getItem("playingTeamID"); awaitingTeamID = localStorage.getItem("awaitingTeamID"); } }
总结
毫无疑问,HTML5将完全改变web世界。这次改革正在进行中,我希望这篇文章能邀请你一起加入这次革命,在这里我们看到了HTML5中的 Canvas,CSS3,音频和本地存储。尽管斯诺克游戏看起来很复杂,但使用了HTML5技术后就变得非常简单了。我从来都没有想过居然会有这么好的效果。
以上是HTML5實現斯諾克桌球俱樂部的範例程式碼(圖)的詳細內容。更多資訊請關注PHP中文網其他相關文章!