egret製作小遊戲:數字華容道及有解判斷(代碼註釋)(評論區有源碼下載~)

繼續學習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步,一個令人悲傷的故事
在這裏插入圖片描述

新手初學,有問題或者不完善歡迎大家糾正評論以及補充~謝謝
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章