公司要做一个水管连接的游戏,花了一天写了个demo一起分享一下
先看结果
用的都是纯色图片,绿色代表水流过的格子
玩法介绍:上方四个管道是将要用到的管道,只有最右方的当前管道可以拖动,,有丢弃和回退一步功能(暂时只能回退到一步,不能连续回退),当所有格子满了以后,判断是否连接到水管,出水口在第一个格子左侧(这里暂时没有图,所以没有展示),显示水流动画(水流动画 也是利用sprite的fillstart 和 fillrange做出的效果),结果根据有几个水管中有水给出相应的奖励,
本文只介绍主要逻辑,感兴趣的我会把demo放到最后方便大家进入项目中查看。
思路
管道玩法一般的话有两种思路,
第一种思路:大多数都是会做一个4x4的双数组类型,将其type保存下来进行计算,即
[1,2,4,5]
[1,2,4,5]
[1,2,4,5]
[1,2,4,5]
通过数组中的类型去计算,这种方法在消消乐游戏中比较常见
第二种思路不用去做双数组保存type,去通过第一个格子的出水口方向,依次遍历出水口下一个格子里的管道类型,然后遇到可以继续出水的继续遍历,如果遇到不通的或者超出区域的return即可
本文采取的是第二种思路
第二种思路详解:
先定义水管方向 上下左右 四个方向,然后每个水管有哪几个口可以出水或者进水,这是所有管道的形状
eg:第一个类型的管道就只有 左右两个方向可以流水,如果水从左侧流入,那么水会从右侧流出,流入下一个管道,
此处需要注意的是,如果水从右侧流出,那么对于下一个管道来说流入方向为上一个管道流出的反方向,即为下个管道水的流入方向为左侧。
eg:第三个类型的管道,那么有上下左右四个方向都可以流入,比如水从上方流入,那么水就会从左、下、右流出,然后去遍历左、下、右三个方向接下来的管道,直至碰到墙壁或者结束
需要注意的是有些管道可能会同时流入水,造成死循环,所以需要一个变量去检测是否该管道已经检测过,如果检测过的话,就return
下面将 方向和所有管道类型 的出入水口 设置成对应的枚举
export enum direction {
up = 1,
down = 2,
left = 3,
right = 4,
}
export let directionByType = {
1: [direction.left, direction.right],
2: [direction.up, direction.down],
3: [direction.up, direction.down, direction.left, direction.right],
4: [direction.down, direction.left],
5: [direction.up, direction.right],
6: [direction.down, direction.right],
7: [direction.up, direction.left],
8: [direction.up, direction.down, direction.right],
9: [direction.up, direction.left, direction.right],
10: [direction.down, direction.left, direction.right],
11: [direction.up, direction.down, direction.left],
}
项目资源
整个项目 用到的资源 就是这11种管道形状,以及cocos自带的纯色图片资源作为背景
项目结构
整个游戏也就这一个界面上的预制体,以及一个需要生成的grid(格子),pipeitem(管道)两个小的预制体;window便是中间那个纯白色的正方形,上面有layout组件用于当作生成pipeitem的父节点,nextpipe_1、2、3、4是上方四个将要生成的管道的父节点
icon是和pipeitem所需要的图片是一模一样的,是需要后期进行的流水动画所需要的
pipeitem就是一个纯色的背景,用来放入拖动的管道
代码
UIJobPipe上面挂的脚本 命名同样为UIJobPipe
//定义两个预制体,一个是 pipeGrid,一个是pipeItem
@property(cc.Prefab)
pipeItem: cc.Prefab = null;
@property(cc.Prefab)
pipeGrid: cc.Prefab = null;
@property([cc.Node])
nextPipeArr: cc.Node[] = [];//上方的四个将要展现出的接下来要用到的物体的父节点数组
在start中调用创建这两个预制体的方法
//4x4的格子,所以双层for循环,i,j对应赋值给PipeGrid中的横竖坐标,用于计算水流出的位置
createGrid() {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
let grid = cc.instantiate(this.pipeGrid);
grid.getComponent(PipeGrid).setData(i, j, this);
grid.setParent(this.window);
}
}
}
//随机生成四种类型的水管
createNextPipe() {
for (let i = 0; i < this.nextPipeArr.length; i++) {
let type = GameUtils.GetRandomNumber(1, 11); //1-11代表水管类型,水管资源
let pipeItem = cc.instantiate(this.pipeItem);
pipeItem.getComponent(PipeItem).setData(type, this);
pipeItem.setParent(this.nextPipeArr[i]);
if (i == 0) {
//只能是第一个最右方的位置才可以拖动,其他地方的水管不能被拖动
pipeItem.getComponent(PipeItem).isMove = true;
}
}
}
生成完物体以后
pipeGrid上的代码
先定义几种类型,direction 方向(上下左右),判断水的流入流出方向
directionByType ,通过pipe的类型,得到此类型有哪几个方向可以流水
EnterDirectionByOutDirection,当确定入水口以后,剔除入水口的方向,剩余的方向为出水口,
注意,当知道出水口方向时,遍历向下一个管子的入水口方向应与当前出水口方向相反
export enum direction {
up = 1,
down = 2,
left = 3,
right = 4,
}
export let directionByType = {
1: [direction.left, direction.right],
2: [direction.up, direction.down],
3: [direction.up, direction.down, direction.left, direction.right],
4: [direction.down, direction.left],
5: [direction.up, direction.right],
6: [direction.down, direction.right],
7: [direction.up, direction.left],
8: [direction.up, direction.down, direction.right],
9: [direction.up, direction.left, direction.right],
10: [direction.down, direction.left, direction.right],
11: [direction.up, direction.down, direction.left],
}
//通过已有的出水口 转化为 下个水管的入水口,即为相反方向
export let EnterDirectionByOutDirection = {
1: direction.down,
2: direction.up,
3: direction.right,
4: direction.left,
}
setData(row: number, col: number, uiJobPipe: UIJobPipe) {
this.row = row;
this.col = col;
this.uiJobPipe = uiJobPipe;
}
pipegrid中的主要逻辑,通过当前的type,确定入水口方向和出水口方向,去遍历看是否可以流入水
connectAni(enterDirection: number, time: number = 0) {
//time为水流时间,为了使水流看起来时一点一点流入,需要延时水流动画
if (time < this.time) {
//因为下面会++,此处相应减1
this.uiJobPipe.connectNum--;
this.unscheduleAllCallbacks();
this.time = time;
} else {
if (this.isCheck) {
return;
}
}
if (this.pipeItemTs) {
let arr: Array<number> = directionByType[this.pipeItemTs.type];
arr = arr.concat();
let index = arr.indexOf(enterDirection);
if (index != -1) {
this.scheduleOnce(() => {
this.uiJobPipe.connectNum++;
this.pipeItemTs.connectAni(enterDirection);
}, time)//0.2为 水管中水流动画的时长,没检测一次增加0.2
time += 0.2;
this.isCheck = true;
arr.splice(index, 1);
let nextRow = 0;
let nextCol = 0;
for (let i = 0; i < arr.length; i++) {
switch (arr[i]) {
case direction.up:
nextRow = this.row - 1;
nextCol = this.col;
break;
case direction.down:
nextRow = this.row + 1;
nextCol = this.col;
break;
case direction.left:
nextRow = this.row;
nextCol = this.col - 1;
break;
case direction.right:
nextRow = this.row;
nextCol = this.col + 1;
break;
default:
break;
}
//检查是否在数组中,超出3的即为数组之外的 不做处理
//将 row,col转化为 grid的parent相对应的子物体索引
if (nextRow >= 0 && nextRow <= 3 && nextCol >= 0 && nextCol <= 3) {
const index = this.getIndexByRow_Col(nextRow, nextCol);
let out = EnterDirectionByOutDirection[arr[i]]
this.uiJobPipe.window.children[index].getComponent(PipeGrid).connectAni(out, time);
}
}
}
}
}
getIndexByRow_Col(row: number, col: number): number {
const maxGrid = 4;//4x4的格子;
return row * maxGrid + col;
}
pipeItem上的代码
@property(cc.Sprite)
icon: cc.Sprite = null;
isMove: boolean = false; //是否可以被拖动,只有在最右方的位置才可以被拖动
type: number = 0; //pipe类型 1-11
uiJobPipe: UIJobPipe = null;//主预制体中挂载的脚本
startPos: cc.Vec2 = null;//初始位置,如果没有放在格子中,或者格子中已经摆放过pipe,则回到初始位置
lastParent: cc.Node = null;//上一个父节点,以为有层级的问题,所以需要每次拖动时,将此节点层级放在上层,等放入格子中时,在切换父节点
isConnect: boolean = false;//如果接通以后,ani方法会return,防止重复调用造成死循环
start() {
//注册点击事件
this.node.on(cc.Node.EventType.TOUCH_START, this.clickStart, this);
this.node.on(cc.Node.EventType.TOUCH_MOVE, this.clickMove, this);
this.node.on(cc.Node.EventType.TOUCH_END, this.clickEnd, this);
}
setData(type: number, uiJobPipe: UIJobPipe) {
this.type = type;
this.uiJobPipe = uiJobPipe;
let path = "texture/jobfair/pipe/" + this.type;
//根据type,得到path,动态生成图片
ResourceManager.getInstance().setSpriteWithPath(game5BundleName.game5_1, path, this.node.getComponent(cc.Sprite));
ResourceManager.getInstance().setSpriteWithPath(game5BundleName.game5_1, path, this.icon);
}
clickStart(event: cc.Touch) {
if (!this.isMove) {
return;
}
this.lastParent = this.node.parent;
//点击开始时,将此节点放在ui层级上,位于高层级
this.node.setParent(this.uiJobPipe.node.parent);
//此方法是得到节点在另一个节点坐标系下的位置
let pos = GameUtils.nodeConvertToNodeSpaceAR(this.node, this.uiJobPipe.node.parent);
this.node.setPosition(pos);
}
clickMove(event: cc.Touch) {
if (!this.isMove) {
return;
}
// console.log("event---", event);
// let startPos = event.target.getUIStartLocation();
let deltaPos = event.getDelta();
this.node.setPosition(this.node.x + deltaPos.x, this.node.y + deltaPos.y);
}
clickEnd(event: cc.Touch) {
if (!this.isMove) {
return;
}
this.uiJobPipe.lastItem = this.node;
let parent = this.checkIsGrid();
let pos = GameUtils.nodeConvertToNodeSpaceAR(this.node, parent);
pos = GameUtils.nodeConvertToNodeSpaceAR(parent, parent.parent);
cc.tween(this.node).to(0.3, { position: cc.v3(pos) }).call(() => {
this.node.setParent(parent);
this.node.setPosition(0, 0);
if (!this.isMove) {
let grid = parent.getComponent(PipeGrid)
grid.pipeItemTs = this;
this.uiJobPipe.refreshItemState();
}
}).start();
}
//此方法用来判断水流向,水从左向右流,还是从上向下流
//主要原理就是利用了sprite组件中的fill类行,调整fillType、fillStart、fillRange的值进行效果显示
connectAni(enterDirection: number) {
this.uiJobPipe.nowConnect++;
console.log("nowConnect---", this.uiJobPipe.nowConnect);
if (this.uiJobPipe.connectNum == this.uiJobPipe.nowConnect) {
this.uiJobPipe.showSettle();
}
if (enterDirection == direction.up) {
this.icon.fillType = cc.Sprite.FillType.VERTICAL;
this.icon.fillStart = 1;
this.icon.fillRange = 0;
cc.tween(this.icon).to(0.2, { fillRange: -1 }).start();
} else if (enterDirection == direction.down) {
this.icon.fillType = cc.Sprite.FillType.VERTICAL;
this.icon.fillStart = 0;
this.icon.fillRange = 0;
cc.tween(this.icon).to(0.2, { fillRange: 1 }).start();
} else if (enterDirection == direction.left) {
this.icon.fillType = cc.Sprite.FillType.HORIZONTAL;
this.icon.fillStart = 0;
this.icon.fillRange = 0;
cc.tween(this.icon).to(0.2, { fillRange: 1 }).start();
} else if (enterDirection == direction.right) {
this.icon.fillType = cc.Sprite.FillType.HORIZONTAL;
this.icon.fillStart = 1;
this.icon.fillRange = 0;
cc.tween(this.icon).to(0.2, { fillRange: -1 }).start();
}
}
//触摸结束检查是否在格子附近
checkIsGrid(): cc.Node {
let gridParent = this.uiJobPipe.window;
let minLength = 1000;
let min_i = 0;//距离最近的child值
for (let i = 0; i < gridParent.children.length; i++) {
let pos = GameUtils.nodeConvertToNodeSpaceAR(this.node, gridParent);
let distance = pos.subtract(gridParent.children[i].getPosition()).len();
if (distance < minLength) {
minLength = distance;
min_i = i;
}
}
//距离足够近 并且没有被放入管道
if (minLength <= 75 && gridParent.children[min_i].children.length == 0) {
this.isMove = false;
return gridParent.children[min_i];
} else {
return this.lastParent;
}
}
最后剩下的就是 回退操作 和丢弃操作
//丢弃第一个位置的pipe
clickAway() {
cc.tween(this.nextPipeArr[0].children[0]).to(0.3, { position: cc.v3(600, 600) }).call(() => {
this.nextPipeArr[0].children[0].destroy();
this.refreshItemState(true);
}).start();
}
//回退到上一步操作
clickLast() {
if (!this.lastItem) {
TipsLayer.showTips("请先进行操作才可以回退!");
return;
}
this.pipeNum--;
console.log("this.pipeNum--", this.pipeNum);
this.nextPipeArr[3].children[0].destroy();
for (let i = 0; i < this.nextPipeArr.length - 1; i++) {
let item = this.nextPipeArr[i].children[0];
item.setParent(this.nextPipeArr[i + 1]);
cc.tween(item).to(0.3, { position: cc.v3(0, 0) }).start();
item.getComponent(PipeItem).isMove = false;
}
this.lastItem.setParent(this.nextPipeArr[0]);
this.lastItem.getComponent(PipeItem).isMove = true;
cc.tween(this.lastItem).to(0.3, { position: cc.v3(0, 0) }).call(() => {
this.lastItem = null;
}).start();
}
//当拖动完成一次后刷新状态 以及生成新的item
refreshItemState(isAway: boolean = false) {
//是否是丢弃操作,丢弃时 不增加pipeNum
if (!isAway) {
this.pipeNum++;
}
console.log("this.pipeNum--", this.pipeNum);
if (this.pipeNum == 16) {
console.log("游戏结束!!!!");
this.checkPipeToConnected();
}
for (let i = 0; i < this.nextPipeArr.length; i++) {
let item = this.nextPipeArr[i].children[0];
if (item) {
item.setParent(this.nextPipeArr[i - 1]);
cc.tween(item).to(0.3, { position: cc.v3(0, 0) }).start();
if (i == 1) {
item.getComponent(PipeItem).isMove = true;
}
}
}
let nextType = GameUtils.GetRandomNumber(1, 11);
let pipeItem = cc.instantiate(this.pipeItem);
pipeItem.getComponent(PipeItem).setData(nextType, this);
pipeItem.setParent(this.nextPipeArr[3]);
}
//当所有格子都放上了管道后,开始从第一个格子检测水流入了哪些管道
checkPipeToConnected() {
this.window.children[0].getComponent(PipeGrid).connectAni(direction.left);
}
showSettle(){
this.settle.active = true;
this.settle.getChildByName("result").getComponent(cc.Label).string = `完成游戏,有${this.connectNum}个水管连通!`;
}
到这里功能基本实现完成
代码基本上就这么多了,如果看不明白的我把项目资源放在评论区。
好了,到这就say goodbye了,觉得有用的可以点个赞哦!