ECS架构

1 简介

  在当今快速发展的软件开发领域,游戏开发、实时模拟等场景对系统的性能、灵活性和可扩展性提出了极高的要求。传统的面向对象架构在面对复杂且动态变化的实体时,往往会出现代码耦合度高、扩展性差等问题。​

  ECS(Entity - Component - System,实体 - 组件 - 系统)架构作为一种新兴的DOD架构模式,凭借其独特的设计理念,能够有效解决这些难题,在众多领域得到了广泛的应用。本文将对 ECS 架构进行全面、深入的技术剖析。

  ECS 架构核心概念:

  • 实体(Entity)​。实体是 ECS 架构中最基础的概念,它代表着游戏或应用中的一个独立个体,例如游戏中的一个角色、一件物品、一个敌人等。从本质上来说,实体本身并没有任何实际的行为和属性,它更像是一个标识符或者说是一个容器,用于关联各个组件。​
    在实现上,实体通常可以用一个唯一的 ID 来表示,这个 ID 就如同实体的 “身份证”,系统通过这个 ID 来识别和管理不同的实体。
  • 组件(Component)​。组件是用于存储数据的结构,它只包含数据,不包含任何行为逻辑。每个组件都对应着实体的某一种特定属性或状态,比如位置组件可以存储实体的坐标信息,速度组件可以存储实体的移动速度,生命值组件可以存储实体的健康状况等。​一个实体可以拥有多个不同的组件,这些组件共同构成了实体的完整特征。例如,一个游戏角色实体可以同时拥有位置组件、速度组件、生命值组件和武器组件等。
  • 系统(System)。系统是 ECS 架构中负责处理行为逻辑的部分,它通过操作实体所拥有的组件数据来实现各种功能。系统不会直接作用于实体本身,而是根据实体所拥有的组件类型来筛选出需要处理的实体,并对这些实体的组件数据进行操作。​
    比如,移动系统会筛选出拥有位置组件和速度组件的实体,然后根据速度组件中的数据来更新位置组件中的坐标信息;碰撞检测系统会筛选出拥有位置组件和碰撞体积组件的实体,通过计算它们的位置和碰撞体积来判断是否发生碰撞。

ECS 架构的工作流程可以简单概括为以下几个步骤:​

  • 创建实体:根据业务需求创建相应的实体,并为每个实体分配唯一的 ID。​
  • 添加组件:为实体添加所需的组件,组件中包含了实体的具体数据。一个实体可以添加多个不同的组件,以描述其各种属性和状态。​
  • 系统处理:各个系统按照自身的逻辑,对拥有特定组件组合的实体进行处理。系统会遍历所有符合条件的实体,读取或修改它们组件中的数据,从而实现各种功能,如移动、碰撞检测、渲染等。​
  • 动态更新:在应用运行过程中,可以动态地为实体添加或移除组件,实体的属性和行为也会随之发生变化。同时,系统也可以根据需要进行动态的加载或卸载,以适应不同的场景需求。

2 ECS vs OOP

  在传统的面向对象架构中,一个对象通常同时包含数据和行为。例如,一个 “角色” 类会包含角色的位置、生命值等数据,以及移动、攻击等方法。这种架构的优点是符合人们的直觉思维,便于理解和设计简单的系统。​然而,当系统变得复杂时,传统面向对象架构会暴露出一些问题。一方面,类之间的继承关系会导致代码耦合度高,一旦父类发生变化,子类可能会受到很大影响,不利于系统的维护和扩展。另一方面,当需要对对象的行为进行修改时,往往需要修改类的内部实现,违反了封装原则。而相比之下ECS 架构优势​:

  • 低耦合高内聚:ECS 架构将数据和行为分离,组件只负责存储数据,系统只负责处理行为,实体只是组件的集合。这种分离使得各部分之间的耦合度大大降低,每个部分可以独立地进行开发、测试和维护,提高了代码的内聚性。​
  • 更好的可扩展性:在 ECS 架构中,当需要添加新的功能时,只需要添加相应的组件和系统即可,不需要修改现有的实体和其他系统。例如,要给游戏角色添加 “魔法” 属性,只需创建一个 “魔法值” 组件,并为需要的实体添加该组件,再创建一个处理魔法相关行为的系统即可,不会对现有的移动、攻击等系统产生影响。​
  • 高效的性能:由于系统只处理拥有特定组件的实体,避免了对无关实体的处理,提高了处理效率。此外,组件的数据存储方式通常是连续的,有利于 CPU 缓存,减少了缓存未命中的情况,从而提升了系统的性能,尤其在处理大量实体的场景下,优势更加明显。​
  • 更好的并行性:ECS 架构中,不同的系统处理不同的组件组合,且系统之间没有直接的依赖关系,这使得多个系统可以并行执行,充分利用多核 CPU 的性能,提高系统的运行效率。

  实体组件架构(ECS)作为游戏开发等领域常用的架构模式,虽在灵活性和性能方面有一定优势,但也存在不少缺点:

  • 学习和理解门槛较高。相比传统的面向对象架构,ECS 的思维模式有很大差异,它将实体、组件、系统进行了明确分离,这种分离式的设计理念对于习惯了类继承和对象封装的开发者来说,需要花费较多时间去适应和掌握。开发者不仅要理解三者各自的职责,还要搞清楚它们之间的交互机制,比如系统如何通过组件来操作实体,实体如何动态添加或移除组件等,这无疑增加了团队的学习成本,尤其是对于新手开发者,可能需要更长时间才能熟练运用;
  • 调试和排查问题难度大。在 ECS 中,实体的状态由多个组件共同决定,而系统则是对组件进行操作的逻辑单元。当某个实体出现异常行为时,问题可能并非集中在某一个地方,而是分散在多个相关的组件和系统中。例如,一个角色移动异常,可能是移动组件的数据错误,也可能是物理系统的计算出现问题,还可能是输入系统传递的指令有误。开发者需要逐一检查相关的组件数据和系统逻辑,这比在面向对象架构中直接跟踪一个对象的方法调用要复杂得多,大大增加了调试的时间和难度。
  • 可能存在缓存利用率低的问题。在 ECS 中,组件通常是按照类型进行存储的,系统在处理实体时,需要遍历具有特定组件组合的实体集合。如果实体的组件分布较为零散,或者系统需要频繁访问不同类型的组件,就可能导致 CPU 缓存命中率降低。因为 CPU 缓存是基于局部性原理工作的,当数据在内存中不连续时,缓存加载会变得频繁且低效,从而影响系统的运行性能。尤其是在大型项目中,实体和组件数量众多,这种缓存问题可能会更加明显,需要开发者进行大量的优化工作才能缓解。
  • 对复杂行为的支持不足。虽然 ECS 架构在处理简单的实体行为时非常高效,但对于一些复杂的行为逻辑,可能需要设计多个系统来协同工作,这会导致系统之间的依赖关系变得复杂。比如,一个角色的攻击行为可能涉及到输入系统、动画系统、碰撞检测系统等多个系统的配合,这种多系统协作的方式在设计和实现上都比较繁琐,容易引入错误。
  • 对小型项目来说可能过于复杂。对于一些功能简单、实体和逻辑较少的小型项目,ECS 架构可能显得过于重量级。采用 ECS 需要设计大量的组件和系统,搭建相应的框架结构,这会增加不必要的开发工作量。相比之下,传统的面向对象架构能够以更简洁的方式实现项目需求,开发效率更高,因此在小型项目中,ECS 的优势难以体现,反而会暴露其复杂性的缺点。

3 实战

  用ECS实现2D场景下物体移动效果,效果如下图所示,下图中红色小球是通过键盘控制移动的,其他物体是自由下落循环播放。
在这里插入图片描述

  • Demo地址 Github-ECS

实体
  ECS中的Entity只是一个标记记号,用来标识不同的实体,实际中的具体实例是Entity标记和Component共同组成的。

using Entity = std::uint32_t;
constexpr static inline std::size_t kMaxEntities = 4096;using ComponentType = std::uint8_t;
constexpr static inline ComponentType kMaxComponents = 128;
using Signature = std::bitset<kMaxComponents>;

组件
  组件就是具体的属性,通常只是一个简单的数据结构。我们的场景中要处理物体的移动,键盘响应等事件,那么对应的组件就有:

  • Player 组件: 存储玩家的状态,场景比较简单,因此只创建一个空组件用来标识玩家。
struct Player{};
  • Transform 组件: 存储实体的位置、旋转和缩放。
struct Transform{glm::vec3 position;glm::vec3 rotation;glm::vec3 scale;
};
  • Velocity 组件: 存储实体的速度。
struct Velocity {glm::vec3 speed;
};
  • Keyboard组件:存储玩家的键盘输入状态。键盘组件可有可无,根据场景需要来定,也可通过其他方式实现。
using KeyType = unsigned int;
struct KeyBoard {//simply a key codeKeyType keyCode;
};
  • Render组件:存储实体的渲染信息。当前场景只需要处理颜色相关数据。
struct RenderColor{float r;float g;float b;float a;
};

系统
  系统就是处理具体事件的地方,和具体的事务强绑定。根据目标简单的拆解,相关的系统有:

  • 窗口管理系统 (WindowSystem):负责创建和管理窗口。
  • 键盘输入系统 (KeyboardSystem):处理键盘输入,控制玩家(红色小球)的移动。
  • 渲染系统 (RenderSystem):负责渲染所有实体,包括玩家和自由下落的物体。
  • 玩家控制系统 (PlayerControlSystem):处理红色小球的移动逻辑。
  • 自由下落系统 (FallingObjectSystem):处理其他物体的下落逻辑。

  这里的实现只是简单的利用set不考虑内存优化。

class SystemStoratge{
protected:std::set<Entity> _entities;
};class ISystem{
public:virtual void init() = 0;virtual void tick(float dt) = 0;virtual ~ISystem() = default;virtual bool isRunning() const = 0;virtual void destroy() = 0;virtual void erase(Entity entity) = 0;virtual void insert(Entity entity) = 0;
};class System : public SystemStoratge, public ISystem{
public:virtual bool isRunning() const {return true;}virtual void init() override {}virtual void tick(float dt) override {}virtual void destroy() override {}virtual void erase(Entity entity) override {_entities.erase(entity);}virtual void insert(Entity entity) override {_entities.insert(entity);}};

  系统具体实现中,通常会遍历所有实体,根据实体的组件来判断是否需要处理。

void PlayerSystem::tick(float dt) {for(const auto& entity : _entities) {try {auto& player = g_worldManager->getComponent<Player>(entity);auto& transform = g_worldManager->getComponent<Transform>(entity);auto& keyboard = g_worldManager->getComponent<KeyBoard>(entity);if(keyboard.keyCode != 0){if (keyboard.keyCode == GLFW_KEY_W) {transform.position.y += 0.1f; // Move forward} else if (keyboard.keyCode == GLFW_KEY_S) {transform.position.y -= 0.1f; // Move backward} else if (keyboard.keyCode == GLFW_KEY_A) {transform.position.x -= 0.1f; // Move left} else if (keyboard.keyCode == GLFW_KEY_D) {transform.position.x += 0.1f; // Move right}LOGI("Player {} moved to position[{}, {}, {}]", (int)entity, transform.position.x, transform.position.y, transform.position.z);keyboard.keyCode = 0; // Reset key code after processing}//LOGI("Player {} moved to position[{}, {}, {}]", entity, transform.position.x, transform.position.y, transform.position.z);} catch (const std::exception& e) {LOGE("Error processing entity {}: {}", (int)entity, e.what());}}
}

Manager
  到此为止我们只是定义了ECS架构中最基本的组件,似乎还是没办法很清晰的认识到不同组件的作用。为了方便管理不同System-Entity,我们引入ComponentManager,EntityManager,SystemManager来分别管理组件,实体,系统,最后使用WorldManager统一管理三者。

  EntityManager 类负责管理实体的生命周期和组件签名。它允许创建和销毁实体,并为每个实体存储其组件的签名,以便于系统识别实体的特性。其中签名是一个bitset用来标识当前Entity包含哪些组件。

Entity EntityManager::createEntity() {if (!availableEntities.empty()) {Entity entity = availableEntities.front();availableEntities.pop();return entity;}if (currentEntity >= kMaxEntities) {throw std::runtime_error("Maximum number of entities reached.");}return currentEntity++;
}void EntityManager::destroyEntity(Entity entity) {if (entity >= currentEntity) {throw std::runtime_error("Entity does not exist.");}signatures[entity].reset(); // 清除该实体的签名availableEntities.push(entity); // 将实体加入可用池
}void EntityManager::setSignature(Entity entity, Signature signature) {if (entity >= currentEntity) {throw std::runtime_error("Entity does not exist.");}signatures[entity] = signature;
}Signature EntityManager::getSignature(Entity entity) const {if (entity >= currentEntity) {throw std::runtime_error("Entity does not exist.");}return signatures[entity];
}

  ComponentArray 类负责管理组件数组。用来存储当前组件都有哪些Entity持有,同时保存正反向索引来加速访问。

template<typename T>
class ComponentArray : public IComponentArray {
public:void insert(Entity entity, T component) {assert(_entityToIndexMap.find(entity) == _entityToIndexMap.end() && "Component added to the same entity more than once.");// Put new entry at endsize_t newIndex = _size;_entityToIndexMap[entity] = newIndex;_indexToEntityMap[newIndex] = entity;_componentArray[newIndex] = component;++_size;}//省略部分代码......private:std::array<T, kMaxEntities> _componentArray{};std::unordered_map<Entity, size_t> _entityToIndexMap{};std::unordered_map<size_t, Entity> _indexToEntityMap{};size_t _size{0}; // 当前存储的大小
};

  ComponentManager 类负责管理组件类型。用来注册组件类型,同时根据组件类型获取对应的ComponentArray。

class ComponentManager {
public:template<typename T>void registerComponent(){static_assert(std::is_standard_layout_v<T>, "Component must be standard layout.");static_assert(sizeof(T) > 0, "Component must have size.");const char* typeName = typeid(T).name();if (_componentTypes.find(typeName) != _componentTypes.end()) {throw std::runtime_error("Component type already registered.");}_componentTypes[typeName] = _componentTypes.size();_componentStorages[typeName] = std::make_shared<ComponentArray<T>>();}template<typename T>void add(Entity entity, T component){getComponentArray<T>()->insert(entity, component);}//省略部分代码......private:template<typename T>std::shared_ptr<ComponentArray<T>> getComponentArray(){const char* typeName = typeid(T).name();assert(_componentTypes.find(typeName) != _componentTypes.end() && "Component not registered before use.");return std::static_pointer_cast<ComponentArray<T>>(_componentStorages[typeName]);}private:std::unordered_map<const char*, ComponentType> _componentTypes;std::unordered_map<const char*, std::shared_ptr<IComponentArray>> _componentStorages;
};

  SystemManager要简单的多主要就是管理系统。

class SystemManager{
public:template<typename T, typename... Args>std::shared_ptr<T> registerSystem(Args&&... args) {const char* typeName = typeid(T).name();assert(_systems.find(typeName) == _systems.end() && "Registering system more than once.");auto system = std::make_shared<T>(std::forward<Args>(args)...);_systems.insert({typeName, system});return system;}template<typename T>void setSignature(Signature signature) {const char* typeName = typeid(T).name();assert(_systems.find(typeName) != _systems.end() && "System used before registered.");_signatures.insert({typeName, signature});}//省略部分代码...............private:std::unordered_map<const char*, std::shared_ptr<ISystem>> _systems;std::unordered_map<const char*, Signature> _signatures; // 系统签名
};

  最后是WorldManager,其实现比较简单,只是大多数接口都是调用SystemManager和EntityManager的。

class WorldManager {
public:WorldManager(): _sysManager(std::make_unique<SystemManager>()),_entityManager(std::make_unique<EntityManager>()),_componentManager(std::make_unique<ComponentManager>()) {}~WorldManager() = default;template<typename T, typename... Args>std::shared_ptr<T> registerSystem(Args&&... args) {return _sysManager->registerSystem<T>(std::forward<Args>(args)...);}void initializeAll() {_sysManager->initializeAll();}//省略部分代码...............
private:std::unique_ptr<SystemManager> _sysManager;std::unique_ptr<EntityManager> _entityManager;std::unique_ptr<ComponentManager> _componentManager;
};

  比较的组件都有了,下一步需要的就是将他们组合起来。首先是需要注册我们即将用到的System和组件。

void RegisterComponents() {g_worldManager->registerComponent<Player>();g_worldManager->registerComponent<Transform>();g_worldManager->registerComponent<KeyBoard>();g_worldManager->registerComponent<RenderColor>();g_worldManager->registerComponent<Velocity>();
}void RegisterSystems() {//省略部分代码................auto renderSystem = g_worldManager->registerSystem<RenderSystem>();{Signature signature;signature.set(g_worldManager->getComponentType<Transform>());signature.set(g_worldManager->getComponentType<RenderColor>());g_worldManager->setSystemSignature<RenderSystem>(signature);}}

  然后是创建实例。

void CreatePlayerEntity() {auto playerSystem = g_worldManager->registerSystem<PlayerSystem>();{Signature signature;signature.set(g_worldManager->getComponentType<Player>());signature.set(g_worldManager->getComponentType<Transform>());signature.set(g_worldManager->getComponentType<KeyBoard>());signature.set(g_worldManager->getComponentType<RenderColor>());g_worldManager->setSystemSignature<PlayerSystem>(signature);}Entity playerEntity = g_worldManager->createEntity();g_worldManager->addComponent(playerEntity, Player{});g_worldManager->addComponent(playerEntity, Transform{ glm::vec3(0.0f), glm::vec3(0.0f), glm::vec3(1.0f) });g_worldManager->addComponent(playerEntity, KeyBoard{ 0 }); // Initialize with a default key codeg_worldManager->addComponent(playerEntity, RenderColor{ 1.0f, 0.0f, 0.0f, 1.0f }); // Red color
}

  使用时需要根据Entity来获取当前Entity对应的组件并且根据场景进行读取和更新,比如下面渲染系统中读取Player和非Player实体的transform和color进行渲染。

void RenderSystem::tick(float dt) {// This is where rendering logic would goglClearColor(0.1, 0.1, 0.1, 1.0);GLint viewport[4];// 获取当前视口glGetIntegerv(GL_VIEWPORT, viewport);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);for(const auto& entity : _entities) {try {// Example: Get a component and log its dataauto& transform = g_worldManager->getComponent<Transform>(entity);auto& color = g_worldManager->getComponent<RenderColor>(entity);if(g_worldManager->hasComponent<Player>(entity)) {//LOGI("Rendering player entity {} at position [{}, {}, {}]", (int)entity, transform.position.x, transform.position.y, transform.position.z);}else{//LOGI("Rendering entity {} at position [{}, {}, {}]", (int)entity, transform.position.x, transform.position.y, transform.position.z);}//LOGI("Rendering entity {} at position [{}, {}, {}]", (int)entity, transform.position.x, transform.position.y, transform.position.z);{glBindVertexArray(_vao);auto projection = glm::perspective<float>(glm::radians(_camera.zoom()), viewport[3] * 1.0f/viewport[2], 0.1f, 100.0f);const auto view = _camera.getViewMatrix();glm::vec3 pos = glm::vec3(transform.position.x,transform.position.y,1);//draw light source{glm::mat4 model = glm::mat4(1.0f); // make sure to initialize matrix to identity matrix firstmodel = glm::translate(model, pos);model = glm::rotate(model, 0.f, glm::vec3(1.0f, 0.f, 0.f));model = glm::scale(model, glm::vec3(0.1, 0.1, 0.1));_glProgram->use();_glProgram->update("projection", projection);_glProgram->update("view", view);_glProgram->update("color", glm::vec4(color.r, color.b, color.b, color.a));_glProgram->update("model", model);glDrawElements(GL_TRIANGLES, _shape.idxSize(), GL_UNSIGNED_INT, 0);glBindVertexArray(0);}}} catch (const std::exception& e) {LOGE("Error processing entity {}: {}", (int)entity, e.what());}}}

4 参考文献

  • Github-ECSDemo
  • entity_component_system
  • 一文看懂ECS架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/92022.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/92022.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/92022.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

.vscode 扩展配置

一、vue快捷键配置 在项目.vscode下新建vue3.0.code-snippets 每当输入vue3.0后自动生成代码片段 {"Vue3.0快速生成模板": {"scope": "vue","prefix": "Vue3.0","body": ["<template>"," &…

一个基于阿里云的C端Java服务的整体项目架构

1.背景介绍 总结一下工作使用到的基于通常的公有云的项目整体架构&#xff0c;如何基于公有云建设安全可靠的服务&#xff0c;以阿里云为例的整体架构&#xff1b;1. 全局流量治理层&#xff08;用户请求入口&#xff09;1.1 域名与 DNS 解析域名注册与备案&#xff1a;通过阿里…

《剥开洋葱看中间件:Node.js请求处理效率与错误控制的深层逻辑》

在Node.js的运行时环境中&#xff0c;中间件如同一系列精密咬合的齿轮&#xff0c;驱动着请求从进入到响应的完整旅程&#xff0c;而洋葱模型则是这组齿轮的传动系统。它以一种看似矛盾的方式融合了顺序与逆序、分离与协作——让每个处理环节既能独立工作&#xff0c;又能感知全…

GaussDB union 的用法

1 union 的作用union 运算符用于组合两个或更多 select 语句的结果集。2 union 使用前提union 中的每个 select 语句必须具有相同的列数这些列也必须具有相似的数据类型每个 select 语句中的列也必须以相同的顺序排列3 union 语法select column_name(s) from table1 union sele…

构建足球实时比分APP:REST API与WebSocket接入方案详解

在开发足球实时比分应用时&#xff0c;数据接入方式的选择直接影响用户体验和系统性能。本文将客观分析REST API和WebSocket两种主流接入方案的技术特点、适用场景和实现策略&#xff0c;帮助开发者做出合理选择。一、REST API&#xff1a;灵活的数据获取方案核心优势标准化接口…

Linux文件系统三要素:块划分、分区管理与inode结构解析

理解文件系统 我们知道文件可以分为磁盘文件和内存文件&#xff0c;内存文件前面我们已经谈过了&#xff0c;下面我们来谈谈磁盘文件。 目录 一、引入"块"概念 解析 stat demo.c 命令输出 基本信息 设备信息 索引节点信息 权限信息 时间戳 二、引入"分区…

基于paddleDetect的半监督目标检测实战

基于paddleDetect的半监督目标检测实战前言相关介绍前提条件实验环境安装环境项目地址使用paddleDetect的半监督方法训练自己的数据集准备数据分割数据集配置参数文件PaddleDetection-2.7.0/configs/semi_det/denseteacher/denseteacher_ppyoloe_plus_crn_l_coco_semi010.ymlPa…

计算机网络:(十)虚拟专用网 VPN 和网络地址转换 NAT

计算机网络&#xff1a;&#xff08;十&#xff09;虚拟专用网 VPN 和网络地址转换 NAT前言一、虚拟专用网 VPN1. 基础概念与作用2. 工作原理3. 常见类型4. 协议对比二、NAT&#xff1a;网络地址转换1. 基础概念与作用2. 工作原理与类型3. 优缺点与问题4. 进阶类型三、VPN 与 N…

数位 dp

数位dp 特点 问题大多是指“在 [l,r][l,r][l,r] 的区间内&#xff0c;满足……的数字的个数、种类&#xff0c;等等。” 但是显然&#xff0c;出题人想要卡你&#xff0c;rrr 肯定是非常大的&#xff0c;暴力枚举一定超时。 于是就有了数位 dp。 基本思路 数位 dp 说白了…

Selector的用法

Selector的用法 Selector是基于lxml构建的支持XPath选择器、CSS选择器&#xff0c;以及正则表达式&#xff0c;功能全面&#xff0c;解析速度和准确度非常高 from scrapy import Selectorbody <html><head><title>HelloWorld</title></head>&…

Netty封装Websocket并实现动态路由

引言 关于Netty和Websocket的介绍我就不多讲了,网上一搜一大片。现如今AI的趋势发展很热门,长连接对话也是会经常接触到的,使用Websocket实现长连接,那么很多人为了快速开发快速集成就会使用spring-boot-starter-websocket依赖快速实现,但是注意该实现是基于tomcat的,有…

行为型设计模式:解释器模式

解释器模式 解释器模式介绍 解释器模式使用频率不算高&#xff0c;通常用来描述如何构建一个简单“语言”的语法解释器。它只在一些非常特定的领域被用到&#xff0c;比如编译器、规则引擎、正则表达式、SQL 解析等。不过&#xff0c;了解它的实现原理同样很重要&#xff0c;能…

SaTokenException: 未能获取对应StpLogic 问题解决

&#x1f4dd; Sa-Token 异常处&#xff1a;未能获取对应StpLogic&#xff0c;typeuser&#x1f9e8; 异常信息 cn.dev33.satoken.exception.SaTokenException: 未能获取对应StpLogic&#xff0c;typeuser抛出位置&#xff1a; throw new SaTokenException("未能获取对应S…

Web前端性能优化原理与方法

一、概述 1.1 性能对业务的影响 大部分网站的作用是&#xff1a;产品信息载体、用户交互工具或商品流通渠道。这就要求网站与更多用户建立联系&#xff0c;同时还要保持良好的用户黏性&#xff0c;所以网站就不能只关注自我表达&#xff0c;而不顾及用户是否喜欢。看看网站性…

第十八节:第六部分:java高级:注解、自定义注解、元注解

认识注解自定义注解注解的原理元注解常用的两个元注解代码&#xff1a; MyTest1&#xff08;注解类&#xff09; package com.itheima.day10_annotation;import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Retent…

北京科技企业在软文推广发稿平台发布文章,如何精准触达客户?

大家好&#xff01;我是你们的老朋友&#xff0c;今天咱们聊聊北京科技企业如何通过软文推广发稿平台精准触达目标客户这个话题。作为企业营销的老司机&#xff0c;我深知在这个信息爆炸的时代&#xff0c;如何让你的品牌声音被目标客户听到是多么重要。下面就让我来分享一些实…

UE蒙太奇和动画序列有什么区别?

在 UE5 中&#xff0c;Animation Sequence&#xff08;动画序列&#xff09;和 Animation Montage&#xff08;动画蒙太奇&#xff09;虽然都能播放骨骼动画&#xff0c;但它们的定位、功能和使用场景有较大区别&#xff1a;1. 概念定位Animation Sequence&#xff08;动画序列…

Nordic打印RTT[屏蔽打印中的<info> app]

屏蔽打印中的 app Nordic原装的程序答应是这样的,这个有" app"打印,因为习惯问题,有时候也不想打印太多造成RTT VIEW显示被冲点,所以要把" app"去掉:这里把prefix_process函数调用屏蔽到,主要涉及到nrf_log_hexdump_entry_process和nrf_log_std_entry_proc…

Python基础和高级【抽取复习】

1.Python 的深拷贝和浅拷贝有什么区别&#xff1f; 浅拷贝【ls.copy()】&#xff1a; 将列表的不可变对象【值】复制一份&#xff0c;同时引用其中的可变对象【列表】&#xff0c;共用一个内存地址 深拷贝【lscopy.deepcopy(list)】&#xff1a; 完全的复制原可变对象&#xff…

TinyPiXOS组件开发(一):开发规范、组件开发方法介绍,快速上手组件开发,创造各种有趣的UI组件!

本文将通过实现一个点击切换进度的电量指示灯组件和exampleGUI组件库介绍如何基于TinyPiXOS开发新组件。主要内容包括组件开发规范、自定义组件开发和组件库开发三部分。 组件开发规范 命名规范 采用tp开头命名组件类&#xff0c;名称具备易读性。 目录规范 头文件放置 in…