Data Flow

Custom ECS engine -- ECS C++

▚ Project summary:


For this project, our team was assigned to create a tower defense game using a custom made 3D game engine.

Our team put a lot of effort into designing and creating a custom engine. After trying different ECS systems, We eventually settled on using EnTT as our entity component system in our custom engine. We called our Engine, the Frac engine. 

Meanwhile, during the main development of the custom engine, the other gameplay programmer and I prototyped, researched, documented and developed important tower defense related features that would later be used in the final engine and concept.

We started the development of Dataflow on May 27th,  in our custom engine Frac. 

The concept for Data Flow was to create a rock-paper-scissor tower defense game with a sci-fi feel in the end. Where you fight off enemies on a rooftop in a sci-fi cityscape with an isometric camera angle. 

During the last 8 weeks of the project, we were able to polish and finalize our game to the current state.

Please have a look at the final result in the video below.

▌Specifications:

Released: July-2021
Studio: studio name
Platform: PC, Windows. Nintendo Switch
Time spent on project: 1 school year on research and prototyping. 8 weeks specifically on this project.
Engine and Tools: Custom ECS Engine - C++

▌My contributions:

○ Created a fully worked out gameplay class diagram
○ Implemented the level grid generation
○ Developed the Ogmo to Houdini workflow
○ Implemented pre level and full level loading
○ Implemented the Json parsing functionality
○ Implemented A* in our data flow project
○ Implemented the entity creator classes, responsible for creating and deleting entities specifically needed in our game. 
○ Implemented smoother enemy movement, with a custom rotate towards function.
○ Implemented proper game object placement on the gird, using offset height.  
○ Implemented tower rotation towards active targets. 
○ Created the final designs/ structures of the levels.

▌Project goals:

○ Create a game, using a custom made Entity Component System engine.
○ Create a fully working, fun to play, tower defense game. Using the theme "Data Flow". 
○ Learn more about C++ and ECS.

▚ Gameplay UML


Antreas and I decided, to prepare ourself as much as possible as we needed to prepare our gameplay structure. The most important decision we made upfront: the structure we wanted to make, would be a cleanly written/working tower defense structure that would be easily expandable to fit our base concept, when needed.

In the class diagram diagram, we first made a rough version of the classes we planned out to use this block.
This was a new challenge, because making a class diagram diagram for an ECS game was something I have never done before.
Link to full class diagram


▚ Ogmo level setup


To create our levels, I developed a work flow where we define our level structure and entity/interactable inside the free to use 2D map editor, OGMO

We created maps, where colors indicate different aspects/details of tiles. 
In our levels, the following colors indicate specific tiles. This data is stored in Json files, and used in both the model generation and level setup in engine.

Black:

Indicates empty null tiles.

Red:

Indicates walkable, empty tiles. 

Blue:

Indicates turret spawn positions. 

Yellow:

Indicates enemy spawner positions. 

Pink:

Indicates the Core entity tile position. 

The OGMO editor

level in OGMO

Using OGMO, we were able to create multiple levels, easily tweak levels, tweak the Json data, update the procedural generated models, but most importantly, improve the feel and playability of our game after reaching the polishing stage.


▚ Ogmo to Houdini workflow


I worked together with Niels Voskens, who is a really talented Visual Artist when it comes to procedural generating meshes/levels, to generate a custom mesh from the exported Ogmo level data.
After researching and testing, we settled on a workflow where the Ogmo level images, are used as input in a custom tool, made in Houdini. 
The tool uses every pixel position as coordinates and the pixel color as indication on what section of the level/ detail of the level should be generated. 

Exported images are directly used in Houdini, to create accurate and fully working level meshes.

Something I really liked, is that we separated the core functionality of the full tower defense game, from the assets of the levels.
Levels are fully working/testable, without having any meshes. The level Json data is used from the exported Ogmo files, on which a custom generated level grid is generated separately from the level mesh. 
I will talk more about this in the later sections of this page.

Generated level in Houdini

Generated level in engine

More about the amazing level generation workflow in Houdini, from Niels Voskens, can be found here.


▚ Pre loading level data


I first decided to pre define which possible levels would be able to load. 
This has 2 reasons, it makes sure I don’t have to pass on specific Json file paths/ or file names when loading a level. And it makes it possible to store specific level data inside a struct. 

For now, I store the level id, Json file name, level target score, camera start position and camera start rotation. 
In the PreLoadLevelData function I use the Nlohmann Json parser to be able to read out and use the json file data in C++.


Especially the camera start position and start rotation are important, when switching levels. 


Using a for loop and the levelData array size, I create different LevelData structs and fill in the right variables with the right json data.

Whenever I want to add a new level, I can create a new levelData array element, which holds the right json file name. 


▚ Parsing the Ogmo json data


Whenever a level is loaded from a exported Json level file, the custom made Ogmo parser is used. 
Inside the Ogmo parser multiple things are happening. 

I first set the active level file, The filepath string is received from the specified Level Data struct (shown in previous section). 

I then use the ParseOgmoGridData function to set the m_gridSize and m_ParsedLevel inside the Ogmoparser class. This is both retrieved from the level Json file.
An example of this level Json file is shown below.

Inside the ParseStaticLevelInteractables function I first clear the static level vectors (inside the Ogmo parser). 
I then loop over the different arrays specified inside the Json file. 
I create specific "Static object data" structs, where I can store these specific variables defined in the json file.

In the example on the right, I am reading the data that is stored in the "CoreEntities" array. This array hold another array, called "values". Here I can get the specifically created variables from the ogmo editor. 

In the example shown, I retrieve ObjectID, CoreHealth and CoreTargetData. This data is set in the custom "Static object data" structs

This is an example, and the needed variables can later be created and used when placing entities when loading a level.


▚ Loading the level data


In the LoadLevel function, I first check if the requested level exists. I do this by checking if the int level, is higher the the predefined number of levels defined in the LevelData.json.
Whenever the requested level is valid, I first get the right level data struct and set that to m_activeLevelData.
I then use the m_ogmoParser, set the active level file (json file).

I then call the ParseOgmoGridData function, which reads out the tiles 2d array values.

I then call the ParseStaticLevelInteractables function (shownin the previous section), which parses and saves data structs with specific defined variables.

I clear my possible old data, and copy the data inside the variables inside the LevelManager
I then use SetGameTileManagerData to preset the size of the tile map in the TileManager.

I call the createStaticLevelModel and CreateAllPreLoadedLevelEntities functions to properly create all the needed entities, with the right data.


▚ Tile entities and Level grid generation


The tile component struct holds the necessary data, which are used, set or accessed by systems such as the FloodFill, the AStar system and during the main gameplay systems. More about this, and how the data is used accordingly in the sections below.


After Finalizing the Ogmo level parsing, the final level data is correctly parsed and usable to send to towards the tile manager. To actually create all the needed tiles as entities I created a custom function to create all these tiles using the right created data.
I do that using the CreateTileMap function in the tilemanager class.
the createTileMap function uses the grid data, to create the right tiles. 
The grid data is used to check for active tiles, buildable tiles and creates the right entities on the right entity id.
 

The CreateTiles Function is responsible for the creation of the entities, creating the right components for these entities and using the parsed level data.

After the function is called, the tiles are generated and the whole tile map is working correctly.

As seen in the images above, the created tile entities are lining up with the loaded level. The data is correctly used, but kept separately from the level mesh. This was intended, to make sure the visual representation of the level is separate from the actual gameplay/level grid system.


▚ A* in Data Flow


To make our enemies move towards the target, we wanted to test with several pathfinding algorithms. For the first pathfinding algorithm we wanted to test, I implemented the AStar algorithm into our Data Flow project. 
All my research done into AStar, helped but I encountered a few challenges.

To store the data, I created a AStarComponent. This holds all the important variables, needed to successfully use the algorithm. In here I first learned how different it was, to implement AStar inside a ECS work environment, in comparison to a OOP work environment.

The RunAStarAlgorithm function uses the grid start position and gridTarget position as input data. I kept this explicitly simple, because I wanted this function to be callable for many different reasons/purposes.
I had to rework my previous written AStar algorithm to work with out entity component system, especially handling getting the right components and data was different. 

After the function is finished and a path is found, the result of the algorithm is retraced and stored inside the "m_LastRetracedPath". This accessible to be used for many different purposes.
The result of the working AStar algorithm, in our data flow project:

We ended up using the FloodFill algorithm to generate a flow path once, which is more usable with our enemies, which spawn from different locations.


▚ Final level designs



I talked a lot with Niels Voskens (VA), and we decided to use a iterative approach while creating the levels.
Niels (VA) created a guide which would help while creating new levels. It included some suggestions/rules.

After going back and forth with Niels (VA) I was able to create 5 different levels, with unique specific features/designs, while keeping the “rules” from Niels (VA) in mind.
I exported the data for each level, made sure the Json files were correct and were using the enemy spawner entities, which are used by the level loading and parsing.

Niels (VA) generated all the height json files, which are needed to correctly place all entities using the right height.

Niels Voskens (VA) Notes

The 5 levels:

Now that the Json files are in place, I was able to test all the levels in engine.
I am really happy with the final results, and the levels actually do feel like they play better then the previous levels.


▚ Closing thoughts


I am really proud on my work on implementing the levels. I really enjoyed trying to make it dynamic. In the end the level data file was correctly used to, for example, set the start resource amount per level.
Being able to dynamically reload specific variables was something I really wanted to be working properly.

When the level grid data aligned with the level models, using the working entity creators, it was really rewarding, including the positive feedback from the team.

I am really proud on how I learned to write better C++ and learned more about working in a Enity Component System work envoirnment.

Data Flow itch.io