Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BodyComponent system #5384

Merged
merged 13 commits into from
Jun 26, 2022
Merged

Conversation

sturnclaw
Copy link
Member

@sturnclaw sturnclaw commented Jun 15, 2022

Fixes #5385.

This PR introduces something I've had sitting in a work branch for several months now: a move towards a composition-based object model rather than a strict inheritance model.

I've added fully-serialized C++ components that can be queried and composed at runtime; other than a need to register a serializer in some sort of centralized fashion the composition functionality is usable at any time by any piece of code without needing to know the concrete type of the body object it is operating on. The ship FixedGuns and Propulsion variables have been migrated to use this new system, although some further work is required to handle querying these components from Lua in a sane manner.

I've also added the ability for Lua to associate arbitrary tables with Body objects as components - also fully serialized - and query them at runtime. This allows for more fully-featured data ownership compared to using a PropertyMap or maintaining a separate lookup table keyed by the object. I intend to migrate the EquipSet functionality to use this LuaComponent feature at some point, though that will likely involve a refactor or rewrite of the equipment handling at the same time.

There is not currently any event handling for LuaComponents when the owning Body object is deleted, though that's something that can be implemented fairly easily if needed. Persisting LuaComponents across body destruction (e.g. when leaving and re-entering a system) is something that will still need to be done manually at a higher level of abstraction.

I know this PR is heavy on implementation and light on use-cases, but it makes moving code to a modular, composition-based approach much easier moving forwards and is already used internally by several of my work branches.

The API related to these two features is quite simple; for C++ objects you need to register a component type, optionally register its serializer, and then just add a new component to the body:

struct MyComponent {
    MyComponent();
    void SaveToJson(Json &out, Space *space);
    void LoadFromJson(const Json &obj, Space *space);
};

void SomeRegisterFunc() {
    BodyComponentDB::RegisterComponent<MyComponent>("MyComponent");
    BodyComponentDB::RegisterSerializer<MyComponent>();
}

void InitSomeBody(Body *body) {
    // Serialization and creation of the component when loading a save is automatic
    MyComponent *comp = body->AddComponent<MyComponent>();
}

Lua components are similar; simply make a new table and call SetComponent on a body with the value.

local TestComponent = utils.inherits(nil, "TestComponent")

Serializer:RegisterClass("TestComponent", TestComponent)

function someTestFunc(body)
    body:SetComponent("TestComponent", TestComponent.New())

    -- This value will be serialized with the body and loaded via TestComponent.Unserialize
    body:GetComponent("TestComponent").someField = "test"
end

Sort of a mini-ECS approach, it's by no means fast, but it is functional.
Only intended as a transitional system to move to an ECS.
Add a virtual destructor to PoolBase and Serializer base to ensure they're properly destructed.

Remove a level of lookup indirection when accessing component pools by index.
Remove DynamicBody::AddFeature and it's users.
Components are serialized to JSON if present in a body object under their own namespace.

Components are automatically deserialized if registered; if the class loading constructor has created a component of the specified type already it will be used to deserialize JSON data into.
Otherwise, a new component of the specified type will be created and deserialized into.
LuaComponents are arbitrary table values that can be registered on Body objects to provide storage for module data that would otherwise have to be added in engine code.

LuaComponents currently do not have any kind of event handling and must be manually registered and deregistered from bodies by Lua code.
Uses the same GetComponent() interface as LuaComponents.
BodyComponents must have a LuaInterface registered to handle pushing the component to Lua.
At the point where __gc is called on a LuaObject handle, it has already been removed from the LuaObjectRegistry by the GC.
We don't need to deregister it, as the object will be destroyed by Lua.
@sturnclaw
Copy link
Member Author

This branch resolves (in a trivial one-liner that could be split into a separate PR admittedly) a very long-running bug with the LuaObject system that was causing lua handles for C++ body objects to incorrectly deregister other valid handles when they were garbage-collected. It resolves the error responsible for the missing reticle on master branch, and may very possibly resolve a number of other heisenbugs we've had with Lua not quite behaving correctly.

In my testing this (or the PropertyMap2.0 PR) seems to have resolved #4803, but I'd appreciate a hefty amount of testing as the regression surface for this level of change is quite large.

C++-owned LuaObjects with externally-managed lifetimes don't need to be GC'd until the underlying object is destroyed.

LuaComponents depend on the handle for a given body remaining persistent as the handle controls the lifetime of the components.

Objects with lua-managed lifetimes (LuaSharedObject / LuaCopyObject / LuaOwnObject) remain in the transient registry and are GC'd when no references remain.
Borrow the registrar pattern from Input to ensure component pool registration happens at a controlled time.
Component files are responsible for adding their own registrars and ensuring the component is registered before attempting to use it.
Reorder and cleanup includes in Pi.cpp.
Add component registrars to FixedGuns and Propulsion.
Address pull request feedback items.
If LuaComponents are loaded at the same time as the bodies they're attached to, errors will occur due to a dependency on Pi::game->m_space.

Until a more permanent fix can be found, deferring deserialization until Space has finished initializing resolves the dependency.
LuaSerializer already allowed the unserialize method for lua classes to return an alternate value.

Registering the returned value in the TableRefs registry allows further references to the original value to return the replacement value returned by the Unserialize method.
if (!bodies.is_array())
return;

// Note: this loop relies on the ordering and contents of Space::m_bodies not changing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a dark comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it is. I'd like a more generic solution that serializes Body references similar to how lua table references are serialized, but the lua serialization is a general pile of order-dependent kludge and nothing can solve that. (No offense to the original author of LuaSerializer is meant, it's very brilliant for the domain of the problem it needs to solve and there's really no way around the ordering issues)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a dark comment.

Makes me think of this poem:

I have seen the dark universe yawning,
Where the black planets roll without aim;
Where they roll in their horror unheeded, without knowledge or lustre or name.

@sturnclaw
Copy link
Member Author

Unless any significant bugs have been found with this branch (please test it, people!) I will be merging this over the weekend and introducing a new PR that refactors cargo handling into a brand-new LuaComponent and finally (finally!) separates it from EquipSet and the Cargo-as-Equipment paradigm.

I think I've ironed out all of the ordering bugs and gotchas related to this new system as a result of developing that branch, so I'm pretty confident that this is ready to merge and doesn't have any issues remaining.

@sturnclaw sturnclaw merged commit 1afaf73 into pioneerspacesim:master Jun 26, 2022
@sturnclaw sturnclaw deleted the body-components branch June 26, 2022 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Missing Reticule
3 participants