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; width: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;width:120px;border:1px solid #cdf;margin:5px;">
<a href="#" οnclick="Game.Start();return false;">Start 开始</a>
<a href="#" οnclick="Game.ReStart();return false;">ReStart 重玩</a>
<a href="#" οnclick="Game.Stop();return false;">Stop 停止</a>
<table cellpadding="0" cellspacing="0">
<tr>
<td colspan="2" style="text-align:center"><a href="#" οnclick="Snake.ChangeDirection('up');return false;">Up</a></td>
</tr>
<tr>
<td><a href="#" οnclick="Snake.ChangeDirection('left');return false;">Left</a></td>
<td><a href="#" οnclick="Snake.ChangeDirection('right');return false;">Right</a></td>
</tr>
<tr>
<td colspan="2" style="text-align:center"><a href="#" οnclick="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>