开始
前几个星期项目组了出了个仿羊了羊的游戏项目需求,临近项目结束写一篇总结一下具体的玩法和详细的逻辑实现。
玩法逻辑
基础
我认为羊了羊整个玩法逻辑就是通过方块的层数叠加的消消乐类型游戏,关键的逻辑就是 层 和方块。 在生成层和方块之前,先了解一些概念。 我们先把整块屏幕分成 7 * 7的虚拟网格,铺满手机屏幕。
以上图为例子,我们根据这一张网格来计算当前层的块和块之间互相之间不会遮挡的坐标。
但是这种网格关系太整齐了,假设我们有两层,随机出来的效果如图:
如果想要达到羊了羊的半遮挡效果
可以看出羊了羊的半透明遮罩永远是半个方块的一半,达到这样的效果第一件事就是再把网格细分 如下图
这个图我们可能看得不是很明白,再标记坐标
一个方块占一个格子,我们把一个格子的坐标再对半切分,这样我们在随机坐标的时候,有更多选择的可能, 如果以这样的网格设计就能达到羊了个羊的展示效果了。
上图是实例效果 。(自动代入网格的设计,还不明白的看下面的每层坐标计算)
代码实现
基础逻辑
基础设计说完,接下说代码实现
1、基础块数:为了保证游戏中所有的块都必须可以被消除完,不然消除到最后发现不够就尴尬了。
基础块数 = 需要多少个块才能消除 * 方块类型数量
2、最小块数:设计的层数 * 设计的每层有多少方块。
3、现在还是没有算出整个局游戏需要需要多少块的,通过使用(最小块数) % (基础块数)如果不能 == 0; 我们重新计算最小块数,否则第三步什么都不做。
//需要多少个才能和合成 * 方块类别数 = 块数单位(总块数必须是该值的倍数)
const blockNumUnit = gameConfig.composeNum * gameConfig.typeNum;
// 需要的最小块数
const minBlockNum =
gameConfig.levelNum * gameConfig.levelBlockNum ;
// 补齐到 blockNumUnit 的倍数
// e.g. minBlockNum = 14, blockNumUnit = 6, 补到 18
totalBlockNum = minBlockNum;
//假设不能整除
// 不能整除的话
if (totalBlockNum % blockNumUnit !== 0) {
console.log('不能整出');
// 计算出倍数 * 块数单位 就是全部块
totalBlockNum =
(Math.floor(minBlockNum / blockNumUnit) + 1) * blockNumUnit;
}
4、现在计算出来整局块数后,我们可以给整个块初始化数据,并且填充基础内容了。
// 2. 初始化块,随机生成块的内容
// 保存所有块的数组
const blockAlls: string[] = [];
//类型数组
const needAnimals = gameConfig.animals.slice(0, gameConfig.typeNum);
// 依次把块塞到数组里
for (let i = 0; i < totalBlockNum; i++) {
blockAlls.push(needAnimals[i % gameConfig.typeNum]);
};
// 打乱数组
const randomblockAlls = _.shuffle(blockAlls); // lodash 库方法
// 初始化
for (let i = 0; i < totalBlockNum; i++) {
const newBlock = {
id: i,
status: 0,
level: i, //层级
originLeveL:0,
type: randomblockAlls[i], //方块类型
higherThanBlocks: [] as BlockType[], // 我遮住了谁
lowerThanBlocks: [] as BlockType[], //谁遮住了我
currentArea:0,
} as BlockType;
allBlocks.push(newBlock);
}
5、下一步就是计算每一层的块数了
let pos = 0;
for (let i = 0; i < gameConfig.levelNum; i++) {
//循环当前难度层次
//通过每层最大数 和 剩余块数(默认全部) 取出最小值,最小不能小于设置的每一层最小数
let nextBlockNum = Math.min(gameConfig.levelBlockNum, leftBlockNum);
// 最后一层,分配所有 leftBlockNum
if (i == gameConfig.levelNum - 1) {
nextBlockNum = leftBlockNum; // 到最后一次循环的时候 nextBlockNum = 全部快的总数
}
//当前层的块数
const nextGenBlocks = allBlocks.slice(pos, pos + nextBlockNum);
//加入层数数组
levelBlocks.push(...nextGenBlocks);
//下一次从 allBlocks 中第几个开始取
pos = pos + nextBlockNum;
console.log(i,'pos');
// 生成层块的坐标
genLevelBlockPos(nextGenBlocks,i,i);
leftBlockNum -= nextBlockNum;
if (leftBlockNum <= 0) {
break;
}
}
6、计算坐标
计算每一层块的坐标,规则是每一层块的坐标互相之间不能有重叠,要达成这个条件需要生成新的网格列表,依然是上图的示例,我们通过代码实现
for (let i = 0; i < maxX; i++) {
// chessBoard[i] = new Array(height);
for (let j = 0; j < maxY; j++) {
// console.log(`${i}-${j}`);
// console.log(`${i}-${j+0.5}`);
xyPosArray.push([i,j])
xyPosArray.push([i,j+0.5])
xyPosArray.push([j+0.5,i])
}
}
生成一个二位数组,例如:[[0,0],[0,0.5],[0,1]]... 计算坐标通过随机生成一个数组下标取对应坐标,然后删除可能和这个坐标遮挡的坐标,如何取计算呢,首先,方块会占一共格子,例如如果随机出来的坐标是[0,0],那实际方块的大小占位是是到[0,1]的,那这样的[0,0.5]的坐标值的方块肯定会互相遮挡的,那如此,把这个坐标剔除掉就能达到想要的效果了。
const genLevelBlockPos = (
blocks: BlockType[],
maxX: number,
maxY: number,
offSetX:number,
offSetY:number
) => {
let xyPosArray = [];//生成全部可选坐标 [[0,0.5],0.]
for (let i = 0; i < maxX; i++) {
// chessBoard[i] = new Array(height);
for (let j = 0; j < maxY; j++) {
// console.log(`${i}-${j}`);
// console.log(`${i}-${j+0.5}`);
xyPosArray.push([i,j])
xyPosArray.push([i,j+0.5])
xyPosArray.push([j+0.5,i])
}
}
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
// 随机生成坐标
let newPosX;
let newPosY;
let XY = xyPosArray.splice(Math.floor(Math.random() * xyPosArray.length),1)
//判断是否和当前坐标重叠
function isCovered(a, b) {
// console.log(a, b,'a, b');
// if (a.originLeveL >= b.originLeveL) return false;
const isXFit1 = a[0] - b[0];
const isYFit1 = a[1] - b[1];
return !((isXFit1 == 0 && (isYFit1 == 0.5 || isYFit1 == -0.5)) ||
(isYFit1 == 0 && (isXFit1 == 0.5 || isXFit1 == -0.5)) ||
((isXFit1 == 0.5 || isXFit1 == -0.5) && (isYFit1 == 0.5 || isYFit1 == -0.5))
)
}
//去除重叠坐标
xyPosArray = xyPosArray.filter(item => isCovered(XY[0],item))
console.log(xyPosArray.length,i,'筛选完事');
newPosX = XY[0][0] ; // 取出横向的随机数
newPosY = XY[0][1] ; // 取出纵向的随机数
block.x = newPosX ;
block.y = newPosY ;
block.originX = newPosX;
block.originY = newPosY;= offSetX ;
block.offSetY= offSetY;;
block.originLeveL = offSetX
}
};
上面方法需要注意,坐标的数量是有限的,假设有一层能放18个块,那实际在生成的过程中做剔除掉可能遮挡的坐标的时候,最多一层 只能放 18/2的块,超过数量会找不到坐标。
7、上面事情做完后,还需要做一些初始化的时候
// 4. 初始化空插槽
const slotArea: BlockType[] = new Array(gameConfig.slotNum).fill(null);
// console.log("随机块情况", randomBlocks);
console.log(chessBoard, `当前棋盘状态`);
//转换实际屏幕坐标,
levelBlocks = levelBlocks.map(block =>{
block.x = (block.x - 2.5) * (widthUnit) // (原始坐标 - 偏移量) * 方块的宽
block.y = (block.y - 2.5) * (heightUnit) // (原始坐标 - 偏移量) * 方块的高
console.log(block.x,block.y,'xy !');
return block;
});
最后的 levelBlocks就是我们可以用在屏幕的xy值。
8、对于遮挡关系的计算 对于层于层之间的遮挡关系,在块的数据初始化里 ,设计 谁遮住了我(lowerThanBlocks),我遮住了谁(higherThanBlocks)两个数组存放对应id,计算a和b方块的坐标绝对值来判断遮挡关系,有了遮挡关系,我们就可以在界面里去做对应的展示。
//计算层级
function genLevelRelation(cardInfos:BlockType[]){
function findAndChangeBottom() {
//双层循环对比数据
cardInfos.forEach((aimCard) => {
cardInfos.forEach((item) =>{
//a 是否被b遮住了
if (isCovered(aimCard, item)) {
setCardBottom(aimCard,item);
//没有遮住
}else{
setCardNormal(aimCard,item);
}
});
});
}
// 判断A B方块绝对值
function isCovered(bottomCard:BlockType, topCard:BlockType) {
//a 和 b 同层不做判断
if (bottomCard.originLeveL >= topCard.originLeveL) return false;
const isXFit = Math.abs(topCard.x - bottomCard.x) < widthUnit;
const isYFit = Math.abs(topCard.y - bottomCard.y) < heightUnit;
return isXFit && isYFit;
}
function setCardBottom(b1:BlockType,b2:BlockType) {
// console.log('被覆盖了');
b1.lowerThanBlocks.push(b2);
b2.higherThanBlocks.push(b1)
}
function setCardNormal(b1:BlockType,b2:BlockType) {
// console.log('没有被覆盖了');
}
findAndChangeBottom()
}
总结
一共进行 初始化块数 - 初始化块的内容 、 计算块坐标 、 计算块遮挡 ,就能拿出对应的方块的数据了,有了方块数据,最后的也仅仅只是对全部块数组的某一个块操作了。
参考:
1、juejin.cn/post/714605… (# 主要参考了实现逻辑,上面部分代码逻辑代码来源于这位作者,然后根据需求改动)
2、bigflowerfat.gitee.io/count-count… (# 代码开源,感兴趣的可参考代码实现手动固定方块阵型,而不是随机)