Html5 Canvas 的测试,先直接上效果:
花了两天写完,欢迎提出修改意见,下面有个操作盘可以用鼠标直接点击操作。要问为什么要操作盘?为了在安卓或iOS上测试呗~
发到博客园后操作盘似乎有点问题,没有反应,可以用键盘上下左右键操作,或者将代码保存成文件后再测试。
Canvas 的渲染效率还是蛮快的,Chrome浏览器运行速度明显好于其他浏览器,IE9则是个惊喜,速度和Chrome很相近了,Opera没有测试过,有兴趣可以试试。
速度没有详细记录下来,我只大约说一下,贪吃蛇移动+绘制10000次约需要1.5秒,我的CPU是T7250M移动酷睿双核,2年半前的DELL笔记本。
代码有几点需要注意:
- 我是按定时绘制的方式,移动可以单独完成,但实际游戏时移动一步就必须绘制一次了,否则就跳帧了
- 贪吃蛇移动时会自动记录轨迹,绘制帧时根据轨迹清除地图,达到最小重绘的目的
- 地图代码的意义:0空地 1这个参数没用到 2墙壁
- 食物代码的意义:apple增加1节 pear增加2节 banana增加3节 speed加速30 slow减速30
以下是源代码,为了方便调用我全部写成静态的了,贪吃蛇还可以改进,我希望可以支持多人游戏,可以在 iPad 上面对面游戏。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Snake</title> <style type="text/css"> #canvas-wrap { border: 0px; margin: 0px; padding: 0px; font-size: 12px; letter-spacing: 2px; font-family: Microsoft YaHei,Tahoma,Helvetica,Arial; font-weight: bold; text-shadow: 0 0 3px #9cf; /* 文字阴影,IE9+ FF3.6+ Chrome10+ */ } #canvas-wrap a { line-height:30px; padding:0 10px; color:#6cf; 100; display:block; } #canvas-wrap img { border: 0px; } </style> <script type="text/javascript"> function print() { var args = arguments; for (var i = 0; i < args.length; i++) document.getElementById('result').innerHTML += args[i] + ' '; document.getElementById('result').innerHTML += '<br />'; } </script> </head> <body> <div>abc</div> <div id="canvas-wrap" style="position:relative;"> <div style="float:left;120px;border:1px solid #cdf;margin:5px;"> <a href="#" onclick="Game.Start();return false;">Start 开始</a> <a href="#" onclick="Game.ReStart();return false;">ReStart 重玩</a> <a href="#" onclick="Game.Stop();return false;">Stop 停止</a> <table cellpadding="0" cellspacing="0"> <tr> <td colspan="2" style="text-align:center"><a href="#" onclick="Snake.ChangeDirection('up');return false;">Up</a></td> </tr> <tr> <td><a href="#" onclick="Snake.ChangeDirection('left');return false;">Left</a></td> <td><a href="#" onclick="Snake.ChangeDirection('right');return false;">Right</a></td> </tr> <tr> <td colspan="2" style="text-align:center"><a href="#" onclick="Snake.ChangeDirection('down');return false;">Down</a></td> </tr> </table> </div> <div style="float:left;border:1px solid #cdf;"> <canvas id="canvas" width="500" height="410">不支持Canvas,请用以下浏览器:IE9+ FF3.6+ Chrome10+</canvas> </div> <div id="result"></div> <script type="text/javascript"> /* Html5 Canvas 应用 - 贪吃蛇范例 - 作者:李萨 */ //初始化 var canvas = document.getElementById('canvas'); var g = canvas && canvas.getContext ? canvas.getContext('2d') : {}; var w = canvas.width, h = canvas.height, size = 10; //建立地图 var Map = { W: 50, H: 30, Matrix: [ [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2], [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2] ] } //地图字典 var MapDict = ['#ccf', 'blue', 'black']; //随机生成器 var Random = function (n) { return Math.floor(Math.random() * n + 1); } //食物 var Food = { Count: 5, Types: [ { Key: 'apple', Name: 'Apply', Color: 'red', S: 51, E: 100 }, { Key: 'pear', Name: 'Pear', Color: '#fc6', S: 21, E: 50 }, { Key: 'banana', Name: 'Banana', Color: '#ff9', S: 11, E: 20 }, { Key: 'speed', Name: 'Speed', Color: 'green', S: 6, E: 10 }, { Key: 'slow', Name: 'Slow', Color: '#999', S: 1, E: 5 } ], Items: [], CheckPoint: function (_x, _y) { //检查地图 if (Map.Matrix[_y][_x] != 0) return false; //检查其它食物 for (var i = 0; i < this.Items.length; i++) { if (_x == this.Items[i].X && _y == this.Items[i].Y) { return false; } } //检查贪吃蛇 for (var i = 0; i < Snake.Body.length; i++) { if (_x == Snake.Body[i].X && _y == Snake.Body.Y) { return false; } } return true; }, Init: function () { for (var i = 0; i < this.Count; i++) { this.Random(i); } }, Random: function (i) { var _x = Random(Map.W); var _y = Random(Map.H); while (!this.CheckPoint(_x, _y)) { _x = Random(Map.W); _y = Random(Map.H); } var seed = Random(100); for (var j = 0; j < this.Types.length; j++) { var type = this.Types[j]; if (seed >= type.S && seed <= type.E) this.Items[i] = { X: _x, Y: _y, Type: type.Key, Name: type.Name, Color: type.Color }; } } } //三角绘制 var DrawTriangle = function (x, y, x1, y1, x2, y2) { g.beginPath(); g.moveTo(x, y); g.lineTo(x1, y1); g.lineTo(x2, y2); g.closePath(); g.fill(); } //按键管理器 var Button = { Board: { X: 55, Y: 355, R: 50 }, Up: { X: 40, Y: 310, W: 30, H: 30, Value: 'up' }, Down: { X: 40, Y: 370, W: 30, H: 30, Value: 'down' }, Left: { X: 10, Y: 340, W: 30, H: 30, Value: 'left' }, Right: { X: 70, Y: 340, W: 30, H: 30, Value: 'right' }, Draw: function () { //背景板 g.fillStyle = '#ccc'; g.arc(this.Board.X, this.Board.Y, this.Board.R, 0, Math.PI * 2, true); g.fill(); //按钮背景 g.fillStyle = '#eee'; g.fillRect(this.Up.X, this.Up.Y, this.Up.W, this.Up.H); g.fillRect(this.Down.X, this.Down.Y, this.Down.W, this.Down.H); g.fillRect(this.Left.X, this.Left.Y, this.Left.W, this.Left.H); g.fillRect(this.Right.X, this.Right.Y, this.Right.W, this.Right.H); //按钮图标 g.fillStyle = '#ddd'; DrawTriangle(55, 315, 45, 330, 65, 330); DrawTriangle(55, 395, 45, 380, 65, 380); DrawTriangle(15, 355, 30, 345, 30, 365); DrawTriangle(95, 355, 80, 345, 80, 365); } } //物件检测器 var BoxChecker = { Event: null, Handler: function (e) { this.Event = e; switch (e.type) { case 'mousedown': Snake.ReadyDirection = this.CheckMouse(); break; case 'mouseup': break; case 'mousemove': break; case 'keypress': //只认符号键,不认功能键,如:Up, Down, Left, Right, Ctrl, Alt, Shift break; case 'keydown': Snake.ReadyDirection = this.CheckKeyboard(); break; case 'keyup': break; } }, //鼠标检测 CheckMouse: function () { //检测位置 var x = this.Event.X, y = this.Event.Y; if (x >= Button.Up.X && x <= Button.Up.X + Button.Up.W && y >= Button.Up.Y && y <= Button.Up.Y + Button.Up.H) { return Button.Up.Value; } if (x >= Button.Down.X && x <= Button.Down.X + Button.Down.W && y >= Button.Down.Y && y <= Button.Down.Y + Button.Down.H) { return Button.Down.Value; } if (x >= Button.Left.X && x <= Button.Left.X + Button.Left.W && y >= Button.Left.Y && y <= Button.Left.Y + Button.Left.H) { return Button.Left.Value; } if (x >= Button.Right.X && x <= Button.Right.X + Button.Right.W && y >= Button.Right.Y && y <= Button.Right.Y + Button.Right.H) { return Button.Right.Value; } }, //键盘检测 CheckKeyboard: function () { var code = this.Event.keyCode; switch (code) { case 37: return Button.Left.Value; case 38: return Button.Up.Value; case 39: return Button.Right.Value; case 40: return Button.Down.Value; } }, //碰撞检测 CheckHit: function () { var p = Snake.Body[0]; //检测地图 if (Map.Matrix[p.Y][p.X] == 2) { return 'die'; } //检测自身 for (var i = 1; i < Snake.Body.length; i++) { if (p.X == Snake.Body[i].X && p.Y == Snake.Body[i].Y) return 'die'; } //检测食物 for (var i = 0; i < Food.Items.length; i++) { var food = Food.Items[i]; if (p.X == food.X && p.Y == food.Y) { Food.Random(i); return food.Type; } } } } //扩展坐标 function PositionExpansion(e) { if (e.layerX || e.layerX == 0) { e.X = e.layerX; e.Y = e.layerY; } else if (e.offsetX || e.offsetX == 0) { e.X = e.offsetX; e.Y = e.offsetY; } var source = e.srcElement; e.X -= source.offsetLeft; e.Y -= source.offsetTop; if (BoxChecker) BoxChecker.Handler(e); } function KeyEvent(e) { if (BoxChecker) BoxChecker.Handler(e); } //附加监听事件 canvas.addEventListener('mousedown', PositionExpansion, false); //canvas.addEventListener('mousemove', PositionExpansion, false); //canvas.addEventListener('mouseup', PositionExpansion, false); //canvas.addEventListener('keypress', KeyEvent, false); document.body.addEventListener('keydown', KeyEvent, false); //document.body.addEventListener('keypress', KeyEvent, false); //document.body.addEventListener('keyup', KeyEvent, false); //贪吃蛇 var Snake = { Length: null, Speed: null, Direction: null, Body: null, Locus: [], Init: function () { this.Length = 3; this.Speed = 150; this.Direction = 'up'; this.Body = new Array(); for (var i = 0; i < this.Length; i++) { this.Body.push({ X: 10, Y: Map.H - 10 + i }) } }, ReadyDirection: null, ChangeDirection: function () { if (this.Direction != this.ReadyDirection) { switch (this.ReadyDirection) { case 'up': if (this.Direction != 'down') this.Direction = this.ReadyDirection; break; case 'down': if (this.Direction != 'up') this.Direction = this.ReadyDirection; break; case 'left': if (this.Direction != 'right') this.Direction = this.ReadyDirection; break; case 'right': if (this.Direction != 'left') this.Direction = this.ReadyDirection; break; } } }, ChangeLength: function (i) { var _x = this.Body[this.Length - 1].X; var _y = this.Body[this.Length - 1].Y; this.Length += i; for (var j = 0; j < i; j++) { this.Body.push({ X: _x, Y: _y }); } }, ChangeSpeed: function (s) { this.Speed += s; if (this.Speed < 50) this.Speed = 50; clearInterval(Game.Timer); Game.Start(); }, Move: function () { var end = this.Body.length - 1; //记录移动轨迹 this.Locus.push({ X: this.Body[end].X, Y: this.Body[end].Y }); //计算方向 if (this.ReadyDirection != null) { this.ChangeDirection(this.ReadyDirection); this.ReadyDirection = null; } //计算位置 for (var i = end; i > 0; i--) { this.Body[i].X = this.Body[i - 1].X; this.Body[i].Y = this.Body[i - 1].Y; } switch (this.Direction) { case 'up': this.Body[0].Y = (this.Body[0].Y > 0) ? this.Body[0].Y - 1 : Map.H - 1; break; case 'down': this.Body[0].Y = (this.Body[0].Y < Map.H - 1) ? this.Body[0].Y + 1 : 0; break; case 'left': this.Body[0].X = (this.Body[0].X > 0) ? this.Body[0].X - 1 : Map.W - 1; break; case 'right': this.Body[0].X = (this.Body[0].X < Map.W - 1) ? this.Body[0].X + 1 : 0; break; } //检测碰撞 var result = BoxChecker.CheckHit(); switch (result) { case 'die': Game.Stop(); g.fillStyle = 'rgba(255,0,0,0.8)'; g.font = "bold 72px Arial"; g.fillText('Game Over', 50, 160); break; case 'apple': Snake.ChangeLength(1); break; case 'pear': Snake.ChangeLength(2); break; case 'banana': Snake.ChangeLength(3); break; case 'speed': Snake.ChangeSpeed(-30); break; case 'slow': Snake.ChangeSpeed(30); break; } //print(result); } }; var times = 0; //建立游戏逻辑 var Game = { ShowInfo: false, Status: null, Timer: null, Init: function () { //初始化贪吃蛇 Snake.Init(); //初始化绘图板 g.fillStyle = '#eee'; g.fillRect(0, 0, w, h); //绘制地图 for (var y = 0; y < Map.Matrix.length; y++) { for (var x = 0; x < Map.Matrix[y].length; x++) { var point = Map.Matrix[y][x]; g.fillStyle = MapDict[point]; g.fillRect(x * size, y * size, size, size); } } //初始化食物 Food.Init(); //绘制操作键盘 Button.Draw(); }, Start: function () { if (!this.Status) { this.Init(); } //循环 this.Timer = setInterval('Snake.Move();Game.Draw();', Snake.Speed); this.Status = 'running'; this.Draw(); }, ReStart: function () { this.Stop(); this.Init(); this.Start(); }, Stop: function () { this.Status = 'stop'; clearInterval(this.Timer); this.Timer = null; }, Draw: function () { if (this.Status == 'running') { //重绘地图 for (var i = 0; i < Snake.Locus.length; i++) { var x = Snake.Locus[i].X, y = Snake.Locus[i].Y; var point = Map.Matrix[y][x]; g.fillStyle = MapDict[point]; g.fillRect(x * size, y * size, size, size); } Snake.Locus = []; //绘制食物 for (var i = 0; i < Food.Items.length; i++) { var food = Food.Items[i]; //print(food.Color); g.fillStyle = food.Color; g.fillRect(food.X * size, food.Y * size, size, size); } //绘制贪吃蛇 g.fillStyle = MapDict[1]; for (var i = 0; i < Snake.Body.length; i++) { var x = Snake.Body[i].X * size, y = Snake.Body[i].Y * size; g.fillRect(x, y, size, size); } //显示消息 if (this.ShowInfo) { this.Info("FPS:" + times + " Speed:" + Snake.Speed + " Length:" + Snake.Length + " X:" + Snake.Body[0].X + " Y:" + Snake.Body[0].Y); } //计数 times++; } }, Info: function (info) { g.fillStyle = '#ccc'; g.fillRect(0, 0, info.length * 8, 20); g.fillStyle = '#000'; g.font = "12px Consolas"; g.fillText(info, 5, 15); } }; Game.Init(); //Game.ShowInfo = true; //Game.Start(); //var p = times; //var t = setInterval('var s = times;print("fps:", s - p, "Time:", new Date().toLocaleString());p = s;', 1000); </script> </div> </body> </html>