继续学习egret,最近写了数字华容道的小游戏,非常简单的小游戏。首先预览一下效果:
数字华容道就是通过移动方块,将方块按照数字的排序进行排列。功能很简单,主要有刷新,提升阶数,如何一定有解,以及简单的存储数据。
(由于找背景图太累,最高关只设置到10阶,这个其实是没有限制的,按照逆序数打乱规则,这个的效率可以支持到很高阶)
想看重点的直接看第二大点~
————————————————————————————————————————
(全局用3阶为例,即3x3的难度)
零、首先创建一个全局需要的数据类
主要存储当前难度(阶数),不同难度的方块大小,最低关卡,最高关卡,玩家数据,当前是否能操作等数据。
class Data {
public static BlockWidth: number = 200;//方块大小(游戏区域为600x600)
public static Order_hard: number = 3;//阶数
public static lowestPass: number = 2;//最低阶数(关卡控制)
public static highestPass: number = 10;//最高阶数(关卡控制)
public static isCanKeyDown: boolean = false;//当前是否能操作
public static playerData: any;//玩家数据
public static alldata: allData;//所有数据
public static playerDataKey: string = '451281425';//存储数据的key值
}
其他就是正常的egret的存储和读取缓存数据的方法。
egret.localStorage.getItem(Data.playerDataKey);
let ccc = JSON.stringify(Data.playerData);
egret.localStorage.setItem(Data.playerDataKey, ccc);
一、创建方块类
(1)首先需要创建方块类,方便后面的移动,交换等操作。
方块有背景图,数字,边框等元素。
class Block extends egret.DisplayObjectContainer {
public constructor(id: number, arrpos: number) {
super();
this.initBlock(id, arrpos);
}
public id: number = -1;//方块的id,即最终的正确位置
public arrPos: number = -1;//现在的位置,被打乱后的位置
public arrUp: number = -1;//方块上方的方块id
public arrDown: number = -1;//方块下方的方块id
public arrLeft: number = -1;//方块左边的方块id
public arrRight: number = -1;//方块右方的方块id
public numStr: egret.TextField;//方块中间显示的数字字符串
public initBlock(id: number, arrpos: number): void {
this.id = id;
this.arrPos = arrpos;
this.saveArrPos(arrpos);
this.initGraphics();
this.numStr = new egret.TextField();
this.numStr.text = (this.id + 1).toString();
this.numStr.bold = true;
this.numStr.size = Data.BlockWidth / 2;
this.numStr.x = this.shape.x + (Data.BlockWidth - this.numStr.width) / 2;
this.numStr.y = this.shape.y + (Data.BlockWidth - this.numStr.height) / 2;
if (id == Math.pow(Data.Order_hard, 2) - 1) {
this.numStr.alpha = 0;
}
this.addChild(this.numStr);
}
}
在这里定义每个方块拥有的参数,比如上下左右的方块id和方块的当前id,为后面的交换和移动做准备。
****注:这里的id为方块的正确位置,而arrpos表示当前方块被打乱后的位置。id的数值从创建起就不会变化。
(2)最后一个方块
if (id == Math.pow(Data.Order_hard, 2) - 1) {
this.numStr.alpha = 0;
}
在这里我其实是创建了阶数的平方个方块,并将最后一个方块的透明度设为0,代表华容道里面的那个空缺位置。即创建了9个方块,但玩家实际上只能看到8个。
(3)给方块设置边界参数
创建的方块中由于不可能每个方块的周围都有方块,所以需要通过参数设置方块的边界
public saveArrPos(id: number): void {
if (Math.floor(id / Data.Order_hard) * Data.BlockWidth == 0) {//第一行
this.arrUp = -1;
} else {
this.arrUp = id - Data.Order_hard;
}
if (Math.floor(id / Data.Order_hard) * Data.BlockWidth == (Data.Order_hard - 1) * Data.BlockWidth) {//最后一行
this.arrDown = -1;
} else {
this.arrDown = id + Data.Order_hard;
}
if (id % Data.Order_hard * Data.BlockWidth == 0) {//第一列
this.arrLeft = -1;
} else {
this.arrLeft = id - 1;
}
if (id % Data.Order_hard * Data.BlockWidth == (Data.Order_hard - 1) * Data.BlockWidth) {//最后一列
this.arrRight = -1;
} else {
this.arrRight = id + 1;
}
this.arrPos = id;
}
二、创建主场景
(1)打乱规则
图中使用的打乱规则就是第二种
打乱的规则主要分为两种,第一种方便易懂,但效率差。第二种效率高,但要理解原因。
1)移动打乱的方法(不推荐)
以3x3为例,创建了9个方块(第9个方块为空缺方块,命名为方块9),将9个方块按正确答案的顺序排列,然后移动一定次数的方块9,并通过以下2点规则进行移动:
- 边界时,只能向非边界方向移动
- 移动方向不得与上一次移动方向相反,即不得回退
通过方块9的不断移动,打乱其他方块的位置。这样可以保证一定有解。
————————————————————
但是在实际操作过程中,有以下几点问题:
- 方块打乱的效率低
- 移动上百次甚至上千次,仍然会有部分甚至超过一半的方块仍在正确答案的位置。打乱效果不好
————————————————————
为了解决上面的问题,在打乱时新增一条规则:
- 寻找当前方块的id和arrpos变量,即方块的所在位置是否与正确答案的位置是否相等,如果超过当前阶数方块数量的一半以上相同,则继续打乱。以3x3为例,如果有5个及以上方块的仍然在它正确答案的位置上,就继续打乱,不停止。
但是这样由于调用方法及运行次数太多,会有程序崩溃的可能,当阶数越大时,崩溃的概率越大。在3阶运行次数大概在800次以内,4阶开始有可能上万,5阶以上崩溃概率大大增加。
2)随机打乱,通过逆序数判断是否有解(这是最终采用的方法)
将9个方块放入数组中,进行随机打乱,然后判断打乱后的数组的逆序数是否符合判断条件。
这里简单说一下逆序数,比如[1,3,2]这样的数列,进行两两比较,如果前面的数大于后面的数,即为一个逆序数对,逆序数则+1。[1,3,2]中有{1,3},{1,2},{3,2},其中{3,2}是逆序数对,则逆序数+1。而[3,2,1]中的逆序数则为3。
注:空缺的那个方块是不参与逆序数的判断的,所以,进行逆序数判断时,要跳过空缺方块,即实际上只对1——8号方块进行逆序数判断,空缺方块的位置是额外的判断条件。(在打乱的数组中,判断如果id为空缺方块,则跳过计数,不+1)
了解了这个就可以开始判断打乱顺序的数组是否符合有解的条件了:
- 阶数为奇数时,逆序为偶数,不用判断空缺方块的位置;
- 阶数为偶数,逆序数为偶数,空缺方块被打乱位置后所在的行数与空缺方块应该在的正确位置的行数的差值为偶数;
- 阶数为偶数,逆序数为奇数,空缺方块被打乱位置后所在的行数与空缺方块应该在的正确位置的行数的差值为奇数;
满足上面任一条件的即为打乱的数组是有解数组。得到了数组就可以开始在主场景中添加方块。
public upset(): void {//打乱顺序
this.luanarr = [];
let max: number = Math.pow(Data.Order_hard, 2);
for (let i: number = 0; i < max; i++) {
this.luanarr.push(Math.pow(Data.Order_hard, 2) - 1 - i);//倒叙数组,以使打乱的数组更乱
}
for (let i: number = 0; i < max; i++) {//打乱倒叙数组
let tempOne: number = Math.floor(Math.random() * max);
let tempTwo: number = Math.floor(Math.random() * max);
let temp = this.luanarr[tempOne];
this.luanarr[tempOne] = this.luanarr[tempTwo];
this.luanarr[tempTwo] = temp;
}
this.loopcheckRight();
}
public loopcheckRight(): void {//检查打乱的数组是否正确
let isRight: boolean = this.checkIsHasAnswer();//检查打乱的数组是否有解
if (isRight) {//如果该数组有解,不进行操作,或者添加一些ui
} else {//如果数组无解,则重新打乱
let max: number = Math.pow(Data.Order_hard, 2);
for (let i: number = 0; i < max; i++) {//打乱倒叙数组
let tempOne: number = Math.floor(Math.random() * max);
let tempTwo: number = Math.floor(Math.random() * max);
let temp = this.luanarr[tempOne];
this.luanarr[tempOne] = this.luanarr[tempTwo];
this.luanarr[tempTwo] = temp;
}
this.loopcheckRight();
}
}
public checkIsHasAnswer(): boolean {//检查打乱的数组是否有解
let inversionNumber: number = 0;//逆序数对的数量
let max: number = Math.pow(Data.Order_hard, 2);
let arrNoemety: Array<number> = [];
for (let i: number = 0; i < max; i++) {
if (this.luanarr[i] != this.luanarr.length - 1) {
arrNoemety.push(this.luanarr[i]);
}
}
for (let i: number = 0; i < arrNoemety.length; i++) {
for (let j: number = i + 1; j < arrNoemety.length; j++) {
if (arrNoemety[i] > arrNoemety[j]) {
inversionNumber++;
}
}
}
if (this.checkSort() == false) {
return false;
}
console.log("逆序数数量:", inversionNumber)
//若格子列数为奇数,则逆序数必须为偶数;
if (Data.Order_hard % 2 == 1 && inversionNumber % 2 == 0) {
return true;
}
//若格子列数为偶数,且逆序数为偶数,则当前空格所在行数与初始空格所在行数的差为偶数;
if (Data.Order_hard % 2 == 0 && inversionNumber % 2 == 0) {
for (let i: number = 0; i < this.luanarr.length; i++) {
if (this.luanarr[i] == this.luanarr.length - 1 && (Math.floor(i / Data.Order_hard) + 1 - Data.Order_hard) % 2 == 0) {
return true;
}
}
}
//若格子列数为偶数,且逆序数为奇数,则当前空格所在行数与初始空格所在行数的差为奇数。
if (Data.Order_hard % 2 == 0 && inversionNumber % 2 == 1) {
for (let i: number = 0; i < this.luanarr.length; i++) {
if (this.luanarr[i] == this.luanarr.length - 1 && (Math.floor(i / Data.Order_hard) + 1 - Data.Order_hard) % 2 == 1) {
return true;
}
}
}
return false;
}
public checkSort(): boolean {//检查打乱后数组中方块位置没有被打乱的数量是否过多
let sameNum: number = 0;
for (let i: number = 0; i < this.luanarr.length; i++) {
if (i == this.luanarr[i]) {
sameNum++;
}
}
if (sameNum >= Math.floor(Math.pow(Data.Order_hard, 2) / 2)) {//相同的数量
return false;
}
return true;
}
(2)添加方块
public oneMoreAgain(): void {
Data.isCanKeyDown = false;//当前不可操作
this.isStartGame = false;//当前没有开始游戏
this.removeChildren();//移除场景中的元素
this.upset();//利用上面的打乱规则获得被打乱的数组this.luanarr
this.blockArr = [];//一个新的方块数组
for (let i: number = 0; i < this.luanarr.length; i++) {//循环被打乱的数组,i即为方块被打乱后的位置
let block: Block = new Block(this.luanarr[i], i);//参数传递方块对象和方块位置
this.resetPos(block, i);//设置方块位置
this.addChild(block);//添加方块到场景
this.blockArr.push(block);//方块数组中添加这个方块
if (this.luanarr[i] == Math.pow(Data.Order_hard, 2) - 1) {//最后一个方块的透明度为0,并且存到一个空方块中单独操作
block.alpha = 0;
this.emptyBlock = block;
}
}
if (this.gameui) {//添加展示玩家数据的ui界面
this.addChild(this.gameui);
}
if (this.timeListen != -1) {//重置时间计数
egret.clearInterval(this.timeListen);
this.timeListen = -1;
}
this.gameui.curStepStr.text = '当前步数:' + 0;//步数计数
this.gameui.curTime.text = '当前时间:' + 0 + 's';//时间计数
this.changeInfoShow();//显示记录
this.stepNum = 0;//当前步数
this.cumulativeTime = 0;//当前时间
Data.isCanKeyDown = true;//可以操作
this.isStartGame = true;//开始游戏
this.timeListen = egret.setInterval(this.timeadd, this, 1000);//开始计时
}
public resetPos(block: Block, id: number): void {//根据在数组中的位置,设置方块位置
block.x = id % Data.Order_hard * Data.BlockWidth + 22;
block.y = Math.floor(id / Data.Order_hard) * Data.BlockWidth + 350;
}
三、操作
(1)移动方块
作为一个移动的游戏,必不可少的当然是上下左右的移动。(这个游戏中虽然看着像移动8个可见方块,但其实是对那个不可见的空缺方块进行的操作。每次交换的都是这个方块和周围的方块。)
/**
* 向上
*/
public up(): void {
if (this.emptyBlock.arrUp != -1) {//判断是否上边界
this.tweenPos(this.emptyBlock.arrUp);
}
}
/**
* 向下
*/
public down(): void {
if (this.emptyBlock.arrDown != -1) {//判断是否下边界
this.tweenPos(this.emptyBlock.arrDown);
}
}
/**
* 向左
*/
public left(): void {
if (this.emptyBlock.arrLeft != -1) {//判断是否左边界
this.tweenPos(this.emptyBlock.arrLeft);
}
}
/**
* 向右
*/
public right(): void {
if (this.emptyBlock.arrRight != -1) {//判断是否右边界
this.tweenPos(this.emptyBlock.arrRight);
}
}
public tweenPos(id: number): void {//按100毫秒移动方块
if (this.isStartGame) {//如果在游戏中
Data.isCanKeyDown = false;//移动中关闭移动操作
let posX: number = this.emptyBlock.arrPos % Data.Order_hard * Data.BlockWidth + 22;//重置方块位置
let posY: number = Math.floor(this.emptyBlock.arrPos / Data.Order_hard) * Data.BlockWidth + 350;
egret.Tween.get(this.blockArr[id]).to({ x: posX, y: posY }, 100).call(function (): void {//移动方块
this.exchange(id);//交换2个方块的数据
this.stepNum++;//步数+1
this.gameui.curStepStr.text = '当前步数:' + this.stepNum;//更新步数显示
}, this);
}
}
public exchange(id: number): void {//交换数据
let temp: Block = this.blockArr[id];
let temparrpos: number = temp.arrPos;
let emptyarrpos: number = this.emptyBlock.arrPos;
temp.saveArrPos(this.emptyBlock.arrPos);
this.resetPos(temp, this.emptyBlock.arrPos);
this.emptyBlock.saveArrPos(temparrpos);
this.resetPos(this.emptyBlock, temparrpos);
this.blockArr[temparrpos] = this.emptyBlock;
this.blockArr[emptyarrpos] = temp;//在这里交换了2个方块的数据,但是注意,这里的id是不会更改的,
//id为那个方块的正确位置,具有唯一且不可更改性
if (this.isStartGame) {//每次交换完成后,检查是否过关
for (let i: number = 0; i < this.blockArr.length; i++) {//循环方块数组
if (this.blockArr[i].id != this.blockArr[i].arrPos) {//如果有某一个方块的位置不对,就跳出循环,表示没过关
Data.isCanKeyDown = true;
break;
}
if (i == this.blockArr.length - 1) {//游戏完成,当前关通过,更新显示的ui数据
this.gameui.congratulationsStr.visible = true;
this.gameui.nextLevelStr.visible = true;
let curdata: allData = Data.playerData.alldata;
if (curdata.everPassHeighestLevel < Data.Order_hard - 1) {
curdata.everPassHeighestLevel = Data.Order_hard - 1;
}
let curdataArr: allData = Data.playerData.alldata.everyLevelData;
if (curdataArr[Data.Order_hard - 2].minimumStepNum > this.stepNum) {
curdataArr[Data.Order_hard - 2].minimumStepNum = this.stepNum + 1;
}
if (curdataArr[Data.Order_hard - 2].minimumTime > this.cumulativeTime) {
curdataArr[Data.Order_hard - 2].minimumTime = this.cumulativeTime;
}
if (Data.Order_hard <= 10) {
this.isMouseCanClick = false;
for (let i: number = 0; i < this.blockArr.length; i++) {
egret.Tween.get(this.blockArr[i].numStr).to({ alpha: 0 }, 800);
}
egret.Tween.get(this.emptyBlock).to({ alpha: 1 }, 800).call(function (): void {
this.isMouseCanClick = true;
}, this);
}
this.changeInfoShow();
Data.savePlayerData();
if (this.timeListen != -1) {
egret.clearInterval(this.timeListen);
this.timeListen = -1;
}
}
}
}
}
(2)难度递增
难度的递增可以通过以下几点进行控制:
- 阶数增加
- 每次刷新及初始化游戏时,通过改变对打乱后数组的排序判断中的没有被打乱的方块数量进行控制。我这里默认是打乱的方块至少要占方块数量的50%以上,否则继续打乱。
- 时间限制
- 步数限制(这里如果要做唯一解,即给玩家指定步数过关,像象棋解决残局一样。推荐使用打乱规则的第一种,这种情况下只有一种情况,无需考虑效率)
四、优化
逻辑做完,最后做一点小小的优化:
- 过关后将方块中的数字隐藏,并将空缺的方块显示出来,让玩家欣赏一下拼好后的图片
- 切图片可以用ps中的切片工具,并且存储为web格式,就可批量导出切好的图片(我一开始真的是手动裁的图片。。。。)
- 优化操作,可以通过键盘控制方块移动,也可以通过鼠标点击控制(滑动也可以做,把点击的判断修改一下即可)
按键:
private keydown(event): void {
if (Data.isCanKeyDown == false) {
return;
}
if (event.keyCode == 38) {//上
game.instance.down();
return;
} else if (event.keyCode == 40) {//下
game.instance.up();
return;
} else if (event.keyCode == 37) {//左
game.instance.right();
return;
} else if (event.keyCode == 39) {//右
game.instance.left();
return;
}
}
点击:
public stageMousePos(e: egret.TouchEvent): void {
if (Data.isCanKeyDown == false) {
return;
}
// console.log("全局X:", e.$stageX);//鼠标的位置
if (this.emptyBlock.arrUp != -1 && e.$stageX > this.emptyBlock.x && e.$stageX < this.emptyBlock.x + Data.BlockWidth
&& e.$stageY > this.emptyBlock.y - Data.BlockWidth && e.$stageY < this.emptyBlock.y) {//在空方块上方
this.tweenPos(this.emptyBlock.arrUp);
}
if (this.emptyBlock.arrDown != -1 && e.$stageX > this.emptyBlock.x && e.$stageX < this.emptyBlock.x + Data.BlockWidth
&& e.$stageY > this.emptyBlock.y + Data.BlockWidth && e.$stageY < this.emptyBlock.y + 2 * Data.BlockWidth) {//在空方块下方
this.tweenPos(this.emptyBlock.arrDown);
}
if (this.emptyBlock.arrLeft != -1 && e.$stageX < this.emptyBlock.x && e.$stageX > this.emptyBlock.x - Data.BlockWidth
&& e.$stageY > this.emptyBlock.y && e.$stageY < this.emptyBlock.y + Data.BlockWidth) {//在空方块左方
this.tweenPos(this.emptyBlock.arrLeft);
}
if (this.emptyBlock.arrRight != -1 && e.$stageX > this.emptyBlock.x + Data.BlockWidth && e.$stageX < this.emptyBlock.x + 2 * Data.BlockWidth
&& e.$stageY > this.emptyBlock.y && e.$stageY < this.emptyBlock.y + Data.BlockWidth) {//在空方块右方
this.tweenPos(this.emptyBlock.arrRight);
}
}
新手初学,有问题或者不完善欢迎大家纠正评论以及补充~谢谢
最后这是我玩的10阶的数据,100个方块,1394秒。23分钟,用了3071步,一个令人悲伤的故事