水管连接游戏(cocos2.4.3)

公司要做一个水管连接的游戏,花了一天写了个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了,觉得有用的可以点个赞哦!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值