代码改变世界

Canvas 测试范例 - 贪吃蛇

2011-03-17 18:18    阅读(859)  评论(0编辑  收藏  举报
Html5 Canvas 的测试,先直接上效果:
花了两天写完,欢迎提出修改意见,下面有个操作盘可以用鼠标直接点击操作。要问为什么要操作盘?为了在安卓或iOS上测试呗~
发到博客园后操作盘似乎有点问题,没有反应,可以用键盘上下左右键操作,或者将代码保存成文件后再测试。
不支持Canvas,请用以下浏览器:IE9+ FF3.6+ Chrome10+

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="#" 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>