做一个2048项目的总结
一个小小的2048游戏真的其实挺复杂的(新手上路)
首先是对静态页面来进行设计,确定整体的页面构造
然后先做出各个数字块静态的模板,设计好圆角,数字显示,大小,颜色
也就是先做出样式来,然后我们再将他们进行隐藏
后面根据需求来对他们进行显示
每一个方块都具有相同的大小,圆角等等
因此将其抽象成对象Tile
每个的位置都有行和列两个属性,因此使用绝对定位来进行布局
//设置整个的布局为绝对定位
.tile-container {
position: absolute;
left: 0;
top: 0;
将所有的方块都进行设计渲染出来
接着把游戏成功或者失败时弹框做出来
<div class="mask status">
<div class="content">Game Over!</div>
<button>Try again</button>
</div>
然后将其display设计为none,先隐藏
然后准备开始引入js来实现动态效果
1.对方块Tile这个对象应该进行一个初始化设计
每个方块是有行,列以及值属性组成:
function Tile(position, value) {
this.row = position.row;
this.column = position.column;
this.value = value;
}
2.对整个Grid容器进行设计,即对整个方块们活动的地方进行一个设置
function Grid(size = 4) {
this.size = size;
this.cells = [];
this.init();
}
//给Grid init方法,进行初始化
//初始化成为一个4*4的数组
Grid.prototype.init = function(size) {
for (let row = 0; row < size; row++) {
this.cells.push([]);
for (let column = 0; column < size; column++) {
this.cells[row].push(null);
}
}
};
再给其加一个add方法,以便于将方块添加进数组里面
Grid.prototype.add = function(tile) {
this.cells[tile.row][tile.column] = tile;
};
完成后,准备进行渲染功能的实现
渲染是指将这个方块在网页上显示出来
function Render() {
this.tileContainer = document.querySelector('.tile-container');
}
// 渲染整个grid
Render.prototype.render = function(grid) {
for (let row = 0; row < grid.size; row++) {
for (let column = 0; column < grid.size; column++) {
// 如果grid中某个cell不为空,则渲染这个cell
if (grid.cells[row][column]) {
this.renderTile(grid.cells[row][column]);
}
}
}
};
// 渲染单个tile
Render.prototype.renderTile = function(tile) {
// 创建一个tile-inner
const tileInner = document.createElement('div');
tileInner.setAttribute('class', 'tile-inner');
tileInner.innerHTML = tile.value;
// 创建一个tile
const tileDom = document.createElement('div');
let classList = [
'tile',
`tile-${tile.value}`,
`tile-position-${tile.row + 1}-${tile.column + 1}`
];
tileDom.setAttribute('class', classList.join(' '));
tileDom.appendChild(tileInner);
this.tileContainer.appendChild(tileDom);
};
渲染单个tile的思路是:首先调用DOM操作,定位在容器那里
然后创建div,设置其类名,并且显示数值为tile的值
这就是为什么之前就要把2到2048在前面都设计出来
这样后面渲染时,设置的类名对了,直接使用前面的样式
随机出现方块的逻辑实现
游戏开始时,方块出现是随机的,并且2和4的概率不同
那么思路应该是:获取Grid所有空闲的方格,然后利用随机数获取其中一个
因此要给Grid添加方法:
// 获取所有可用方格的位置
Grid.prototype.availableCells = function() {
//设置一个Cell用于记录空闲
const availableCells = [];
//对cell进行遍历
for (let row = 0; row < this.cells.length; row++) {
for (let column = 0; column < this.cells[row].length; column++) {
// 如果当前方格没有内容,则其可用(空闲)
if (!this.cells[row][column]) {
//记录该点位的位置
availableCells.push({ row, column });
}
}
}
return availableCells;
};
随机某个可用方格
// 随机获取某个可用方格的位置
Grid.prototype.randomAvailableCell = function() {
// 调用之前设计的方法,获取到所有的空闲方格
const cells = this.availableCells();
//如果可用的大于0
if (cells.length > 0) {
// 利用Math.random()随机获取其中的某一个
return cells[Math.floor(Math.random() * cells.length)];
}
};
利用随机空闲的位置创建节点
let grid = new Grid();
let render = new Render();
for (let i = 0; i < 2; i++) {
// 90%概率为2,10%为4
const value = Math.random() < 0.9 ? 2 : 4;
// 随机一个方格的位置
const position = grid.randomAvailableCell();
// 添加到grid中
grid.add(new Tile(position, value));
}
//调用函数进行渲染
render.render(grid);
在做项目的时候,一定要对代码进行分割,不同的代码放到不同的包里
不同功能的函数进行分开,各自完成自己相应的功能
可能这就是架构思维吧,我是菜鸡,慢慢学
接下来要进行键盘监听,以方便对输入的操作进行处理
键盘的键位都对应着一个keyCode
可以查阅,也可以设计这个代码:
window.addEventListener('keyup', function(e) {
console.log(e.keyCode);
});
然后分别按下方向键,来获取对应的keycode
38 => 上
37 => 左
39 => 下
40 => 右
然后要进行监听回调
增加一个监听器,用于监听键盘事件
//listener.js
function Listener({ move: moveFn }) {
window.addEventListener('keyup', function(e) {
switch (e.keyCode) {
case 38:
moveFn('向左');
break;
case 37:
moveFn('向上');
break;
case 39:
moveFn('向下');
break;
case 40:
moveFn('向右');
break;
}
});
}
this.listener = new Listener({
move: function(direction) {
console.log(direction);
}
});
将监听到的存储于moveFn回调,传回到listener
方向向量化
这些方向,必须转化为向量,计算机才能读懂
因此我们要将这些键盘的动作进行向量化
上下左右的移动对应其实是行,列的值的变化
function Listener({ move: moveFn }) {
window.addEventListener('keyup', function(e) {
switch (e.keyCode) {
case 38:
moveFn({ row: -1, column: 0 });
break;
case 37:
moveFn({ row: 0, column: -1 });
break;
case 39:
moveFn({ row: 0, column: 1 });
break;
case 40:
moveFn({ row: 1, column: 0 });
break;
}
});
}
移动
移动个人感觉是最负责的地方,尽量详细分析,大家一起学习吧
移动应该遵循什么样的规则?
1.同一排,同一列的方块应该向同一个方向进行移动
2.每个方块都是移动到最后一个空白的位置
为了让方格移动,肯定是要对每一个方格都进行遍历的
那么遍历肯定是有先后顺序
正常是左上到右下顺序
Manager.prototype.getPaths = function(direction) {
let rowPath = [];
let columnPath = [];
for (let i = 0; i < this.size; i++) {
rowPath.push(i);
columnPath.push(i);
}
return {
rowPath,
columnPath
};
};
当方向向右的时候,遍历顺序应该是从右往左
当方向向下的时候,遍历顺序应该是从下往上
优化一下:
Manager.prototype.getPaths = function(direction) {
let rowPath = [];
let columnPath = [];
for (let i = 0; i < this.size; i++) {
rowPath.push(i);
columnPath.push(i);
}
// 向右的时候
if (direction.column === 1) {
columnPath = columnPath.reverse();
}
// 向下的时候
if (direction.row === 1) {
rowPath = rowPath.reverse();
}
return {
rowPath,
columnPath
};
};
然后就要寻找目标位置,进行移动
定义一个next元素,使用循环使其定位一步一步加,并且每加一次就调用一次get方法和Outofrange方法
如果get方法有返回值说明此处有方块存在,或者判定是否出范围
// 寻找移动方向目标位置
Manager.prototype.getNearestAvaibleAim = function(aim, direction) {
// 位置 + 方向向量的计算公式
function addVector(position, direction) {
return {
row: position.row + direction.row,
column: position.column + direction.column
};
}
aim = addVector(aim, direction);
// 获取grid中某个位置的元素
let next = this.grid.get(aim);
// 如果next元素存在(也就是此目标位置已经有Tile),或者是超出游戏边界,则跳出循环。目的:就是找到最后一个空白且不超过边界的方格
while (!this.grid.outOfRange(aim) && !next) {
aim = addVector(aim, direction);
next = this.grid.get(aim);
}
// 这时候的aim总是多计算了一步,因此我们还原一下
aim = {
row: aim.row - direction.row,
column: aim.column - direction.column
};
return {
aim,
next
};
};
移动的整体思路:
根据键盘返回的操作,进行相应顺序的遍历
遍历的过程中,发现此处有Tile,则对他进行移动
根据方向,获取目标位置
定义一个moved变量,只要进行移动,就重新渲染
// 移动核心逻辑
Manager.prototype.listenerFn = function(direction) {
// 定义一个变量,判断是否引起移动
let moved = false;
const { rowPath, columnPath } = this.getPaths(direction);
for (let i = 0; i < rowPath.length; i++) {
for (let j = 0; j < columnPath.length; j++) {
const position = { row: rowPath[i], column: columnPath[j] };
const tile = this.grid.get(position);
if (tile) {
// 当此位置有Tile的时候才进行移动
const { aim, next } = this.getNearestAvaibleAim(position, direction);
// 区分合并和移动,当next值和tile值相同的时候才进行合并
if (next && next.value === tile.value) {
// 合并位置是next的位置,合并的value是tile.value * 2
const merged = new Tile(
{
row: next.row,
column: next.column
},
tile.value * 2
);
//分数等于原分数加新合成的数字
this.score += merged.value;
//将合并以后节点,加入grid
this.grid.add(merged);
//在grid中删除原始的节点
this.grid.remove(tile);
//判断游戏是否获胜
if (merged.value === this.aim) {
this.status = 'WIN';
}
merged.mergedTiles = [tile, next];
tile.updatePosition({ row: next.row, column: next.column });
moved = true;
} else {
this.moveTile(tile, aim);
moved = true;
}
}
}
}
方块合并逻辑:
移动到无法移动并且下一个位置的value和当前一样
有移动,原来的地方的方块就要删除
然后新的方块value值x2
Grid.prototype.remove = function(tile) {
this.cells[tile.row][tile.column] = null;
};
在之前的确定目标位置的时候,是设置了两个变量
一个aim,用于确认目标位置
一个next,用于确认下一个位置
合并时就可以使用nexr.value*2来进行增值
2048初见雏形