소개
우리는 HTML5 뒤에 숨은 위대한 웹 개발 혁명을 목격했다는 데 의심의 여지가 없습니다. 수년간 HTML4가 지배한 이후 새로운 움직임이 현재 웹 세계를 완전히 변화시키려고 합니다. 현대적인 분위기와 풍부한 사용자 경험 덕분에 Flash 및 Silverlight와 같은 프레임워크에서 실행되는 고유한 플러그인이 빠르게 만들어졌습니다.
아주 어린 개발자라면 HTML5를 이제 막 배우기 시작했기 때문에 큰 변화를 느끼지 못할 수도 있습니다. 언제든지 이 기사가 도움이 되기를 바라며, 물론 저와 같은 베테랑도 이 기사에서 새로운 요령을 배울 수 있기를 바랍니다.
저에게는 귀하의 리뷰가 매우 중요하므로 귀하의 의견을 기다리겠습니다. 물론 저를 더욱 흥분시키는 것은 게임 화면을 마우스 오른쪽 버튼으로 클릭하고 몰래 "야, 이건 플래시가 아니야! 실버라이트도 아니야!"라고 말할 때입니다.
시스템 요구 사항
이 글에서 제공하는 HTML5 당구 애플리케이션을 사용하려면 다음 브라우저를 설치해야 합니다: Chrome 12, Internet Explorer 9 또는 Fire Fox 5
게임 규칙
이 게임이 어떤 것인지 이미 알고 계실 것입니다. 예, 이것은 "British Snooker"입니다. 실제로는 스누커 게임의 모든 규칙이 구현되지 않았기 때문에 "Simplified British Snooker"라고 더 정확하게 부릅니다. 당신의 목표는 다른 플레이어보다 더 많은 점수를 얻기 위해 타겟 볼을 포켓에 넣는 것입니다. 자신의 차례가 되면 슛을 해야 합니다. 프롬프트에 따라 먼저 빨간 공을 득점해야 1점을 얻을 수 있습니다. 득점하면 다른 공을 계속해서 플레이할 수 있지만 이번에는 색깔이 있는 공만 플레이할 수 있습니다. . 공(즉, 빨간 공이 아닌 공). 성공적으로 득점하면 각 색깔의 공에 해당하는 점수를 얻게 됩니다. 그러면 득점된 색깔의 공이 테이블로 돌아가고, 계속해서 다른 빨간 공을 칠 수 있습니다. 실패할 때까지 이것을 반복하세요. 빨간색 공을 모두 플레이하면 테이블 위에 6개의 색깔 공만 남게 됩니다. 목표는 이 6개의 색깔 공을 노란색(2점), 녹색(3점), 녹색(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;
플레이어 플래시
애니메이션 게임 두 명의 플레이어가 참여합니다. 각 플레이어는 자신의 닉네임과 아바타를 가지고 있습니다. 우리는 단순히 플레이어의 닉네임을 "플레이어 1"과 "플레이어 2"로 명명했습니다. (아마도 사용자가 직접 입력하는 것이 더 멋질 것입니다.) 각 플레이어의 아바타는 당구장에서 놀고 있는 귀여운 강아지입니다. 한 플레이어의 차례가 되면 해당 플레이어의 초상화에 깜박이는 애니메이션 효과가 적용되고, 상대방의 초상화에는 깜박임이 멈춥니다.img 요소
animatio
함수의 CSS3 속성을 변경하여 opacity的值来实现的:我们使用jquery的
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 () { }); }
opacity的值在0-1.0之间变化。
전원 제어 바
<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 중국어 웹사이트의 기타 관련 기사를 참조하세요!