TheOpenSpaceProgram / osp-magnum

A spaceship game
https://www.openspaceprogram.org/
MIT License
216 stars 32 forks source link

Beef up sessions and scenarios #288

Closed Capital-Asterisk closed 1 month ago

Capital-Asterisk commented 5 months ago

A few related things are required that are closely related:

A) Allow changing scenarios while the window is open

Requires a bit of refactoring to scenarios.

Sessions for keeping the window open (setup_window_app and setup_magnum) are already managed separately, found in main.cpp.

https://github.com/TheOpenSpaceProgram/osp-magnum/blob/c42ed3195318c3925373b5107636aaac69d17305/src/testapp/main.cpp#L270-L271

This means that scene-related sessions (setup_scene, setup_common_scene, setup_scene_renderer, setup_magnum_scene, setup_camera_ctrl, etc...) can be closed and new ones can be loaded in while the window stays open.

When a session is requested to change...

  1. Detect request to change session, likely at the end of CommonMagnumApp::draw.
  2. Call TestApp::closesessions (runs all tasks with `.cleanup(Run)`) so data can be destructed gracefully.
  3. Clear scene and scene renderer sessions.
  4. Call new scene setup and renderer setup functions.

B) Have some API to open new sessions

Imagine starting with just a main menu session that only uses UI. The user can then select and load up a flight scenario, launching new sessions (this is why sessions are called sessions).

Challenges:

In a more finalized product, a list of flights scenes, their sessions, and additional data about them (eg: associations with the universe) can be stored somewhere at the top-level of the application. Renderers can then be optionally assigned to them for the application's compositor function to call into.

C) Automatic runtime Session dependencies

Right now, dependencies between sessions are intended to be easy to write and copy-paste around manually, relying on lists of variable names using macros. None of this can really be reconfigured to create custom scenarios at runtime.

Note how similar the physics test scenario and the vehicles test scenario is. You can just put if statements around the vehicle-related setup functions to turn the vehicles test scenario into the physics test scenario conditionally. It's possible to make one mega-sized scenario instead individual physics/vehicle/universe ones, but this can probably be done in a smarter way for the "more finalized product."

Ideally, the interface for this would be to just list off sessions, and dependencies will be automatically resolved. To do this, we need some way to identify/lookup sessions and determine what dependencies they require (make a trait system :3). A bunch of tables of data works for sure, but part of the problem is how to write a nice interface around it.

This might require a full rewrite of all the session stuff. Sessions are just groups of tasks, data, and pipelines, and maybe dealing with them individually 'per-scene' and removing sessions entirely may be easier for an automated solution. we'll see.

Capital-Asterisk commented 4 months ago

Here's a bit of a mockup of a new interface I'm planning:

Before

src/testapp/identifiers.h:

#define TESTAPP_DATA_PHYS_SHAPES 1, \
    idPhysShapes
struct PlPhysShapes
{
    PipelineDef<EStgIntr> spawnRequest      {"spawnRequest      - Spawned shapes"};
    PipelineDef<EStgIntr> spawnedEnts       {"spawnedEnts"};
    PipelineDef<EStgRevd> ownedEnts         {"ownedEnts"};
};

src/testapp/sessions/physics.cpp:

Session setup_phys_shapes(
        TopTaskBuilder&             rBuilder,
        ArrayView<entt::any> const  topData,
        Session const&              scene,
        Session const&              commonScene,
        Session const&              physics,
        MaterialId const            materialId)
{
    OSP_DECLARE_GET_DATA_IDS(commonScene,   TESTAPP_DATA_COMMON_SCENE);
    OSP_DECLARE_GET_DATA_IDS(physics,       TESTAPP_DATA_PHYSICS);
    auto const tgScn    = scene         .get_pipelines<PlScene>();
    auto const tgCS     = commonScene   .get_pipelines<PlCommonScene>();
    auto const tgPhy    = physics       .get_pipelines<PlPhysics>();

    Session out;
    OSP_DECLARE_CREATE_DATA_IDS(out, topData, TESTAPP_DATA_PHYS_SHAPES);
    auto const tgShSp = out.create_pipelines<PlPhysShapes>(rBuilder);

    rBuilder.pipeline(tgShSp.spawnRequest)  .parent(tgScn.update);
    rBuilder.pipeline(tgShSp.spawnedEnts)   .parent(tgScn.update);
    rBuilder.pipeline(tgShSp.ownedEnts)     .parent(tgScn.update);

    top_emplace< ACtxPhysShapes > (topData, idPhysShapes, ACtxPhysShapes{ .m_materialId = materialId });

    rBuilder.task()
        .name       ("Schedule Shape spawn")
        .schedules  ({tgShSp.spawnRequest(Schedule_)})
        .sync_with  ({tgScn.update(Run)})
        .push_to    (out.m_tasks)
        .args       ({           idPhysShapes })
        .func([] (ACtxPhysShapes& rPhysShapes) noexcept -> TaskActions
    {
        return rPhysShapes.m_spawnRequest.empty() ? TaskAction::Cancel : TaskActions{};
    });
// ...

src/testapp/scenarios.cpp:

#define SCENE_SESSIONS      scene, commonScene, physics, physShapes, droppers, bounds, newton, nwtGravSet, nwtGrav, physShapesNwt
#define RENDERER_SESSIONS   sceneRenderer, magnumScene, cameraCtrl, cameraFree, shVisual, shFlat, shPhong, camThrow, shapeDraw, cursor

using namespace testapp::scenes;

auto const  defaultPkg      = rTestApp.m_defaultPkg;
auto const  application     = rTestApp.m_application;
auto        & rTopData      = rTestApp.m_topData;

TopTaskBuilder builder{rTestApp.m_tasks, rTestApp.m_scene.m_edges, rTestApp.m_taskData};

auto & [SCENE_SESSIONS] = resize_then_unpack<10>(rTestApp.m_scene.m_sessions);

// Compose together lots of Sessions
scene           = setup_scene               (builder, rTopData, application);
commonScene     = setup_common_scene        (builder, rTopData, scene, application, defaultPkg);
physics         = setup_physics             (builder, rTopData, scene, commonScene);
physShapes      = setup_phys_shapes         (builder, rTopData, scene, commonScene, physics, sc_matPhong);
// ...

After

src/testapp/identifiers.h:

struct FIPhysShapes
{
    struct DataIds
    {
        TopDataId physShapes;
    };

    struct Pipelines
    {
        PipelineDef<EStgIntr> spawnRequest      {"spawnRequest      - Spawned shapes"};
        PipelineDef<EStgIntr> spawnedEnts       {"spawnedEnts"};
        PipelineDef<EStgRevd> ownedEnts         {"ownedEnts"};
    };
};

src/testapp/sessions/physics.cpp:

void ft_phys_shapes( FeatureBuilder fb )
{
    fb.name("Physics Shapes");

    auto [diScn, plScn] = fb.use_interface<FIScene>();
    auto [diCS,  plCS]  = fb.use_interface<FICommonScene>();
    auto [diPhy, plPhy] = fb.use_interface<FIGenericPhysics>();

    auto [dPhSh, lPhSh] = fb.implement_interface<FIPhysShapes>();

    fb.pipeline(lPhSh.spawnRequest)   .parent(plScn.update);
    fb.pipeline(lPhSh.spawnedEnts)    .parent(plScn.update);
    fb.pipeline(lPhSh.ownedEnts)      .parent(plScn.update);

    fb.data(dPhSh.physShapes).emplace< ACtxPhysShapes >( ACtxPhysShapes{ .m_materialId = materialId } );

    fb.task()
        .name       ("Schedule Shape spawn")
        .schedules  ({lPhSh.spawnRequest(Schedule_)})
        .sync_with  ({plScn.update(Run)})
        .args       ({       dPhSh.physShapes })
        .func([] (ACtxPhysShapes& rPhysShapes) noexcept -> TaskActions
    {
        return rPhysShapes.m_spawnRequest.empty() ? TaskAction::Cancel : TaskActions{};
    });
// ...

src/testapp/scenarios.cpp:


auto ctxBuilder = rTestApp.builder().create_context();

rTopData.m_currentScene = ctxBuilder.context_id();

ctxBuilder.add_feature(&ft_scene);
ctxBuilder.add_feature(&ft_scene_common);
ctxBuilder.add_feature(&ft_physics);
ctxBuilder.add_feature(&ft_phys_shapes);
// ...
Capital-Asterisk commented 4 months ago

putting this here to not be so dependent on discord.

A couple iterations later: https://godbolt.org/z/Gb3GvPqar

C++ metaprogramming is inherently a little cursed but that's that.

I'm not expecting many people to understand how the template nonsense works (I put quite a bit of effort into making it actually readable, though it would be a fun challenge to dive into).

I mostly care about the user code below, which can drastically reduce the amount of boilerplate in testapp.

Calls to fb.use_interface<...> and fb.implement_interface<...> from the previous comment won't be needed anymore as function arguments are instead read to determine dependencies:

auto const gc_ftrPhysShapes = feature_def([] (FeatureBuilder& rBuilder, Implement<FIPhysShapes> phySh, DependOn<FIScene> scn, DependOn<FIGenericPhysics> phys)
{
    // Initialize values of TopData (top_emplace),
    // setup pipelines (rBuilder.pipeline(...).parent(...)),
    // and setup tasks here
});

quoting my previous comment:

allow specifying dependencies to Feature Interfaces elsewhere, so they're accessible before this function is even called

Capital-Asterisk commented 1 month ago

https://github.com/TheOpenSpaceProgram/osp-magnum/pull/298