Hazel游戏引擎(076)介绍实体组件系统(ECS)

文章探讨了ECS(实体组件系统)在游戏开发中的好处,从传统的继承模式出发,分析其导致的代码膨胀和复杂性问题。接着介绍了ECS1.1优化模式,通过组件池减少类层次,但指针访问仍存在性能瓶颈。最后,提出了ECS2.0设计,通过将同类型组件存储在一起,减少内存访问次数,提高性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文中若有代码、术语等错误,欢迎指正

前言

  • 此节目的

    介绍实体组件系统(ECS)的好处以及为什么要实体组件系统

  • ECS简介

    是麻省理工啥(不知道有没有错)设计的

    基于ECS设计的Entt库源码很多行,可以去读。

  • 各个商业引擎的都存在ECS模式

    Unity叫Entity为GameObject

    UE叫Entity为actor

  • 本文编写参考文章

    实体组件系统介绍

  • 提前声明

    此节的内容参考的资料较少,带很多有自己的理解,可能与实际情况存在较大偏差

ESC1.0继承模式下实现

假设在一个场景中,包含两个不同的实体(Entity1和Entity2),每个实体都需要具备各自特定的组件

例如Entity1需要一个Light组件,而Entity2需要一个Audio组件

  • 用单继承的代码写

    class Scene {
    	std::vector<Entity*> entities;
    };
    class Entity {
    	Mat4 Transform;
    	string Tag;
    };
    class Light : public Entity {
    	vec3 Color;
    	float Intensity;
    };
    class Audio : public Entity {
    	AudioClip* clip;
    };
    // 一个场景
    Scene sc;
    // 有两个不同组件的实体
    Entity* e1 = new Light;
    Entity* e2 = new Audio;
    sc.entities.push_back(e1);
    sc.entities.push_back(e2);
    
  • 新需求

    当这个场景再需要一个实体(Entity3)并具有Audio和Mesh这两个组件时

    就需要再次声明一个新的子类(例如AudioMesh),并且需要继承自Audio类。

    class AudioMesh : public Audio {
    	Mesh* mesh;
    };
    Entity* e3 = new AudioMesh;
    sc.entities.push_back(e3);
    
  • 类图大概是

    请添加图片描述

  • 缺点

    这样的实现方式可能导致代码量增加类层次结构复杂等问题,影响程序的可读性和可维护性。

    造成这样的原因是面向对象的思想,类具有属性和它的函数(行为),有点高聚合。(个人理解)

ESC1.1 优化继承模式

介绍

假设在一个场景中,包含两个不同的实体(Entity1和Entity2),每个实体都需要具备各自特定的组件

例如Entity1需要一个Light组件,而Entity2需要一个Audio组件

  • 优化继承模式代码

    class Scene {
    	std::vector<Entity*> entities;
    };
    class Entity {
    	Mat4 Transform;
    	string Tag;
    	std::vector<Component*> components;// 注意这个
    };
    struct Component {// 注意这个
    };
    struct Light : public Component {
    	vec3 Color;
    	float Intensity;
    };
    struct Audio : public Component {
    	AudioClip* clip;
    };
    Scene sc;
    Entity e1, e2;
    e1.components.pushback(new Light);
    e2.components.pushback(new Audio);
    sc.entities.push_back(e1);
    sc.entities.push_back(e2);
    

    由代码中可以看到,多了一个Component结构体父类

  • 新需求

    • 再需要一个实体Entity,并具有Audio、Mesh这两个组件

    • 在目前的设计下,就不需要再写一个新的子类AudioMesh来继承Audio与Mesh类,而是可以写一个Mesh类继承Component。

      实体的vector<Component*> components再添加这个Mesh组件,即可完成此新需求

    struct Mesh : public Component {
    	Data* data;
    };
    Entity e3;
    e3.components.pushback(new Mesh);
    sc.entities.push_back(e3);
    
  • 类图

    请添加图片描述

  • 优点

    减少高聚合(个人理解)

新缺点

  • 引发新缺点的情况

    在一个拥有100万个实体的场景中,需要播放这些实体的音频

    如果使用Scene中的vector存储实体的方式是指针,同时实体的vector又以指针方式存储组件,则在处理数据时将经历二次访问,这样就可能导致CPU性能的浪费和影响游戏性能。

  • 示例代码

    class Scene {
    std::vector<Entity*> entities;
       void PlayAllAudio(){
           for(auto &en : entities){ 
               for(auto &au : en->components){ // 一次指向,因为调用了(->)或者(*po.)代表执行获取指针指向的数据
                   if(*au == audio){ 		
                       *au.play();				// 二次指向
                  }
              }
          }
      }
    };
    class Entity {
        Mat4 Transform;
        string Tag;
        std::vector<Component*> components;
    };
    
  • 再次说明造成此问题的原因

    1. 造成耗时的主要原因之一是实体与组件散布在内存中,需要通过指针来访问,无法统一管理
    2. 这种设计方式可能导致CPU进行二次访问,从而增加程序的运行时间和资源消耗。

ECS2.0

  • ECS2.0介绍

    是在ECS1.1的基础上优化,克服二次访问缺点

  • 克服1.1二次访问缺点理论

    将同类型的组件放置在连续的存储块中,从而减少对内存的访问次数,并且可以达到更好的性能表现。

  • 示例代码

    在一个拥有100万个实体的场景中,需要播放这些实体的音频

    class Scene {
        std::vector<Entity*> entities;
        
        std::vector<Audio*> audios;// 新增这个,同类型的组件放置放置在连续的存储卡
       void OnUpdate(){
           for(auto &au : audios){
               if(au != nullptr){
              	*au.play();// audios vector的元素 一次指向
              }
          }
      }
    };
    class Entity {
       int id;// 新增这个
        Mat4 Transform;
        string Tag;
        std::vector<Component*> components;
    };
    Scene sc;
    sc.audios.resize(1000);
     
    Entity e1, e2;
    Component *a2 = new Audio;
    e1.components.pushback(new Light);
    e2.components.pushback(a2);
    // 在音频数组的位置2,放入实体2的音频。
    sc.audios[e2.id] = a2; // e2.id是实体的ID
    

    将同一类型的组件放置在一个连续的数组中,并使用实体的ID作为下标来与实体进行关联

  • 参考图

    请添加图片描述

    • 解释图

      第一行的数字代表实体的ID

    • 竖着看解释例子

      • 第一列代表实体0组件在对应类型数组的位置

        如图,实体0只有Light组件,并且实体0的Light组件在Light组件类型数组0号位置

      • 第三列代表实体2组件在对应类型数组的位置

        如图,实体2有Audio、SpriteRenderer组件

        并且实体2的Audio组件在Audio组件类型数组2号位置

        并且实体2的SpriteRenderer组件在SpriteRenderer组件类型数组2号位置

小结

  • 此节

    • 此节的内容参考的资料较少,带很多有自己的理解,可能与实际情况存在较大偏差
    • ECS1.0、ECS1.1、ECS2.0是我自己取的名来讲解ECS的优化设计过程,不是权威的
  • ECS1.0、ECS1.1、ECS2.0

    这三个的Demo代码都是根据视频讲解和参考资料自己写的,可能不正确

  • 个人认为ECS2.0

    这ECS2.0的设计模式应该接近现代ECS的设计模式(但是也只是我自己的推测,请参考权威文档)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘建杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值