简介
小时候经常玩连连看小游戏。在游戏中,当找到2个相同的元素就可以消除元素。
本文会借助react实现连连看小游戏。
实现效果
实现难点
1.item 生成
1. 每一个图片都是一个item,items数组的大小为size*size。
item对象包括grid布局的位置,key。
key是标识符,可以标识图片, 相等判断等。
2. items 可以先顺序生成,最后再调用shuffle算法随机排序。
const size = 8; // 大小为 8 * 8
const itemImgSize = 20; // 圖片素材大小
const [items, setItems] = useState([]);
useEffect(() => { // 初始化元素
const initItems = [];
let idx = 0;
while (initItems.length < size * size) {
// 一次插入2個
initItems.push({
key: (idx % itemImgSize) + 1,
x: parseInt(idx / size),
y: parseInt(idx % size)
});
initItems.push({
key: (( idx)% itemImgSize) + 1,
x: parseInt(( idx + 1) / size),
y: parseInt((idx + 1)% size)
});
idx = idx + 1;
}
const nArr = [...shuffleArray(initItems)];
setItems(nArr);
}, [])
function shuffleArray(arr) {
for (let i = arr.length - 1; i >= arr.length / 2; i--) {
const j = Math.floor(Math.random() * (size - 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
if (arr[i] instanceof Array) {
shuffleArray(arr[i]);
shuffleArray(arr[j])
} else {
// 交换key
let key = arr[i].key;
arr[i].key = arr[j].key;
arr[j].key = key;
}
}
return arr;
}
1. 判断选择的2个item可以消除
基于dfs算法实现,以其中一方为原点,另一方为终点。找到一条成功的路径。
tips: dfs 可以改成bfs, dfs 当item 消除大半后,会变慢。
/**
* 判断 (i,j) 与(x,y)是否可达
*/
function dfs(i, j, visited, x, y) {
if (res.current === true) {
return;
}
if (i < 0 || i >= size + 2 || j < 0 || j >= size + 2) { // 边界
return;
}
if (i === x && j === y) {
res.current = true;
return;
}
if (visited[i][j] === 1) {
return;
}
if (board.current[i][j] === 1) { // 只能走空白
return;
}
visited[i][j] = 1;
dfs(i - 1, j, visited, x, y);
dfs(i + 1, j, visited, x, y);
dfs(i, j + 1, visited, x, y);
dfs(i, j - 1, visited, x, y);
visited[i][j] = 0;
}
boards标记数组是根据items数组生成,若item存在,则boards对应标记为1,反之为null。
item 对应位置为(item.x+1,item.y+1)
boards 的大小为(size + 2) * (size + 2) , + 2是为了解决边界上的2点相连处理。
// 二维int数组,标记是否存在元素 (size + 2) * (size + 2), +1是为了边界可以连接
const board = useRef([]);
useEffect(() => {
const nBoard = new Array(size + 2);
// init
for (let i = 0; i < size + 2; i++) {
nBoard[i] = new Array(size + 2);
for (let j = 0; j< size + 2;j++){
nBoard[i][j] = 0;
}
}
//根据items设置boards
items.map((item) => {
nBoard[item.x + 1][item.y + 1] = 1;
})
board.current = (nBoard)
}, [items]);
整体代码
import bgImg from './imgs/bg.png'
import {useEffect, useRef, useState} from "react";
export const LinkGame = () => {
const size = 8; // 大小为 8 * 8
const itemImgSize = 20; // 圖片素材大小
const [items, setItems] = useState([]);
useEffect(() => { // 初始化元素
const initItems = [];
let idx = 0;
while (initItems.length < size * size) {
// 一次插入2個
initItems.push({
key: (idx % itemImgSize) + 1,
x: parseInt(idx / size),
y: parseInt(idx % size)
});
initItems.push({
key: (idx % itemImgSize) + 1,
x: parseInt((idx + 1)/ size),
y: parseInt((idx + 1) % size)
});
idx = idx + 2;
}
const nArr = [...shuffleArray(initItems)];
setItems(nArr);
}, [])
function shuffleArray(arr) {
for (let i = arr.length - 1; i >= arr.length / 2; i--) {
const j = Math.floor(Math.random() * (size - 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
if (arr[i] instanceof Array) {
shuffleArray(arr[i]);
shuffleArray(arr[j])
} else {
// 交换key
let key = arr[i].key;
arr[i].key = arr[j].key;
arr[j].key = key;
}
}
return arr;
}
// 二维int数组,标记是否存在元素 (size + 2) * (size + 2), +1是为了边界可以连接
const board = useRef([]);
useEffect(() => {
const nBoard = new Array(size + 2);
// init
for (let i = 0; i < size + 2; i++) {
nBoard[i] = new Array(size + 2);
for (let j = 0; j< size + 2;j++){
nBoard[i][j] = 0;
}
}
//根据items设置boards
items.map((item) => {
nBoard[item.x + 1][item.y + 1] = 1;
})
board.current = (nBoard)
}, [items]);
// 当选择2个时候,判断是否能消除,如果能消除,则消除,不能则复原。
const res = useRef(false);
useEffect(() => {
const checkedList = [];
items.map(item => {
if (item.checked) {
checkedList.push(item);
}
if (checkedList.length === 2) {
const a = checkedList[0];
const b = checkedList[1];
if (a.key !== b.key) {
a.checked = false;
b.checked = false;
setItems([...items])
} else {
// 判断 a 和 b 直接是否能连接
const visited = new Array(size + 2);
for (let i = 0; i < size + 2; i++) {
visited[i] = new Array(size + 2);
}
const i = a.x + 1;
const j = a.y + 1;
const x = b.x + 1;
const y = b.y + 1;
dfs(i + 1, j, visited, x, y)
dfs(i - 1, j, visited, x, y)
dfs(i, j + 1, visited, x, y)
dfs(i, j - 1, visited, x, y)
if (res.current === true) { // 存在线路相连
// 移除 a 和 b
const nItems = [];
items.map((item) => {
if (item !== a && item !== b) {
nItems.push(item);
}
})
setItems(nItems)
res.current = false; //init
} else {
a.checked = false;
b.checked = false;
setItems([...items])
}
}
}
})
}, [items]);
/**
* 判断 (i,j) 与(x,y)是否可达
*/
function dfs(i, j, visited, x, y) {
if (res.current === true) {
return;
}
if (i < 0 || i >= size + 2 || j < 0 || j >= size + 2) { // 边界
return;
}
if (i === x && j === y) {
res.current = true;
return;
}
if (visited[i][j] === 1) {
return;
}
if (board.current[i][j] === 1) { // 只能走空白
return;
}
visited[i][j] = 1;
dfs(i - 1, j, visited, x, y);
dfs(i + 1, j, visited, x, y);
dfs(i, j + 1, visited, x, y);
dfs(i, j - 1, visited, x, y);
visited[i][j] = 0;
}
function onItemClick(item) {
item.checked = !item.checked;
setItems([...items]);
}
const gameBoardStyle = { // 游戏区域样式
display: 'grid',
gridTemplateColumns: `repeat(${size}, 1fr)`,
gridTemplateRows: `repeat(${size}, 1fr)`,
width: '60vw',
height: '80vh',
backgroundImage: 'url(' + bgImg + ')',
backgroundSize: 'cover'
};
const gameBoardItemStyle = (item) => {
if (item.checked) {
return ({
gridRowStart: item.x + 1,
gridColumnStart: item.y + 1,
opacity: 0.4
})
}
return ({
gridRowStart: item.x + 1,
gridColumnStart: item.y + 1,
});
}
return <>
<div id={'link-game'}>
<div style={gameBoardStyle}>
{
items.map((item, idx) => (
<div style={gameBoardItemStyle(item)}
onClick={() => onItemClick(item)} key={'item-' + idx}>
<img src={require(`./imgs/${item.key}.png`)}/>
</div>
))
}
</div>
</div>
</>
}