TypeScript:鱼群算法(CocosCreator小游戏)

最近想学一下CocosCreator,于是,编辑器下载,启动。

众所周知,边写边学才是最快的学习方法,得写个Demo练练手,那么写什么呢?听说现在《墨虾探蝌》挺火的,那就抄(学习的事怎么能叫抄呢?)写一个类似的小游戏吧!

(在《墨虾探蝌》中,鱼的位置固定,到达一定数量后玩家会升级,不会出现一大群鱼的情况,本项目其实和它不同,没有升级进化,是会有一大群鱼的,每条鱼也不是固定位置,而是有自己的运动逻辑,其实和另一个游戏更像,不过我不知道叫什么。。。)

(初学CocosCreator和TypeScript,有写的不好的地方你TM来打我啊还请大家多多包涵。)

本文算法参考了大佬的文章:http://blog.sina.com.cn/s/blog_4a2183a60101avwt.html

效果展示:

 

正文开始

首先整一个玩家player:

TypeScript:鱼群算法(CocosCreator小游戏)_第1张图片

图片资源用的是CocosCreator官方Demo的图片,照着官方Demo学习了一下,懒得找鱼的图片就直接把图片拿来用了,这个项目目前只用了两张图片

有了player就得写个player控制脚本,点击一个方向,player就会一直向这个方向移动。那么我们首先需要获取玩家点击的位置,然后计算出player移动的方向,我们把这个写在GameManager里面,所以新建一个脚本GameManager,这个脚本挂在Canvas上。

先定义两个变量,玩家节点和方向向量:

    @property(cc.Node)

    player: cc.Node = null;

    dir: cc.Vec2 = cc.Vec2.ZERO;

获取方向的方法:

    getClickDir(event) {
        let pos: cc.Vec2 = event.getLocation();
        //转本地坐标
        let localPos = this.node.convertToNodeSpaceAR(pos);
        let playerPos: cc.Vec2 = new cc.Vec2(
            this.player.position.x,
            this.player.position.y
        );

        let len = localPos.sub(playerPos).mag();

        this.dir.x = localPos.sub(playerPos).x / len;
        this.dir.y = localPos.sub(playerPos).y / len;
    }

这方法在onMouseDown和onMouseMove时调用:

    onMouseDown(event) {
        if (event.getButton() == cc.Event.EventMouse.BUTTON_LEFT) {
            this.getClickDir(event);
        }
    }

    onMouseMove(event) {
        if (event.getButton() == cc.Event.EventMouse.BUTTON_LEFT) {
            this.getClickDir(event);
        }
    }

    onLoad() {
        cc.director.getCollisionManager().enabled = true;
        cc.director.getPhysicsManager().enabled = true;

        this.node.on(cc.Node.EventType.MOUSE_DOWN, this.onMouseDown, this);
        this.node.on(cc.Node.EventType.MOUSE_MOVE, this.onMouseMove, this);
    }

    onDestroy() {
        this.node.off(cc.Node.EventType.MOUSE_DOWN, this.onMouseDown, this);
        this.node.off(cc.Node.EventType.MOUSE_MOVE, this.onMouseMove, this);
    }

有了方向向量,就可以让玩家移动了,新建一个FishPlayer脚本。

为了不让玩家乱跑,我们先 build the wall:

TypeScript:鱼群算法(CocosCreator小游戏)_第2张图片

墙上加上物理碰撞体:

TypeScript:鱼群算法(CocosCreator小游戏)_第3张图片

然后就可以开始写FishPlayer脚本了,先把要用到的变量都定义一下:

    @property(cc.Node)
    camera: cc.Node = null;

    @property(cc.Node)
    gameManager: cc.Node = null;

    game: GameManager;
    speed: number = 170;
    velocity: cc.Vec3 = cc.Vec3.ZERO;

在onLoad()中给game赋值:

    onLoad() {
        this.game = this.gameManager.getComponent("GameManager");
    }

通过射线来检测边界,判断玩家是否能移动的方法:

    canMove() {
        var flag: boolean = true;
        //前方有障碍物
        var pos = this.node.convertToWorldSpaceAR(cc.Vec3.ZERO);
        var endPos = pos.add(this.node.up.mul(40));
        var hit: cc.PhysicsRayCastResult[] = cc.director
            .getPhysicsManager()
            .rayCast(
                new cc.Vec2(pos.x, pos.y),
                new cc.Vec2(endPos.x, endPos.y),
                cc.RayCastType.All
            );
        if (hit.length > 0) {
            flag = false;
        }
        return flag;
    }

在update中控制玩家移动:

    update(dt) {
        if (this.game.dir.mag() < 0.5) {
            this.velocity = cc.Vec3.ZERO;
            return;
        }

        let vx: number = this.game.dir.x * this.speed;
        let vy: number = this.game.dir.y * this.speed;

        this.velocity = new cc.Vec3(vx, vy);
        //移动
        if (this.canMove()) {
            this.node.x += vx * dt;
            this.node.y += vy * dt;
        }

        //相机跟随
        this.camera.setPosition(this.node.position);

        //向运动方向旋转
        let hudu = Math.atan2(this.game.dir.y, this.game.dir.x);
        let angle = hudu * (180 / Math.PI);
        angle = 360 - angle + 90;
        this.node.angle = -angle;
    }

玩家的移动逻辑写完了,接下来写鱼群。

新建一个FishGroupManager脚本和一个FishGroup脚本,FishGroupManager挂在Canvas上,FishGroup挂在player上。

FishGroupManager中定义一个静态fishGroups变量,用来管理所有Group(因为场景中可能有多个玩家,多个鱼群,现在只有一个玩家,这里方便之后扩展):

    static fishGroups: FishGroup[]; //所有组

来一个把group加入groups的静态方法:

    static AddGroup(group: FishGroup) {
        if (this.fishGroups == null) this.fishGroups = new Array();

        if (this.fishGroups.indexOf(group) == -1) this.fishGroups.push(group);
    }

再来一个获取group的静态方法(根据索引获取):

    static GetFishGroup(index: number) {
        for (var i = 0; i < this.fishGroups.length; i++)
            if (this.fishGroups[i].groupID == index) return this.fishGroups[i];
    }

FishGroupManager就写完了,接下来再写FishGroup,把上面用到的groupID定义一下,还有鱼群数组:

    groupID: number = 0; //组id    
    fishArr: cc.Component[] = new Array();

在onLoad中将自身加到fishGroups里面:

    onLoad() {
        FishGroupManager.AddGroup(this);
    }

现在鱼群有了,但是里面一条鱼都没有,所以我们还要一个抓鱼的方法:

    catchFish(fish) {
        this.fishArr.push(fish);
    }

再定义一些要用到的参数,FishGroup就写完了:

    keepMinDistance: number = 80;
    keepMaxDistance: number = 100;
    keepWeight: number = 1; //成员保持距离和保持距离权重
    moveWeight: number = 0.8; //和成员移动权重

接下来就到了重头戏了——鱼群中其他小鱼的运动逻辑。

直接将player复制一下,把挂载的FishPlayer和FishGroup脚本去掉,命名为fish,这就是我们的小鱼了,把它做成预制。然后新建一个FishBehaviour脚本,这个脚本挂在player和普通小鱼身上。

首先实现“抓鱼”功能,当player靠近小鱼后,小鱼就被捕获,成为该player鱼群中的一员。

定义相关变量:

    @property(cc.Node)
    gameManager: cc.Node = null;
    game: GameManager;
    isPicked: boolean = false;
    pickRadius: number = 50; //抓取距离

    groupId: number = -1; //组 id
    myGroup: FishGroup;

同样,在onLoad()中给game赋值:

    onLoad() {
        this.game = this.gameManager.getComponent(GameManager);
    }

判断和player距离的方法:

    getPlayerDistance() {
        let dist = this.node.position.sub(this.game.player.position).mag();
        return dist;
    }

加入鱼群方法:

    onPicked() {
        //设置group
        this.groupId = this.game.player.getComponent(FishGroup).groupID;
        this.myGroup = FishGroupManager.GetFishGroup(this.groupId);

        if (this.myGroup != null) {
            this.myGroup.catchFish(this);
            this.isPicked = true;
        }
    }

在update中调用:

    update(dt) {
        if (this.isPicked) {
            //随着鱼群移动
        }
        else {
            if (this.getPlayerDistance() < this.pickRadius) {
                this.onPicked();
            }
        }
    }

OK,现在小鱼到鱼群中了,怎么随着鱼群一起移动呢?

这里主要有两个点:

1.小鱼会随着周围“邻居鱼”一起移动

2.小鱼之间要保持距离,不能太过拥挤

所以我们需要计算小鱼周围一定范围内鱼群运动向量的平均值,这样还不够,还要判断是否“拥挤”,“拥挤”的话就增加一个远离的趋势,太远的话就增加一个靠近的趋势,再分别乘以权重,加起来,就可以得到我们要的向量了,代码如下:

 

定义变量:

    moveSpeed: number = 170;
    rotateSpeed: number = 40; //移动旋转速度
    neighborRadius: number = 500; //距离小于500算是邻居

    speed: number = 0;
    currentSpeed: number = 0;
    myMovement: cc.Vec3 = cc.Vec3.ZERO;

求平均向量:

GetGroupMovement() {
        var v1: cc.Vec3 = cc.Vec3.ZERO;
        var v2: cc.Vec3 = cc.Vec3.ZERO;

        for (var i = 0; i < this.myGroup.fishArr.length; i++) {
            var otherFish: FishBehaviour = this.myGroup.fishArr[i].getComponent(
                FishBehaviour
            );

            var dis = this.node.position.sub(otherFish.node.position); //距离

            //不是邻居
            if (dis.mag() > this.neighborRadius) {
                continue;
            }

            var v: cc.Vec3 = cc.Vec3.ZERO;
            //大于最大间隔,靠近
            if (dis.mag() > this.myGroup.keepMaxDistance) {
                v = dis.normalize().mul(1 - dis.mag() / this.myGroup.keepMaxDistance);
            }
            //小于最小间隔,远离
            else if (dis.mag() < this.myGroup.keepMinDistance) {
                v = dis.normalize().mul(1 - dis.mag() / this.myGroup.keepMinDistance);
            } else {
                continue;
            }

            v1 = v1.add(v); //与周围单位的距离
            v2 = v2.add(otherFish.myMovement); //周围单位移动方向
        }

        //添加权重因素
        v1 = v1.normalize().mul(this.myGroup.keepWeight);
        v2 = v2.normalize().mul(this.myGroup.moveWeight);
        var ret = v1.add(v2);
        return ret;
    }

现在,可以把update补全了:

update(dt) {
        //随着鱼群移动
        if (this.isPicked) {
            var direction = cc.Vec3.ZERO;
            if (this.node.name != "player") {
                direction = direction.add(this.GetGroupMovement());
            }

            this.speed = cc.misc.lerp(this.speed, this.moveSpeed, 2 * dt);
            this.Drive(direction, this.speed, dt); //移动
        }
        //捕获
        else {
            if (this.getPlayerDistance() < this.pickRadius) {
                this.onPicked();
            }
        }
    }

Drive()方法:

    Drive(direction: cc.Vec3, spd: number, dt) {
        var finialDirection: cc.Vec3 = direction.normalize();
        var finialSpeed: number = spd;
        var finialRotate: number = 0;
        var rotateDir: number = cc.Vec3.dot(finialDirection, this.node.right);
        var forwardDir: number = cc.Vec3.dot(finialDirection, this.node.up);

        if (forwardDir < 0) {
            rotateDir = Math.sign(rotateDir);
        }

        //防抖
        if (forwardDir < 0.98) {
            finialRotate = cc.misc.clampf(
                rotateDir * 180,
                -this.rotateSpeed,
                this.rotateSpeed
            );
        }

        finialSpeed *= cc.misc.clamp01(direction.mag());
        finialSpeed *= cc.misc.clamp01(1 - Math.abs(rotateDir) * 0.8);
        if (Math.abs(finialSpeed) < 0.01) {
            finialSpeed = 0;
        }

        //移动
        if (this.canMove()) {
            this.node.x += this.node.up.x * finialSpeed * dt;
            this.node.y += this.node.up.y * finialSpeed * dt;
        }

        //旋转
        var angle1 = finialRotate * 8 * dt;
        var angle2 = this.node.angle - angle1;
        this.node.angle = angle2 % 360;

        this.currentSpeed = finialSpeed;
        this.myMovement = direction.mul(finialSpeed);
    }

    canMove() {
        var flag: boolean = true;
        //前方有障碍物
        var pos = this.node.convertToWorldSpaceAR(cc.Vec3.ZERO);
        var endPos = pos.add(this.node.up.mul(40));
        var hit: cc.PhysicsRayCastResult[] = cc.director
            .getPhysicsManager()
            .rayCast(
                new cc.Vec2(pos.x, pos.y),
                new cc.Vec2(endPos.x, endPos.y),
                cc.RayCastType.All
            );
        if (hit.length > 0) {
            flag = false;
        }
        return flag;
    }

最后这个Drive()方法我是拿大佬的原方法改的,里面那个“防抖”我也没弄懂啥意思,大家一起研究研究。

好了,本篇文章就到这里。

你可能感兴趣的:(CocosCreator)