Canvas 游戏——俄罗斯方块

我第三个动画游戏终于可以玩了,^_^。

试玩

Demo:http://ambar.github.com/Tetris/

截图

preview.png

起源

所得

如何实现

由于 wikipedia 的素材很像砖块,我代码里就把 Tetromino 称为了砖块(brick),整个拼板称为墙面(wall)——分别对应了游戏中玩家控制目标和地图。

变形

首先观察 " J " 形状转置(T)和旋转(R)的图形,零表示空心部位,非零实心部位:


J =>
1,0,0
1,1,1

T =>
1,1
0,1
0,1

R =>
1,1
1,0
1,0

转置即每一行变成了目标的每一列。再比较转置和旋转的图形,可以发现,它们的区别在于每一行的元素顺序是相反的,这对应的数组方法很明晰了,即 pop 和 unshift ,实现:


// 用二维数组表示'形状'
var shape_array = 
	[
	[1,0,0],
	[1,1,1],
	]

// 转置,行列交换
var transpose = function(ary){
	var ret = [], row, rows = ary.length, cols = ary[0].length;
	cols.times(function(y){
		ret.push(row = [])
		rows.times(function(x){
			row.push(ary[x][y])
		})
	})
	return ret;
}

// 向右旋转(顺时针),每行生成列元素时顺序相反
var rotate = function(ary){
	var ret = [], row, rows = ary.length, cols = ary[0].length;
	cols.times(function(y){
		ret.push(row = [])
		rows.times(function(x){
			row.unshift(ary[x][y])
		})
	})
	return ret;
}

这时,再看看向左旋转的图形,与转置图形比较可以显然看出来,把转置后 行的顺序逆转 就可以了,即可用现成的数组的 reverse 方法:


/* 逆时针旋转图示
0,1
0,1
1,1
*/

// 逆时针旋转算法
var rotate_anticlockwise = function(ary){
	return transpose(ary).reverse();
}

我开始的游戏是用的逆时针旋转,我以为这更自然。试玩了一下网上其他的游戏实现,结果都是顺时针的 =_= 。

砖块定义

我定义了三个主要属性:

  • position : 左下角参照坐标
  • parts : 每个可见部分坐标
  • shape : 形状

/*
* 以一个形状(二维数组)的左下角为参照位置,把它映射成一个位置列表(一维数组)。零为空位,全部舍弃
* @pos {vector}
* @shape {array}
*/
var mapShape = function(pos,shape) {
	var ret = [], rows = shape.length, cols = shape[0].length;
	rows.times(function(x){
		cols.times(function(y){
			shape[rows-x-1][y] && ret.push( pos.add( V([y,-x]) ) )
		});
	});
	return ret;
}

若某砖块的参照坐标为Vector[3,4],shape 为上面演示的 J,它的 parts 则为:


// parts = mapShape( V([3,4]), shape_ary ) =>
[ Vector[3,4], Vector[4,4], Vector[5,4], Vector[3,3] ]

有了恰当的属性定义之后,移动和拼到墙面就是轻而易举的了。

地图

地图关系存储及碰撞部分,结构也和上面的形状一样,使用二维数组。

首先要注意的是屏幕上的坐标系统和数组不是直接对应的:


screen : {x,y}
┏━━━x
┃ 
y

array : [x][y]
┏━━━y
┃ 
x

屏幕上的点转置才能对应上数组的元素。

我游戏的前半程是这样存储的:


map[p.x][p.y] = type;

点 p 与地图数组元素一 一对应,这样用起来很爽,但是有两个缺点:

  • 观察或打印结构不方便,需要转置地图数组。(如果拼板是 20*10,此时地图结构是的 10*20)
  • 检测消行不方便,也要转置列为行。

因此,在后半程,改正了过来:


map[p.y][p.x] = type;
消行

我用的一种相当简易的做法,毫无特效——直接删除一行,再用充满零元素的模板行塞到地图头部:


function eraseRow(idx) {
	this.map.splice(idx,1);
	this.map.unshift(this.tmplRow);
}

碰撞

es5 的 some 方法最适合处理这个,直接接受上面的 parts 参数就好:


function collideWith(vectors) {
	var map = this.map, height = this.height;
	return vectors.some(function(v) {
		var row = map[v.y];
		return v.y >= height || (row && row[v.x]);
	})
}

细节

  • 明暗变化的颜色,用HSL比RGB方便太多了。
  • 画多边形时,斜线会有黑色的锯齿,可以再在上面画一条线段解决。
  • canvas 绘制文字消耗太大,尤其是firefox。一定要用的话,新建一个 canvas 层做背景,文字绘制到它上面,并用CSS定位把它到主画布下面。
  • 未开户硬件加速的情况下,仅仅绘制普通图形,半屏之后也很卡——解决办法,用 getImageData 缓存绘制过的图形。

AMD

游戏代码后期改用 AMD 方式组织,加载器挑选了国人写的 seajs

AMD 是 CommonJS 倡议的模块定义方式,干掉了老套的用全局变量做命名空间的形式。 与老式的命名空间相比,AMD 的方式更加容易组织代码,结构也更干净、更一致。

这个游戏花了我几个小时来做转换,总结:

  • 如果一个模块是一组功能的集合,就用 exports
  • 如果一个模块是一个单一的类,就用 module.exports
  • 模块定义(define函数)的第一个参数‘模块名’是完全无意义的。我开始为了能够简单的合并文件,就全部命名了一遍,后面发现太麻烦——因为你要引用它时,就可能不得不打开文件去找它取了什么名字,这很悲剧。后面就自然的全部改了。
    文件的摆放层次 就是就是你的命名空间,这应该是 AMD 隐含的核心理念。

seajs 实际使用上,可能会碰到的问题:

  • 循环引用。seajs 会报一个警告,仔细检查可以排除。但是,不排除也可以工作。有点纠结,现在的折衷做法是导出了一个全局变量 TetrisGame。
  • define 第二个依赖参数也可能会出现模块引用的奇怪问题。同样是循环引用,但没有错误提示什么的,加载的模块也没有加载到——许久之后,提示超时了。

使用


// 网格单位长度, 默认 40
// TetrisGame.unit = 20;

// 显示网格线, 默认 true
// TetrisGame.showGrid = false;

// 主题类型 ["classic", "window", "bubble"], 默认 classic
// TetrisGame.theme = 'window';

// canvas,列数,行数,缩放
TetrisGame.init('#tetris-game',10,20,1);
// TetrisGame.init('#tetris-game',10,20,.5);

查看或下载

https://github.com/ambar/Tetris

posted @ 2011-10-26 22:31  ambar  阅读(2379)  评论(4编辑  收藏