您好,登錄后才能下訂單哦!
如何用HTML 5打造斯諾克桌球俱樂部,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
如何利用HTML5技術來打造一款非常酷的斯諾克桌球游戲,文章中詳細地列出了開發的全過程,并解說了實現這個游戲的幾個關鍵點。
毫無疑問,我們已經目睹了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
下面的這段代碼展示了我是如何來計算犯規的:
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分)。代碼如下:
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之間變化。
function 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 () { }); }
力量控制條
一個優秀的斯諾克選手都能很好地把握住每一桿的力度.不同的技巧需要不同的擊球方式:直接的,間接的,或者利用邊角的等等。不同方向和不同力度的組合可以構造成千上萬種可能的路徑。幸運的是,這個游戲提供了一個非常漂亮的力度控制條,可以幫助選手在擊球前調整他們的球桿。
為了達到這一點,我們使用了HTML5的meter元素標簽,它可以完成測量距離的工作。meter標簽最好在知道這次測量的最小值和最大值的情況下使用。在我們的這個例子中,這個值在0到100之間,因為IE9不支持meter,所以我用了一張背景圖來替代,這樣效果也是一樣的。
#strengthBar { position: absolute; margin:375px 0 0 139px; width: 150px; color: lime; background-color: orange; z-index: 5;}
當你點擊了力度條后,你實際上是選擇了一個新的力度。一開始你可能不是很熟練,但在真實世界中,這是需要時間來訓練自己的能力的。點擊力度條的代碼如下:
$('#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元素覆蓋在用戶頭像上的,所以你在頭像上看到的那個球,他看起來像是在標準的div上蓋了一個img元素,但是這個球并不是img實現的。當然我們也不能直接在div上畫圓弧和直線,這就是為什么我要將canvas覆蓋到頭像上的原因了。看看代碼吧:
<canvas id="player1BallOn" class="player1BallOn"> </canvas> <canvas id="player2BallOn" class="player2BallOn"> </canvas>
var 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格式的電扇圖片。只是我們并沒有用電扇本身的圖片,我們用他的投影。通過顯示風扇在球桌上的投影,讓我們覺得它在屋頂上旋轉,這樣就達到了我們目的:
#roofFan { position:absolute; left: 600px; top: -100px; width: 500px; height: 500px; border: 2px solid transparent; background-image: url('/Content/Images/roofFan.png'); background-size: 100%; opacity: 0.3; z-index: 2;} . . . <div id="roofFan"> </div>
為了獲得更為逼真的氣氛,我用Paint.Net軟件將電扇圖片平滑化了,現在你再也看不到電扇的邊緣了。我覺得這是達到如此酷的效果最為簡單的辦法。
除了用了這圖像處理的把戲,我們僅僅使用了一個帶背景圖的普通的div元素,這并沒有什么特別。既然我們已經得到了電扇圖片,我們就要讓它開始旋轉了。這里我們使用CSS3的rotate屬性來實現這一切。
球桿動畫
球桿的動畫對于這個游戲也不是必需的,但是這的確為此添加了不少樂趣。當你開始用鼠標在球桌上移動時,你會注意到球桿的確是跟著你的鼠標在轉動。這就是說球桿會一直保持跟隨鼠標的移動,就像你身臨其境一般真實。因為選手只能用他的眼睛來瞄準,所以這個效果也會對選手有所幫助。
球桿是單獨一張PNG圖片,圖片本身不直接以img的形式展現,也不以背景的形式展現,相反,它是直接展現在一個專門的canvas上的。當然我們也可以用div和css3來達到同樣的效果,但我覺得這樣能更好的說明如何在canvas上展現圖片。
首先,canvas元素會占據幾乎整個頁面的寬度。請注意這個特別的canvas有一個很大的z-index值,這樣球桿就可以一直在每個球的上方而不會被球遮蓋。當你在球桌上移動鼠標時,目標點會實時更新,這時候球桿圖片會進行2次轉換:首先,通過計算得到母球的位置,其次翻轉母球周圍的球桿,通過這2步我們就得到了鼠標所在點和母球的中心點。
#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改變母球和球桿的距離實現了這一效果。當達到一個極點是球桿會被拉回來,然后到達另一個極點時又會被向前推。這樣周而復始,知道選手停止移動鼠標。
var 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; } . . .
顯示目標路徑
當選手移動鼠標時,我們會在母球和當前鼠標點之間畫一條虛線。這對選手們長距離瞄準相當的便利。
這條目標路徑只有在等待用戶擊球時才會顯示:
if (!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網站上發布了一個關于這個畫虛線的帖子:
//function kindly provided by phrogz at: //http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas var 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提供。
var tracingQueue = new Queue();
一旦球開始運動,我們就將母球的實時位置壓入這個Queue中:
if (renderStep % 2 == 0) { draw(); enqueuePosition(new Vector2D(cueBall.position.x, cueBall.position.y)); }
enqueuePosition函數確保了我們只保存前20個點的位置,這也就是為什么我們只讓顯示最近的母球的運動路徑的原因。
function enqueuePosition(position) { tracingQueue.enqueue(position); var len = tracingQueue.getLength(); if (len > 20) { tracingQueue.dequeue(); } }
接下來,我們要遍歷Queue中的數據,從而來創建這條跟蹤路徑:
//drawing the tracing line var 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效果也是這樣做出來的。
function 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,然后在上面繪制投影,再繪制小球,最后更新小球的位置坐標,這樣周而復始。在這個期間,我們需要檢查小球是否與另一個小球發生了碰撞,我們通過對小球的碰撞檢測來完成這些的。
function 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)。然后我們找到一個“碰撞口”,也就是在碰撞的那一刻將它們移動到準確的位置。要完成這些我們需要做一些矢量運算。下一步就是要計算最終碰撞的沖力,最后就是要改變兩個小球的沖量,也就是用它的沖力去加上或減去其速度向量得到的結果。當碰撞結束后,它們的位置和速度都將發生變化。
function 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) ball1ball1.position = ball1.position.add((mtd.multiply(im1 / (im1 + im2)))); if (!ball2.isFixed) ball2ball2.position = ball2.position.subtract(mtd.multiply(im2 / (im1 + im2))); // impact speed var v = ball1.velocity.subtract(ball2.velocity); var vvn = 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) ball1ball1.velocity = ball1.velocity.add(impulse.multiply(im1)); if (!ball2.isFixed) ball2ball2.velocity = ball2.velocity.subtract(impulse.multiply(im2)); }
檢測小球與轉角間的碰撞
咋眼看,要檢測小球與轉角之間的碰撞似乎有點復雜,但幸運的是有一個非常簡單卻有效的方法來解決這個問題:由于轉角也是圓形元素,我們可以把它們想象成固定的小球,如果我們能正確的確定固定小球的大小和位置,那么我們就像處理小球之間的碰撞那樣解決小球和轉角的碰撞問題。事實上,我們可以用同一個函數來完成這件事情,唯一的區別是這些轉角是固定不動的。
下圖是假設轉角都是一些小球,那就會這樣子:
分析小球與轉角之間的碰撞
如上面說到的那樣,小球之間的碰撞和小球與轉角的碰撞唯一不同的是后者我們要確保他保持固定不動,代碼如下:
function resolveCollision(ball1, ball2) { . . . // push-pull them apart based off their mass if (!ball1.isFixed) ball1ball1.position = ball1.position.add((mtd.multiply(im1 / (im1 + im2)))); if (!ball2.isFixed) ball2ball2.position = ball2.position.subtract(mtd.multiply(im2 / (im1 + im2))); . . . // change in momentum if (!ball1.isFixed) ball1ball1.velocity = ball1.velocity.add(impulse.multiply(im1)); if (!ball2.isFixed) ball2ball2.velocity = ball2.velocity.subtract(impulse.multiply(im2)); }
檢測小球與矩形邊緣的碰撞
我們通過小球與矩形邊緣的碰撞檢測來知道小球是否到達了球桌的上下左右邊緣。檢測的方式非常簡單:每個小球需要檢測4個點:我們通過對小球的x、y坐標的加減來計算出這些點。然后將它們和我們定義的球桌矩形范圍進行對比,看它們是否在這個范圍內。
分析小球與矩形邊緣的碰撞
上圖來自維基百科
處理小球與矩形邊緣的碰撞比處理小球之間的碰撞簡單很多。我們需要在矩形邊界上找到離小球中心點最近的點,如果這個點在小球的半徑范圍內,那就說明碰撞了。
播放音頻
沒有一個游戲是沒有聲音的,不同的平臺處理音頻的方式不同。幸運的是HTML5給我們提供了一個audio標簽,這簡化了我們定義音頻文件,加載音頻和調節音量的工作。
一般的HTML5例子都是給大家看audio的標準用法,就是展現一個播放控制條。在這個游戲中,我們使用了不同的方法,并隱藏了音頻播放控制條。這樣做是有道理的,因為音頻的播放不是直接由用戶控制的,而是由游戲中的事件觸發的。
頁面上一共有8個audio標簽,其中6個小球碰撞的聲音,一個是擊打的聲音,一個則是小球掉入袋中的聲音。這些聲音可以同時播放,所以我們不用考慮并發的情況。
當選手射擊母球時,我們就根據用戶選擇的力度來播放對應音量的擊球聲音頻。
$('#topCanvas').click(function (e) { . . . audioShot.volume = strength / 100.0; audioShot.play(); . . . });
當一個小球碰到了另一個小球,我們就計算出碰撞的強度,然后選擇合適音量的audio標簽播放。
function 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”這個文件:
function 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框架。
我們使用本地存儲主要用來保存用戶的游戲狀態。簡而言之,我們是要允許用戶在開始游戲一段時間后,關閉瀏覽器,第二天打開還能繼續往下玩。
當游戲開始后,我們需要檢索在本地是否有數據存儲著,有的話就加載它們:
jQuery(document).ready(function () { ... retrieveGameState(); ...
另一方面,游戲開始后我們需要對每一次射擊的數據進行保存。
function render() { ... processFallenBalls(); saveGameState(); ... }
本地存儲是由一個字符串字典實現的。這個簡單的結構體接受傳入的字符串和數字。我們只需要用setItem來將數據存儲到本地。下面的代碼說明了我們是如存儲時間數據,小球位置坐標數據,選手數據和當前擊球選手與等待擊球選手的id:
function 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); } }
我覺得除了下面的部分,上面的代碼都已經解釋了自己的作用了:
localStorage.setItem("balls", $.toJSON(balls)); localStorage.setItem("teams", $.toJSON(teams));
目前為止,本地存儲還不能工作,我們需要將它們字符化,上面的2行代碼是利用了jquery的toJSON方法將復雜的對象轉換成了json字符串。
[{"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方法來檢索他們。
function 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技術后就變得非常簡單了。我從來都沒有想過居然會有這么好的效果。
關于如何用HTML 5打造斯諾克桌球俱樂部問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。