Types From Data Files

Learn to load game object types and instances from external text files
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

View Full CourseGet Started for Free
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

The Type Object pattern introduced in the previous lesson provides a flexible way to manage variations among game entities by externalizing their common properties into data. This avoids a proliferation of C++ classes and makes game updates and content creation more accessible.

In this final lesson in the chapter, we put this theory into practice, combining it with the SDL_RWops techniques covered previously. We will build the foundations of a system to load these type objects and their corresponding entities from external data, process that data, and use it to instantiate our C++ objects.

We'll use data serialized as text in this project. In the next chapter, we'll build a program using binary serialization, so we can establish experience in both approaches.

Starting Point

We'll build upon the code we created in the previous lesson. A complete version of that code is provided below:

Files

main.cpp
Entity.h
MonsterType.h
MonsterType.cpp
Select a file to view its content

Reading Type Objects

Let's start with our serialized type objects. We'll represent our types as comma-seperated values within a file called types.txt. An example collection of types might look something like this:

Goblin,3,100,15,goblin.png,Swarm Dragon,7,500,45,dragon.png,RangedSnipe

If you want to follow along, this file should be in the same directory as your compiled executable.

Using JSON

In the previous lesson, we were demonstrating the concept of serialized type objects using the friendlier JSON format:

{
  "Goblin": {
    "name": "Goblin",
    "level": 3,
    "health": 100,
    "damage": 15,
    "art": "goblin.png",
    "fightingStyle": "Swarm"
  },
  "Dragon": {
    "name": "Dragon",
    "level": 7,
    "health": 500,
    "damage": 45,
    "art": "dragon.png",
    "fightingStyle": "RangedSnipe"
  }
}

We could use that in our implementation instead of our more basic comma-seperated rows. And, in a more complex project, perhaps we should.

However, writing the code to deserialize JSON is significantly more difficult. If we wanted to use JSON, we'd generally enlist the help of a third-party library. We have a dedicated lesson on handling JSON using a popular library created by Niels Lohmann:

In our MonsterType struct, let's add a static function that will read our file, and return a std::vector of our MonsterType type objects. We'll create a Bestiary alias for this type:

// MonsterType.h
#pragma once
#include <string>
#include <vector>

class Entity;
class MonsterType;
using Bestiary = std::vector<MonsterType>;

enum class FightingStyle { /*...*/ }; struct MonsterType {
static Bestiary LoadMonsterTypes(); };

Let's start our implementation by loading the entire types.txt file into a std::string, using techniques we covered earlier in the chapter. To keep things focused, we'll skip error checking in these examples:

// MonsterType.cpp
#include <SDL.h>
#include <string>
#include <vector>

// ...

Bestiary MonsterType::LoadMonsterTypes() {
  char* Raw{static_cast<char*>(
    SDL_LoadFile("types.txt", nullptr))};
  std::string Data(Raw);
  SDL_free(Raw);

  Bestiary MonsterTypes;
  // Populate the array (next)
  return MonsterTypes;
}

Deserializing Type Objects

Next, let's focus on populating the local MonsterTypes array within our function, so we can return all the types stored in our text file. Having loaded our serialized file, our Data string currently looks like this:

Goblin,3,100,15,goblin.png,Swarm Dragon,7,500,45,dragon.png,RangedSnipe

To deserialize this into a collection of type objects, we'll do three things:

Firstly, we'll identify all of the line breaks in our string, and then create a substring for each line. For this example file, that will leave us with two strings:

  1. "Goblin,3,100,15,goblin.png,Swarm"
  2. "Dragon,7,500,45,dragon.png,RangedSnipe"

Then, for each of these strings, we create a substring for each of the comma-seperated values. Each line will result in 6 substrings which, for our goblin example, will look like this:

  1. "Goblin" (the name)
  2. "3" (the level)
  3. "100" (the health)
  4. "15" (the damage)
  5. "goblin.png" (the art file)
  6. "Swarm" (the fighting style)

Finally, we need to create a MonsterType object using these values. This will involve converting the strings to the correct types that our MonsterType struct expects.

Splitting Strings into Tokens

Let's start by splitting a string into it's substrings (or "tokens") based on a delimiter. To split our string into its lines, our delimiter will be the new line character, \n. Then, to split each of those strings into the comma-seperated values, we'll split on the comma , delimiter.

This string-splitting capability will be useful in multiple locations, so we'll create an inline function for it in a new Utilities.h file. This function uses the same techniques we covered in our earlier lesson on parsing data using a std::string:

// Utilities.h
#pragma once
#include <string>
#include <vector>

namespace Utilities {
inline std::vector<std::string> SplitString(
  const std::string& Str, char Delimiter
) {
  std::vector<std::string> Tokens;
  size_t StartPos{0};
  size_t FindPos;

  while (
    (
      FindPos = Str.find(Delimiter, StartPos)
    ) != std::string::npos
  ) {
    Tokens.push_back(
      Str.substr(StartPos, FindPos - StartPos)
    );
    StartPos = FindPos + 1;
  }

  // Don't forget the last token
  if (StartPos <= Str.length()) {
    Tokens.push_back(Str.substr(StartPos));
  }

  return Tokens;
}
}

Advanced: String Streams and std::getline()

Our SplitString() function can be implemented in a slightly simpler way using the standard library's string stream utility, in conjunction with std::getline(). Both are available within <sstream>:

// Utilities.h
#pragma once
#include <sstream>
#include <vector>
#include <string>

namespace Utilities{
inline std::vector<std::string> SplitString(
  const std::string& Str, char Delimiter
) {
  std::vector<std::string> Tokens;
  std::string Token;
  std::istringstream TokenStream(Str);

  while (std::getline(
    TokenStream, Token, Delimiter
  )) {
    Tokens.push_back(Token);
  }
  return Tokens;
}
}

We don't cover standard library streams here, but we have a full chapter on them in our advanced course, including a dedicated lesson on string streams specifically:

Over in our MonsterType.cpp file, let's #include our new utility function and use it to first split our string into it's lines based on the line break \n delimiter, and then split each of these lines into its 6 values based on the comma , delimiter:

// MonsterType.cpp
#include "Utilities.h"
// ...

Bestiary MonsterType::LoadMonsterTypes() {
  char* Raw{static_cast<char*>(
    SDL_LoadFile("types.txt", nullptr))};
  std::string Data(Raw);
  SDL_free(Raw);

  Bestiary MonsterTypes;
  using namespace Utilities;
  for (std::string& Line : SplitString(Data, '\n')) {
    std::vector Fields{SplitString(Line, ',')};
    // Create and push the type object
    // ...
  }

  return MonsterTypes;
}

Creating Type Objects

Within each iteration of our for loop, our Fields vector now has the 6 strings it needs to create the current type object - that is, each instance of MonsterType represented by the current line we're processing.

However, each of the fields is a std::string, so we need to convert it to the correct type expected by the MonsterType struct. In order, the fields correspond to:

  1. MonsterType::Name - this is is a std::string, so we can use our deserialized value directly - no conversion needed
  2. MonsterType::Level - this is an int, so we'll use std::stoi() to convert our string to an integer
  3. MonsterType::Health - this is an int so we'll use std::stoi()
  4. MonsterType::Damage - this is an int so we'll use std::stoi()
  5. MonsterType::ArtFileDamage - this is a std::string so no conversion needed
  6. MonsterType::Style - this is a value from our FightingStyle enum. We'll create a function to map the std::string to the corresponding FightingStyle.

We don't have any float values in this example but, if we did, we'd convert the strings to the floating point value using std::stof().

Let's start by adding our std::string to FightingStyle enum mapper. This function is only needed in the MonsterType.cpp file, so we'll add it there. We'll put it in an anonymous namespace to prevent polluting our global namespace:

// MonsterType.cpp
// ...

namespace{
FightingStyle StringToFightingStyle(
  const std::string& StyleStr
) {
  if (StyleStr == "BruteForce") {
    return FightingStyle::BruteForce;
  }
  if (StyleStr == "RangedSnipe") {
    return FightingStyle::RangedSnipe;
  }
  return FightingStyle::Swarm; // Default
}
}

// ...

Finally, in LoadMonsterTypes(), we'll populate our MonsterTypes array before returning it:

// MonsterType.cpp
// ...

using Bestiary = std::vector<MonsterType>;
Bestiary MonsterType::LoadMonsterTypes() {
  char* Raw{static_cast<char*>(
    SDL_LoadFile("types.txt", nullptr))};
  std::string Data(Raw);
  SDL_free(Raw);

  Bestiary MonsterTypes;
  for (std::string& Line : SplitString(Data, '\n')) {
    std::vector Fields{SplitString(Line, ',')};
    MonsterTypes.push_back({
      // Name
      Fields[0], 
      // Level
      std::stoi(Fields[1]), 
      // Health
      std::stoi(Fields[2]),
      // Damage
      std::stoi(Fields[3]), 
      // Art File
      Fields[4], 
      // Fighting Style
      StringToFightingStyle(Fields[5])
    });
  }

  return MonsterTypes;
}

Over in main, let's confirm everything is working:

// main.cpp
#include "MonsterType.h"
#include "Entity.h"

int main() {
  Bestiary Types{MonsterType::LoadMonsterTypes()};
  std::cout << "Types: ";
  for (MonsterType& Type : Types) {
    std::cout << Type.Name << ", ";
  }
}
Types: Goblin, Dragon,

Enum Casting

In this section, we added a function for mapping strings to enum values. That function required us to check every possible value in the enum. This is fine for simple cases but, in more complex projects, we'd want to build a better system, or use a library to help us.

A popular choice is Magic Enum which, among other things, allows strings to be converted to the enum value with the same name. That would allow us to replace our StringToFightingStyle() function, and any similar function we might have needed in the future, with code that looks like this:

// If Fields[5] is a string like "Swarm",
// this will return FightingStyle::Swarm
magic_enum::enum_cast<FightingStyle>(Fields[5])

Loading Dungeon Data

Now that our type objects are being loaded successfully, let's add some data to represent our dungeon. Each monster in our dungeon will be an Entity, whose Type is one of the type objects we deserialized.

Our serialized dungeon data might look something like below, where every line will represent an enemy, and the commas within each line delimit the type, x position, and y position of that enemy.

We'll call this file dungeon.txt and store it alongside our executable:

Goblin,5,3
Goblin,5,6
Goblin,7,4
Dragon,9,9

We can construct all the objects our dungeon requires, attaching the correct type object. For this example, we'll store them in a std::vector within a Dungeon instance.

Our Dungeon constructor will will be responsible for populating its Enemies array. To help with that, we'll provide a reference to our Bestiary - the array of type objects we deserialized earlier:

// Dungeon.h
#pragma once
#include <vector>
#include "Entity.h"
#include "MonsterType.h"

struct Dungeon {
  Dungeon(const Bestiary& Types) {
    // Populate Enemies (next)
  }

  std::vector<Entity> Enemies;
};

Deserializing Dungeons

To populate our Enemies from the serialized data, let's start by following the same pattern as before. We'll load our dungeon.txt file into a std::string. We'll then use our SplitString() utility to split that string into a vector of lines, and then each line into a vector of values:

// Dungeon.h
#pragma once
#include <string>
#include <vector>
#include <SDL.h>
#include "Entity.h"
#include "MonsterType.h"
#include "Utilities.h"

struct Dungeon {
  Dungeon(const Bestiary& Types) {
    char* Raw{static_cast<char*>(
      SDL_LoadFile("dungeon.txt", nullptr))};
    std::string Data(Raw);
    SDL_free(Raw);

    using namespace Utilities;
    for (std::string& Line : SplitString(Data, '\n')) {
      std::vector Fields{SplitString(Line, ',')};
      // Construct an entity and add to Enemies array
    }
  }

  std::vector<Entity> Enemies;
};

This time, each entry in our Fields array has only three values but, as before, they're all std::strings. To construct an Entity, we need the following conversions:

  1. Entity::Type is a MonsterType pointer. We'll need to convert the std::string to the appropriate pointer within our type object collection, which is the Bestiary provided to our constructor.
  2. Entity::X_Pos is an int, so we'll convert the second std::string in Fields to an int using std::stoi().
  3. Entity::Y_Pos is also an int, so we'll use std::stoi() on the third value in Fields too.

Let's start by creating a static function that will convert the type string to the corresponding MonsterType pointer from the bestiary. The MonsterType struct seems like the natural place for such a function:

// MonsterType.h
// ...

struct MonsterType {
  // ...
  
  static const MonsterType* FindType(
    const Bestiary& Types,
    const std::string& DesiredType
  ) {
    for (const MonsterType& Type : Types) {
      if (Type.Name == DesiredType) return &Type;
    }
    return nullptr;
  }
};

Design Problems

When we find ourselves searching through an array for a specific object, that is often a sign that our design can be improved.

That is the case here, too - an array such as a std::vector works, but there are other types of container more suited to finding specific objects within the collection. Examples include std::map or std::unordered_map.

We'll leave our bestiary as a std::vector is for now, as we're primarily focused on getting experience with serialization, and don't want to add additional complications. We'll revisit maps later in the course.

Back in our Dungeon constructor, let's use this new FindType() function, alongside std::stoi() , to populate our collection of enemies:

// Dungeon.h
#pragma once
#include <string>
#include <vector>
#include <SDL.h>
#include "Entity.h"
#include "MonsterType.h"
#include "Utilities.h"

struct Dungeon {
  Dungeon(const Bestiary& Types) {
    char* Raw{static_cast<char*>(
      SDL_LoadFile("dungeon.txt", nullptr))};
    std::string Data(Raw);
    SDL_free(Raw);

    using namespace Utilities;
    for (std::string& Line : SplitString(Data, '\n')) {
      std::vector Fields{SplitString(Line, ',')};
      Enemies.emplace_back(Entity{
        // Type Object
        MonsterType::FindType(Types, Fields[0]),
        std::stoi(Fields[1]), // X Position
        std::stoi(Fields[2]), // Y Position
      });
    }
  }

  std::vector<Entity> Enemies;
};

Finally, let's create a Dungeon and confirm everything is working:

// main.cpp
#include "MonsterType.h"
#include "Entity.h"
#include "Dungeon.h"

int main(int argc, char** argv) {
  Bestiary TypeObjects{
    MonsterType::LoadMonsterTypes()
  };
  std::cout << "Types: ";
  for (const MonsterType& Type : TypeObjects) {
    std::cout << Type.Name << ", ";
  }

  Dungeon DragonsLair{TypeObjects};
  std::cout << "\nEnemies: \n";
  for (const Entity& E : DragonsLair.Enemies) {
    std::cout << "  " << E.Type->Name
      << " (" << E.X_Pos << ", "
      << E.Y_Pos << ")\n";
  }

  return 0;
}
Types: Goblin, Dragon,
Enemies:
  Goblin (5, 3)
  Goblin (5, 6)
  Goblin (7, 4)
  Dragon (9, 9)

Complete Code

A complete version of the program we created over the last two lessons is provided below:

Files

main.cpp
Entity.h
MonsterType.h
MonsterType.cpp
Dungeon.h
Utilities.h
Build
Select a file to view its content

Summary

In this lesson, we put the Type Object pattern into action by reading MonsterType definitions and Entity configurations from external files. We used SDL_LoadFile() for file input and developed string parsing logic to convert text data into C++ MonsterType and Entity objects, creating a data-driven foundation for game content.

Key Takeaways:

  • External files (like types.txt, dungeon.txt) can define game object characteristics.
  • SDL's file utilities like SDL_LoadFile() help read data for parsing.
  • Parsing often involves splitting file content into smaller parts, often recursively (eg, data into lines, and then lines into individual fields.
  • String tokens need conversion to specific data types like int using std::stoi(), or custom enums using helper functions or external libraries.
  • Loaded type objects can be stored (e.g., in a Bestiary) and used to instantiate game entities.
  • This method externalizes game data, making it easier to manage and modify without requiring C++ knowledge.
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

View Course
Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Updated
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

View Full CourseGet Started for Free
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 120 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

View Course
Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved