純前端實現(xiàn)「羊了個羊」小游戲 ……
作者:QCY
https://juejin.cn/post/7143897892531486727
背景
最近簡單的「羊了個羊」小游戲火到出圈,據(jù)說狂賺幾百幾千萬。這么弱智的玩意,即便是前端,我看也行!
最終成果

游戲本體
如果一直白屏可以換這個地址 https://1enozc.csb.app/
地圖模擬
游戲本體長這樣

可以很明顯的觀察到,卡片是以 1/4 為單位排列的
1. 單層

假設有這種布局,模擬成二維數(shù)組應該如下表示,每個格子就是一個數(shù)字元素
[
[0, 0, 1, 0, 0, 0],
[1, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0],
]

2. 多層

[
// 第1層
[
[1, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0],
],
// 第2層
[
[0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
],
]
地圖生成
1. 基礎生成
最基礎的地圖只關乎當前層,假設當前需要判定是否放置卡片的坐標為 [i, j],那么下面四個位置就不能存在卡片,否則就會出現(xiàn)同層卡片重疊

[i-1,j] != 1[i,j-1] != 1[i-1, j-1] ! = 1[i-1, j+1] ! = 1
同時我們加入一個隨機系數(shù),保證每次生成的地圖不同
Math.random() < 0.3 === true的時候該位置才放置卡片
2. 優(yōu)化地圖
以一層為例,按上面的邏輯只能生成最簡單的地圖,實際我們觀察游戲,會發(fā)現(xiàn)卡片的放置是有一定規(guī)律的:

左右對稱 從頂層到底層越來越往中心聚集,卡片越來越少 上一層不會完全覆蓋下一層
加上這兩點優(yōu)化之后,地圖應該如下展示:
頂層

底層

3. 卡片渲染
每次位置和隨機數(shù)判定合格,我們應該實際放置一張卡片,一個實際的 dom。然后根據(jù)卡片的 x、y、z、寬高 值設置實際位置
const style = {
position: "absolute",
top: (y * CardItem.height) / 2 + offset + "px",
left: (x * CardItem.width) / 2 + offset + "px",
width: CardItem.width + "px",
height: CardItem.height + "px",
}
覆蓋關系

我們可以先按一層的大小初始化一個處理遮擋用的二維數(shù)組 coverMap,然后在之前生成的游戲地圖上,從最后一層往第一層遍歷。
[
// 第1層
[
[1, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0],
],
// 第2層
[
[0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
],
]
先遍歷第二層,發(fā)現(xiàn) [1,1] 位置有卡片(由于是最上層可以先不考慮本身被遮擋的情況)所以我們把 coverMap 的對應 4 個坐標置為 1,第 [1,4] 位置的卡片也一樣處理。
處理完頂層之后的 coverMap 結果如下
const coverMap = [
[0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0],
]
第二次(底層)遍歷到 [0,0] 位置發(fā)現(xiàn)有卡片,并且實際會占據(jù):
[0,0][0,1][1,0][1,1]
而其中 [1,1] 位置,是被遮擋的,所以這張卡片也應該被判定成遮擋狀態(tài)。依次處理完這一層所有卡片,同時遮擋數(shù)組更新
const coverMap = [
[1, 1, 0, 0, 1, 1],
[1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1],
[1, 1, 0, 0, 1, 1],
]
填充數(shù)據(jù)
整改游戲的核心邏輯是 pending 區(qū)域存在 3 個同樣圖案的卡片時會消除。所以我們有兩個關鍵點要注意
1. 保證卡片是 3 的倍數(shù)
之前都是用 0、1 代指卡片,實際之前設置卡片的時候,我們可以新建 CardItem 類的實例,每個卡片實例會記錄自己的位置、樣式、是否被覆蓋等狀況。并且我們可以用一個 cardList 數(shù)組保存下這些實例
并且在地圖生成完之后,根據(jù)數(shù)組數(shù)量除 3 的余數(shù),從前開始刪除對應數(shù)量的卡片
可以想想為什么不從后面刪~
2. 填充卡片類型
我們需要隨機的把指定種類的卡片類型,以 3 的倍數(shù)填充到現(xiàn)有卡片中去
隨機
創(chuàng)建一個新數(shù)組,并且隨機交換順序即可
const tempList = [...this.cardList];
const listLength = tempList.length;
for (let i = 0; i < listLength; i++) {
const j = Math.ceil(Math.random() * listLength);
const tempItem = tempList[i];
tempList[i] = tempList[j];
tempList[j] = tempItem;
}
填充
假設有 cardType 種類型的卡片,那么按 3 張重復填充即可
for (let i = 0; i < listLength; i++) {
const item = tempList[i];
item.setVal(Math.floor(i / 3) % this.cardType);
}
點擊交互
1. 是否可以點擊
只有頂層可以被點擊,我們之前已經(jīng)判定過卡片是否被覆蓋的邏輯,做對應處理即可。一個簡單的方法是給被覆蓋的卡片設置一個特殊 style
.covered-item {
pointer-event: none;
}
這樣對應卡片上的任何事件都不會生效
2. 點擊卡片
點擊到最上層的卡片之后,我們按如下步驟處理:
把點擊到的卡片實例 push 到暫存數(shù)組
pendingList中把卡片實例從
cardMap、cardList中去除pendingList遍歷如果 pendingList中存在 3 張相同的卡片,則消除這 3 張卡片如果不存在,且 pendingList中卡片數(shù)為 7,游戲結束 。本局失敗如果 cardList中的卡片數(shù)量為 0,游戲結束。本局成功
總結
到這一步,整個游戲的基礎框架就已經(jīng)搭建好了。剩下的難點還有
道具的實現(xiàn) 暫存道具 隨機道具 撤銷道具 動效的實現(xiàn) 從排堆進入 pendding 區(qū)域 從 pendding 區(qū)域進入暫存區(qū) 使用隨機道具時候的動畫 集齊 3 個卡片時候的消除動畫 樣式美化
這些基本屬于錦上添花了,感興趣的同學可以自行探索一下。提供的實時代碼里基本已經(jīng)實現(xiàn)了大部分.
最后,再給大家推薦我之前寫的10萬字Springboot經(jīng)典學習筆記,在Java開發(fā)寶典里回復:筆記,即可領取。 點贊是最大的支持

