详解CocosCreator游戏之鱼群算法

前言

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

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

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

效果展示:

正文

首先整一个玩家player:

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

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

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

@property(cc.Node)
player: cc.Node = null;
ir: 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:

墙上加上物理碰撞体:

然后就可以开始写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<cc.Component>();

在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;
}

以上就是详解CocosCreator游戏之鱼群算法的详细内容,更多关于CocosCreator鱼群算法的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解CocosCreator优化之DrawCall

    前言 在游戏开发中,DrawCall 作为一个非常重要的性能指标,直接影响游戏的整体性能表现. 无论是 Cocos Creator.Unity.Unreal 还是其他游戏引擎,只要说到游戏性能优化,DrawCall 都是绝对少不了的一项. 本文将会介绍什么是 DrawCall,为什么要减少 DrawCall 以及在 Cocos Creator 项目中如何减少 DrawCall 来提升游戏性能. 什么是 DrawCall DrawCall就是CPU调用图形库(比如DirectX或OpenGL)的图

  • CocosCreator入门教程之网络通信

    网络通信概况 开发一个网络游戏,难免要处理网络通信.有几点问题需要注意: 1.服务端为了快速开发可能前期使用http通信,后面再改成websocket/socket. 2.同时存在http和websocket/socket通信 3.通信数据格式可能需要随时替换为json或protocol buffer,可能需要添加额外头部. 4.在新手引导里使用本地数据做模拟,请求不需要发送给后端. 5.重连后数据简单同步 具体实施 针对第一.二个问题,我在设计通信模块时会考虑将通信尽量抽象为统一的对外接口,在

  • 详解CocosCreator消息分发机制

    概述 本篇开始介绍游戏业务架构相关的内容.在游戏业务层,所有需要隔离的系统和模块间通信都可以通过消息分发解耦.例如网络返回通知.数据更新同步到界面等. 消息分发基于观察者模式设计.需要处理消息的地方向消息中心注册监听回调,派发消息时,调用消息中心的派发接口遍历该消息的监听队列,调用对应的回调方法. 具体方案 先定义监听回调类型 /** * 消息监听回调方法 */ export type NotifyListener = (src: any, data: any) => void; 通过key-v

  • 详解CocosCreator MVC架构

    概述 这一篇将介绍在游戏客户端常用的架构MVC架构.一个游戏的MVC如下划分: M:1)单例全局的数据中心World,所有游戏模块的数据在World中有入口,2)各个模块自己的数据结构. V:1)通过creator预制体制作的UI界面.场景,2)各个界面显示逻辑的ViewCtrl C:1)全局的MainCtrl,2)各个模块的业务逻辑类ModuleCtrl 具体模块 先介绍M部分.由于一个模块的数据,在其他模块也有访问的需求,例如好友模块,在聊天的时候也需要访问,在排行榜里需要访问.数据应该有一

  • 详解CocosCreator系统事件是怎么产生及触发的

    环境 Cocos Creator 2.4 Chrome 88 概要 模块作用 事件监听机制应该是所有游戏都必不可少的内容.不管是按钮的点击还是物体的拖动,都少不了事件的监听与分发. 主要的功能还是通过节点的on/once函数,对系统事件(如触摸.点击)进行监听,随后触发对应的游戏逻辑.同时,也支持用户发射/监听自定义的事件,这方面可以看一下官方文档监听和发射事件. 涉及文件 其中,CCGame和CCInputManager都有涉及注册事件,但他们负责的是不同的部分. 源码解析 事件是怎么(从浏览

  • 如何在CocosCreator里画个炫酷的雷达图

    前言 雷达图(Radar Chart) 也称为网络图.星图或蜘蛛网图. 是以从同一点开始的轴上表示的三个或更多个定量变量的二维图表的形式显示多元数据的图形方法. 适用于显示三个或更多的维度的变量. 雷达图常用于

  • 如何在CocosCreator中做一个List

    CocosCreator版本:2.3.4 cocos没有List组件,所以要自己写.从cocos的example项目中找到assets/case/02_ui/05_listView的demo来改造. 自写一个虚拟列表,有垂直布局,水平布局,网格布局和Padding的List Demo地址:https://files-cdn.cnblogs.com/files/gamedaybyday/cocos2.3.4_ListViewDemo_Grid.7z cocos原来的LayOut做列表,有100个数

  • 详解CocosCreator华容道数字拼盘

    前言 华容道是啥玩意? 这种数字拼图游戏大家都玩过吧,他就是典型的华容道之一. 华容道是古老的中国民间益智游戏,以其变化多端.百玩不厌的特点与魔方.独立钻石棋一起被国外智力专家并称为"智力游戏界的三个不可思议". 今天咱们就来了解一下这个华容道. 正文 今天咱们主要以3*3的布局来进行,菜鸟用cocos creator 写了一个简单的demo,下面咱们逐步说一下 1.面板 首先咱们随机生成一个面板排列 2.华容道求解 思路: 穷举法:大家都知道这种游戏的玩法,滑动其中可以滑动的方格,将

  • 如何用CocosCreator制作微信小游戏

    CocosCreator在1.8版本开始,就支持一键发布微信小程序,下面是详细的发布步骤: 1.在微信公众平台下载微信开发者工具  地址: https://mp.weixin.qq.com/debug/wxagame/dev/devtools/download.html?t=2018115,根据需要选择相应的版本. 2.cocoscreator设置 选择Cocos Creator --> 偏好设置 --> 原生开发环境,配置WechatGame程序路径,点击保存并关闭. 3.配置构建发布 下面

  • 详解CocosCreator游戏之鱼群算法

    前言 最近想学一下CocosCreator,于是,编辑器下载,启动. 众所周知,边写边学才是最快的学习方法,得写个Demo练练手,那么写什么呢?听说现在<墨虾探蝌>挺火的,那就抄(学习的事怎么能叫抄呢?)写一个类似的小游戏吧! (在<墨虾探蝌>中,鱼的位置固定,到达一定数量后玩家会升级,不会出现一大群鱼的情况,本项目其实和它不同,没有升级进化,是会有一大群鱼的,每条鱼也不是固定位置,而是有自己的运动逻辑,其实和另一个游戏更像,不过我不知道叫什么...) 效果展示: 正文 首先整一个

  • 详解CocosCreator制作射击游戏

    场景布置 游戏资源 炮塔旋转 机制与之前手柄实例的小车相同,使用touchmove监听触摸事件, 获取触摸位置 通过位置用signAngle方法将该位置与cc.v2(1,0)位置的角度差求出(记得要加负号,比较所得逆时针为负,赋值angle逆指针为正). 所求的的角度即为最终角度. onLoad(){ //初始化为90度 this.node.angle=90; this.node.on('touchstart',this.onTouchStart,this); this.node.on('tou

  • 详解CocosCreator项目结构机制

     一.项目文件夹结构 初次创建并打开一个 Cocos Creator 项目后,开发者的项目文件夹将会包括以下结构: 下面我们将会介绍每个文件夹的功能. 1.资源文件夹(assets) assets 将会用来放置游戏中所有的本地资源.脚本和第三方库文件.只有在 assets 目录下的内容才能显示在 资源管理器 中.assets 中的每个文件在导入项目后都会生成一个相同名字的 .meta 文件,用于存储对应的资源配置和索引信息..meta 文件需要一并提交到版本控制系统,详见 资源管理注意事项 --

  • 详解CocosCreator中几种计时器的使用方法

    一.setTimeOut 3秒后打印abc.只执行一次. setTimeout(()=>{console.log("abc"); }, 3000); 删除计时器,3秒后不会输出abc. let timeIndex; timeIndex = setTimeout(()=>{console.log("abc"); }, 3000); clearTimeout(timeIndex); setTimeout这样写,test函数中输出的this是Window对象

  • 详解C++实现拓扑排序算法

    一.拓扑排序的介绍 拓扑排序对应施工的流程图具有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行.为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行.通常,我们把这种顶点表示活动.边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简

随机推荐