接上篇教程:H5小遊戲開發教程之頁面基礎布局的開發
很抱歉,讓大家久等了,從上周開始,工作很忙,一直沒時間寫,在這期間,我也在思考是否有更好或更簡單的實現方案,在不同的設備上都能有不錯的體驗;通過這篇教程,我為大家帶來一個非常簡單的掃雷遊戲實現方案;原本打算用兩篇文章的,由于過于簡單,就用一篇文章搞定了;
我們先欣賞下本篇文章實現的遊戲界面:
掃雷遊戲界面
掃雷遊戲界面
我想,Windows的掃雷遊戲大家應該都玩過吧?其實,這個遊戲是有成功訣竅的,它考察了你思考問題的能力;如果1個格子的數值是1,那麼它的周圍8個方向有且隻有一個雷;同理,格子數值是2,它的周圍8個方向有且隻有2個雷;由于1個格子最多有8個相鄰格子,所以1個格子周圍最多包含8個雷;
現在,我們正式開始。首先,我們在src根目錄創建一個文件:shared.js文件,這個文件用于定義所有遊戲公用的變量及函數;我們在該文件中定義一個genArr函數;該函數非常簡單,用于創建一個指定長度的數組并用指定的值填充;在我們的遊戲教程中,會大量使用該函數生成用于遍曆的數組;
export const genArr = (len, v) = Array(len).fill(v)
然後,在src/components/mine文件夾下創建一個文件game.js。我們首先用JS文檔注釋聲明2個類型,并引入一些我們将要用到的函數;通過語義,大家應該能明白這2個類型中字段的含義吧?GameOptions類型中的rows是行數,cols是列數,mineCount是雷的數量;Block類型的num是格子的值,open是打開标識,flag是插旗标識,插旗是用于标記已确定了的雷,以防誤點擊;
/** * @typedef {{rows: number, cols: number, mineCount: number}} GameOptions 遊戲選項 * * @typedef {{num: number, open: boolean, flag: boolean}} Block 方格子 */ import { computed, reactive } from vue import { genArr } from ../../shared
然後,我們導出一個匿名函數。大家切記:我們以下所有的JS代碼全部寫在該函數内部;
export default () = {}
然後,我們創建一個用于保存遊戲狀态的響應式對象
const state = reactive({ rows: 9, // 行數 cols: 9, // 列數 mineCount: 10, // 雷數量 /** @type {Block[][]} 存放格子的二維數組 */ blocks: [], isOver: false, // 遊戲結束 isFirstClick: true // 是否首次點擊 })
然後,我們定義一個根據網格行列數生成二維數組陣列的函數,初始格子的值num全部設為0,open和flag屬性都為false;
/** @returns {Block[][]} */ const genBlocks = (rows, cols) = { return genArr(rows).map(() = genArr(cols).map(() = ({ num: 0, open: false, flag: false }))) }
然後,我們定義一個獲取格子對象的函數,由于我們很多地方需要獲取格子對象,所以定義一個函數比較好;
const getBlock = (row, col) = (state.blocks[row] || [])[col]
掃雷遊戲有一個原則就是,首次單擊的格子不能是地雷,所以,我們必須在玩家首次點開一個格子後,再生成地雷分布圖;我們生成地雷分布圖的函數需要一個行列坐标,來确保該坐标一定不是地雷。如下是生成地雷分布圖函數:
const genMap = (row, col) = { const { blocks } = state genArr(state.rows) .reduce((t, _, i) = [...t, ...genArr(state.cols).map((_, j) = [i, j])], []) // 行列坐标構成的一維數組 .filter(_ = !(_[0] === row _[1] === col)) // 過濾掉玩家首次單擊的坐标 .sort(() = Math.random() - .5) // 對坐标随機排序 .slice(0, state.mineCount) // 根據雷的數量對數組切片 .forEach(_ = { blocks[_[0]][_[1]].num = 9 // 遍曆坐标數組,将對應坐标的格子對象的值設置為9,9代表雷 }) // 如下遍曆用于更新每個非雷的格子周圍雷的數量,num的值就是雷的數量 blocks.forEach((a, i) = { a.forEach((b, j) = { if (b.num 9) { b.num = [ getBlock(i - 1, j - 1), getBlock(i - 1, j), getBlock(i - 1, j 1), getBlock(i, j 1), getBlock(i 1, j 1), getBlock(i 1, j), getBlock(i 1, j -1), getBlock(i, j - 1) ].filter(_ = _.num 8).length } }) }) }
當玩家點開一個格子後,如果該格子的值是0,那麼我們需要深度遞歸遍曆,将相鄰的值為0和1的格子全部自動打開;如下是自動打開格子的函數定義:
const openBlocks = (row, col) =[ [row - 1, col], [row, col 1], [row 1, col], [row, col - 1] ].forEach(coords = { const block = getBlock(...coords) // es6參數展開 if (block !block.open !block.flag) { // 如果格子存在并且沒被打開且沒被插旗 if (block.num 2) { // 如果格子值為0和1,将格子打開 block.open = true } if (block.num 1) { // 如果值為0,進行深度遞歸遍曆 openBlocks(...coords) } } }) }
當玩家點擊的格子值為9時,我們需要打開所有的地雷,并結束遊戲;如下是自動打開所有地雷的函數:
const openMineBlocks = () = { state.blocks.forEach(a = { a.forEach(b = { if (b.num 8) { b.open = true } }) }) }
當玩家打開了所有不是雷的格子後,我們需要結束遊戲,如下是用于判斷是否已完成挑戰的函數:
const isFinish = () = { return state.blocks.every(a = a.filter(b = b.num 9).every(b = b.open)) }
我們需要一個函數,用于開始新遊戲,該函數用于對遊戲狀态進行重置或更新,并啟動遊戲;如下是開始遊戲函數定義:
/** @param {GameOptions} options */ const start = (options = {}) = { Object.keys(options).forEach(key = { if (options[key]) { state[key] = options[key] } }) state.isOver = false state.isFirstClick = true state.blocks = genBlocks(state.rows, state.cols) }
我們需要一個用于處理格子單擊事件的函數。
const onBlockClick = (row, col) = { const block = getBlock(row, col) if (state.isOver || block.flag || block.open) return // 如果遊戲結束或格子插了旗或格子已打開,直接返回 block.open = true if (state.isFirstClick) { // 如果是首次單擊格子,那麼生成地雷分布圖 state.isFirstClick = false genMap(row, col) } if (block.num 8) { // 如果該格子是地雷,結束遊戲并自動炸開所有的地雷 state.isOver = true openMineBlocks() return setTimeout(() = confirm(挑戰失敗!是否重新開始? start(), 100) } else if (block.num 1) { openBlocks(row, col) // 使用深度遞歸遍曆,打開值為0和1的相鄰格子 } // 如果挑戰成功,那麼給玩家2個選擇:挑戰更高難度或重新挑戰該難度 isFinish() setTimeout(() = confirm(挑戰成功!是否挑戰更高難度?) ? start({ rows: state.rows 1, mineCount: state.mineCount state.cols }) : start(), 100) }
我們需要一個用于處理格子上下文菜單的函數,該函數用于插旗和移除旗之間切換。
/** @param {PointerEvent} evt */ const onBlockContextmenu = (row, col, evt) = { evt.preventDefault() if (state.isOver) return const block = getBlock(row, col) if (!block.open) { block.flag = !block.flag } }
如下是3個計算屬性定義,分别用于統計插旗數量,打開的格子數量,未打開的格子數量。
const flagCount = computed(() = { return state.blocks.reduce((t, a) = t a.filter(_ = _.flag).length, 0) }) const openCount = computed(() = { return state.blocks.reduce((t, a) = t a.filter(_ = _.open).length, 0) }) const unopenCount = computed(() = state.rows * state.cols - openCount.value)
最後,我們在導出的匿名函數的底部返回組件中使用到的變量和函數。
return { state, flagCount, openCount, unopenCount, start, onBlockClick, onBlockContextmenu }
如下是src/components/mine/Index.vue文件源碼,我們使用table承載遊戲界面;我認為table很适合二維數組數據的可視化;
templatediv :class=cls header :class=`${cls}_header` 網格布局:b class=blue{{ state.rows }}×{{ state.cols }} 雷數量:b class=red{{ state.mineCount }} 插旗數量:b class=green{{ flagCount }} 已打開數量:b class=green{{ openCount }} 未打開數量:b class=blue{{ unopenCount }} button class=btn @click=start新遊戲/button /header table :class=`${cls}_table` tr v-for=(a, i) in state.blocks :key= td v-for=(b, j) in a :key= div v-if=b.open :class=`${cls}_block is-open` img class=back src=./img/back.png span v-if=b.num :class=`${cls}_mine`/span span v-else-if=b.num :class=[`${cls}_num`, getNumCls(b.num)]{{ b.num }}/span div v-else :class=`${cls}_block` @click=onBlockClick(i, j) @contextmenu=onBlockContextmenu(i, j, $event) img class=back src=./img/front.png span v-if=b.flag :class=`${cls}_flag`/span /table/div/templatescript setup import useGame from ./game const { state, flagCount, openCount, unopenCount, start, onBlockClick, onBlockContextmenu } = useGame() start() const cls = com-mine const getNumCls = num = num 3 ? red : num 2 ? yellow : num 1 ? blue : white/scriptstyle lang=less .com-mine { _header { background-color: #eceff1; margin-bottom: .5em; border-radius: 4px; padding: .8em; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 1px 1px rgba(0, 0, 0, 0.14), 0 2px 1px -1px rgba(0, 0, 0, 0.12); display: flex; align-items: flex-end; ul { flex: 1; width: 0; li { display: inline-block; margin: .3em 1em .3em 0; } } } _block { position: relative; :not(.is-open) { cursor: pointer; } .back { display: block; width: 100%; } } _num, _flag, _mine { position: absolute; top: 0; right: 0; bottom: 0; left: 0; } _num { display: flex; align-items: center; justify-content: center; font-size: 16px; } _flag { background: url(./img/flag-color.png) no-repeat center center; background-size: 60%; } _mine { background: url(./img/bomb-color.png) no-repeat center center; background-size: 30%; } table { border-radius: 4px; padding: 1px; background-color: #b0bec5; } } /style
如下是本篇教程中用到的幾張圖片,由于是在網上找的,擔心有版權問題,僅提供截圖,就不放到文章裡面了,大家可以用自己的圖片替代,将圖片放到src/components/mine/img文件夾中;其實這個不是重點,重點是實現原理。
感謝閱讀!以上就是本篇教程的全部内容,童鞋們都理解了嗎?我感覺掃雷遊戲的實現非常簡單,幾乎沒什麼難度,童鞋們應該都能理解吧?如果還有疑問,可以問我;
上一篇:H5小遊戲開發教程之頁面基礎布局的開發
,
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!