[ECS框架系列]-与四叉树的一次结合(2)

317 阅读7分钟

前言

大家好,我IT侠又双叒叕来了,上一篇文章说了下ECS框架在H5下是如何使用的。本篇文章就来聊聊主角-四叉树。

还是在开头说一下:

  • 不了解四叉树的可以自行查阅一下相关资料。因为篇幅问题,IT侠会下方简单的介绍下。

  • 因为demo是基于ECS框架,所以如果不了解ECS框架的话,可以看看我的ECS框架系列文章,获取我已经开源的ECS框架。或者扫文末的二维码联系我,给我“加个鸡腿”支持一下!19.9 交个朋友。

正文

最终效果

先给大家看看demo运行起来的效果,看看四叉树在对象很多的情况下的威力

  • 每一个"田字"矩形是一个4叉树节点,方便调试的使用
  • 高亮黄色的就是需要检查是否与主角发生碰撞的块
  • 高亮紫色的就是当前和主角发生碰撞的块

四叉树应用

生成了超级多块后四叉树的表现

四叉树牛逼

最后是关闭了调试线后的效果

关闭了调试线

四叉树

四叉树(quad-tree)是一种数据结构,是一种每个节点最多有四个子树的数据结构。四叉树常应用于二维空间数据的分析与分类。所以把四叉树应用在二维的有效率之碰撞侦测上是非常适合的。

代码实现

源码里的方法我就不一一列举出来了,因为四叉树的实现不是本文的重点,只介绍下四叉树构造函数,还有包含哪些方法,以及方法的简单解释。github上开源很多四叉树的实现,如果想深入了解可以自行查阅学习。

    constructor(bounds: IBounds, maxObjects?: number, maxLevels?: number, level?: number,location?:number,parent?:QuadTree) {
        // 树节点的边界(包围盒)
        this.bounds = bounds; 
        // 节点上最多存放物体的数量,为了测试默认设置为3,具体设定开发者可以自行决定
        this.maxObjects = maxObjects || 3; 
        // 4叉树的最大层级
        this.maxLevels = maxLevels || 4; 
        // 当前节点在树中的层级
        this.level = level || 0;
        // 当前节点上所有对象
        this.objs = []; 
        //4个象限(节点),如果该节点上的对象超过设定的最大值后就进行分裂
        this.nodes = []; 
        //标记一下当前节点是那个象限
        this.location = location;
        // 当前节点的父节点,方便后续删除子树
        this.parent = parent || null;
    }

四叉树类有以下成员方法组成:

  • **「getIndex」** 返回当前对象在哪些象限上
  • **「insert」** 将一个对象加入到当前节点上
  • **「clear」** 清空树上所有节点和节点上的对象
  • **「recovery」** 如果一个子节点上没有任何对象,那么进行一次初始化
  • **「split」**分裂子树节点,生成4个叶子节点(象限)
  • **「retrieve」** 获取树上可能与指定对象发生碰撞的对象
  • **「getBounds」** 获取所有节点的包围盒,方便调试
  • **「toJSON」** 转成json格式,方便调试输出树信息

ECS中使用四叉树

当有Canvas组件的实体产生时候,捕获该实体,创建一个四叉树,根节点包围盒就用canvas节点的位置和宽高。然后将四叉树对象保存到ecs对象上方便其他系统访问。

     _canvas.on("Canvas", function (e) {
            //获取Canvas组件上的canvas属性
            //该属性 保存的就是canvas对象
            let canvas = e.Canvas.canvas;
            // 创建一个四叉树
            toy.qt = new toy.QuadTree(
                {
                    x: canvas.width / 2,
                    y: canvas.height / 2,
                    width: canvas.width,
                    height: canvas.height
                });
        });

接下来写一个系统"关心一下"带有 "Mob","Appearance","Props" 这3个组件的实体的产生。因为有这3个组件的实体我们认定是一个怪物,怪物会和主角发生碰撞,所以需要放进四叉树中等待提取。

调用四叉树实例上的 「insert」 方法把怪物的包围盒和怪物实体一起打包到一个对象上保存进四叉树上的节点中。

     _character.on(["Mob","Appearance","Props"],function(ent){
              //调用四叉树实例上的 insert 方法
               toy.qt.insert({
                   _node:ent,
                   width:ent.Props.props.width,
                   height:ent.Props.props.height,
                   x:ent.Props.props.x,
                   y:ent.Props.props.y
               });

        });

现在怪物都被放进了四叉树上,接着我们需要写一个用户输入系统,监听玩家控制主角移动等事件。这里用的是 **「hammer.js」**库来实现。

定义一个用户输入系统,该系统关心"LocalControl","Props","Node","Character" 组件。有这4个组件的实体就是我们的主角实体。之后分别监听 「panstart」(开始触摸屏幕) ,「panmove」(移动手指), 「panend」 (手抬起) 这3个事件。

            let _userInput = toy.system("userInputSystem",101);

            _userInput.on(["LocalControl","Props","Character","Node"],function(ent){
                let props = ent.Props.props;

                let canvasEnt = toy.getFirstEnt("Canvas");
                if(!canvasEnt){
                    return;
                }

                let mc = new Hammer.Manager(canvasEnt.Canvas.canvas)
                // pan的距离判断为0 手指不限定
                mc.add(new Hammer.Pan({threshold:0,pointers:0}))

                mc.on("panstart",function(ev){
                    props.isTouch = true;
                    let rect = canvasEnt.Canvas.canvas.getBoundingClientRect();

                    props.x = props.lastX = ev.center.x - rect.left;
                    props.y = props.lastY = ev.center.y - rect.top;

                    toy.__output.innerHTML = JSON.stringify(props, null, 4);

                });

                mc.on("panmove", function onPanMove(ev) {
                    toy.comOnce("RecoverColor");
                    props.lastX = props.lastX;
                    props.lastY = props.lastY;

                    props.x = props.lastX + ev.deltaX;
                    props.y = props.lastY + ev.deltaY;

                    ent.Node.node.x =  props.x;
                    ent.Node.node.y =  props.y;

                    props.isTouch = true;
                    toy.__output.innerHTML = JSON.stringify(props, null, 4);           
                    //尝试获取带有"HighLight"组件的实体
                    let e = toy.getFirstEnt("HighLight");
                    // 保证世界上只有一个实体带有"HighLight"
                    if(!e){
                        //创建一个带有"HighLight"组件的实体。
                        //之后湖北关心该组件的系统捕获
                        toy.com("HighLight")
                    }

                });

                mc.on("panend", function onPanEnd(ev){
                    props.isTouch = false;

                    props.lastX = props.x;
                    props.lastY = props.y;

                    toy.__output.innerHTML = JSON.stringify(props, null, 4);          
                    let e = toy.getFirstEnt("HighLight");
                    //这边主角停止移动 我就删除了带有"HighLight"组件的实体.
                    //其实也可以不删除,我这边删除该实体那么在主角停止移动的时候负责高亮的系统就不会工作了。
                    e && toy.removeCom(e,"HighLight")
                });

            });  

可以看到IT侠在"panmvoe"事件回调中创建了一个"HighLight"组件的实体,目的就是为了告知(world)引擎我需要高亮,如果有关心"HighLight"的系统存在的话,那么该系统就会工作。那么接下来就是定义一个关心该组件的系统。

      // 改系统是一个update方法 每一帧都会执行
      _sketch.onUpdate("HighLight",function(dt,es){
            //如果没有所关心的实体那么直接返回
            if(!es[0]){
                return;
            }
             //拿到转化为json的四叉树数据
            let data = toy.qt.toJSON()
            // 拿到主角实体
            let selfEnt = toy.getFirstEnt("LocalControl")

            if(!selfEnt){
                return;
            }
            //获取所有怪物实体
            let foods = toy.getAllEnt(["Mob","Health"])  

            // 获取可能与主角发生碰撞的怪物
            let hlFood = toy.qt.retrieve(selfEnt.Node.node);

            if( !hlFood[0]){
                return;
            }

            hlFood.forEach((food)=>{

                //检查碰撞 
                if(food._node.OldColor){
                   return;
                }
                //判断2个物体是否将相交
                let isIntersects = toy.intersects(selfEnt.Node.node,food._node.Node.node);

                //在自己身上挂一个组件来保存自己原来的颜色,方便后面还原
                toy.com("OldColor",{color:toy.deepClone(food._node.Props.props.color)},food._node);   

                if(false === isIntersects){

                    food._node.Props.props.color = {r:219,g:100,b:0}
                }else{
                    food._node.Props.props.color = {r:106,g:9,b:125}                  
                    //告诉关心带有"Collisioned"组件实体的系统 来处理碰撞
                    toy.comOnce("Collisioned",{character:selfEnt,target: food._node});
                }
            });
        });

碰撞逻辑就不属于demo功能了。开发者可以自行处理

       let _collision = toy.system("CollisionSystem",100);

        _collision.on("Collisioned",function(ent){
            let character = ent.Collisioned.character;
            let target = ent.Collisioned.target;
            //todo 写自定义的碰撞需求
        });

最后就是怪物消失,我们需要更新一下树节点状态。写一个死亡系统关心带有"Mob","Dead" 组件的实体,这些实体被定义为了已经死亡的实体。

         let _dead = toy.system("DeadSystem",100);
        _dead.on(["Mob","Dead"],function(ent){
            // 从实体的Tree组件上获取保存在树节点上的引用
            let treeNodes = ent.Tree.node;

            if(!treeNodes[0]){
                return;
            }

           //检查是否该实体所在的子节点是空闲的
          //如果空闲则从四叉树上删除该子节点
            treeNodes.forEach((node)=>{
                node.remove(ent);
            });
            // 从world中删除该实体
            toy.destroy(ent);

        });

结束语

现在有了 创建四叉树,将对象加入树,检查是否和与主角发生碰撞,还有删除树上空闲的子节点。等功能,相信大家已经大概了解 ECS框架是如何与四叉树结合来做碰撞检测的了。

如果需要源码的可以微信和我联系哈。支持一下 交个朋友。

谢谢看到最后(ps: 「点赞」 「在看」 支持一下呗)

上篇回顾

[ECS框架系列]-与四叉树的一次结合(1)

ECS系列文章

[ECS系列] 框架中必不可少的工具类

[ECS系列] World的设计思路

[ECS系列] 实体,组件,系统的设计思路

[ECS系列] 欢迎来到ECS的世界

更多精彩

CocosCreator 教学系列-对象池

无限嵌套的富文本打字机

CocosCreator 教学系列-打字机效果(1)

CocosCreator 教学系列-打字机效果(2)

公众号

我是IT侠来了,下面是我的公众号,专注于分享IT圈内各种技术干货,内容涉及后端技术,前端技术等等,希望大家喜欢。再次感谢关注。

我的公众号

有什么问题可以加本人微信,一起交流。

本人微信