Map Objects vs. Engine-Side Map Data

edited 2007 Apr 18 in Developers
<i>This post was originally made by <b>skyjake</b> on the dengDevs blog. It was posted under the categories: Engine, Games.</i>

There is still a large part of map data that is shared between the engine and the games: the mobjs. The current design of the DMU does not account for them in any fashion. However, mobjs are quite different in nature to, e.g., the sectors and lines. Mobjs are created dynamically at runtime as gameplay progresses. The properties of mobjs may vary wildly during their lifetime. We need to make decisions regarding the direction to which mobjs will be developed.

We need a mechanism that <ol><li>informs the engine of all changes to mobjs so that netgame servers and other subsystems can update their status accordingly; <li>does not prevent the runtime creation of massive numbers of mobjs; <li>allows games to have easy access to mobj properties, preferably so that a minimum number of work needs to be done to upgrade old code (best case: no changes); <li>is forwards compatible so that in the future, we can replace the engine completely.</ol>

Unfortunately, these characteristics are quite conflicting.

I suspect a DMU-style API would be terribly unwieldy for mobjs, due to the number of properties in mobjs and the frequency at which they're used in the games. In this regard, keeping the current "exposed struct" would be good. However, there is the question of change notifications. If we can have a reliable way to inform the engine of mobj property changes, it might be enough to use that and keep the current mobj struct solution.

At least, we should move mobj creation/destruction services to the engine, and collect all the properties currently used in Doom, Heretic, and Hexen to the same <tt>mobj_s</tt> struct.

Perhaps we could devise a system where all map objects live on engine-side and are only accessible as read-only in the games. Whenever the game needs to do a change, it has to <i>lock</i> the mobj. When the mobj is <i>unlocked</i>, the engine will examine which properties were changed and generate the change notifications automatically.

Now the question remains, how do the games extend the mobjs? In other words, how to create xmobjs?

Comments

  • I'm still thinking about a method for creating the xmobjs and reading up on various types of API design.

    Can you tell me a bit more about how you plan to <em>lock</em> the members of a "shared" structure?
  • When it comes to xmobjs, maybe we can do something similar like with the dummies/x-dummies?

    The idea of locking would be that when the game wants to modify the object, it calls a locking function provided by the engine. The function will take a copy of the object. The game can then do changes to the object. When finished, it calls the unlocking function, which will compare each property of the object to the versions that were saved when the object was locked. Change notifications are done, and the locked copy of the object can be discarded.

    If there is need to make this more efficient, the caller could tell the locking function which properties it intends to change when the object is locked. Then it would be sufficient to only save/compare those ones.
  • Ah I see.

    So how would we enforce the readonly restriction when unlocked given that the members are part of a shared structure?
  • I'm not sure there is a good way to enforce it. We could try playing around with <tt>const</tt> members and pointers, but it may just lead to more trouble than it's worth. Even so, in C it's always possible to easily typecast consts away, but it would be nice to have the compiler complain in a typical usage scenario...
  • One thing worth noting is the frequency of changes to mobjs. For example if the engine is going to have to compare the properties each time a change is made then if the games make numerous changes to the same mobj at different times during a tick that is a lot of time spent doing comparison work.

    I'm thinking ideally that the engine should only check each (lock flipped) mobj once at the end of a tick. In order to do that though the temporary "unlocked" mobjs would need to persist to the end of a tick and any references to mobj data made by the games (after a mobj has been lock flipped) should reference the "unlocked" version instead.

    This would only be of benefit though if there is a clean divide between when during the tick the mobjs are accessed by the engine and the game and if there is a divide between when a change to a mobj influences any engine subsystems.
  • Yes, mobjs are changed quite often. The game would have to lock and unlock as seldom as possible, or in other words, group the changes together.

    Though this starts to sound like a lot of work to implement game-side.

    Needs more thinking about...
  • <blockquote>When it comes to xmobjs, maybe we can do something similar like with the dummies/x-dummies?</blockquote>
    We could implement a system based on "pools" of unused mobjs (with preattached xmobjs) so we don't need to worry about loosing any addresses that could occur as a result of reallocating memory. Which would at the same time benefit us by not having to allocate one at a time.

    When the existing pools are almost full we would allocate a new one. Mobjs would need a hidden member to record which pool they are from and their "in-pool" ID, then use the that plus the ID of the pool itself when calculating an index for the mobj.
  • I think its time we revisited this topic and tried to hammer out some more concrete plans.

    game_import_t is now starting to thin out nicely but what I'm looking at currently (removing the public, shared thinkercap) has ramifications here.

    Engine side deallocation is already in-place and it would be fairly trivial to replace iteration with an engine side "iterate and callback" for thinkers. So that leaves allocation and layout of the mobj<>thinker structs.

    I'm thinking something along the lines of:

    <code>struct mobj_s {
    ... // NOTE: thinker_t thinker is not present.
    } mobj_t;

    // The public part of the thinker.
    struct thinker_s {
    think_t func;
    thid_t id;
    void *data; // e.g: (mobj_t*)
    } thinker_t;

    // The engine internal thinker.
    struct ddthinker_s {
    thinker_t thinker; // the public version
    struct ddthinker_s *next, *prev;
    } ddthinker_t;
    </code>
  • Yes, removing the shared thinkercap should be the first step.

    What about this sort of a plan?

    <ol>
    <li>Create a unified mobj_t structure which contains all the information that a mobj can have in all of the games, and nothing extra beyond that.</li>
    <li>Make that new mobj_t an engine-side data object.</li>
    <li>Position changes of mobjs is only allowed through engine-side functions. (Already the case?) Implicit DMU notifications from within these functions.</li>
    <li>State changes of mobjs is only allowed through engine-side functions.</li>
    <li>When other changes are made to mobj properties, e.g. by the game, the mobj needs to be "marked" (i.e., flagged as having been changed during the tick). It shouldn't be too difficult to add the mark_mobj calls the places where actual changes are done to a mobj. (Less modifications required than for locking/unlocking calls.)</li>
    <li>The engine has two copies of each mobj_t data. In the end of a tick, the marked mobjs are compared against their old data to see what has changed (does not include position or state).
    <li>The marks are cleared by updating the second copy of each marked mobj.
    </ol>

    This would mean we have are exposing an engine-side data structure, the mobj, to the plugins.

    To fight the problem of having to expose a data structure, maybe we should solve the problem by breaking it down to many smaller structures? One for physical location, another for state, one for health properties etc., one for player data, ... One could be the game-side extra data. Then we could introduce new structs in the future without having to break compatibility.

    We could even make it so that one has to explicitly request either a read-only or a read-write struct (pos, state, playerdata, etc.), and if the read-write pointer is requested, that part of the mobj is automatically marked as changed. This would make it possible to keep much of the existing code that accesses structs directly, and provide us with the notification abilities needed for DMU, and also help in staying compatible (as the structs are basically just a description of the real internal map objects, not the real deal).
  • <blockquote>Position changes of mobjs is only allowed through engine-side functions. (Already the case?) Implicit DMU notifications from within these functions.</blockquote>
    Yes and no. Currently position changes do happen game-side and we rely on the game to instigate the calls for linking mobjs to lines/subsectors etc.
    <blockquote>What about this sort of a plan? ...</blockquote>
    Sounds workable for mobjs but what about other thinkers, plane movers, particle generators etc?
    <blockquote>To fight the problem of having to expose a data structure, maybe we should solve the problem by breaking it down to many smaller structures? One for physical location, another for state, one for health properties etc., one for player data, … One could be the game-side extra data. Then we could introduce new structs in the future without having to break compatibility.</blockquote>
    I think that might become impractical. What you suggest would mean that the game would have to call the engine to get a mutable ptr to each "chunk" it planned on changing. This would incur more of a performance hit than a object-level locking/unlocking scheme.

    One thing I would like is to split thinkers into separate lists in order to improve searching (but this doesn't really affect this I know).
Sign In or Register to comment.