微信小程序开发-2048游戏开发
一.项目结构
除了游戏图片资源目录image和配置文件,主函数文件外,主要目录解构如下:
./js
├── base
│ └─ sprite.js // 游戏基本元素精灵类
├── libs
│ ├── storage.js // localStorage封装
│ └── weapp-adapter.js // 小游戏适配器
├── runtime
│ ├── background.js // 背景类
│ ├── gamehelp.js // 用于展示帮助界面
│ ├── gamelayer.js // 用于展示游戏主界面
│ └── gameover.js // 用于展示分数和结算界面
├── databus.js // 管控游戏状态
└── main.js // 游戏入口主函数
- game.json
{
"deviceOrientation": "portrait" //设置成竖屏
}
| key | 说明 | 默认值 |
|---|---|---|
| deviceOrientation | 支持的屏幕方向 | portrait |
| showStatusBar | 是否显示状态栏 | false |
| networkTimeout | 网络请求的超时时间,单位:毫秒 | 60000 |
| networkTimeout.request | wx.request 的超时时间,单位:毫秒 | 60000 |
| networkTimeout.connectSocket | wx.connectSocket 的超时时间 | 60000 |
| networkTimeout.uploadFile | wx.uploadFile 的超时时间 | 60000 |
| networkTimeout.downloadFile | wx.downloadFile 的超时时间 | 60000 |
- game.js (引入小游戏适配器,实例化游戏主函数Main)
import './js/libs/weapp-adapter';
import Main from './js/main';
new Main();
二.游戏数据类、绘制及事件
创建databus.js负责管理游戏状态数据,使用封装类storage将部分保存到本地localStorage
this.status = 'help'; //游戏状态:help|start|over
this.restart = false; //是否可以重新开始
this.table = [...Array(16)].map(_ => 0); //方块数值列表
this.new = null; //最新的块,用来渲染动画的
this.score = 0; //当前分数
this.best = storage.get('best') || 0; //最高分数,缓存
定义aniId,用于维护requestAnimationFrame,使用requestAnimationFrame来渲染canvas
restart(){
databus.reset();//重置数据
window.cancelAnimationFrame(this.aniId);// 清除上一局的动画
this.aniId = 0;
this.aniId = window.requestAnimationFrame(this.bindLoop, canvas);
this.bindLoop = this.loop.bind(this);
}
loop() {
databus.frame++;
this.render();
this.aniId = window.requestAnimationFrame(this.bindLoop, canvas);
}
定义canvas重绘方法render,根据databus.status,每一帧重新绘制所有的需要展示的元素
使用ctx.clearRect清除上一帧绘制的图片,由于我的游戏界面是640*640的,所以绘制界面时使用ctx.scal按比例缩放,以适应屏幕大小
/** canvas重绘函数*/
render() {
ctx.clearRect(0, 0, canvas.width + 1, canvas.height + 1);
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, screenWidth, screenHeight);//设置背景色为黑色
ctx.save();
ctx.translate(0, screenHeight - screenWidth >> 1);
ctx.scale(screenWidth / 640, screenWidth / 640);
if ('help' == databus.status) {//如果是帮助界面,则绘制帮助界面
this.gameHelp.render(ctx);
} else {
this.gameLayer.render(ctx);
if ('over' == databus.status) {//如果游戏结束了,则将结束界面出来
this.gameOver.render(ctx);
}
}
ctx.restore();
}
定义游戏事件方法,用于监听canvas的touchstart和touchend事件,判断用户操作方向
initEvent() {
let mouseX = 0, mouseY = 0;
canvas.addEventListener("touchstart", (e => {
e.preventDefault();
let x = e.targetTouches[0].clientX;
let y = e.targetTouches[0].clientY;
mouseX = x;
mouseY = y;
}).bind(this));
canvas.addEventListener("touchend", (e => {
e.preventDefault();
let x = e.changedTouches[0].clientX;
let y = e.changedTouches[0].clientY;
x = x - mouseX;
y = y - mouseY;
let t;
if (x > 0) {
if (y > x) t = 'down';
else if (y < 0 && -y > x) t = 'up';
else t = 'right';
} else {
if (y > -x) t = 'down';
else if (y < 0 && -y > -x) t = 'up';
else t = 'left';
}
if ((x * x + y * y) > 100) this.move(t);
}).bind(this))
}
三.游戏算法和判断
定义游戏开始方法,创建一个随机位置的2,定义addRandNum方法,用于向空白位置随机产生2或者4,其中2的概论为4的两倍
start() {
databus.reset();
databus.table[Math.floor(Math.random() * 16)] = 2;
this.addRandNum();
}
/*
* 生成随机数
*/
addRandNum() {
let n = Math.floor(Math.random() * this.getNullCount());//生成随机位置
databus.table.forEach((value, index) => {
if (0 == value && 0 == n--) {
databus.table[index] = Math.random() > .6 ? 2 : 4;//设定生成2的概率是4的概率的两倍
databus.new = {
index,
scale: 0
};
}
});
}
/*
* 获取空位置数量
*/
getNullCount() {
return databus.table.filter(value => 0 == value).length;
}
定义移动方法,根据用户移动方向top、right、bottom、left来移动位置
完整代码如下:
move(type) {
let changes = [], isAddNum = false;
//console.log(type);
for (let i = 0; i < 4; i++) {
let b;
switch (type) {
case 'up':
b = [0, 1, 2, 3].map(val => i + val * 4);
break;
case 'right':
b = [3, 2, 1, 0].map(val => val + i * 4);
break;
case 'down':
b = [3, 2, 1, 0].map(val => i + val * 4);
break;
case 'left':
b = [0, 1, 2, 3].map(val => val + i * 4);
break;
}
for (let j = 1, k = 0; j < 4; j++) {
let b1 = b[k], b2 = b[j], b3 = b[k + 1], t = databus.table;
/* 找出b1后面第一个不为空的项,下标为b2,之后分三种情况 */
if (t[b2] > 0) {
if (t[b1] == t[b2]) {//P1情况
t[b1] = 2 * t[b1];
t[b2] = 0;
changes[b1] = 1;
k = k + 1;
isAddNum = true;
} else if (t[b1] == 0) { //P2情况
t[b1] = t[b2];
t[b2] = 0;
isAddNum = true;
} else {// P3情况
t[b3] = t[b2];
/* 原先两数不挨着 */
if (j != k + 1) {
t[b2] = 0;
isAddNum = true;
}
k = k + 1;
}
}
}
}
//计算分数
changes.forEach((value, index) => {
if (value && 0 != value) {
databus.score += databus.table[index];
}
});
if (isAddNum) {
this.addRandNum();
}
if (this.checkGameOver()) {
databus.setBest(databus.score);
databus.status = 'over';
this.restart = false;
setTimeout(_ => {
this.restart = true;
}, 1000);
}
//console.log(databus.table);
}
循环每一列,根据方向,取出每一列的索引,分别对应如下:
up:[0, 4, 8, 12], [1, 5, 9, 13], [2, 6, 10, 14], [3, 9, 11, 15]
right:[3, 2, 1, 0], [7, 6, 5, 4], [11, 10, 9, 8], [15, 14, 13, 12]
bottom:[12, 8, 4, 0], [13, 9, 5, 1], [14, 10, 6, 2], [15, 11, 9, 3]
left:[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]
for (let i = 0; i < 4; i++) {
let b;
switch (type) {
case 'up':
b = [0, 1, 2, 3].map(val => i + val * 4);
break;
case 'right':
b = [3, 2, 1, 0].map(val => val + i * 4);
break;
case 'down':
b = [3, 2, 1, 0].map(val => i + val * 4);
break;
case 'left':
b = [0, 1, 2, 3].map(val => val + i * 4);
break;
}
根据对应的方向列[d1, d2, d3, d4],使用b1~3指向,找出b1后面第一个不为空的项,下标为b2,步骤如下:
(1) 将b1指向d1,将b2、b3指向b1的next,此时是b2;
(2) 判断b2对应的值,如果为0,跳到(3),否则跳到(4);
(3) 将b2指向b2的next,跳到(2);
(4) 如果b1等于b2,跳到(5),如果b1等于0,跳到(6),否则,跳到(7);
(5) b1的值*2,b2设置为0,跳到(8);//P1情况
(6) 将b1的值设置为b2,b2设置为0,将b1指向b1的下一个,跳回(3);P2情况
(7) 将b3的值设置为b2, 如果b2和b3指向的不是同一个,则将b2设置为0,最后跳到(8);P3情况
(8) 将b1指向b1的下一个,跳回(1);
for (let j = 1, k = 0; j < 4; j++) {
let b1 = b[k], b2 = b[j], b3 = b[k + 1], t = databus.table;
/* 找出b1后面第一个不为空的项,下标为b2,之后分三种情况 */
if (t[b2] > 0) {
if (t[b1] == t[b2]) {//P1情况
t[b1] = 2 * t[b1];
t[b2] = 0;
k = k + 1;
} else if (t[b1] == 0) { //P2情况
t[b1] = t[b2];
t[b2] = 0;
} else {// P3情况
t[b3] = t[b2];
/* 原先两数不挨着 */
if (j != k + 1) {
t[b2] = 0;
}
k = k + 1;
}
}
}
定义isAddNum,判断是否需要添加新的值,如果需要则调用addRandNum,添加随机值,否则说明列表没有变动,保存不变;
定义checkGameOver方法,检查游戏是否结束,如果列表存在为空,或者相邻位置存在相同,则判定未结束,否则为结束;
checkGameOver() {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
//如果存在空的,游戏没结束
if (0 == databus.table[i + 4 * j]) {
return false;
} else if (3 != j) {
//如果存在相邻两个有相同的,游戏没结束
if (databus.table[i + 4 * j] == databus.table[i + 4 * (j + 1)]
|| databus.table[j + 4 * i] == databus.table[j + 1 + 4 * i]) {
return false;
}
}
}
}
return true;
}
四.最后
以上就是2048游戏的核心算法,以及基本思路了,还有一些分数计算和动画表现等,最后游戏界面如下:
源码分享:https://github.com/hsuna/WeApp_test/tree/master/2048Game
相关文章
评论 (0)
- 还没有评论,来第一条吧。