tl;dr
EDF is a practical text format to configure game objects in a custom game engine.
About EDF
As you can read in my last article jbrosi and I created our own game engine in C++. An essential part of that engine was the creation of the Entity Definition Format EDF
. It allows you to describe configuration of an entity in a simple text file.
An entity definition is the blueprint (or prefab) for instantiating an Entity. It is like a template and in some sense similar to what a class
is for programming languages like Java
. You create entities from entity definitions in the same way you create instances of classes. If you work on a custom engine and think of means to manage and define your objects this approach might be helpful.
The EDF model
The file is the textual representation of a specific data model. I understand there are different approaches to the entity & component pattern and no engine is alike. However, to some degree they should be compatible to the following model.
The EDF model looks like this:
{
name: "MyObjectType", // (A)
components: [
"Component1", // (B)
"Component2",
// ...
"ComponentN"
],
properties: {
"someProperty1": "some value", // (C)
"someProperty2": "23",
"someProperty3": "{change: 20, item: 10}",
// ...
"somePropertyN": "true",
},
static: false // (D)
}
This isn’t the format yet. This is just the data model.
Let’s analyze the model in detail:
(A) name
Each definition has a unique name. You use this name to reference definitions. For example in your game-level-map you would point toMyObjectType
. Or in your game code somewhere you writecreateInstance("MyObjectType");
(B) components
An ordered list of components. This are the names of the classes that our entity is composed of. Every time your engine instantiates an entity from this definition it should instantiate this component classes and add them to the entity object.(C) properties
They are best seen asMap<String, String>
. They should be available during initialization of the entity instance and the component classes can read the configuration from it.(D) static
Is a simple flag that tells the engine that we need one instance of this definition everytime we load a map. For those things your game needs all the time like the head up display in a shooter or other UI objects.
If you wonder which property is bound to which component the answer is: all to all. The properties refer to the object-type and not to a specific component. If you like to bind a property to a specific component or worry about name collision you can simply use a prefix. So, instead of using layer
I suggest to use spriteLayer
and collisionLayer
as property names.
In summary the model consist of a name for referencing, a list of components for initialization and a map of properties for configuration. Pretty simple, but not yet useful. Let’s go on with the EDF.
Entity Definition Format
The EDF syntax
Before we get to the good stuff, let’s check out the EDF syntax. Here is the example from before but in EDF syntax:
[MyObjectType] // (A)
@Component1 // (B)
@Component2
@ComponentN
someProperty1 = some value // (C)
someProperty2 = 23
someProperty3 = {change: 20, item: 10}
someProperty3 = 60% nothing 35% gold 5% unique_item
somePropertyN = true
We start by defining a new Entity Definition
by using []
and give it a name (see A). Then a list of components follow. To make them searchable and look different from properties they start with an @
(see B). After that we can add a list of properties. The example also shows that the property definition can be anything. EDF does not enforce any rule for a type. When the components initialize they are responsible to make sense of the configuration.
If we like to make the Entity Definition
static we add a !
before the name: [!MyObjectType]
. Simple as that.
EDF Inheritance
With this model we can define simple object types. As an additional concept EDF has the inheritance feature which allows us to reuse a definition and modify it’s properties.
Take a look at the following example:
[Bomb] // << EDF name
@BombBehavior // << Components
@Carryable
@HitCollision
@Sprite
timeToExplode = "3sec" // << Properties
hitCountToTrigger = 5
color = blue
This is an EDF file.
In this example I like to have a bomb type. The given components allow the player to carry it around and it explodes on hit. The properties define the details of that behavior. For example the timeToExplode
property is parsed by BombBehavior
and defines how long it takes for the bomb to explode. In our level editor or in the game code we now use the term Bomb
to reference to this Entity Definition. This is how we place bombs: we simply reference to the EDF-Name.
At a later stage, we like to create a different kind of bomb. One that is heavier and can only be carried when we own the item silver glove. It also shall look different. We could simply copy & paste the other bomb and modify it. Like this:
[SilverBomb] // << New EDF name
@BombBehavior // << Same components
@Carryable
@HitCollision
@Sprite
timeToExplode = "3sec" // << Some properties differ
hitCountToTrigger = 5
color = silver
pickUpLevel = "SILVER_GLOVES"
However, we can do better. We can simply inherit the definition and just modify the properties that change:
[SilverBomb(Bomb)] // << New EDF name, inherits Bomb
color = silver // << Overwrite existing property
pickUpLevel = "SILVER_GLOVES" // << Add new property
This is called inheritance in the EDF. It is a copy with same components and properties but you are able to overwrite some or all properties. This reduces the size of the EDF file but also creates a connection between the two types. The idea behind this is that they share the same code base. And if you modify your component code and it effects your configuration you won’t need to change all of the definitions but only the properties.
I like to mention that you should not allow child-types to add components to the definition. It is tempting but I think that breaks the idea of having a common denominator with a little adjustment. If you need to modify a type in a way that you need to modify components I suggest to simply copy&paste the definition and go from there.
EDF Property groups
Inheritance allows you to reuse composition of functionality (components). A similar thing can be achieved for properties. Depending on the game it can be very useful to define groups of properties that apply to a group of objects. To do that you can define Property Groups
in EDF.
As an example let’s image a 2D game with multiple layers. Further let’s assume your graphics engine refers to those layers via name. For your visual objects you have a @SpriteComponent
. During initialization the sprite component needs to know on which layer to put the sprite. For that it reads the parameter spriteLayer
. Also you have a @PhysicComponent
that connects an entity to the global physics simluation. For that you have to provide a gravity factor gravityFactor
. Obviously we will have lots of objects that have a sprite and physic component. In most cases they all will have the same gravity factor and placed on the same layer in the graphics engine.
For such a case you can define a PropertyGroup.
[*spriteDefault] // << PropertyGroup spriteDefault
spriteLayer = grassland
color = 1, 1, 1, 1
[*physicDefault] // << PropertyGroup physicDefault
gravityFactor = 0, -10
Similar to an entity definition we define a property group by giving it a name and set properties. The *
helps identifies it as a property group and no components are allowed here.
To use a property group we have to link it in an entity definition:
[Player:spriteDefault,physicDefault] // << Reference to both groups
@SpriteComponent
@PhysicComponent
[Enemy1:spriteDefault,physicDefault] // << Reference to both groups
@SpriteComponent
@PhysicComponent
color = 0, 0, 1, 1 // << Overwrite a property
Both entity definitions (Player and Enemy) link to the property groups spriteDefault and physicDefault. If we now need to change the setup of our map or the global gravity factor we have a central place for that. Other than inheritance it is possible to link multiple property groups. When using property groups it is still possible to overwrite parameters (see the color property in Enemy1).
It is also possible to combine both features:
[*waterPhysic]
gravityFactor = 0, 0
inWater = true
[Enemy2(Enemy1):waterPhysic] // << inherit and link another property group
color = 1, 1, 0, 1 // overwrite
Global Properties
Additionally to property groups I found it useful to have one global property group. If you like to have one value, shared by all entity definitions you can add it to the global group.
[*] // global group
gravity = [0, -10]
levelDirection = right
animation = idle
All entity definitions now have those three properties.
EDF property order
The features inheritance and property groups require us to think about in what order properties are overwritten. Let’s have a look at Enemy2
and see how they effect the order of property overwriting.
[*]
animation = idle
[*spriteDefault]
spriteLayer = grassland
color = 1, 1, 1, 1
[*physicDefault]
gravityFactor = 0, -10
[Enemy1:spriteDefault,physicDefault]
@SpriteComponent
@PhysicComponent
color = 0, 0, 1, 1
[*waterPhysic]
gravityFactor = 0, 0
inWater = true
[Enemy2(Enemy1):waterPhysic] // << inherit and link another property group
color = 1, 1, 0, 1 // overwrite
The order is defined by the entity definition. The property order is not defined by the position in the file. EDF does not consider line number positions.
Now, let’s investigate the properties of Enemy2
:
- global properties
- spriteDefault
- physicDefault
- Enemy1 properties
- waterPhysic
- Enemy2 properties
Which translates to:
[Enemy2]
@SpriteComponent
@PhysicComponent
animation = idle
spriteLayer = grassland
color = 1, 1, 1, 1
gravityFactor = 0, -10
color = 0, 0, 1, 1
gravityFactor = 0, 0
inWater = true
color = 1, 1, 0, 1
If we now strike out the duplications the final model of Enemy2
simply is:
[Enemy2]
@SpriteComponent
@PhysicComponent
animation = idle
spriteLayer = grassland
gravityFactor = 0, 0
inWater = true
color = 1, 1, 0, 1
Import EDF files
The final aspect of EDF is the support of include. One EDF file can import another. For that we have the keyword [$require]
.
[$require]
somefile.edf
somefile2.edf
Require must be the first statement in a EDF file and there must only be one in each file. The required files are more or less simply copied into the file to one gigantic EDF. This combined file can then be parsed.
In this combined file the same rules apply as in one file:
- a entity definition name may only be defined once
- a property group name may only be defined once
The global property group is an exception. Those need to be merged.
Navigation
EDF was build around the text editor and text search. If your game grows so will your EDF files. The notation supports that so its easy to do semantic searches:
CTRL
/CMD
+ F
and type:
[<term>
find the single definition of that type!<term>
find a static definition(<term>
find only types that inherit that type*<term>
find a the definition of a property group@<term>
find the usage of a component:<term>
or,<term>
find reference to a property group
This is an advantage compared to raw formats like YAML or JSON.
Pseudo implementation
There is currently no useful implementation available. The following example shows how the parts could be put together.
// example.edf ----------------------------------------------
[Enemy1]
@SpriteComponent
@PhysicComponent
@Talkable
spriteFile = enemies.xml
spriteName = emeny1
physicSize = 20
talkScript = "defaultNpc.xml"
[Enemy2(Enemy1)]
spriteName = enemy2
// mapFile.json ---------------------------------------------
[
{
"name": "guard1",
"type": "Enemy1",
"position": [100,150]
},
{
"name": "guard2",
"type": "Enemy1",
"position": [200,150],
"props": {
"talkScript": "guardTalk.xml"
}
},
{
"name": "gaurdAssist",
"type": "Enemy2",
"position": [150,160]
}
]
// game.java ------------------------------------------------
void loadLevel() {
EDFDatabase edf = edfParser.load("example.edf");
Map map = mapParser.load("mapFile.json");
for(MapObject obj: map.getObjectList()) {
EntityDefinition ed = edf.find(obj.getType());
Entity entity = new Entity(obj.getName());
for(String cmpName: ed.getComponents()) {
Component cmp = entity.createComponentByName(cmpName);
cmp.parseProperties(ed.getProperties(), obj.getProperties());
}
entityManager.add(entity, ed.isStatic());
}
}
Final words
That’s pretty much it. I used EDF for a larger fan project and it felt incredible helpful. A friend built a custom engine using a variation of EDF for the game Botlike. Feel free to use and adapt EDF as you see fit. If you do let me know.