3
\$\begingroup\$

The Context

I'm currently working on a game and its engine using C# and MonoGame. The engine is data-driven and I want people to be able to mod the game or make their own using my engine. All the assets and data are processed using the MonoGame Content Pipeline.

Currently, I'm loading the game data and passing it into a sort behavior object (I guess this could be seen as a business model?) like so:

// Data object, in the real world it's less like a DTO and more like an anemic model.
// Almost always originally a *.json compiled to *.xnb by the content pipeline.
public class CharacterData
{
    public string Name { get; set; }
    public string TexturePath { get; set; }
    public string WeaponPath { get; set; }
    public string RivalPath { get; set; }
}

// Actual behavior object the game interacts with. May be serialized in a savegame file.
public class Character
{
    public Character(CharacterData data, ContentManager content)
    {
        Level = 1,
        Hp = 234,
        Name = data.Name;
        Texture = content.Load<Texture2D>(data.TexturePath):
        Weapon = new Weapon(content.Load<WeaponData>(data.WeaponPath));

        // Possible circular reference leading to a StackOverflowException if it points 
        // to the same data object on disk or if two Characters are each other's rival.
        Rival = new Character(content.Load<CharacterData>(data.RivalPath)); 
    }
}

The Problem

The problem I'm facing right now seems to be an architectural one that leads to large object graphs, circular references and StackOverflowExceptions.

The above example is relatively simple but I know if I keep doing this I'll run into a much worse scenario. For example:

A Map that has a list of Characters which themselves have a list of Spells or Items which themselves point to yet another data object and so on and so forth.

In the end, I'm going to load almost the entirety of the game assets because of that, which I obviously don't need and don't want most of the time. That's not even mentioning the poor design and likelihood of infinite recursion as I mentioned above, let alone serializing this graph.

What am I missing here? What sort of architecture is best suited for what I want to achieve? I'm sure I need to refactor this but I'm unsure how. Any tips would be greatly appreciated.

A Few Notes

  • Somewhat relevant to my current architecture, I also have an editor (WPF + MVVM). The data objects are created there and passed to ViewModels in a similar fashion to what I'm doing with the behavior objects.
  • The editor doesn't care about the game's behavior, only the data that it will use.
  • This whole thing is extensible, people can create their own libraries with their own data types, meaning that I can't control everything.

Thanks!

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Separate your contentloading from the class that holds your object data.

public static class Art
{
    private static Dictionary<string, Texture2D> _textures;
    private ContentManager _cm;
    
    public static Init(ContentManager cm)
    {
        _cm = cm;

        //add each texture to the dictionary of textures with the path- you were sure to load anyway.
    }

    public static void LoadAsset(string path)
    {
        if(_textures.KeyExists(path))
          return;

        _textures.Add(path,_cm.Load<Texture2D>(path)); // add your checks for the existence of file etc.
    }
    
    public Texture2D Sprite(string path)
    {
        return _textures(path); // ofcourse do some checking if a path exists in the dictionary etc.
    }
}

Now your paths are no longer causing duplicate content to load because you decoupled that so:

// Actual behavior object the game interacts with. May be serialized in a savegame file.
public class Character
{
    public Character(CharacterData data)
    {
        Level = 1,
        Hp = 234,
        Name = data.Name;
        Texturepath = data.TexturePath;
        Art.LoadAsset(Texturepath);
    }
}

Now in your draw function you can point to the Art.Sprite() to pick up the texture;

Next to solve the rivals circular references- you shouldn't declare "new" characters in each character you create. Create a pool of characters first. And then appoint rivals afterwards.

Weapons can be a mixed bag. Let's say you have a weapon like "excalibur"- one exists, but any character could own it- so you may want to create it outside of the character. On the other hand, each weapon could be unique in the sense that it has its own durability stat- in that way you can create one when needed (when a character is created, a chest is opened etc).

\$\endgroup\$
1
  • \$\begingroup\$ You're right, creating objects inside other objects was definitely not the way to go for me. Turns out creating them outside works quite well in my scenario. I also created a non-static ContentLoader similar to what you sugged that I pass to the constructor and it's been working quite well too. Thanks for the help! \$\endgroup\$
    – sboutin4
    Feb 2, 2022 at 3:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .