Technical information about the inner workings of Transport Fever entity component system (ecs).
Note: This pretty much my limited view on it, and even may change in the future!
For curious people like me, a lot of technical infos, please simple ignore this post if you don't want to look into game internals.
Entities
All game relevant stuff in TPF2 are handled in entities.
Each Entity has an numeric id. Since the api update, you have access to entity components via api.engine.getComponent.
ecs::components are structs, not classes. However they need to be copy able, so pretty much the act class like.
ecs::components are mostly PODs, so they don’t have actually code.
TPF(2) has relevant Systems to manipulate the necessary data.
If a tram moves around, there is a Move System handling that kind of entity. A line system for stop management and so on.
You can ask a lot of Systems to give information. See api.engine.*System
ecs::Engine and ecs::ComponentManager
The ecs::Engine and ecs::ComponentManager hold the game data,
if TPF2 changes something or need data it looks into the components of Entity, this is handled by these.
Most code assumes that the necessary data to be present, if not a it != it->components.end exception is a result.
When loading or saving, your game save data is handled by the component manager and it does game version difference handling. Upgrade savegames when loading.
As the structure needs to be known it's not easy to write an external savegame editor.
Flow of Savegame System is as this:
For each ecs::component:
SaveComponentData<struct ecs::component>Write()
Write each component data field via std io functions to savegame. There is no header, only the count of data in vectors are written.
The load functions my transform the data or link entity components in ways the game needs.
After loading, there are some fixups done, if a earlier game did something bad, or a savegame version fundamental changed something.
Game States
Each Game has at least two currently active GameStates with a own copy of the entity data. An own instance of ecs::Engine,
GameRes is shared across states, and shouldn't actively change. (That's a reason you can't write to api.res from UGs point of view)
There is a time frame where each System may actively allow to change the data.
Each entity component will be flagged if changed.
Every game state flip changes are copied, or you do a savegame as example, you simply use a GameState for saving, the game can continue running.
You can actively follow this, as game_scripts get there relevant load and save events.
If you mess with the lua state of you game script, you get an error, because TPF2 can't transfer your lua variable state from one GameState to the next.
If copying takes to long, or a gamestate is still is used by an industry developer or street expansion, you get micro stops in the game.
This system was actively changed in the performance updates and got several changes.
Technical you can't write to entities via lua api. You are allow to send api.cmds or influence decision by commands, if the command does a gamescript callback.
ecs::Engine internal workings:
Internally each component data is hold into an vector<struct ecs::component::something)
and the ecs::Engine, ecs::ComponentManager is responsible to manage lifetime.
A basic c++ code (that doesn't support high level revision system) straight from CommonAPI.
The typeidptr is actually a c++ std::type_index of a ecs::component::something
struct typeindexid2index {
int typeindexid;
int dataindex;
};
// This is actually a kind of vector struct, however the size changes between game compilation
struct Engine_m_entityComponents_entry {
ecs::typeindexid2index *components_start;
ecs::typeindexid2index *components_end;
__int64 dataIndex;
};
template<typename T> T* getComponentFromEntity(ecs::Engine* engine, int entityid, void* typeidptr) {
void* mytypeid = typeidptr;
int typeIndex = ecs::ComponentManagerFn::GetComponentTypeIndex(&engine->ComponentManager, &mytypeid);
auto components2typeindex = engine->m_entityComponents[entityid];
auto end = components2typeindex.components_end;
auto ptr = components2typeindex.components_start;
if (ptr == end) {
return 0;
}
while ( ptr->typeindexid != typeIndex) {
// Technical TPF2 does an iteration here, if it fails you get it != it->components.end exception,
// my code simply returns null without crashing.
++ptr;
if (ptr == end) {
return 0;
}
}
std::vector<T>* ptrdata = (std::vector<T>*)&engine->ComponentManager.m_componentDatas[typeIndex]->data;
return &(*ptrdata)[ptr->dataindex];
}
Alles anzeigen
Depending on compiler decision the entity iterator code is inlined. Means the compiler thinks for performance reasons it's worth to not call a function but use a copy of the code in place.
UI::CSelector
UI::CSelector is the execution root of UI user input, if something in the deeps of the game crashes, a catch Exception block will print the error to stdout.txt
You can think about the actually flow of the game like this:
UI::CSelector -> UI and GameScript running -> LUA -> your function maybe -> LUA -> api.* function -> c++ function throws exception,