-
Notifications
You must be signed in to change notification settings - Fork 5
RFL interface
This page attempts to document the RFL (Riot Function Library) architecture as used by the Riot Engine and replicated by OpenDrakan.
Much of the game logic in the Riot Engine is contained in a so called Riot Function Library, or RFL for short. RFLs basically are just DLLs that define a set of classes. These classes' methods are hidden to the level designer, but their member variables can be accessed modified via the level editor.
Since in the Riot Engine, there are two distinct entities called "class", we have to introduce a distinction. There are the classes defined by the RFL, and the classes defined in the engine's databases.
The latter really aren't classes in OOP terms~ they are more of an "object template". They contain an RFL class type, a default value for each of that RFL class's fields, and may contain a reference to a model. However, since "class" is the term the Riot Engine editor uses for these, that's the terminology we chose to use in OpenDrakan.
To distinct database classes from classes from an RFL, the former are referred to as "database class" or simply "class", while the latter are always called "RFL classes".
OpenDrakan uses an entirely different namespace for everything RFL-related (namespace odRfl).
As mentioned above, the primary part of RFL classes are their member fields. Their values are stored in the level and database files. Everything else is determined by the DLL they are loaded from and not visible in the files or the editor.
RFL class members have a name (more of a description; names aren't necessarily unique per RFL class), a data type and can belong to a category.
When placing an object in a level, this object's behaviour is determined by the RFL class it belongs to/is an instance of.
While one could think of objects in the level as direct instances of RFL classes or database classes, this is not the case in OpenDrakan. This is due to the fact RFL classes are sometimes used without there beeing an associated level object (like material classes for textures). Also, going with composition over inheritance has proven to be a concept most developers agree with.
Instead, all objects OpenDrakan places in the level are instances of the class od::LevelObject. Each level object references it's database class (which are unique
and shared among all od::LevelObject instances), and contain an instance of an RFL class created from the database class.
The LevelObject determines the objects position in the level, hooks the model and skeleton into the scenegraph and handles other logic common to all objects, while
any specific logic is handled by the RFL class instance.
There are a lot of RFL classes in an RFL, and different games using the Riot Engine might have their own RFLs. Thus, OpenDrakan provides a portable interface for defining and registering RFL classes.
A basic RFL class definition might look something like this:
class TestClass : public RflClass
{
public:
TestClass();
virtual void probeFields(RflFieldProbe &probe) override;
protected:
RflInteger mSomeInteger;
RflFloat mSomeFloat;
};The probeFields(...) method is how OpenDrakan implements the reflection necessary for working with RFL classes.
This is a visitor pattern used via children of RflFieldProbe.
When something in the engine needs to know which fields are defined in an instance of an RflClass, it creates an RflFieldProbe
suiting that need. That field probe is then applied to the RFL class by passing it to the probeFields(...) method.
In this method, it is the RFL class's job to register with the field probe all fields it wants to expose.
This is done like this:
void TestClass::probeFields(RflFieldProbe &probe)
{
probe("Category Name") // this starts a category
(mSomeInteger, "Field name for mSomeInteger") // this registers a field
(mSomeFloat, "Field name for mSomeFloat");
}Note the functor-like syntax provided by the field probe. There are named methods in RflFieldProbe providing the same function
(probe.beginCategory(...) and probe.registerField(...)), but they can create enormous walls of text for RFL classes with many member variables.
Some contain more than 100 members, so the functor syntax looks much cleaner for those. But of course you can use the named functions if you like.
Categories are just cosmetic right now. OpenDrakan doesn't use them for anything.
During field registration, more can happen than just the field probe noticing that there is field with that name. For example: RflClassBuilder, a child of RflFielProbe,
is used when creating level objects from a database class. The class builder is filled with the default field values from the database class and then applied to the
RFL class instance. During field registration, RflClassBuilder will automatically fill the field with it's default value.
Note that the order of field registration is vitally important, as the index of a field in an RFL class is used to identify that field. Field names may not be unique, so they can't be used to identify a field. However, OpenDrakan uses the field name for consistency checks. This way, when a developer confuses the order of fields, OpenDrakan will complain because the field name will most likely not match the one it expected.
To react to events in the engine, RFL classes can define a set of hooks. Of course, OpenDrakan is far from finished, and it might become necessary to add more hooks in the future, so this is not a complete list. Right now, the defined hooks are:
void onLoaded(od::Engine &e, od::LevelObject *obj);
void onSpawned(od::LevelObject &obj);
void onDespawned(od::LevelObject &obj);
void onUpdate(od::LevelObject &obj, double simTime, double relTime);
void onMessageReceived(od::LevelObject &obj, od::LevelObject &sender, RflMessage message);
void onMoved(od::LevelObject &obj);
void onDestroyed(od::LevelObject &obj);Note that all of these pass the associated LevelObject, so an RFL class doesn't have to store it LevelObject when loading/spawning.
The onLoaded(...) hook is called right after the RFL class instance has been loaded and all fields
filled with their final value. Some RFL classes have important functions for the engine, like the Human Control class.
These RFL classes use the loaded hook to register themselves with the engine, and only start interacting with other RFL-dependent features
once their object gets spawned. This is because level objects might get loaded in any order, so some RFL-dependent features might not yet be available
when an RFL class gets loaded.
This is the only hook that gets called for non-level object RFL classes like Materials. For those, the second argument will be nullptr.
The onSpawned(...) and onDespawned(...) hooks are called when a level object is placed in the world or removed from it, respectively.
When an object gets removed from the level, this might also happen because it is too far away or not currently visible, and it could respawn again if needed.
The onUpdate(...) hook is a way to implement periodic or timed behavior. When enabled via LevelObject::setEnableRflUpdateHook(...), it gets called every update traversal (which means every frame as of now).
It won't get called if it has not been enabled. This is to prevent weighing down the update traversal with unnecessary updates.
The simTime argument is the absolute time in seconds, while relTime is the time that passed since the last update.
The onMessageReceived(...) hook gets called when the associated level object receives a message (duh). Messages are a way for level objects to communicate with one another. They are basically just an integer value with a name (like 'On', 'Off', 'Triggered' etc.), and their interpretation is entirely up to the RFL class receiving them.
Messages can be sent using the LevelObject::messageAllLinkedObjects(...) method.