AmigaPorts / AmigaPorts/ACE

Add entity/component system?

未关闭
#154 2 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
enhancement help wanted question
主要语言
C
星标
223
派生
38
平均合并
1 天 5 小时
30 天内合并 PR
6

描述

I'm writing this as a reaction to [Nivrig](https://github.com/johngirvin)'s and [Kwahu](https://github.com/kwahu)'s discussion on Amiga Game Dev discord. Some points of reference for ECS systems:

- https://github.com/SanderMertens/flecs
- https://github.com/soulfoam/ecs

# The unity-like gameobject system

This thing is better written in OOP languages, so it would need https://github.com/AmigaPorts/ACE/issues/153

Basically the thing used by Unity and most game engines nowadays. The most basic ECS is something like this:

```cpp
for(AceObject Entity: Entities) {
for(AceBehavior Component: Entity.Components) {
Component.process();
}
}
```

along with constructors and destructors for them. Now, all ACE managers are already written to have create/process/destroy fns and are sometimes nested and cross-referencing each other so they could be converted to such components.

This is my quick and dirty proposal, I'm not a big fan of it at this point, so it definitely needs more work and thoughts. There are some problems to solve and I'd like to hear some more input about it.

## Components of components

If you look at the ACE view/viewport system, it already looks like ECS. Each view has viewports and are processed on the list, so they could be treated as view's components. Then, each viewport has viewport managers - be it camera, simplebuffer, etc. So that would mean that... each viewport can be treated as an entity and have the viewport managers as components. This deep nesting breaks ECS pattern, so something else would be needed.

Instead, it could be solved by limiting it to component system. That means, ACE would expose primitives to manage and process list of components. This way, each class could have its components and those could be nested indefinitely. The code could look something like this:

```cpp
void insideGameState() {
view.process();
}

void tView::process() {
// do your own stuff - as little as possible, preferably everything is in components
components.process(); // this processes the list of components (here: viewports), calling their process() method
}

// the same pattern is repeated for tVPort::process() and processes viewport managers
```

then, the view could be a component of the game state and insideGameState() could be just done with `gameState.components.process()`.

The `tComponentList` could expose following functionality:

- processing the list of components - just calling their `process()` method
- `getComponentById()` where `id` is an unique string or enum or int - could be sped up by using `std::map` instead of plain array or `std::vector` or having both for those two access types, or make it use a vector but emphase that searching for references is slow and should be done in the component constructor or post-construct step
- `getComponentByType(lastSearchPos, type)` - the `tSimpleBufferManager` looks for already present `tCameraManager` and creates the new one if it isn't found. Unity ECS enforces components to have unique type, but since my proposal skips entity lists, we'd have to work around this limitation, so making this function re-entrant to get next result is a must.
- perhaps there should be the way to get the component of the parent's parent etc. - e.g. viewport manager should be able to access the view, but that could be done by the vport fields and holding the ref to the parent inside the component. Viewport manager could get the `tVPort &` arg in its constructor and work from there to the view on its own.

## The post-construct step

Finding reference to other components brings another problem - when done in constructors, it will only work with already-constructed components. There would have to be another step to doing post-init jobs like finding references. This could be skipped by making ref search inside `process()` function very fast, but perhaps it would expose more unexpected problems of same nature later on.

Perhaps the component constructors should to as little as possible and have the dedicated `Construct()` method which would be called after all lists are filled.

## Zero-cost abstractions

The first thing which comes to mind is that `tComponentList` should hold something following and just call the `process()` method:

```cpp
class tComponent {
virtual void process(void);
virtual ~tComponent(void);
};
```

although deriving all components from such class allows for putting different component types into same list, this would introduce some overhead in scenarios where they are all the same (think of viewports inside a view):

- each object is bigger because it has to store the pointer to its virtual method table,
- it heavily obstructs compiler's ability to deduce that `process()` functions are all the same and can be inlined and optimized further

The solution would be to have the whole component system templated:

```cpp
// require via concept that t_tComponent has process() method, I don't remember the syntax
template
class tComponentList {
std::map m_mComponents;
};

// no virtual thingies here
class tVPort {
void process(void);
};

// inside tView class:
tComponentList m_VPorts;
```

this would result in calling a same `process()` function in a loop with different objects passed in an arg - perhaps even giving optimization opportunity to unroll it. Which shows that maybe even component storage could be passed to the template - either use `std::vector` or `std::array` of predefined size for extra optimizing lists of constant size.

## Order of execution is crucial

Currently, view must execute viewports in order of their appearance. Also, there is a strict order for viewport managers execution or else everything explodes. Also, when writing gamestates for most of the games, it quickly becomes apparent that most operations must be done in strict order, so component list must be ordered. They probably could store an ordinal number and be sorted with each addition of component, so that the process() wouldn't have to waste time on determining the order of execution. ACE could define them in hundred- or thousand-increments to allow users adding custom logic in between.

That solves the display, input and other core components, but doesn't solve the game logic components, should ACE bundle them. Their order may vary between projects. Perhaps those should accept ordinal number as a parameter in constructor but that adds a bit of tedious manual management.

Another option would be setting the components when instantiating the list, e.g.

```cpp
// components here are in correct order of execution
components = new tComponentList({
new Component1(),
new Compontent2(),
new Compontent3()
});
```

but that prevents instantiating additional components by others (e.g. `tSimplebuffer`/`tScrollbuffer` instantiates `tCamera`). Perhaps someone would like to have enemies or other entities dynamically spawned and managed as a list of components. What then?

## Changing which components are executed and reordering them

Some components may need to be occasionally disabled (enemy component is dead, projectile has ended its lifespan and is ready for reuse). Skipping components which have `m_isDisabled` set to true is a one way to solve this, but having lots of those lists introduces significant overhead in checking if any of them should be skipped. Perhaps there should be a list of enabled components acting as a cache, but updating it should be done on-demand and e.g. once per frame.

Also, in case someone is mad enough to do single-buffered game and have the blittable entities as components, there should be a way to Y-sort the relevant component list so that blits are always done from top to bottom.

## Forcing a code layout

This one is big. Shifting ACE into component system would enforce that code layout on all projects instead of allowing each project to have their own less or more hardcoded systems. I don't know how I feel about taking away that degree of freedom from ACE users. This would drastically shift ACE from being a mostly-library framework-ish to being full-blown engine/framework.

## Blitter concurrency

I thought this might be a problem, but then if the components are in fixed order then it is possible to have blits scattered across the code, preventing prolonged blitter idle times. Other blitter management methods exist, but I prefer to not prevent any of them as ACE should be as versatile and not limiting tool as possible.

贡献指南

这个仓库没有索引到贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。