Skip to content

GraphXTheatre File Format

Electron edited this page Feb 26, 2025 · 20 revisions

Introduction to GraphXTheatre Files

What are GraphXTheatre Files?

GraphXTheatre files are what the engine use to create Theatres, and Theatres are pretty much just GraphX's version of game scenes, levels, maps, etc. I created the GraphXTheatre file format to replace my old method of making and loading Theatres... using header files.

Wait, you made a file format?

Well, kinda. .gt files are pretty much just .txt files that are read, parsed, and interpreted by custom code in the engine. What I really made was a parser/interpreter and a bunch of rules that .gt files need to follow. Speaking of which, that brings us to the next section.

The Filename and Load Order

The first and one of the most important rules to follow involves what you actually name the .gt file itself. Well, ironically, the actual "name" of the file doesn't really matter at all, but what comes before it is pretty important. Every .gt file must begin with a number and a period; that number is the load-order of that Theatre. Under the hood, Theatre files are embedded into the engine as reeeeeally long strings inside of a map called embedded_theatres; the key values in that map are long numbers that are taken from the filename. Because the engine uses a map instead of an unordered map, if a Theatre has the same load-order number as a Theatre that's already been embedded, it will actually replace that other Theatre. This is by design, as it allows users to replace the default Theatres that come pre-embedded into GraphX. In layman's terms: it lets you replace the game's default "levels" with your own.

Well, with all that out of the way, it's time to move on to the meat and potatoes of this page.

GraphXTheatre Syntax

This section will go into detail about the dos, donts, cans, and cants when making a Theatre. First thing's first:

Names, Classes, & Braces (dear god!)

The very first line of a GraphXTheatre file must always be the name of the Theatre and must always start with the @ symbol.

Here's an example of a Theatre named "HelloWorld":

@HelloWorld

Simple, right? Now the fun stuff: class creation. Theatres are basically just holding all the stuff that the engine is currently interacting with; stuff like the player, all the different lights, all the meshes and materials being used, etc. Luckily, creating and adding these to a Theatre using a GraphXTheatre file is actually pretty simple, and tries to at least resemble C++ syntax at a glance.

Creating an Object

The basic syntax looks like this:

Class (name)
{
    Variable1 [code_reference]
    Variable2 <theatre_reference>
    Variable3 (raw_data)
    ...
}

And here's an actual example from one of the default Theatres in the engine:

Collider (Test_Collider_1)
{
    MotionType  [Dynamic]
    ObjectLayer [Moving]
    Activation  [Activate]
    Shape       [BoxShape]
}
RigidBodyActor (Falling_Cube_1)
{
    Mesh:Material <Cube>:<Doom_Shiny>
    Collider      <Test_Collider_1>
    Scale         (1.0, 1.0, 1.0)
    Position      (-2.0, 9.0, -6.0)
}

As you can see, there are two objects being created in this example; a Collider named "Test_Collider_1", and a RigidBodyActor named "Falling_Cube_1".

Warning

While there's nothing stopping you from using the same name for different objects, I'd recommend not doing that. The engine has a few functions for interacting with Actors and Devices based on their Unique Identifier (UID), but figuring out which UID an object has is really annoying, so I also added functions that let you search for objects based on their name. If those functions are used to look for an Actor/Device with a name that other Actors/Devices also have, the wrong one could very easily be returned.

Similarly to C++, all the variables for an object are written inside a pair of curly brackets. Every object is required to have these brackets, even if no variables are set, like in the case of this LightFlashlight object:

LightFlashlight (Player_Flashlight)
{}

Now that you know how to make a new Actor/Device, it's time to learn how to set them up properly.

The Four Types of Variables

Variable names are just strings that the C++ classes look for when told to process their "settings" (the parsed data from the .gt file). I'll go into more detail later about variable names, how easy it is to make new ones, and how they don't affect a C++ variable's default value. First, though, you need to know the different types of data that GraphXTheatre variables use, as well as their syntax. Variable data comes in four different types:

(Raw Data)

Raw data variables are the simplest form of variable, but also the most rigid and, admittedly, in need of fleshing out. Raw data variables can be a single number, a vector of two or three numbers, a quaternion (which is just a vector of four numbers), a boolean value, or a single string. Raw data variables are surrounded by parentheses.

Examples of raw data variables include an object's name:

Light (Player_Flashlight)

An Actor's position, rotation, and scale:

StaticBodyActor (Platform_1)
{
    ...
    Scale    (10.0, 50.0, 10.0)
    Position (32.1, 75.0, -29.7)
    Rotation (0.0, 20.0, 0.0)
}

A Material's specular strength and sharpness:

Material (Doom_Dull)
{
    ...
    SpecularSharpness (16)
    SpecularStrength  (0.5)
}

Note

All numerical raw data variables get converted to floats when parsed. In the future, I might instead have them converted to longs, but for now be aware that floating-point imprecision may be a factor in the values you set.

[C++ References]

C++ references are, unsurprisingly, references to variables in the engine's code. They are surrounded by square brackets. The actual references are defined in a map in t_interpreter.cpp called "cpp_definitions". The map's keys are what you would write in the GraphXTheatre file.

As an example, here's that Collider object that I showed you earlier:

Collider (Test_Collider_1)
{
    MotionType  [Dynamic]
    ObjectLayer [Moving]
    Activation  [Activate]
    Shape       [BoxShape]
}

And here's a small section of the "cpp_definitions" map:

std::map<std::string, std::any> cpp_definitions =
{
    ...
    {"Dynamic", JPH::EMotionType::Dynamic},
    {"Moving", Layers::MOVING},
    {"Activate", JPH::EActivation::Activate},
    {"BoxShape", ColliderShapes::BOX},
    ...
};

As you can see, the reference variables are the map's string keys, and the actual C++ variables being referenced are the values. This means that to add new definitions, all you have to do is add another pair to this map. This also means that you can account for different spellings/capitalizations or even give a single reference multiple names by doing something like this:

std::map<std::string, std::any> cpp_definitions =
{
    ...
    {"Dynamic", JPH::EMotionType::Dynamic},
    {"dynamic", JPH::EMotionType::Dynamic},
    {"rigidbody collider", JPH::EMotionType::Dynamic},
    ...
};

<Theatre References>

Theatre reference variables are one of the two things that I am most proud of being able to create and implement correctly, because they are effectively pointers to previously created objects in the GraphXTheatre file. Theatre reference variables are surrounded by angled brackets.

Warning

Theatre references only work if the object being referenced is above the object that needs it. The reason for this is that I didn't feel like writing a parser and an interpreter that would have to loop through the file twice and remember what they saw. Fuck that.

Here's an example of an Actor setting its mesh variable to a previously created Mesh object:

Mesh (Mesh_1)
{
    MeshData [GRAPHX_CUBE]
}
Actor (Actor_1)
{
    Mesh <Mesh_1>
}

<Sandwich>:<References>

Sandwich references are probably what I'm most proud of. They are a way to both reference a previously created object and change one or more of its variables without having to manually re-create that object. They are multiple variable names and Theatre references, separated by colons.

Here's an example of three Actors using the same Mesh with different Materials. The first two directly change the Material using sandwich references, while the third Actor doesn't. This will result in all three Actors using the same "cube" Mesh, but actor_1 will be using "material_1", actor_2 will be using "material_2", and actor_3 will be using "material_3".

Material (material_1)
{
    ...
}
Material (material_2)
{
    ...
}
Material (material_3)
{
    ...
}
Mesh (cube_mesh)
{
    MeshData [GRAPHX_CUBE]
    Material <material_3>
}
Actor (actor_1)
{
    Mesh:Material <cube_mesh>:<material_1>
}
Actor (actor_2)
{
    Mesh:Material <cube_mesh>:<material_2>
}
Actor (actor_3)
{
    Mesh <cube_mesh>
}

Important

Currently, sandwiches only work with Theatre references, so writing something like this: Mesh:Material <cube_mesh>:<material_2> is valid, but writing something like this: Mesh:MeshData <cube_mesh>:[GRAPHX_PYRAMID] is not. However, that is a feature I want to add in a future update so keep in mind that this limitation shouldn't stick around forever.

Variable Names

The way Actors and Devices interact with their "settings" (the data from GraphXTheatre files) is what dictates variable names, and is a system I am rather proud of. Instead of trying to make sure the interpreter knows every single variable name and exactly what variable in each class that name corresponds to (which would be fucking awful), the interpreter just doesn't give a fuck! Instead, every class derived from Actor or Device is responsible for their own variables. Thanks to template functions, this process is not only simple, but also allows for what's essentially like function overloading for but for variable names. Let's start by talking about what happens when an Actor gets a callback.

Actor::youGotACallBack & Device::loadSettings

Actor::youGotACallBack and Device::loadSettings are functions that get called by Theatre::createActor and Theatre::createDevice respectively. They each take a single gSettings argument named "new_settings" (gSettings is just a typedef for std::unordered_map<std::string, std::any>). However, that "new_settings" argument's use is for an Actor/Device to load some additional settings without overriding their "settings" variable. When an Actor/Device is created by a Theatre, that argument is left unused.

Warning

GraphX uses a unique gSettings variable called empty_settings as the default for all gSettings variables. empty_settings contains only a single key-value pair, which uses a global string variable called empty_settings_identifier for its key. empty_settings_identifier is equal to the string "FUCKYOU". This allows for a quick way to check if a gSettings variable is unset using a simple if statement. However, if you, for whatever reason, decide to use the variable name FUCKYOU in a GraphXTheatre file, the specific class that uses that variable name will not load any of the settings you give it and will keep all its default C++ values. This may result in a crash for certain Actors/Devices who set certain pointer variables to nullptr by default.

Clone this wiki locally