使用Vue3實(shí)現(xiàn)羊了個(gè)羊的算法
前言
這兩天社區(qū)很多羊了個(gè)羊的web實(shí)現(xiàn),雖然各種實(shí)現(xiàn)花里花哨,然而,并沒有一個(gè)一個(gè)jy能給他說清楚到底怎么實(shí)現(xiàn)的,由于可怕的求知欲,自己來吧!
大綱
羊了個(gè)羊這個(gè)現(xiàn)象級(jí)游戲之所以能成功,不是因?yàn)樗?code>原神一樣,靠著質(zhì)量、體驗(yàn)、劇情你愛不釋手
他靠的是爛,讓你愛不釋手,人家玩的是營(yíng)銷,玩的是人性,也許你壓根就過不了關(guān)!
他的技術(shù)實(shí)現(xiàn),其實(shí)相當(dāng)簡(jiǎn)單,在技術(shù)上從來沒有什么高深的東西,
果然,高深的技術(shù)總是顯得這么樸實(shí)無華!
最難的部分也就是算法了,我也大致的鉆研了一下,但是這個(gè)算法坦率的講不是我發(fā)明的, 我只是站在巨人的肩膀上
他的算法實(shí)現(xiàn)的難點(diǎn)我以為有四方面
- 1、 初始化的隨機(jī)位置算法
- 2、 檢查是否被覆算法
- 3、 三連匹配算法
- 4、隊(duì)列區(qū)排序算法
在線演示
https://code.juejin.cn/pen/7144922644788297735
初始化的隨機(jī)位置算法
在理解算法之前,我們先大致看元數(shù)據(jù)
他需要包含 一些必備的屬性, 默認(rèn)的覆蓋情況,是否被選中的狀態(tài),icon 圖標(biāo),icon 的唯一id x 坐標(biāo) y坐標(biāo)
const scene=({
isCover: false, // 默認(rèn)都是沒有被覆蓋的
status: 0,// 是否被選中的狀態(tài)
icon,// 圖標(biāo)
id: randomString(4), // 生成隨機(jī)id
x: column * 100 + offset, //x 坐標(biāo)
y: row * 100 + offset,// y坐標(biāo)
}然后再來說算法,他的算法,本質(zhì)上其實(shí)就是限定的畫布內(nèi),隨機(jī)生成位置
在當(dāng)前這個(gè)算法中他使用一個(gè)8x8的網(wǎng)格中,生成方塊,然后利用隨機(jī)偏移量,來造成隨機(jī)堆疊的樣子

// 以下感謝大佬們提供的算法
const makeScene = (level) => {
// 獲取當(dāng)前關(guān)卡
const curLevel = Math.min(maxLevel, level);
// 獲取當(dāng)前關(guān)卡應(yīng)該擁有的icon數(shù)量
const iconPool = icons.slice(0, 2 * curLevel);
// 算出偏移量范圍具體細(xì)節(jié)范圍
const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + curLevel);
// 最終的元數(shù)據(jù)數(shù)組
const scene = [];
// 確定范圍
//在一般情下 translate 的偏移量,如果是百分比的話,是按照自身的寬度或者高度去計(jì)算的,所以最大的偏移范圍是百分800%
// 然后通過Math.random 會(huì)小于百分之八百
// 所以就會(huì)形成當(dāng)前區(qū)間的隨機(jī)數(shù)
const range = [
[2, 6],
[1, 6],
[1, 7],
[0, 7],
[0, 8],
][Math.min(4, curLevel - 1)];
const randomSet = (icon: string) => {
// 求偏移量
const offset = offsetPool[Math.floor(offsetPool.length * Math.random())];
// 偏移求列數(shù)
const row = range[0] + Math.floor((range[1] - range[0]) * Math.random());
// 求偏移行數(shù)
const column = range[0] + Math.floor((range[1] - range[0]) * Math.random());
console.log(offset, row, column);
// 生成元數(shù)據(jù)對(duì)象
scene.push({
isCover: false, // 默認(rèn)都是沒有被覆蓋的
status: 0,// 是否被選中的狀態(tài)
icon,// 圖標(biāo)
id: randomString(4), // 生成隨機(jī)id
x: column * 100 + offset, //x 坐標(biāo)
y: row * 100 + offset,// y坐標(biāo)
});
};
// 如果級(jí)別高了就加點(diǎn)icon 花哨一點(diǎn)
let compareLevel = curLevel;
while (compareLevel > 0) {
iconPool.push(...iconPool.slice(0, Math.min(10, 2 * (compareLevel - 5))));
compareLevel -= 5;
}
// 生成元數(shù)據(jù),初始狀態(tài)下 iconPool的內(nèi)容少生 隨著增加,就會(huì)越來越難
for (const icon of iconPool) {
for (let i = 0; i < 6; i++) {
randomSet(icon);
}
}
// 返回元數(shù)據(jù)
return scene;
};解釋一下, 我們?cè)诔跏蓟臅r(shí)候, 會(huì)生成一個(gè)范圍,來初始化 他的預(yù)計(jì)位置
const range = [
[2, 6],
[1, 6],
[1, 7],
[0, 7],
[0, 8],
][Math.min(4, curLevel - 1)];range 最后的結(jié)果,就表示格子范圍,這里是為了跟關(guān)卡結(jié)合,在初始化的時(shí)候 由于圖標(biāo)少, 所以就會(huì)在 在8x8之內(nèi)的更小的格子
例如這樣:

當(dāng)關(guān)卡越來越多的時(shí)候就會(huì)如下圖:

以為在后面關(guān)卡的時(shí)候?qū)⑺械母褡訐螡M了為8x8
那么如何計(jì)算偏移量呢?
const randomSet = (icon: string) => {
// 求偏移量
const offset = offsetPool[Math.floor(offsetPool.length * Math.random())];
// 偏移求列數(shù)
const row = range[0] + Math.floor((range[1] - range[0]) * Math.random());
// 求偏移行數(shù)
const column = range[0] + Math.floor((range[1] - range[0]) * Math.random());
console.log(offset, row, column);
// 生成元數(shù)據(jù)對(duì)象
scene.push({
isCover: false, // 默認(rèn)都是沒有被覆蓋的
status: 0,// 是否被選中的狀態(tài)
icon,// 圖標(biāo)
id: randomString(4), // 生成隨機(jī)id
x: column * 100 + offset, //x 坐標(biāo)
y: row * 100 + offset,// y坐標(biāo)
});
};其實(shí)偏移量的核心就是 Math.random這個(gè)函數(shù),來生成0-1的隨機(jī)數(shù),我們需要求 offset基礎(chǔ)偏移量 row列的偏移量 column行的偏移量
由于為了導(dǎo)致位置的總體差異,和細(xì)節(jié)差異,來達(dá)到符合預(yù)期的亂序效果,所以最終他生成的坐標(biāo)需要 基礎(chǔ)偏移和行列偏移來結(jié)合
檢查是否被覆算法
檢查是否被覆蓋算法其實(shí)本質(zhì)上來說 ,就是祖?zhèn)鞯?code>碰撞檢測(cè)算法
根據(jù)是否碰撞,來計(jì)算覆蓋情況
代碼如下:
// 檢查是否被覆蓋
const checkCover = (value) => {
// 深拷貝一份
const updateScene = value.slice();
// 是否覆蓋算法
// 遍歷所有的元數(shù)據(jù)
// 雙重for循環(huán)來找到每個(gè)元素的覆蓋情況
for (let i = 0; i < updateScene.length; i++) {
// 當(dāng)前item對(duì)角坐標(biāo)
const cur = updateScene[i];
// 先假設(shè)他都不是覆蓋的
cur.isCover = false;
// 如果status 不為0 說明已經(jīng)被選中了,不用再判斷了
if (cur.status !== 0) continue;
// 拿到坐標(biāo)
const { x: x1, y: y1 } = cur;
// 為了拿到他們的對(duì)角坐標(biāo),所以要加上100
//之所以要加上100 是由于 他的總體是800% 也就是一個(gè)格子的換算寬度是100
const x2 = x1 + 100,
y2 = y1 + 100;
// 第二個(gè)來循環(huán)來判斷他的覆蓋情況
for (let j = i + 1; j < updateScene.length; j++) {
const compare = updateScene[j];
if (compare.status !== 0) continue;
const { x, y } = compare;
// 處理交集也就是選中情況
// 兩區(qū)域有交集視為選中
// 兩區(qū)域不重疊情況取反即為交集
if (!(y + 100 <= y1 || y >= y2 || x + 100 <= x1 || x >= x2)) {
// 由于后方出現(xiàn)的元素會(huì)覆蓋前方的元素,所以只要后方的元素被選中了,前方的元素就不用再判斷了
// 又由于雙層循環(huán)第二層從j 開始,所以不用擔(dān)心會(huì)重復(fù)判斷
cur.isCover = true;
break;
}
}
}
scene.value = updateScene;
};碰撞檢測(cè)
所謂碰撞檢測(cè),就是計(jì)算兩個(gè)東西的坐標(biāo)有沒有重疊,也就是求交集

主要算法如下,就是比較他們的各個(gè)方向的位置
function isButt(obj1,obj2){
var l1=obj1.offsetLeft;
var t1=obj1.offsetTop;
var r1=l1+obj1.offsetWidth;
var b1=t1+obj1.offsetHeight;
var l2=obj2.offsetLeft;
var t2=obj2.offsetTop;
var r2=l2+obj2.offsetWidth;
var b2=t2+obj2.offsetHeight;
return!(r1<l2||b1<t2||r2<l1||b2<t1)
}覆蓋算法實(shí)現(xiàn)
覆蓋算法其實(shí)實(shí)現(xiàn)也非常簡(jiǎn)單,就是一個(gè)雙重for循環(huán) 來將每個(gè)方塊的位置做比較,做一個(gè)碰撞檢測(cè),從而能篩選出來被遮擋的方塊
值得注意的是
- 1、j的值需要從i+1開始,為了防止已經(jīng)比較過的
方塊再次比較 - 2、由于元數(shù)據(jù)的渲染,的后方物體天然的會(huì)遮擋前方物體,所以當(dāng)碰撞檢測(cè)成功之后是只需要遮擋前方
方塊即可
for (let i = 0; i < updateScene.length; i++) {
// 第二個(gè)來循環(huán)來判斷他的覆蓋情況
for (let j = i + 1; j < updateScene.length; j++) {
// 執(zhí)行碰撞檢測(cè)
}
}三連匹配算法
三連匹配其實(shí)相比于前兩點(diǎn),就非常簡(jiǎn)單了
我們只需要拿到相同的方塊的icon名, 湊夠三個(gè)直接改變方塊樣式即可
// 點(diǎn)擊item
const clickSymbol = async (idx: number) => {
// 如果已經(jīng)完成了,就不處理
if (finished.value || animating.value) return;
// 拷貝一份Scene
const symbol = scene.value[idx];
// 覆蓋了和已經(jīng)在隊(duì)列里的也不處理
if (symbol.isCover || symbol.status !== 0) return;
//置為可以選中狀態(tài)
symbol.status = 1;
queue.value.push(symbol);
// 制造動(dòng)畫效果中防止點(diǎn)擊
animating.value = true;
//三百毫秒的延遲
await waitTimeout(300);
// 拿到與他匹配的所有icon
const filterSame = queue.value.filter((sb) => sb.icon === symbol.icon);
// 選中的三個(gè)配對(duì)成功表示已經(jīng)是三連了
if (filterSame.length === 3) {
// 由于icon的類型一樣,留下隊(duì)列中的不一樣的剩余內(nèi)容重新賦值
queue.value = queue.value.filter((sb) => sb.icon !== symbol.icon);
// 隱藏iocn,dom
for (const sb of filterSame) {
const find = scene.value.find((i) => i.id === sb.id);
// 將他們的狀態(tài)變?yōu)? 通過opacity 屬性 來隱藏icon
if (find) find.status = 2;
}
}
// 當(dāng)格子沾滿了,那么久表示已經(jīng)失敗了
if (queue.value.length === 7) {
tipText.value = '失敗了'
finished.value = true;
}
if (!scene.value.find((s) => s.status !== 2)) {
// 如果完成所有關(guān)卡,那就過了所有關(guān)了
if (level.value === maxLevel) {
tipText.value = '完成挑戰(zhàn)';
finished.value = true
return;
}
//否則加一關(guān)
level.value = level.value + 1;
queue.value = []
// 重新初始化
checkCover(makeScene(level.value + 1));
} else {
// 處理覆蓋情況
checkCover(scene.value);
}
// 動(dòng)畫結(jié)束
animating.value = false;
};以上代碼中,我們只需要 改變?cè)獢?shù)據(jù)的status的狀態(tài)值即可 ,然后再配合css的視覺效果,來達(dá)到消失的效果,其實(shí)dom 還是在頁面中,并沒有消失移除,因?yàn)樵獢?shù)據(jù)沒變
隊(duì)列區(qū)排序算法

在隊(duì)列中我們發(fā)現(xiàn)如果湊夠三個(gè)他需要排序,
比如說在有一個(gè)叉子,就會(huì)排在米飯的前面然后消失
實(shí)現(xiàn)如下:
// 隊(duì)列區(qū)排序
watchEffect(() => {
const cache = {};
// 通過當(dāng)前的icon的標(biāo)識(shí),將相同的icon歸納到一塊
// 方便后續(xù)排序
for (const symbol of queue.value) {
if (cache[symbol.icon]) {
cache[symbol.icon].push(symbol);
} else {
cache[symbol.icon] = [symbol];
}
}
const temp = [];
for (const symbols of Object.values(cache)) {
temp.push(...(symbols as any));
}
const updateSortedQueue = {};
let x = 50;
// 拿到更新后的隊(duì)列區(qū)數(shù)據(jù),計(jì)算權(quán)重
for (const symbol of temp) {
updateSortedQueue[symbol.id] = x;
x += 100;
}
//賦值 ,這個(gè)是為了將選中的排序后的內(nèi)容移動(dòng)到隊(duì)列區(qū)
sortedQueue.value = updateSortedQueue
// 檢查覆蓋情況
checkCover(scene.value);
})他的實(shí)現(xiàn)原理其實(shí)就是利用緩存對(duì)隊(duì)列計(jì)算先后權(quán)重,從而計(jì)算他排序的位置,其實(shí)他的元數(shù)據(jù)或者選中順序并沒有變
只是在視覺上更改了css 的樣式
總結(jié)
到此這篇關(guān)于使用Vue3實(shí)現(xiàn)羊了個(gè)羊的算法的文章就介紹到這了,更多相關(guān)vue羊了個(gè)羊算法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決vuex數(shù)據(jù)異步造成初始化的時(shí)候沒值報(bào)錯(cuò)問題
今天小編大家分享一篇解決vuex數(shù)據(jù)異步造成初始化的時(shí)候沒值報(bào)錯(cuò)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11
vue3+element-plus props中的變量使用 v-model 報(bào)錯(cuò)及解決方案
這篇文章主要介紹了vue3+element-plus props中的變量使用 v-model 報(bào)錯(cuò)及解決方案,prop 是單向數(shù)據(jù)流,這里只能用:model-value,不能用v-model,本文給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-10-10
Vue Element Sortablejs實(shí)現(xiàn)表格列的拖拽案例詳解
這篇文章主要介紹了Vue Element Sortablejs實(shí)現(xiàn)表格列的拖拽案例詳解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09
elementplus實(shí)現(xiàn)多級(jí)表格(最后一級(jí)展示圖片)
本文主要介紹了elementplus實(shí)現(xiàn)多級(jí)表格(最后一級(jí)展示圖片),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
關(guān)于vue的element-ui web端引入高德地圖并獲取經(jīng)緯度
這篇文章主要介紹了關(guān)于vue的element-ui web端引入高德地圖并獲取經(jīng)緯度,高德地圖首先要去申請(qǐng)key和密鑰,文中提供了部分實(shí)現(xiàn)代碼和解決思路,感興趣的朋友可以學(xué)習(xí)一下2023-04-04

