项目布景
前段时刻 羊了个羊 这款小游戏火爆朋友圈,能够说是圈粉无数。国庆期间暂时起意,计划自己也完成一个相似的小游戏,所以有了 果了个果 在线体验
完成了项目还不过瘾,计划将开发项目过程中的思路和细节进行梳理,让广大朋友都能学会,完成自己的专属“羊了个羊”!(项目源码地址在文章末)
觉得文章不错、或对自己开发有所协助,欢迎点赞保藏!❤❤❤
项目预备
工欲善其事必先利其器,首要得为自己的小游戏找到适宜的资料
1、iconfont
2、花瓣网
3、羊了个羊本地文件
预备好了项目资料和音效文件,正式开始咱们的项目开发。
项目思路
技能栈选型
- 项目运用 Vue3+TS+Vite(首要原因是笔者对Vue更加了解),而且Vue相对来说更简略上手。
- 卡片层级联系运用 z-index 来完成
z-index 特点设置元素的堆叠次序。拥有更高堆叠次序的元素总是会处于堆叠次序较低的元素的前面。
- 卡片方位联系运用 absolute 来完成
布局思路
能够将布局拆分为3部分:
- HeaderSection:写日期显现、布景音乐、设置等功用
- CardSection:展示卡片布局、小草布景
- FooterSection:寄存被点击的卡片、功用按钮
数据存储
能够界说3个数组用来存储卡片数据:
- CardList:寄存默许生成的卡片
- RemoveList:寄存被移出的卡片
- StoreList:寄存被点击但是没有被消除的卡片
卡片组件
能够界说3种卡片组件
- Card:默许生成的卡片组件(动画时刻较长)
- RemoveCard:被移出的卡片组件(动画时刻较短)
- StoreCard:被点击但是没有被消除的卡片组件(动画时刻较短+消除动画)
项目拆解
卡片类型界说
- 界说 left 和 top 特点标识card的地点方位
- 界说id特点用作card的仅有标识,
- 界说zIndex特点来表明card的堆叠联系,层级较高的会显现、层级较低的则被遮挡
- 界说index特点标识card地点的相同层级下的索引
- 界说parents数组特点,从层级较低向层级较高逐级遍历寄存和card有交集(遮挡)的card并放入数组中
- 界说row特点用来辅佐计算card的left方向间隔
- 界说column特点用来辅佐计算card的top方向的间隔
- 界说state特点用来标识card的各种状况,比如不能点击、能够点击、现已被点击、被移出等
- 界说ref特点用作该card对应的dom的引证(动态改动left、top的值完成动画交互)
- 界说type特点用作card显现的图标类型
- 界说imgUrl特点用作card的图片文件途径
所以咱们能够得到以下CardNode类型:
// 卡片节点类型
type CardNode = {
id: string; // 卡片仅有id
type: string; // 卡片的图标类型
imgUrl: string; // 卡片的图标途径
zIndex: number; // 卡片地点的图层
index: number; // 地点图层中的索引
parents: CardNode[]; // 卡片的父类card数组
row: number; // 卡片地点行
column: number; // 卡片地点列
top: number; // 卡片top间隔
left: number; // 卡片left间隔
state: number; // 卡片四种状况 0: 无状况 1:可点击 2:已选 3:已消除
ref?: undefined | HTMLElement // 卡片本身的dom引证
};
游戏事情类型界说
- 界说winCallback用作游戏成功的事情回调
- 界说loseCallback用作游戏失败的事情回调
- 界说clickCallback用作card点击事情回调
- 界说removeCallback用作移出3个card事情回调
- 界说rollCallback用作回退1个card事情回调
- 界说dropCallback用作3个同类型的card消除事情回调
所以咱们能够得到以下GameEvents类型:
interface GameEvents {
clickCallback?: (card: CardNode) => void;
removeCallback?: () => void;
rollCallback?: (card: CardNode) => void;
dropCallback?: () => void;
winCallback?: () => void;
loseCallback?: () => void;
}
游戏设置类型界说
- 界说container用作展示card列表的父容器的dom引证(便于计算card的初始方位)
- 界说cardNum用作表明card显现的图标类型(比如香蕉、梨子、苹果等)
- 界说layerNum用作表明card堆叠的层数(操控游戏难度以及card数量)
- 界说events用作表明游戏各种事情
所以咱们能够得到以下GameConfig类型:
interface GameConfig {
container?: Ref<HTMLElement | undefined>; // cardNode容器
cardNum: number; // card类型数量
layerNum: number; // card层数
events?: GameEvents; // 游戏事情
}
游戏类型界说
- 界说cardList数组用作寄存生成的一切card
- 界说selectedList数组用作寄存被点击的card
- 界说removeList数组用作寄存被移出的card
- 界说removeFlag用作表明该局游戏有没有运用过移出功用
- 界说backFlag用作表明该局游戏有没有运用过回退功用
- 界说shuffleFlag用作表明该局游戏有没有运用过打乱功用
- 界说selectCardHandler用作card的点击事情办法
- 界说selectRemoveCardHandler用作被移出的card的点击事情办法
- 界说shuffleCardListHandler用作打乱card列表事情办法
- 界说rollbackOneCardHandler用作回退1个card的事情办法
- 界说removeThreeCardHandler用作移出3个card的事情办法
- 界说initCardList用作游戏初始化办法
所以咱们能够得到以下Game类型:
export interface Game {
cardList: Ref<CardNode[]>;
selectedList: Ref<CardNode[]>;
removeList: Ref<CardNode[]>;
removeFlag: Ref<boolean>;
backFlag: Ref<boolean>;
shuffleFlag: Ref<boolean>;
selectCardHandler: (node: CardNode) => void;
selectRemoveCardHandler: (node: CardNode) => void;
shuffleCardListHandler: () => void;
rollbackOneCardHandler: () => void;
removeThreeCardHandler: () => void;
initCardList: (config?: GameConfig) => void;
}
项目难点
怎么批量导入图标文件
const moduleFiles = import.meta.globEager('../../assets/icons/*.png');
/**vite晋级3.0以上版别选用以下写法**/
const modulesFiles: Record<string, any> = import.meta.glob('../../assets/icons/*.png', {
eager: true
});
项目选用 import.meta.globEager(vite3.0以上版别import.meta.glob) 来导入图标文件,glob是依据插件fast-glob完成的,一个*
用来匹配icons文件夹下一切以png为后缀的文件。
It’s a very fast and efficientgloblibrary forNode.js。
这是一个依据 node.js 且十分高效的全局库。
咱们打印一下 moduleFiles ,能够看到它是一个{文件途径:Module}
类型的目标
对moduleFiles稍作处理替换一下图片途径称号一起对Moudule进行解构
const moduleFiles = import.meta.globEager("../../assets/icons/*.png");
/**vite晋级3.0以上版别选用以下写法**/
const modulesFiles: Record<string, any> = import.meta.glob('../../assets/icons/*.png', {
eager: true
});
const imgMapObj = Object.keys(moduleFiles).reduce(
(module: { [key: string]: any }, path: string) => {
const moduleName = path
.replace("../../assets/icons/", "")
.replace(".png", "");
module[moduleName] = moduleFiles[path].default;
return module;
},
{} as Record<string, string>
);
咱们将得到一个{图片称号:图片途径}
的目标,此刻引进图片途径就很简略了,直接imgMapObj[图片称号]
即可
怎么完成动画音效
直接运用audio和source标签来引进音频文件,独自运用audio标签在Vite环境下会报错
<audio ref="clickAudioRef" style="display: none;" preload="auto" controls>
<source src="@/assets/audios/click.mp3" />
</audio>
此刻播放音频文件,直接调用以下代码即可
const clickAudioRef = ref();
clickAudioRef.value.play();
怎么生成card数组
1、首要依据cardNum 和 layerNum 生成循环遍历得到一个itemList
// 生成节点池
let itemList = [];
let itemTypes = [];
for (let i = 0; i < cardNum; i++) itemTypes.push(i + 1);
for (let i = 0; i < 3 * layerNum; i++) itemList = [...itemList, ...itemTypes];
itemList是一个由图片类型组成的数组而且 itemList的个数为cardNum * layerNum * 3
2、接下来依照层级联系由第一层逐级向上生成层级数组
// 打乱节点
itemList = shuffle(shuffle(itemList));
// 初始化各个层级节点
let floorList = [];
let len = 0;
let floorIndex = 1;
const itemLength = itemList.length;
while (len <= itemLength) {
const maxFloorNum = floorIndex * floorIndex;
const floorNum = ceil(random(maxFloorNum / 2, maxFloorNum));
floorList.push(itemList.splice(0, floorNum));
len += floorNum;
floorIndex++;
}
此刻咱们打印一下floorList
它表明的是层级以及该层级下存在的card的个数和类型
3、有了层级数组floorList,接下来生成中心部分cardList
let perFloorNodes: CardNode[] = [];
const containerWidth = container!.value!.clientWidth;
const containerHeight = container!.value!.clientHeight;
const width = containerWidth / 2;
const height = containerHeight / 2;
const cardList = ref<CardNode[]>([]);
const indexSet = new Set();
// 生成中心部分卡牌
floorList.forEach((o, index) => {
indexSet.clear();
let i = 0;
const floorNodes: CardNode[] = [];
o.forEach((k, index1) => {
i = floor(random(0, (index + 1) ** 2));
while (indexSet.has(i)) i = floor(random(0, (index + 1) ** 2));
const row = floor(i / (index + 1));
const column = index ? i % index : 0;
const node: CardNode = {
id: `${index}-${i}`,
type: shuffleCardImgArr[k],
imgUrl: imgMapObj[shuffleCardImgArr[k]],
zIndex: index,
index: i,
row,
column,
top: height + (size * row - (size / 2) * index),
left: width + (size * column - (size / 2) * index),
parents: [],
state: 0,
};
const xy = [node.top, node.left];
perFloorNodes.forEach((e) => {
if (
Math.abs(e.top - xy[0]) <= size &&
Math.abs(e.left - xy[1]) <= size
)
e.parents.push(node);
});
floorNodes.push(node);
indexSet.add(i);
});
cardList.value = cardList.value.concat(floorNodes);
perFloorNodes = floorNodes;
});
生成左右两头cardList
const leftTotal = Number((cardNum * 3) / 2);
const rightTotal = cardNum * 3 - leftTotal;
const topOffset = containerHeight - (layerNum > 5 ? size : 2 * size);
// 生成左右两头的卡牌池
for (let i = 0; i < 3; i++) itemList = [...itemList, ...itemTypes];
// 打乱节点
itemList = shuffle(shuffle(itemList));
// 生成左边部分卡牌
for (let j = 0; j < leftTotal; j++) {
const node: CardNode = {
id: `left-${j}`,
type: shuffleCardImgArr[itemList[j]],
imgUrl: imgMapObj[shuffleCardImgArr[itemList[j]]],
zIndex: j,
index: j,
row: j,
column: 1,
top: topOffset,
left: j * 7,
parents: [],
state: 0,
};
leftNodes.push(node);
}
for (let j = 0; j < leftTotal; j++) {
for (let k = leftTotal - 1; k > j; k--) {
leftNodes[j].parents.push(leftNodes[k]);
}
}
// 生成右边部分卡牌
for (let k = 0; k < rightTotal; k++) {
const node: CardNode = {
id: `right-${k}`,
type: shuffleCardImgArr[itemList[leftTotal + k]],
imgUrl: imgMapObj[shuffleCardImgArr[itemList[leftTotal + k]]],
zIndex: k,
index: k,
row: k,
column: 1,
top: topOffset,
left: containerWidth - k * 7 - size,
parents: [],
state: 0,
};
rightNodes.push(node);
}
for (let j = 0; j < rightTotal; j++) {
for (let k = rightTotal - 1; k > j; k--) {
rightNodes[j].parents.push(rightNodes[k]);
}
}
cardList.value = cardList.value.concat(leftNodes).concat(rightNodes);
4、改动cardList中card的state状况
cardList.value.forEach((o) => {
o.state = o.parents.every((p) => p.state > 0) ? 1 : 0;
});
every() 办法运用指定函数检测数组中的一切元素,假如数组中检测到有一个元素不满足,则整个表达式回来 false ,且剩下的元素不会再进行检测。假如一切元素都满足条件,则回来 true。
关于最上层的card,它的parents为空数组,此刻状况会被设置为1,其他则会被设置为0
怎么完成动画作用
完成动画作用很简略,只需要给card组件添加 transition: all .4s ease-in-out;
即可,动画时长能够依据自己需求来设定
为了精准操控card的移动方位,咱们需要提早计算出上述7个点的left和top
let positionList = [
{top: 200,left: 0},
{top: 200,left: 50},
{top: 200,left: 100},
{top: 200,left: 150},
{top: 200,left: 200},
{top: 200,left: 250},
{top: 200,left: 300},
{top: 200,left: 350}
]
const confirmCardPosition = (card) => {
const top = positionList[selectedList.length].top;
const left = positionList[selectedList.length].left;
card.ref?.setAttribute('style', `position: absolute; z-index: ${card.zIndex}; top: ${top}px; left: ${left}px;`);
}
那么咱们只需要在点击card的回调事情中动态改动style中的top和left即可。
同理提早计算出移出card的3个坐标方位,依照上述过程在移出card的点击事情回调中动态改动left和top完成动画作用。
怎么让点击事情次序履行
快速点击card,调用card的点击事情时,数组计算和视图渲染会出现问题,导致游戏不会产生成功结果
let historyList = [];
let count = 1;
const clickHandler = (card) => {
historyList.push(card);
if (count !== historyList.length) {
historyList.pop();
return;
} else {
setTimeout(() => {
fn(); // 履行卡片处理逻辑
count += 1;
}, 500);
}
}
项目选用上述代码,通过比对count和historyList数组长度,确保点击事情是依照次序履行。
怎么适配移动端
由于移动端的机型、系统还有浏览器环境千差万别,最好将项目拆分为PC端和移动端两个版别,本项目选用User-Agent配合Nginx服务器来进行项目适配
User-Agent是Http协议中的一部分,属于头域的组成部分,User Agent也简称UA。简略来说,是一种向拜访网站供给你所运用的浏览器类型、操作系统及版别、CPU 类型、浏览器渲染引擎、浏览器言语、浏览器插件等信息的标识。UA字符串在每次浏览器 HTTP 请求时发送到服务器!
只需要修正一下Nginx配置文件即可完成
server
{
listen 80;
listen 443 ssl;
server_name 你的项目域名;
index index.php index.html index.htm default.php default.htm default.html;
root 你的PC端项目途径;
location / {
if ($http_user_agent ~ "(MIDP)|(WAP)|(UP.Browser)|(Smartphone)|(Obigo)|(Mobile)|(AU.Browser)|(wxd.Mms)|(WxdB.Browser)|(CLDC)|(UP.Link)|(KM.Browser)|(UCWEB)|(SEMC-Browser)|(Mini)|(Symbian)|(Palm)|(Nokia)|(Panasonic)|(MOT-)|(SonyEricsson)|(NEC-)|(Alcatel)|(Ericsson)|(BENQ)|(BenQ)|(Amoisonic)|(Amoi-)|(Capitel)|(PHILIPS)|(SAMSUNG)|(Lenovo)|(Mitsu)|(Motorola)|(SHARP)|(WAPPER)|(LG-)|(LG/)|(EG900)|(CECT)|(Compal)|(kejian)|(Bird)|(BIRD)|(G900/V1.0)|(Arima)|(CTL)|(TDG)|(Daxian)|(DAXIAN)|(DBTEL)|(Eastcom)|(EASTCOM)|(PANTECH)|(Dopod)|(Haier)|(HAIER)|(KONKA)|(KEJIAN)|(LENOVO)|(Soutec)|(SOUTEC)|(SAGEM)|(SEC-)|(SED-)|(EMOL-)|(INNO55)|(ZTE)|(iPhone)|(Android)|(Windows CE)|(Wget)|(Java)|(curl)|(Opera)") {
root 你的移动端项目途径;
}
try_files $uri $uri/ /index.html;
index index.html index.htm;
}
}
怎么处理打乱card排序事情
与生成cardList的办法相似,咱们只需要遍历cardList数组重置state为0和1的card的特点即可,在这里要注意改动card的id特点,不然视图可能不会渲染
当在进行列表渲染的时分,vue会直接对已有的标签进行复用
card.id = card.id + "shuffle";
写在最后
至此整个项目讲解现已悉数完毕,你学会了吗?
果了个果 现已开源
gitee地址: 源码地址
github地址: 源码地址
觉得文章不错、或对自己开发有所协助,欢迎点赞保藏!❤❤❤
一起推荐几个作者参与的开源项目,假如项目有协助到你,欢迎star!
一个简略的依据Vue3、TS、Vite、qiankun技能栈的后台管理项目
:www.xkxk.tech
一个依据Vue3、Vite的仿element UI的组件库项目
:ui.xkxk.tech
一个依据Vue3、Vite的炫酷大屏项目
:screen.xkxk.tech