- 游戏开发
【免费下载链接】flecs
A fast entity component system (ECS) for C & C++
Flecs Script 是 Flecs(一个面向 C 与 C++ 的实体组件系统)内置的运行时解释型 DSL,专门用于以声明方式创建实体、层级与组件,适合搭建场景、制作资产与编写配置文件。本教程将带你从 Explorer 的空白画布开始,逐步用纯脚本构建一堵可复用的参数化围栏资产:先掌握实体、组件、变量与with作用域等基础语法,再依次封装为 Prefab、模板(Template),并用Grid组件与嵌套模板将资产组合成可调尺寸的封闭围场。读完本文,你将能独立用 Flecs Script 编写、运行、参数化并复用你自己的 ECS 场景资产,并能在 C/C++ 代码中加载与实例化它们。
说明:本教程的教学环境是 Flecs Explorer 与 Playground(渲染画布 + 预置模块),与运行时使用同一套 Flecs Script 语法。关于脚本语言在当前版本中的完整手册,可参考 docs/FlecsScript.md。
一、Flecs Script 是什么
Flecs Script 是一种声明式语言,让你无需编写并编译 C/C++ 代码即可创建实体。它可以用于不同目的,例如构建场景(scenes)、资产(assets),或作为配置文件(configuration files)的语言。在 Flecs 生态中,脚本对 ECS 的意义类似于 HTML/JSX 对浏览器:它以文本形式描述实体、层级、组件与关系,并在运行时被解释执行,原生集成了 Flecs 的反射(meta)、预制体(prefab)、层级(hierarchy)与关系(relationships)等特性。
从 docs/FlecsScript.md 可以看到它支持的能力清单:
- 命名实体、层级与继承的原生支持;
- 为组件赋值;
- 表达式与变量(如
var + 10); - 条件与循环(如
if var > 10、for i in [0..10]); - 与模板(procedural assets)的原生集成。
使用前提:应用需要使用FLECS_SCRIPTaddon 构建才能使用 Flecs Script。脚本在源码中的入口 API 定义在 include/flecs/addons/script.h,包括ecs_script_run()、ecs_script_run_file()、ecs_script_parse()/ecs_script_eval()以及托管脚本ecs_script_init()等。
二、环境准备:在 Explorer 与 Playground 中运行脚本
Flecs Explorer 是一个基于 Web 的应用,让我们编写脚本并直接看到结果。教程将 Explorer 与 Flecs playground 组合使用——playground 自带渲染画布,并预加载了多个模块与资产。
打开 Explorer 与 playground:
https://www.flecs.dev/explorer/?wasm=https://www.flecs.dev/explorer/playground.js页面打开后类似这样:
页面布局由三部分组成:
- 左侧:实体树视图(entity treeview),显示场景中所有实体;
- 中间:画布(canvas),显示场景中可渲染的实体;
- 右侧:编辑器(editor),用于编写 Flecs 脚本。
任何时候都可以点击面板右上角的"x"关闭面板;被关闭的面板可以通过左侧菜单栏中的按钮重新打开。另一个重要的控件是屏幕右上角的链接按钮(link button),用它可以把当前内容生成一个链接,是保存进度或与他人分享成果的好方式。
三、基础语法:实体、组件与层级
3.1 创建实体与标签
当前 Explorer 显示的是默认场景,先把编辑器中的代码全部删掉,你会看到一个空画布(空画布截图)。接着在编辑器中输入实体名:
my_entity输入时实体就会出现在左侧树视图中。Flecs 脚本的规则是:实体如果不存在会自动创建。试试输入两次同名实体:
my_entity my_entity树视图中只会出现一个实体——第二次解析my_entity时它已经存在,因此无需任何操作。这与 examples/script/hello_world.flecs 中的行为一致:脚本声明实体的语法本身就是"创建或引用"。
3.2 添加标签与作用域
为实体添加一个名为SpaceShip的标签:
my_entity :- SpaceShip注意:我们不必预先显式声明SpaceShip,它会自动作为实体出现在树视图中。可以这样给实体添加多个标签:
my_entity :- SpaceShip my_entity :- FasterThanLight为了避免反复写实体名,可以用{}为实体开启一个作用域。在作用域内,用-前缀列出组件的写法:
my_entity { - SpaceShip - FasterThanLight }点击树视图中的实体可以打开实体检查器(entity inspector)查看其内容。可以看到SpaceShip和FasterThanLight标签都显示出来,此外还有一个Script: main标签——这是 Flecs 用来跟踪"哪些实体由我们的脚本创建"的标记。
3.3 添加组件:带值的标签
添加组件与添加标签类似,只是多了一个值。playground 预加载了flecs.components.transform模块,下面添加其中的Position3组件:
my_entity { - SpaceShip - FasterThanLight - flecs.components.transform.Position3{1, 2, 3} }每次都要输入完整模块名很麻烦,可以在脚本顶部添加:
using flecs.components.*之后就可以直接写Position3{1, 2, 3}。添加Position3后,检查器中还会出现Transform和WorldCell组件——这是因为 playground 导入的模块实现了世界分区与变换,使用Position3组件会连带获得这些能力。
组件也可以是标量直接赋值(详见 docs/FlecsScript.md),例如Mass: 100、Mass: 50 + 50、Mass: $weight。
3.4 关系对与子实体层级
除了组件与标签,还可以给实体添加关系对:
- (OwnedBy, Player)实体还可以创建为层级。子实体在父实体作用域内创建,写法与组件/标签的区别是不带-前缀:
cockpit { pilot :- (Faction, Earth) }展开树视图中的my_entity即可看到这个层级。至此你已经掌握了创建实体、层级、添加组件与标签的全部基础,接下来让实体在画布上可见。
四、绘制形状:渲染所需的组件组合
渲染器使用常规的 ECS 查询(query)来寻找要渲染的实体。要让实体进入这些查询,它至少需要以下三个组件:
Position3Rgb(颜色)Box或Rectangle
先从画地面开始。删掉编辑器里除using flecs.components.*之外的所有代码,然后添加:
plane { - Position3{} Rectangle: {100, 100} - Rgb{0.9, 0.9, 0.9} }画面上出现了东西,但看起来不太对(错误朝向的地面)——矩形旋转方向不对。修复方式是把它在 x 轴上旋转 90 度(即π/2弧度)。先在脚本中把π定义为常量:
const PI = 3.1415926再在plane作用域中添加:
- Rotation3{$PI / 2}现在地面方向正确了(正确的地面)。把平面边长加大到10000,雾效会让它和背景融合,形成地平线的错觉:
plane { - Position3{} - Rotation3{$PI / 2} - Rectangle{10000, 10000} - Rgb{0.9, 0.9, 0.9} }注意:PI变量不会出现在树视图中——变量不会创建实体,只存在于脚本上下文中。这与 docs/FlecsScript.md 中"变量用const关键字创建,可用于重复出现的值"的描述一致。
接着添加一个立方体:
box { - Position3{} Box: {10, 10, 10} Rgb: {1, 0, 0} }盒子与地面相交了(与地面相交的立方体)。通过设置Position3的y成员为它高度的一半来抬升它:
box { - Position3{y: 5} Box: {10, 10, 10} Rgb: {1, 0, 0} }整个立方体现在完全可见。相机操作提示:先点击画布使其获得焦点,即可用 WASD 键移动相机;再次点击画布释放焦点(绿色边框消失),焦点交还给 Explorer。
五、变量与with语句:消除魔法数字
绘制围栏只需要把不同尺寸的盒子组合起来。先删掉立方体的代码。
5.1 用with统一添加组件
如果每个实体都添加相同值的颜色组件,改颜色时会非常繁琐。使用with语句可以避免重复:
with Rgb{0.15, 0.1, 0.05} { // Boxes go here }在with作用域内创建围栏左右两根柱子:
with Rgb{0.15, 0.1, 0.05} { left_pillar { - Position3{x: -10, y: 5} Box: {2, 10, 2} } right_pillar { - Position3{x: 10, y: 5} Box: {2, 10, 2} } }效果见两根柱子。with语句是 docs/FlecsScript.md 中正式支持的特性:它可以把相同组件/标签应用到其作用域内的所有实体,等价于为每个实体单独添加。with还支持多个标签(with SpaceShip, HasWeapons)、带括号的组件值(with Color(38, 25, 13))以及带$前缀的变量(with $color)。
5.2 用变量重构
上面的代码有大量魔法数字,难以维护。先定义颜色与柱子形状两个变量——复合值需要显式声明类型:
const color: Rgb = {0.15, 0.1, 0.05} const pillar_box: Box = {2, 10, 2}进一步让柱子形状也参数化:
const height = 10 const color: Rgb = {0.15, 0.1, 0.05} const pillar_width = 2 const pillar_box: Box = { $pillar_width, $height, $pillar_width }再加一个表示围栏宽度(两根柱子间的距离)的width变量:
const width = 20现在柱子的代码可以这样写:
with $color, $pillar_box { left_pillar { - Position3{x: -$width/2, y: $height/2} } right_pillar { - Position3{x: $width/2, y: $height/2} } }这会重建出完全相同的场景,但现在可以通过调整变量来改变围栏形状(试试不同取值的效果,见修改变量后的柱子)。在 Flecs Script 中,$前缀用于引用变量,避免变量名与实体名冲突(详见 docs/FlecsScript.md 的 Variables 一节)。
5.3 添加横杆
只有柱子还不够。先加一根横杆看看效果:
bar { - Position3{y: $height / 2} Box: {$width, 2, 1} - $color }效果见两根柱子和一根横杆。再加第二根横杆,并注意两个实体名字要不同,否则会覆盖之前的实体值:
top_bar { - Position3{y: $height/2 + 2} Box: {$width, 2, 1} - $color } bottom_bar { - Position3{y: $height/2 - 2} Box: {$width, 2, 1} - $color }现在更像围栏了(简易围栏)。继续消除冗余:定义横杆间距、横杆高度与深度,让横杆厚度随柱子宽度缩放:
const bar_sep = 4 const bar_height = 2 const bar_depth = $pillar_width/2 const bar_box: Box = {$width, $bar_height, $bar_depth}清理横杆实体代码:
with $color, $bar_box { top_bar { - Position3{y: $height/2 + $bar_sep/2} } bottom_bar { - Position3{y: $height/2 - $bar_sep/2} } }围栏基本成型且完全由变量驱动。代码变长后可以用注释保持条理:
// Create two bar entities with $color, $bar_box { top_bar { - Position3{y: $height/2 + $bar_sep/2} } bottom_bar { - Position3{y: $height/2 - $bar_sep/2} } }当前代码仍有不足:
- 代码没有打包成易于复用的形式;
- 围栏拉宽或拉高后会显得不自然,理想情况下柱子与横杆数量应随尺寸增加。
六、Prefab:把资产打包复用
想要两堵围栏,复制全部代码不现实。解决方案是把现有代码打包进一个prefab(预制体)。只需在围栏代码外围加上:
Prefab Fence { // fence code goes here }这会创建一个带Prefab标签的实体Fence,并把此前创建的所有实体存为Fence的子实体。它等价于下面这段代码,只是更便捷:
Fence { - Prefab // fence code goes here }把围栏实体放进 prefab 作用域后你会注意到:
- 围栏从画布上消失了;
- 围栏实体在树视图中出现在
Fence之下(见围栏 prefab 层级)。
围栏消失的原因是:Prefab是一个内建标签,默认会被查询忽略,渲染器因此也不渲染它;prefab 的子实体也会成为 prefab,同样被忽略。
6.1 继承并实例化 Prefab
现在可以创建一个继承自Fenceprefab 的实体:
my_fence : Fence围栏回来了!而且可以实例化两次,只需给两个实例不同的位置:
fence_a : Fence { - Position3{-10} } fence_b : Fence { - Position3{10} }这里的:记法本质是添加IsA关系(my_spaceship : SpaceShip等价于my_spaceship { (IsA, SpaceShip) },见 docs/FlecsScript.md 的 Inheritance 一节)。examples/script/prefabs.flecs 也展示了同族写法:prefab SpaceShip {...}、prefab Freighter : SpaceShip {...}、my_spaceship : Freighter {...}。
6.2 在 C/C++ 中加载脚本并实例化
可以把脚本保存为fence.flecs,加载进游戏后从常规 C/C++ 代码实例化 prefab:
// In C ecs_script_run_file(world, "fence.flecs"); ecs_entity_t fence = ecs_lookup(world, "Fence"); ecs_entity_t fence_a = ecs_new_w_pair(world, EcsIsA, fence); ecs_entity_t fence_b = ecs_new_w_pair(world, EcsIsA, fence); ecs_set(world, fence_a, EcsPosition3, {-10}); ecs_set(world, fence_b, EcsPosition3, {10});// In C++ using namespace flecs::components::transform; ecs_script_run_file(world, "fence.flecs"); auto fence = world.lookup("Fence"); auto fence_a = world.entity().is_a(fence); auto fence_b = world.entity().is_a(fence); fence_a.set<Position3>({-10}); fence_b.set<Position3>({10});ecs_script_run_file()等脚本运行 API 的完整签名见 include/flecs/addons/script.h:除了从文件运行,还可以用ecs_script_run()直接运行代码字符串,或用ecs_script_parse()+ecs_script_eval()多次运行同一脚本,以及用ecs_script_init()创建可追踪实体归属的托管脚本。
不过这里仍有遗留问题,将在下一节解决:
- prefab 是静态的,无法改变围栏参数;
- 围栏尺寸增大时,柱子和横杆数量不会增加。
七、模板:可参数化的资产
Prefab 是可以多次实例化的静态实体与组件集合;模板(template)则是可以参数化的 prefab 组合。换句话说,模板让我们在创建围栏的同时指定宽度和高度。
把 prefab 改成模板只需一行:
template Fence { // fence code }编辑器会报错:
template 'Fence' has no properties含义是:与 prefab 不同,模板需要 properties(属性),它们是模板的"输入"。对我们而言,输入可以是width和height值。把部分const变量改为prop即可对外暴露为属性,并且属性必须显式声明类型。习惯上把 props 放在模板代码顶部,便于一眼看出它的输入:
template Fence { prop width: f32 = 20 prop height: f32 = 10 prop color: Rgb = {0.15, 0.1, 0.05} // fence code }改为模板后围栏又从画布消失了——因为对 prefab 是"继承",而对模板是"赋值"。让围栏重新可见,把实例化代码改成:
fence_a { - Fence{} - Position3{-10} } fence_b { - Fence{} - Position3{10} }围栏以 props 提供的默认值回来了。现在有了新能力——为两个实例传不同的参数:
fence_a { Fence: {width: 10, height: 20} - Position3{-10} } fence_b { Fence: {width: 25, height: 10} - Position3{10} }注意给模板赋值与给组件赋值看起来多么相似——这不是巧合:模板会被翻译成一个组件,每个属性成为该组件的一个成员。这一点从源码级测试可以得到印证:test/script/src/Template.c 中的Template_template_prop测试用prop height: flecs.meta.f32 = 0定义一个模板后,断言Tree实体上出现EcsStruct组件,且其成员数量为 1、成员名为height、类型为f32——模板即组件、属性即成员的机制被测试直接验证。
7.1 在 C/C++ 中像赋值组件一样使用模板
利用"模板即组件"的特性,可以在 C/C++ 中让模板的赋值像普通组件赋值一样简单:
// In C typedef struct { float width; float height; EcsRgb color; } Fence; // Register a regular component ECS_COMPONENT(world, Fence); // Because the Fence template has the same name as the // component it will "bind" to it. ecs_script_run_file(world, "fence.flecs"); // Set the component as usual ecs_entity_t fence_a = ecs_insert(world, ecs_value(Fence, {10, 20})); ecs_entity_t fence_b = ecs_insert(world, ecs_value(Fence, {25, 10})); ecs_set(world, fence_a, EcsPosition3, {-10}); ecs_set(world, fence_b, EcsPosition3, {10});// In C++ using namespace flecs::components::transform; struct Fence { float width; float height; flecs::components::graphics::Rgb color; } // Because the Fence template has the same name as the // component it will "bind" to it. ecs_script_run_file(world, "fence.flecs"); auto fence_a = world.entity().set<Fence>({10, 20}); auto fence_b = world.entity().set<Fence>({25, 10}); fence_a.set<Position3>({-10}); fence_b.set<Position3>({10});要使"同名绑定"生效,必须保证脚本中属性的类型与顺序,和组件类型中成员的类型与顺序完全一致。关于模板的更多细节(prop默认值推导、mut可变属性、嵌套模板组合等),参见 docs/FlecsScript.md 的 Templates 一节。
八、Grid:让柱子与横杆数量随尺寸变化
现在解决"数量随尺寸增长"的问题。乍看这需要循环,而 Flecs Script 不支持循环;但有一个绕行方案:使用Grid组件替我们完成实例化。
Grid组件由flecs.game模块提供,使用前先在脚本顶部添加:
using flecs.gameGrid 组件可以按 1、2 或 3 维网格创建实体的多个实例。把它应用到柱子数量上,让柱子数随围栏宽度缩放。我们需要两样东西:围栏宽度(已有)和柱子最小间距:
const pillar_spacing = 10据此计算柱子数量:
const pillar_count = $width / $pillar_spacingGrid组件不能直接消费柱子代码,它接受一个 prefab。把现有柱子代码改成 prefab:
Prefab Pillar { - $color - $pillar_box }注意这里去掉了Position3,因为位置将由Grid组件设置。然后创建一排柱子:
pillars { - Position3{y: $height/2} - Grid{ x.count: $pillar_count x.spacing: $pillar_spacing prefab: Pillar } }这段代码的含义:
- 创建一个
pillars实体; - 把它放在
y: $height/2位置,柱子才不会陷进地面; - x 轴上排布
$pillar_count根柱子; - 间距为
$pillar_spacing; - 用
Pillarprefab 实例化实体。
把实例化代码改成一个围栏,并把width默认值加大到60:
fence :- Fence{}prop width: f32 = 60现在柱子数量与围栏长度匹配了(多柱围栏)。但还有一点不对:柱子没有与围栏两端精确对齐。用一个简单计算修正:根据柱子数量反推网格间距。
const grid_spacing = $width / ($pillar_count - 1)这个公式考虑了"柱子数量比柱子间空隙数量多一"的事实。把网格中的pillar_spacing换成grid_spacing后,柱子对齐正确(对齐后的围栏)。
横杆可以用完全相同的方法处理,只是方向换成 y 轴,这里直接给出结果(多柱多杆围栏):
// Bar parameters const bar_spacing = 3 const bar_height = 2 const bar_depth = $pillar_width / 2 const bar_count = $height / $bar_spacing const b_grid_spacing = $height / $bar_count // Bars bars { - Position3{y: $height/2} - Grid{ y.count: $bar_count, y.spacing: $b_grid_spacing prefab: Bar } }其中Bar同样用with Prefab, $color { Bar :- Box {...} }的形式定义。为了在放大时更美观,各参数取值被微调过,你可以自由调整变量得到自己喜欢的效果。参考 docs/FlecsScript.md 的 Component values 一节,脚本还能直接读取其他实体上的组件值(如Grid: { Game[Level].width, Game[Level].depth }),进一步把布局数据外置到配置实体上。
九、嵌套模板:组合出封闭围场
围栏模板已经就绪,接下来构建一个更高层级的模板来复用Fence——一个典型的做法是实例化 4 次Fence,围出一块封闭空间。这部分不涉及任何新语法。
在脚本底部新建模板,提供width和depth属性定义矩形区域,并加一个color和height属性透传给Fence模板:
template Enclosing { prop width: f32 = 40 prop height: f32 = 10 prop depth: f32 = 40 prop color: Rgb = {0.15, 0.1, 0.05} // enclosing code goes here }实例化 4 次Fence,分别对应矩形的四个边。加几个便捷变量,避免重复写同样的除法:
const width_half = $width / 2 const depth_half = $depth / 2 const PI = 3.1415926 left { - Position3{x: -$width_half} - Rotation3{y: $PI/2} Fence: {width: $depth, height:$, color:$} } right { - Position3{x: $width_half} - Rotation3{y: $PI/2} Fence: {width: $depth, height:$, color:$} } back { - Position3{z: -$depth_half} Fence: {width: $width, height:$, color:$} } front { - Position3{z: $depth_half} Fence: {width: $width, height:$, color:$} }注意透传参数写成了height:$——这是height: $height的简写记法,在处理嵌套模板时可以省去大量输入。这种简写同样适用于一般复合初始化的成员赋值(Tree: {color: $, height: $}),在 docs/FlecsScript.md 的 Initializers 一节有正式说明。
把实例化代码从fence :- Fence{}改成:
enclosing :- Enclosing{}效果如下。可能需要用相机拉远一点才能看到完整的围栏(点画布获得焦点后,用 WASD 移动相机;再点一次画布把焦点还给 Explorer):
现在可以非常方便地通过参数修改围场:
enclosing { Enclosing: {width: 100, height: 30} }十、总结与后续探索
至此你已经掌握了用 Flecs Script 创建资产的完整路径:从基础实体与组件语法,到变量与with作用域,再到 Prefab 打包、模板参数化、Grid程序化排布以及嵌套模板组合。围栏最终可以被width、height、depth、color等参数完整驱动,这正是"声明式 + 参数化"资产管线的核心工作方式。
想继续深入,可以从以下方向入手:
- 查阅当前仓库中的脚本语言手册 docs/FlecsScript.md,掌握表达式运算符、
if/for控制流、字符串插值、match表达式、函数与方法、struct/enum/bitmask类型定义、module/include/using语句等完整语法; - 阅读脚本 API 头文件 include/flecs/addons/script.h,了解
ecs_script_run、ecs_script_run_file、ecs_script_parse/eval、托管脚本ecs_script_init、外部函数ecs_function与向量函数注册等编程接口; - 运行仓库自带的脚本示例,如 examples/script/hello_world.flecs(实体/层级/关系)、examples/script/prefabs.flecs(prefab 继承链)、examples/script/expressions.flecs(变量与表达式)以及 examples/script/with.flecs(
with语句); - 阅读脚本模板的测试用例 test/script/src/Template.c,通过断言理解模板到组件的转换细节(属性即成员、同名绑定等)。
在 Explorer 中,还可以尝试实例化 playground 预载的资产之一:把编辑器内容替换为下面的脚本,生成一座小镇:
using flecs.components.* using templates const PI = 3.1415926 plane { - Position3{} - Rotation3{$PI / 2} Rectangle: {10000, 10000} - Rgb{0.9, 0.9, 0.9} } town :- Town{}现在,去创建属于你自己的可参数化资产吧。
- 游戏开发
【免费下载链接】flecs
A fast entity component system (ECS) for C & C++
相关推荐
Raycast Script Commands实战:从零构建你的第一个脚本命令
Raycast Script Commands实战:从零构建你的第一个脚本命令 本文详细介绍了Raycast Script Commands的开发实践,从基础模
开发工具桌面应用Flecs 开源项目教程
Flecs 开源项目教程 项目介绍 Flecs 是一个快速、灵活的实体组件系统(ECS),专为 C 和 C++ 设计。ECS 是一种设计模式,广泛应用于游戏开发
游戏开发终极Script Kit教程:从零开始构建你的第一个自动化脚本
终极Script Kit教程:从零开始构建你的第一个自动化脚本 Script Kit是一款强大的自动化工具,能够帮助你轻松实现各种任务的自动化处理。无论你是编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考