实现思路
MVC思路。
三个端分工合作。
可以仔细看。
整个俄罗斯方块为一个二维数组。
C控制M, M控制二维数组,V只要把二维数组渲染出来就行 。
我下面的实现思路可以应用到任何一个这种二维数组感觉的小游戏。
扩展性还算不错。
视图层
html部分
js部分
这里就是画背景和画全部方框的方法。
// 画背景
function drawBG(){
CTX.lineWidth = 1;
CTX.fillStyle = "#000";
CTX.fillRect(0,0, CANVAS.width, CANVAS.height);
CTX.strokeStyle = "#eee";
// 画横线
for(let i = BLOCK_SIZE; i < CANVAS.height; i +=BLOCK_SIZE){
CTX.beginPath(); //新建一条path
CTX.moveTo(0, i);
CTX.lineTo(CANVAS.width, i);
CTX.closePath();
CTX.stroke(); //绘制路径。
}
// 画竖线
for(let i = BLOCK_SIZE; i < CANVAS.width; i += BLOCK_SIZE){
CTX.beginPath(); //新建一条path
CTX.moveTo(i, 0);
CTX.lineTo(i, CANVAS.height);
CTX.closePath();
CTX.stroke(); //绘制路径。
}
}
// 绘制全部格子
function drawAllBlock(){
for(let i = 0; i < Y_MAX; i++){
for(let j = 0; j < X_MAX; j ++){
CTX.lineWidth = 8;
let cell = BLOCK_LIST[i][j];
if(!cell){
continue
}
CTX.strokeStyle = cell.color;
CTX.strokeRect( cell.x * BLOCK_SIZE + 4, cell.y*BLOCK_SIZE + 4, BLOCK_SIZE - 8, BLOCK_SIZE-8);
}
}
}
模型层
Cell。
我以一个单元格为一个对象。
这个格子有自己的颜色,坐标。
当然还可以扩展,例如图片之类的。
在这个游戏里面,格子还会移动。
所以还包含移动的方法。
// 单元类(就是一个一个的格子)
class Cell{
// 位置
x = 0;
y = 0;
//颜色
color = "#000";
// 单元格的id,区别不同的单元格(谁叫 js 没罚判断对象是否同一个)
id = "";
/**
* 构造器
* @param
* {Number} x: x轴位置
* {Number} y: y轴位置
*/
constructor({x, y, color}) {
this.x = x || 0;
this.y = y || 0;
this.color = color || "#000";
this.id = `${new Date().getTime()}${x}${y}`;//生成唯一 id 瞎写逻辑,不重复就行
if(this.y >= 0){
BLOCK_LIST[this.y][this.x] = this;
}
}
/**
* 移动方法
*
* @params {String} type: 类型,LEFT、LEFT、DOWN 对应的移动的方向
*
* @returns 返回移动结果坐标
*/
getMovePositionByType(type){
let position = {
x: this.x,
y: this.y
}
switch(type){
case "LEFT":
position.x -= 1;
break;
case "RIGHT":
position.x += 1;
break;
case "DOWN":
position.y += 1
break;
}
return position;
}
/**
* 设定位置
* @params { int } x: x坐标
* @params { int } y: Y坐标
*/
setCellPoisition({x, y}){
// 记住上一次位置
let beforX = this.x;
let beforY = this.y;
// 如果上一个格子没有被征用就回收
if(beforY >= 0 && BLOCK_LIST[beforY][beforX].id == this.id){
BLOCK_LIST[beforY][beforX] = null;
}
if( y >= 0 ){
BLOCK_LIST[y][x] = this;
}
// 赋值给 x y
this.x = x;
this.y = y;
}
}
Block类
这个就是这个游戏的主角,所有形状的格子都可以理解为多个 Cell 的集合。
所以这个类强调的是集合,并不是块本身。
所以这个类没有坐标,用一个数组表示自己。
这个块主要拥有下落,和旋转方法。
碰撞检测
整体设计清楚后,这个反而很简单,只要判断当前坐标在二维数组里面是否有对象就行了。
/**
* 移动碰撞检测方法
*
* @arg type: 移动类型
* @returns true 撞了
*/
isCollision(type){
// 碰撞检测
let result = false;
for(let i = 0, len = this.body.length; i < len; i ++ ){
let position = {};
// 移动类型
if( type != "ROTATION"){
// 拿到移动的下一个坐标
position = this.body[i].getMovePositionByType(type);
}else{
// 旋转类型
position = this.getOffsetPosition(i);
}
// 边界判断
// 看看x有没有撞
if(position.x < 0 || position.x >= X_MAX){
result = true
break;
}
// 看看y有么有撞
if( position.y >= Y_MAX){
result = true
break;
}
// 如果y为负数,不用验证,直接过
if(position.y < 0){
break;
}
// 看看有没有碰到格子
// 排除自身碰撞 !this.body.some( e => e.id == targetBlock.id) 只要有任意一个是自己body里面的cell就不算碰撞
let targetBlock = BLOCK_LIST[position.y][position.x];
if(targetBlock && !this.body.some( e => e.id == targetBlock.id)){
result = true
break;
}
}
// 如果是下落的移动方式且也碰到了东西,我们认为这个块已经结束使命了。
if( type == "DOWN" && result){
this.stopActive = true;
}
return result
}
旋转 这个方法比较有意思。
旋转的实现我自己是这么理解的。
他其实也是一个新的块。
只是他们之间的位置一样。
所以这个类在创建之初,就需要输入对应旋转的各个状态。
以及旋转中心。
//旋转方法
rotationBlock(){
// 没有旋转中心的不转
if(this.centerPoint == -1){
return
}
// 撞了就打断,不转了
if(this.isCollision("ROTATION")){
return
}
// 偏移值
this.body.forEach( (e, index) => {
e.setCellPoisition(this.getOffsetPosition(index))
})
this.rotate += 1;
if( this.rotate == this.rotationList.length){
this.rotate = 0;
}
}
// 偏移值计算方法
getOffsetPosition(cellIndex){
let index = this.rotate + 1;
if(this.rotate == this.rotationList.length -1){
index = 0;
}
let targetPoisition = this.rotationList[index][cellIndex];
let centerPoisition = this.rotationList[index][this.centerPoint];
return {
x: this.body[this.centerPoint].x + (targetPoisition.x - centerPoisition.x),
y: this.body[this.centerPoint].y + (targetPoisition.y - centerPoisition.y)
};
}
下落 方法就是定时器,每隔个几秒执行一次移动,碰撞就停止
// 下落方法
down(){
this.downTimer = setInterval(()=>{
// 是否已经停止活动
if(this.stopActive){
clearInterval(this.downTimer);
return
}
// 游戏运行状态才可以移动
if(GAME_STATE == 0){
this.move("DOWN");
}
}, this.downSpeed)
}
基础结束后,我们可以开始扩展了。
比如我们添加一个L字格子
L字格子
我这里是用继承类来设置,其实可以不用。
因为到 Block 类哪里,已经代表全部了,可以不用在扩展类了
// L形格子
class LBlock extends Block {
/**
* 构造器
*/
constructor({x, y, color}) {
super()
this.color = color || "#00FFFF";
this.centerPoint = 2; // 第几个块是旋转中心
this.rotate = 0; // 当前旋转角度
this.rotationList = [
[{x: 0, y: 0}, {x: 0, y: 1}, {x: 1, y: 1}, {x: 2, y: 1}], // 原来形状
[{x: 2, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 1, y: 2}], // 旋转 90
[{x: 2, y: 2}, {x: 2, y: 1}, {x: 1, y: 1}, {x: 0, y: 1}], // 旋转 180
[{x: 0, y: 2}, {x: 1, y: 2}, {x: 1, y: 1}, {x: 1, y: 0}] // 旋转 270
]
this.initBody(x || 4, y || -1); // 初始化位置(就是你希望他一开始在哪里出现)
}
}
运行
终于到最后一步了采用 requestAnimationFrame 来刷新故方法如下
// 运行
function run(){
// 使用 AnimationFrame 刷新
window.requestAnimationFrame(run);
// 游戏非暂停运行阶段都停止渲染
if(GAME_STATE != 0){
return
}
// 绘制背景
drawBG();
// 绘制二维表格
drawAllBlock();
// 如果活动格子停止活动了,就开始算分和创建新的格子
if(ACTIVE_BLOCK.stopActive){
// 算分
computeScore();
// 更新最顶的格子
setTopCellPoisition()
// 是否游戏结束
gameOver();
// 创建格子
ACTIVE_BLOCK = getRandomBlock();
}
}
// 启动方法
function start(){
// 设置为运行状态
GAME_STATE = 0;
OVER_DOM.style.display = "none";
// 初始化二维数组
initBodyList();
// 创建正在活动的格子
ACTIVE_BLOCK = getRandomBlock();
// 启动渲染
window.requestAnimationFrame(run);
}
// 启动
start()
这里游戏就跑起来了。
你可能会疑问,菜鸟教程有一篇文章这么提到
原文链接
其实仔细观察代码就可以发现,我是把画背景的方法在画全部二维数组格子前执行。
背景直接覆盖掉前面的所有东西,就实现了动画的效果。
下面是全部源代码
俄罗斯方块
按 W 旋转, ASD移动, 空格暂停
分数:
游戏结束
游戏暂停
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)