本文介绍了如何利用HTML5技术来打造一款非常酷的斯诺克桌球游戏,文章中详细地列出了开发的全过程,并解说了实现这个游戏的几个关键点。在文章末尾我向大家提供了游戏的在线实例页面和源码下载链接,如果你只是想玩玩(需要使用支持HTML5的浏览器,建议使用Chrome 12, Internet Explorer 9 或者 Fire Fox 5及其以上版本),那你可以跳过正文拉到页面最底端去玩玩那个游戏或者下载源码,但我建议你好好看看实现过程,对我们学习HTML5非常有帮助。
目前创新互联建站已为近千家的企业提供了网站建设、域名、虚拟主机、网站托管、服务器托管、企业网站设计、汨罗网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。
推荐专题:HTML 5 下一代Web开发标准详解
毫无疑问,我们已经目睹了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覆盖到头像上的原因了。看看代码吧:
- 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;}
- .
- .
- .
为了获得更为逼真的气氛,我用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的效果。最终的效果实在是太酷了。
#p#
推拉球杆
这个球杆动画模仿了真实人类的特征:你是否看到过斯诺克选手在瞄准的时候会推拉球杆?我们通过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,然后在上面绘制投影,再绘制小球,最后更新小球的位置坐标,这样周而复始。在这个期间,我们需要检查小球是否与另一个小球发生了碰撞,我们通过对小球的碰撞检测来完成这些的。
网页题目:用HTML 5打造斯诺克桌球俱乐部
- 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;
- }
当前网址:http://www.shufengxianlan.com/qtweb/news35/305235.html网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联