這篇文章要跟大家介紹的文章內容是關於H5實現消滅星星遊戲的詳細內容,有很好的參考價值,希望可以幫助到有需要的朋友。
「消滅星星」是一款很經典的「消除類遊戲」,它的玩法很簡單:消除相連的同色磚塊。
1. 遊戲規則
「消滅星星」存在多個版本,不過它們的規則除了「關卡分數」有些出入外,其它的規則都是一樣的。作者介紹的版本的遊戲規則整理如下:
1. 色磚分佈
10 x 10 的表格
5種顏色- 紅、綠、藍,黃,紫
每類色磚個數在指定區間內隨機
5類色磚在10 x 10 表格中隨機分佈
#2. 消除規則
#兩個或兩個以上同色磚塊相連通即是可被消除的磚塊。
3. 分數規則
消除總分值= n * n * 5
獎勵總分值= 2000 – n * n * 20
#“n”表示磚塊數量。上面是「總」分數的規則,還有「單」個磚塊的分數規則:
消除磚塊得分值= 10 * i 5
剩餘磚塊扣分值= 40 * i 20
“i”表示磚塊的索引值(從0 開始)。簡單地說,單一磚塊「得分值」和「扣分值」是一個等差數列。
4. 關卡分數
關卡分數 = 1000 (level – 1) * 2000;「level」即目前關卡數。
5.通關條件
可消除色塊不存在
累計分數>= 目前關卡分數
上面兩個條件同時成立遊戲才可以通關。
2. MVC 設計模式
筆者這次又是使用了 MVC 模式來寫「消滅星星」。星星「磚塊」的資料結構與各種狀態由Model 實現,遊戲的核心在Model 中完成;View 映射Model 的變化並做出對應的行為,它的任務主要是展示動畫;用戶與遊戲的交互由Control 完成。
從邏輯規劃來看,Model 很重而View 與 Control 很輕,不過,從程式碼量上看,View 很重而 Model 與 Control 相對很輕。
3. Model
10 x 10 的表格以長度為 100 的陣列可完美地繪製遊戲的星星「磚塊」。
[ R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P ]
R – 紅色,G – 綠色,B – 藍色,Y – 黃色,P – 紫色。 Model 的核心任務是以下四個:
產生磚牆
#消除磚塊(產生磚塊分數)
夯實磚牆
清除殘磚(產生獎勵分數)
3.1 產生磚牆
磚牆分兩步驟產生:
色磚數量分配
打散色磚塊
理論上,可以將100 個格子可以均分到5 類顏色,不過筆者玩過的「消滅星星」都不使用均分策略。透過分析幾款「消滅星星」,其實可以發現一個規律 —— 「色磚之間的數量差在一個固定的區間內」。
如果把傳統意義上的均分稱作「完全均分」,那麼「消滅星星」的分配是一種在均分線上下波動的「不完全均分」。
筆者把上面的「不完全均分」稱作「波動均分」,演算法的具體實現可以參見「波動均分演算法」。
「打散色磚」其實就是將陣列亂序的過程,筆者推薦使用「 費雪耶茲亂序演算法」。
以下是偽代碼的實作:
// 波动均分色砖 waveaverage(5, 4, 4).forEach( // tiles 即色墙数组 (count, clr) => tiles.concat(generateTiles(count, clr)); ); // 打散色砖 shuffle(tiles);
3.2 消除磚塊
「消除磚塊」的規則很簡單— 相鄰相連通相同色即可以消除。
前兩個組合符合「相鄰相連通相同色即可以消除」,所以它們可以被消除;第三個組合雖然「相鄰相同色」但是不「相連通」所以它不能被消除。
「消除磚塊」的同時有一個重要的任務:產生磚塊對應的分數。在「遊戲規則」中,筆者已經提供了對應的數學公式:「消除磚塊得分值 = 10 * i 5」。
「消除磚塊」演算法實作如下:
function clean(tile) { let count = 1; let sameTiles = searchSameTiles(tile); if(sameTiles.length > 0) { deleteTile(tile); while(true) { let nextSameTiles = []; sameTiles.forEach(tile => { nextSameTiles.push(...searchSameTiles(tile)); makeScore(++count * 10 + 5); // 标记当前分值 deleteTile(tile); // 删除砖块 }); // 清除完成,跳出循环 if(nextSameTiles.length === 0) break; else { sameTiles = nextSameTiles; } } } }
清除的演算法使用「遞迴」邏輯上會清晰一些,不過「遞迴」在瀏覽器上容易「堆疊溢出」,所以筆者沒有使用「遞歸」實作。
3.3 夯實磚牆
磚牆在消除了部分磚塊後,會出現空洞,此時需要對牆體進行夯實:
向下夯实 向左夯实
向左下夯实(先下后左)
一种快速的实现方案是,每次「消除砖块」后直接遍历砖墙数组(10×10数组)再把空洞夯实,伪代码表示如下:
for(let row = 0; row < 10; ++row) { for(let col = 0; col < 10; ++col) { if(isEmpty(row, col)) { // 水平方向(向左)夯实 if(isEmptyCol(col)) { tampRow(col); } // 垂直方向(向下)夯实 else { tampCol(col); } break; } } }
But… 为了夯实一个空洞对一张大数组进行全量遍历并不是一种高效的算法。在笔者看来影响「墙体夯实」效率的因素有:
定位空洞
砖块移动(夯实)
扫描墙体数组的主要目的是「定位空洞」,但能否不扫描墙体数组直接「定位空洞」?
墙体的「空洞」是由于「消除砖块」造成的,换种说法 —— 被消除的砖块留下来的坑位就是墙体的空洞。在「消除砖块」的同时标记空洞的位置,这样就无须全量扫描墙体数组,伪代码如下:
function deleteTile(tile) { // 标记空洞 markHollow(tile.index); // 删除砖块逻辑 ... }
在上面的夯实动图,其实可以看到它的夯实过程如下:
空洞上方的砖块向下移动
空列右侧的砖块向左移动
墙体在「夯实」过程中,它的边界是实时在变化,如果「夯实」不按真实边界进行扫描,会产生多余的空白扫
如何记录墙体的边界?
把墙体拆分成一个个单独的列,那么列最顶部的空白格片段就是墙体的「空白」,而其余非顶部的空白格片段即墙体的「空洞」。
笔者使用一组「列集合」来描述墙体的边界并记录墙体的空洞,它的模型如下:
/* @ count - 列砖块数 @ start - 顶部行索引 @ end - 底部行索引 @ pitCount - 坑数 @ topPit - 最顶部的坑 @ bottomPit - 最底部的坑 */ let wall = [ {count, start, end, pitCount, topPit, bottomPit}, {count, start, end, pitCount, topPit, bottomPit}, ... ];
这个模型可以描述墙体的三个细节:
空列
列的连续空洞
列的非连续空洞
// 空列 if(count === 0) { ... } // 连续空洞 else if(bottomPit - topPit + 1 === pitCount) { ... } // 非连续空洞 else { ... }
砖块在消除后,映射到单个列上的空洞会有两种分布形态 —— 连续与非连续。
「连续空洞」与「非连续空洞」的夯实过程如下:
其实「空列」放大于墙体上,也会有「空洞」类似的分布形态 —— 连续与非连续
它的夯实过程与空洞类似,这里就不赘述了。
3.4 消除残砖
上一小节提到了「描述墙体的边界并记录墙体的空洞」的「列集合」,笔者是直接使用这个「列集合」来消除残砖的,伪代码如下:
function clearAll() { let count = 0; for(let col = 0, len = this.wall.length; col < len; ++col) { let colInfo = this.wall[col]; for(let row = colInfo.start; row <= colInfo.end; ++row) { let tile = this.grid[row * this.col + col]; tile.score = -20 - 40 * count++; // 标记奖励分数 tile.removed = true; } } }
4. View
View 主要的功能有两个:
UI 管理
映射 Model 的变化(动画)
UI 管理主要是指「界面绘制」与「资源加载管理」,这两项功能比较常见本文就直接略过了。View 的重头戏是「映射 Model 的变化」并完成对应的动画。动画是复杂的,而映射的原理是简单的,如下伪代码:
update({originIndex, index, clr, removed, score}) { // 还没有 originIndex 或没有色值,直接不处理 if(originIndex === undefined || clr === undefined) return ; let tile = this.tiles[originIndex]; // tile 存在,判断颜色是否一样 if(tile.clr !== clr) { this.updateTileClr(tile, clr); } // 当前索引变化 ----- 表示位置也有变化 if(tile.index !== index) { this.updateTileIndex(tile, index); } // 设置分数 if(tile.score !== score) { tile.score = score; } if(tile.removed !== removed) { // 移除或添加当前节点 true === removed ? this.bomb(tile) : this.area.addChild(tile.sprite); tile.removed = removed; } }
Model 的砖块每次数据的更改都会通知到 View 的砖块,View 会根据对应的变化做对应的动作(动画)。
5. Control
Control 要处理的事务比较多,如下:
绑定 Model & View
生成通关分值
判断通关条件
对外事件
用户交互
初始化时,Control 把 Model 的砖块单向绑定到 View 的砖块了。如下:
Object.defineProperties(model.tile, { originIndex: { get() {...}, set(){ ... view.update({originIndex}) } }, index: { get() {...}, set() { ... view.update({index}) } }, clr: { get() {...}, set() { ... view.update({clr}) } }, removed: { get() {...}, set() { ... view.update({removed}) } }, score: { get() {...}, set() { ... view.update({score}) } } })
「通关分值」与「判断通关条件」这对逻辑在本文的「游戏规则」中有相关介绍,这里不再赘述。
对外事件规划如下:
name | detail |
#pass |
通關 |
#pause |
暫停 |
resume | 恢復 |
恢復 |
|
使用者互動APIs 規劃如下: | #name | |
deltail |
#init |
|
初始化遊戲 |
next |
|
進入下一關 |
| method |
進入指定關卡 | ##pause | method|
method |
||
method |
6. 問題
#在有一個關於有一個關於「消滅星星」的話題:popstar關卡是如何設計的?
這個主題在最後提出了一個問題 —— 「無法消除和最大得分不滿足過關條件的矩陣」。求「矩陣」的最大得分是一個「背包問題」,求解的演算法不難:對當前矩陣用「遞歸」的形式把所有的消滅分支都執行一次,並取最高分值。但是 javascript 的“遞歸”極易“棧溢出”導致演算法無法執行。
其實在知乎的話題中提到一個解決方案:
網上查到有程式提出做個工具隨機生成關卡,自動計算,把符合得分條件的關卡篩選出來
這個解決方案代價是昂貴的!筆者提供有源碼並沒有解決這個問題,而是用一個比較取巧的方法:進入遊戲前檢查是事為“無法消除矩陣”,如果是重新生成關卡矩陣。 相關推薦:
######使用canvas實作迷宮遊戲######以上是H5開發:實現消滅星星遊戲的詳細內容的詳細內容。更多資訊請關注PHP中文網其他相關文章!