Einführung
Es besteht kein Zweifel, dass wir Zeuge der großen Webentwicklungsrevolution hinter HTML5 geworden sind. Nach so vielen Jahren der Dominanz von HTML4 ist eine neue Bewegung dabei, die aktuelle Webwelt völlig zu verändern. Es ist die moderne Atmosphäre und das umfassende Benutzererlebnis, die es schnell zu einem einzigartigen Plug-in machen, das auf Frameworks wie Flash und Silverlight läuft.
Wenn Sie ein sehr junger Entwickler sind, haben Sie vielleicht gerade erst angefangen, HTML5 zu lernen, sodass Sie möglicherweise keine großen Veränderungen bemerken. Ich hoffe jederzeit, dass dieser Artikel Ihnen hilft und natürlich, dass Veteranen wie ich einige neue Tricks daraus lernen können.
Ihre Bewertung ist mir sehr wichtig, daher freue ich mich darauf, von Ihnen zu hören. Was mich natürlich noch mehr begeistert, ist, wenn man mit der rechten Maustaste auf den Spielbildschirm klickt und insgeheim sagt: „Hey, das ist nicht Flash! Es ist auch nicht Silverlight!“
Systemanforderungen
Um die in diesem Artikel bereitgestellte HTML5-Billardanwendung verwenden zu können, müssen Sie die folgenden Browser installieren: Chrome 12, Internet Explorer 9 oder Fire Fox 5
Spielregeln
Vielleicht wissen Sie bereits, wie das Spiel aussieht Ja, das ist „British Snooker“, genauer gesagt wird es „Simplified British Snooker“ genannt, da nicht alle Regeln des Snookerspiels umgesetzt werden. Ihr Ziel ist es, die Zielbälle einzulochen, um mehr Punkte als andere Spieler zu erzielen. Wenn Sie an der Reihe sind, müssen Sie den Schuss ausführen: Um 1 Punkt zu erhalten, müssen Sie laut Aufforderung zunächst einen roten Ball erzielen. Wenn Sie einen Punkt erzielen, können Sie weiterhin andere Bälle spielen – dieses Mal jedoch nur farbige Bälle . Bälle (also andere Bälle als rote Bälle). Wenn Sie erfolgreich punkten, erhalten Sie Punkte entsprechend den jeweiligen farbigen Kugeln. Dann kommt der farbige Ball, der gepunktet wurde, zurück auf den Tisch und Sie können weiterhin andere rote Bälle schlagen. Wiederholen Sie dies immer wieder, bis Sie scheitern. Nachdem Sie alle roten Bälle gespielt haben, bleiben nur noch 6 farbige Bälle auf dem Tisch. Ihr Ziel ist es, diese 6 farbigen Bälle in der folgenden Reihenfolge in den Beutel zu legen: Gelb (2 Punkte), Grün (3 Punkte), Braun (4 Punkte), Blau (5 Punkte), Rosa (6 Punkte), Schwarz (7 Punkte). Wenn ein Ball nicht in der oben genannten Reihenfolge gewertet wird, landet er auf dem Tisch, andernfalls landet er im Beutel. Wenn alle Bälle gespielt wurden, endet das Spiel und die Person mit den meisten Punkten gewinnt.
Handhabung von Fouls
Um Ihr Foul zu bestrafen, erhalten andere Spieler Ihre Strafpunkte:
4 Strafpunkte, wenn der weiße Ball in den Beutel fällt
Wenn der erste vom weißen Ball getroffene Ball ein Fehler ist, ist der erste Strafpunkt der Strafwert
Der erste falsche Ball, der in den Sack fällt, ist die erste Ballstrafe wert
Der Strafpunkt beträgt mindestens 4
Der folgende Code zeigt, wie ich Fouls berechne:
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'); } } } } }
Score
Wir verwenden die folgenden Regeln, um Scores zu berechnen: Rot (1 Punkt), Gelb (2 Punkte), Grün (3 Punkte), Braun (4 Punkte), Blau (5 Punkte), Rosa (6 Punkte), Schwarz (7 Punkte). Der Code lautet wie folgt:
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;
Spieler blinktAnimationAvatar
< 🎜 Das Spiel umfasst zwei Spieler. Jeder Spieler hat seinen eigenen Spitznamen und Avatar. Wir haben die Spitznamen der Spieler einfach als „Spieler 1“ und „Spieler 2“ bezeichnet (vielleicht wäre es bequemer, sie vom Benutzer selbst eingeben zu lassen). ). hübsch). Der Avatar jedes Spielers ist ein süßer Welpe, der Billard spielt. Wenn ein Spieler an der Reihe ist, blinkt sein Porträt und das Porträt des Gegners hört auf zu blinken. Diesen Effekt erreichen wir, indem wir das CSS3-Attribut des img-Elements animatioopacity的值来实现的:我们使用jquery的
-Funktion ändern, um 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 () { }); }
<p><code><span style="color:#f79646;font-size:18px"><strong>力量控制条</strong></span>
Power Control Bar<🎜><🎜>一个优秀的斯诺克选手都能很好地把握住每一杆的力度.不同的技巧需要不同的击球方式:直接的,间接的,或者利用边角的等等。不同方向和不同力度的组合可以构造成千上万种可能的路径。幸运的是,这个游戏提供了一个非常漂亮的力度控制条,可以帮助选手在击球前调整他们的球杆。
为了达到这一点,我们使用了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-Beispielcode zur Implementierung von Snooker Club (Bild): 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技术后就变得非常简单了。我从来都没有想过居然会有这么好的效果。
Das obige ist der detaillierte Inhalt vonHTML5-Beispielcode zur Implementierung von Snooker Club (Bild). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!