使用Odin打造的简单易用实体组件系统 (ECS)

Lobsters Hottest 工具

摘要

moecs 是一个专为Odin编程语言打造的简单易用实体组件系统 (ECS) 库,简化了软件项目中实体、组件和系统的管理。

<p><a href="https://lobste.rs/s/hdef54/easy_use_entity_component_system_ecs">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/19 22:35

helioscout/moecs

来源:https://github.com/helioscout/moecs

moecs

moecs — 用 Odin 打造的易用实体组件系统(ECS)。


安装
世界
元素
可变性与延迟操作
资源
实体
组件
标签
系统
观察者
关系
运行世界
遍历实体
性能
使用 moecs 制作
内存概念
为什么是一只蜜蜂
引语

安装

将 moecs 仓库克隆到您的项目或附近目录,然后导入 src 文件夹。

import ecs "moecs/src"  

main :: proc() { /* ... */ }  

世界

顶层容器是由世界(worlds)组成的空间(ecs)。您可以创建任意数量的世界,它们都将独立运行。但通常来说,一个世界就足够了。

import ecs "moecs/src"  

main :: proc() {  
  /* 初始化空间(ecs)。 */  
  ecs.init()  
  /* 创建新世界。 */  
  world : ^ecs.World = ecs.new_world()  
  /* 销毁所有对象并释放内存。 */  
  ecs.destroy()  
}  
过程描述
init()初始化 ecs。在执行任何 ecs 相关操作前调用此函数。
size()获取世界数量。
new_world()创建新世界并返回指向它的指针。
destroy()释放空间中的所有世界。在应用退出或不再需要 ecs 时调用。

元素

世界可以包含的元素类型由 Element 枚举表示。

成员描述
COMPONENT组件元素类型(如 PositionMassVelocity)。必须定义为结构体或自定义的 distinct 类型。
TAG标签元素类型,用于为实体标记某种特性。必须定义为 typedef,且底层为简单的基本类型(如 typedef Tag = int)。
RESOURCE资源元素类型,用于存储世界中唯一实例(单例)的数据。必须定义为结构体或 typedef 自定义类型。与其他 ECS 不同,资源不是无组件的实体,它们有自己的存储和方法。
SYSTEM系统元素类型,用于在世界进度的每一步执行操作。
RELATION实体之间的关系。必须定义为结构体或自定义的 distinct 类型。关系可以关联用于某些游戏逻辑的数据,类似于带有目标/附加实体的组件。

在运行世界之前,您必须注册世界元素。

import ecs "moecs/src"  

main :: proc() {  
  ecs.init()  
  world := ecs.new_world()  

  /* 注册组件 */  
  ecs.register(world, .COMPONENT, Position)  
  ecs.register(world, .COMPONENT, Rotation)  
  /* 注册资源。 */  
  ecs.register(world, .RESOURCE, GameState)  
  ecs.register(world, .RESOURCE, Sprites)  
  /* 注册标签。 */  
  ecs.register(world, .TAG, Player)  
  ecs.register(world, .TAG, Asteroid)  
  /* 注册关系。 */  
  ecs.register(world, .RELATION, Joint)  

  ecs.destroy()  
}  
过程描述
register()为世界注册元素类型。

可变性与延迟操作

获取资源和组件的方法有 get()get_mut()。仅当您需要修改获取的资源/组件类型的至少一个实例时使用 get_mut(),否则使用 get()——它稍快一些。同样,使用重载过程可以通过一次调用获取一组元素,设置值时使用 set() 过程也是如此。批量方法性能更好,因为减少了内存读写操作。

当您 despawn(销毁)实体时,这些操作会被延迟。我们需要将实体保留在原型中,直到当前进度步骤结束,否则系统代码中的迭代器可能导致错误,因为它们正在遍历我们需要从中删除实体的原型集合。实体将被标记为 DESPAWNING,但在执行阶段才会真正销毁(从块中删除)。此外,如果一个新实体被写入已删除实体的位置,那么错误将不可避免,因为对已删除实体的引用仍会保留在原型集合中。

如果您为销毁实体设置了观察者,回调也会延迟到执行阶段,直到销毁的实际时刻。

当您 add(添加)/ remove(移除)组件,或 set(设置)/ unset(取消设置)标签,或 relate(关联)/ unrelate(解除关联)实体时,它们在当前进度步骤结束前仍会保留在当前原型中。当 tags(标签)/ components(组件)/ relations(关系)正在被添加/移除到实体且世界已经运行时,实体不应在当前进度步骤结束前移动到其他原型,因此这个 archetyping(原型化)操作被延迟到执行阶段。

这意味着变更只会在下一个世界进度步骤生效。
设置资源/组件/关系的值会立即生效(不延迟)。

资源

资源是您只需要一个实例的数据结构,代表游戏状态、精灵(纹理)、物理参数等。您需要在 getting(获取)它们之前 register(注册)其类型并 set(设置)其值。您可以添加数量小于或等于 MAX_RESOURCES_COUNT 的资源,如果需要更多,请手动更改此常量。在设置资源值之前,必须 run(运行)世界。

import ecs "moecs/src"  
import b2 "vendor:box2d"  

main :: proc() {  
  ecs.init()  
  world := ecs.new_world()  

  /* ...注册资源类型。 */  

  /* 在填充世界元素前,您应该运行世界。 */  
  ecs.run(world)  

  /* 设置资源值(将被复制到存储中)。 */  
  ecs.set(world, GameState, &GameState {  
    screen     = .Playing,  
    fullscreen = false,  
    zoom       = 1.0,  
    scaled     = time.now()  
  })  

  /* 获取资源值(从存储中复制)。 */  
  state, sprites := ecs.get(world, GameState, Sprites)  
  /* 获取指向资源值的指针(可变)。 */  
  physics := ecs.get_mut(world, Physics)  

  world_def := b2.DefaultWorldDef()  
  world_def.gravity = { 0.0, 0.0 }  
  /* 修改资源值(通过指针/就地修改)。 */  
  physics.world_id = b2.CreateWorld(world_def)  

  ecs.destroy()  
}  
过程描述
set_resource()按类型设置 一个 资源值。
get_resource()按类型获取 一个 资源值。
get_resource_mut()按类型获取 一个 资源值的引用(指针)。
set()设置一组资源值(推荐)。
get()获取一组资源值(推荐)。
get_mut()获取一组资源指针以修改资源字段(推荐)。

实体

实体是世界的主要元素。它是可以被组件和标签指定,并且相互关联的抽象数据结构。实体不仅仅是一个 id,它有一些字段,但您不应关心它们,而是使用过程来处理它。在内部,有位集字段定义了实体拥有哪些组件和/或标签,和/或关系。因此,在删除组件/关系或添加/移除标签时,不会发生内存读写操作。

当您 despawn(销毁)一个实体时,此操作会延迟到当前进度步骤结束,因此如果您想在当前进度步骤(游戏帧)中忽略这些实体,请使用 despawning 过程来检查其状态。如果您从之前的进度步骤获得了指向它的指针,可能需要检查实体是否确实已被销毁,此时请使用 deleted 过程。

所有实体都有生命周期(.DYNAMIC.STATIC)。但您不会在实体本身中找到此数据,它仅在实体所属的内存块中定义,并且一旦实体被生成,其生命周期就不能更改。

import ecs "moecs/src"  

main :: proc() {  
  ecs.init()  
  world := ecs.new_world()  
  ecs.run(world)  

  /* 生成静态实体。 */  
  asteroid: ^ecs.Entity = ecs.spawn(world, .STATIC)  
  /* 生成动态实体(.DYNAMIC 生命周期是默认值)。 */  
  ship := ecs.spawn(world)  

  ecs.destroy()  
}  
过程描述
spawn()在世界中生成 一个 新实体。
despawn_entity()从世界中销毁 一个 实体。
despawn_entities()从世界中销毁 多个 实体。
despawn()用于销毁一个或多个实体的重载过程(推荐)。
despawning()检查实体是否在执行阶段被延迟销毁。
deleted()检查实体是否已被完全删除(销毁)。
is_dynamic()检查实体是否属于动态生命周期块。
is_static()检查实体是否属于静态生命周期块。

组件

组件存储在块的 chunk(块)中,内部是连续的内存块,通过指针运算进行读写。我们知道实体索引、组件大小和块大小(DYNAMIC_CHUNK_SIZESTATIC_CHUNK_SIZE),因此通过指针访问非常简单。

实体可以拥有的组件数量小于或等于 MAX_COMPONENTS_COUNT 常量。默认情况下它等于 128,如果您需要更多,请手动更改此常量的值。

组件类型在使用前必须注册到 ecs 中,并且在开始添加组件之前世界应该已经运行。

当您添加组件或设置先前添加的组件的值时,更改会立即存储在内存中,您可以在同一个世界进度步骤中读取这些值,完全没有缓存。但是,adding(添加)/ removing(移除)组件会延迟原型重新绑定(原型化),直到 perform(执行)阶段,因此您的系统匹配查询将只在下一个进度步骤考虑它们。

实体中特定组件的存在由实体结构中的一个特殊字段中的位标志决定。从实体中移除组件不会导致任何内存访问,只是设置相应的位。读取此位应用于检查实体中是否存在组件。

优先使用重载过程通过一次过程调用来 add/set/get 一组组件。批量方法性能更好,因为减少了内存读写操作。

import ecs "moecs/src"  

main :: proc() {  
  ecs.init()  
  world := ecs.new_world()  
  /* ...在这里注册组件类型。 */  
  ecs.run(world)  

  ship := ecs.spawn(world)  

  /* 向实体添加组件。 */  
  ecs.add(ship,  
    Position, &Position { x = 350, y = 170 },  
    Rotation, &Rotation { angle = 90 },  
    Velocity, &Velocity { 50 },  
    Weapon,   &Weapon   { kind = .Rocket })  

  /* 获取一个组件(可变方法)。 */  
  if weapon, ok := ecs.get_mut(ship, Weapon); ok {  
    weapon.kind = .Bullet  
  }  

  /* 设置先前添加的组件的值。 */  
  ecs.set(ship,  
    Position, &Position { x = 700, y = 900 },  
    Rotation, &Rotation { angle = 180 })  

  /* 获取多个组件(用于读取,从存储中复制)。 */  
  pos, rot := ecs.get(ship, Position, Rotation)  
  /* 获取指向组件值的指针(可变方法)。 */  
  vel, weapon := ecs.get_mut(ship, Velocity, Weapon)  

  /* 从实体中移除组件。 */  
  ecs.remove(ship, Position, Rotation)  

  /* 检查实体是否有某个组件。 */  
  if ecs.has(ship, Position) {  
    ecs.set(ship, Position, &Position { x = 0, y = 0 })  
  }  

  ecs.destroy()  
}  
过程描述
add_component()按类型和实例(初始化器)向实体添加 一个 组件。
set_component()设置 一个 先前添加的组件值。
get_component()按其类型获取 一个 组件值(从存储中复制)。
get_component_mut()按其类型获取 一个 组件值的引用。
remove_component()从实体中按其类型移除 一个 组件。
remove_components()从实体中移除所有传递类型的 多个 组件。
has_component()检查实体是否有某个组件。
has_components()检查实体是否有所有传递类型的组件。
add()添加一组组件(推荐)。
set()设置一组组件(推荐)。
get()获取一组组件(推荐)。
get_mut()获取一组组件指针以修改其值(推荐)。
remove()按其类型移除任意数量的组件(推荐)。
has()按其类型检查是否存在任意数量的组件(推荐)。

标签

标签只是可以为实体 set(设置)/ unset(取消设置)的属性(标记)。就像组件一样,实体有一个特殊的位字段,其中每个位对应一个标签。然而,与组件不同,标签不存储在内存块中。添加/移除标签仅意味着设置实体字段中的相应位。

实体可以拥有的标签数量小于或等于 MAX_TAGS_COUNT 常量。默认情况下它等于 128,如果您需要更多,请手动更改此常量的值。标签类型在添加到实体之前必须注册。

设置/取消设置…

相似文章

MoDev

Product Hunt

MoDev 是一个专为移动设备设计的AI开发环境。

理解Odin编程语言

Hacker News Top

一本教授Odin编程语言的书,涵盖手动内存管理、参数多态和数据导向设计。1.10版本已发布并包含更新。

EMO:用于涌现模块化的专家混合模型预训练

Hugging Face Daily Papers

EMO 是一种专家混合模型(Mixture-of-Experts),通过将相似领域的词元与共享专家分组实现模块化部署,在保持与标准 MoE 相当的性能的同时,支持显著的专家剪枝(保留 25% 的专家即可保留 99% 的性能)且不会导致性能下降。