这篇文章参考自从零实现ECS(entity component system),增加了些自己的理解。
通过简单的代码搞清楚ECS的实现逻辑。
所有的程序员都知道面向对象的编程模式,我们看下面向对象的实现有什么问题
假设一个游戏场景里有Dog、Platypus(鸭嘴兽)、Duck,继承关系如上图。
如果再有猎犬,猎犬又得继承自Dog的特性,如此,整个类层级结构就很容易膨胀、且变得难以维护。
解决面向对象层级结构的第一步,就是把"继承关系"重构成"组合"
ECS(Entity Component System)就是一种分拆、组合的实现。
面向对象的思想是把数据、行为都封装在一个类里,ECS有点像MVC,但是做的更彻底,边界划分的更清晰。
E:Entity,实体。一般用一个唯一值的int型表示,可以理解为一个ID。一个怪兽、一把枪,就是一个ID,至于怪兽的外观、血量、速度则是Component表示。
C:Component,组件,也可以理解为data。Component是个很泛的概念,所有的属性都能以Component的形式存在,如:怪兽的外观材质、速度、名字、位移、血量等
S:System,系统。类似MVC中的Controller,控制逻辑。可以按照Component的类别设计System种类
了解一门技术最好的方法,就是造一遍轮子
内容参考:[C++] An Entity-Component-System From Scratch
实现一个简单的逻辑,按键W、S、A、D控制小鸟上下左右移动:
完整代码:
https://github.com/ThoSe1990/sdl-ecs-example/tree/prototype
只有两个类game.hpp、main.cpp,一共不到250行代码,非常容易懂。
输入和界面用SDL库。SDL的基本使用比较简单,此处不做展开。
第一步实现整个游戏框架结构,设计Game类和main中更新的逻辑。
class game
{
public:
game() {/* SDL details here to create a window */}
~game() {/* SDL details to cleanup allocated memory */}
// this we'll use in our mainloop to check if our game is running
bool is_running() { return m_is_running; }
// read keyboard / mouse input
void read_input()
{
// read sdl events
SDL_Event sdl_event;
SDL_PollEvent(&sdl_event);
// get all keys here
const Uint8* keystates = SDL_GetKeyboardState(NULL);
// quit / close our window by pressing escape
if (keystates[SDL_SCANCODE_ESCAPE]) {
m_is_running = false;
}
}
void update() {/* here we'll update all components*/}
void render(){/* here we'll render*/}
private:
// our members for now ...
std::size_t m_width;
std::size_t m_height;
SDL_Window* m_window;
SDL_Renderer* m_renderer;
bool m_is_running = true;
};
// ....
// which will create a window by running this main
// and closes the window by pressing escape
int main(int argc, char* argv[])
{
// create a game on a 800x600 window
cwt::game game(800, 600);
while(game.is_running())
{
game.read_input();
game.update();
game.render();
}
return 0;
}
Entity最简单,就是唯一的id,这里用自增的逻辑实现
实际项目中可能会涉及到删除entity处理,更复杂点
using entity = std::size_t;
entity max_entity = 0;
std::size_t create_entity()
{
static std::size_t entities = 0;
++entities;
max_entity = entities;
return entities;
}
这个demo中,小鸟只有三个属性
数据结构可以设计为:
struct sprite_component{
SDL_Rect src;
SDL_Rect dst;
SDL_Texture* texture;
};
struct transform_component{
float pos_x;
float pos_y;
float vel_x;
float vel_y;
};
struct keyinputs_component{
};
另外,还需要增加registry类,用来存放实际的component数据,此处用三个map存放
struct registry {
std::unordered_map sprites;
std::unordered_map transforms;
std::unordered_map keys;
};
三个component对应三个system,比较好理解。
struct sprite_system
{
void update(registry& reg)
{
for (int e = 1 ; e <= max_entity ; e++) {
if (reg.sprites.contains(e) && reg.transforms.contains(e)){
reg.sprites[e].dst.x = reg.transforms[e].pos_x;
reg.sprites[e].dst.y = reg.transforms[e].pos_y;
}
}
}
void render(registry& reg, SDL_Renderer* renderer)
{
for (int e = 1 ; e <= max_entity ; e++) {
if (reg.sprites.contains(e)){
SDL_RenderCopy(
renderer,
reg.sprites[e].texture,
®.sprites[e].src,
®.sprites[e].dst
);
}
}
}
};
struct transform_system
{
float dt = 0.1f;
void update(registry& reg)
{
for (int e = 1 ; e <= max_entity ; e++) {
if (reg.transforms.contains(e)){
reg.transforms[e].pos_x += reg.transforms[e].vel_x*dt;
reg.transforms[e].pos_y += reg.transforms[e].vel_y*dt;
}
}
}
};
struct movement_system
{
void update(registry& reg)
{
const Uint8* keys = SDL_GetKeyboardState(NULL);
for (int e = 1 ; e <= max_entity ; e++) {
if (reg.transforms.contains(e) && reg.keys.contains(e)){
if (keys[SDL_SCANCODE_A]) { reg.transforms[e].vel_x = -1.0f; }
if (keys[SDL_SCANCODE_S]) { reg.transforms[e].vel_y = 1.0f; }
if (keys[SDL_SCANCODE_W]) { reg.transforms[e].vel_y = -1.0f; }
if (keys[SDL_SCANCODE_D]) { reg.transforms[e].vel_x = 1.0f; }
if (!keys[SDL_SCANCODE_A] && !keys[SDL_SCANCODE_D]) { reg.transforms[e].vel_x = 0.0f; }
if (!keys[SDL_SCANCODE_S] && !keys[SDL_SCANCODE_W]) { reg.transforms[e].vel_y = 0.0f; }
}
}
}
};
#include "game.hpp"
#include "bird.hpp"
int main(int argc, char* argv[])
{
// 创建游戏窗口 800 * 600
cwt::game game(800, 600);
// 创建第一只小鸟,
cwt::entity bird_1 = cwt::create_entity();
// 增加第一只小鸟的外观实体
game.get_registry().sprites[bird_1] = cwt::sprite_component {
SDL_Rect{0, 0, 300, 230},
SDL_Rect{10, 10, 100, 73},
IMG_LoadTexture(game.get_renderer(), bird_path)
};
// 第一只小鸟有移动、响应键盘的属性,增加对应的transforms、keys属性
game.get_registry().transforms[bird_1] = cwt::transform_component { 10, 10, 0, 0};
game.get_registry().keys[bird_1] = cwt::keyinputs_component { };
// 创建第二只小鸟
cwt::entity bird_2 = cwt::create_entity();
// 增加第二只小鸟的外观实体
game.get_registry().sprites[bird_2] = cwt::sprite_component {
SDL_Rect{0, 0, 300, 230},
SDL_Rect{0, 0, 100, 73},
IMG_LoadTexture(game.get_renderer(), bird_path)
};
// 第二只小鸟有transforms组件,没有键盘响应的component,静止
game.get_registry().transforms[bird_2] = cwt::transform_component { 10, 500, 0.01f, -0.01f};
// 第三只小鸟只有sprit组件
cwt::entity bird_3 = cwt::create_entity();
game.get_registry().sprites[bird_3] = cwt::sprite_component {
SDL_Rect{0, 0, 300, 230},
SDL_Rect{200, 300, 100, 73},
IMG_LoadTexture(game.get_renderer(), bird_path)
};
while(game.is_running())
{
// 交互:处理输入逻辑
game.read_input();
// 处理更新
game.update();
// 渲染上屏
game.render();
}
return 0;
}
read_input、update、render逻辑实现
void read_input()
{
SDL_Event sdl_event;
SDL_PollEvent(&sdl_event);
const Uint8* keystates = SDL_GetKeyboardState(NULL);
if (keystates[SDL_SCANCODE_ESCAPE] || sdl_event.type == SDL_QUIT) {
m_is_running = false;
}
}
void update()
{
m_transform_system.update(m_registry);
m_movement_system.update(m_registry);
m_sprite_system.update(m_registry);
}
void render()
{
SDL_RenderClear(m_renderer);
m_sprite_system.render(m_registry, m_renderer);
SDL_RenderPresent(m_renderer);
}
原文作者还写了一篇,基于开源框架的Entt改造Demo的文章
https://www.codingwiththomas.com/blog/use-entt-when-you-need-an-ecs
[C++] An Entity-Component-System From Scratch
Unity教程Entity Component System
Specs and Legion, two very different approaches to ECS
欢迎关注公众号:sumsmile /专注图形学