handling data/state in a board game - composition/coupling

Started by
9 comments, last by Mussi 8 years, 5 months ago

I'm pretty new to gamedev and I'm starting to make more complex games, but handling the growing amount of data, classes and functions, while still trying to use best design-practices, is proving tricky!

Hypothetically, say I want to make a version of the board game "monopoly". In very pseudo-code (I'm using C++ but this probably applies to other languages), I'd try to model the data showing the current state of the game like this:


class Board {
private:
    container<Square> squares; //board locations to land on. Square is a base class
                               //from which classes like Property, Station inherit
    container<ChanceCard> chanceCards;
    container<CommChestCard> commChestCards;
    Bank bank; //to begin, bank owns all Deed objects, which are linked to Property squares
    unsigned freeParkingMoney; //amount of money in the center of the board
                               //that you win by landing on free parking
    //etc..
};


class Player {
private:
    string name;
    unsigned money;
    Token token; //hat, iron, shoe ...
    container<Deed> ownedDeeds;
    //etc
};

class GameData {
private:
    Board board;
    container<Player> players;
    Dice dice;
    Player* activePlayer;
};

Rather than having a huge class with all the states in, GameData, I've tried to use modularity and break things up into sub-parts, using composition. (There are some missing class definitions of course, this is just a sketch.) This is all separate from the game's rendering / state stack / gameloop / input handling etc.

The problem with this comes when a player wants to take some action in the game. For example, say I wanted the current player to buy a property deed using the function buyDeed().

  1. How does it access all the data it needs?

I have reduced the size & responsibility of the GameData class and made everything more modular. But on the flip side, the data is spread all over the place and hard to connect together. buyDeed() would need access to GameData::board, and then Board::bank, and then Bank::deeds. Then it would hand over ownership of that deed and give it to the active player, and have the player pay for it, which requires access to GameData::activePlayer, and Player::ownedDeeds and Player::money. I'm not sure how to address this - I could make a lot of the data global/static/public, which is generally considered bad design. Or store a reference/pointer to each dependent object in each class - which tightly couples all my classes.

2. Where should the function go?

Because the function would act on a lot of data in several different class scopes, it isn't clear where to define the function. I could pass the dependencies along through member functions in each class, so each class operates on its own data, using pass-through functions - but that would quickly lead to spaghetti code I think, and it would be hard to track the program flow. Or have the logic in a separate GameEngine class that can modify GameData - that would be nice and separate, but I get back to how GameEngine accesses the data.

I've tried reading up on game design patterns, like dependency injection, or components etc. But most articles focus on decoupling the rendering / physics / input in games, not amongst the actual game data in a composition-style design. So I'd be interested in how more experienced gamedevs would go about approaching this. Or, if the design is just in general bad, how you would fix it? Thanks!

[edit: typo]

Advertisement

Unusual Terminology Warning: I'll refer to "state" as being the data you're holding in memory that describes the current state of the game in progress and "scene", which is a state in your main FSM that transitions between things like the main menu and playing the game.

Using a class to encapsulate your persistent state isn't a bad idea. You want to separate the idea of persistent state from scene data, which is ephemeral. You can use composition to store the various types of persistent data in a single object and access it from whatever scene is currently running. This gives a few non-obvious advantages in that you can also attach it to your messaging system, and if you want to have a save/load feature then all of the data you'd need to serialize is in one location. It's also a good place to have single-instance utility objects, like a timer or an mt19937.

One way to reach the object is via dependency injection; passing a pointer to it into your scene objects when they are constructed. Less preferable methods involve singletons or other forms of global state, which are sometimes problematic.

Generally I'll have a "Game" object that encapsulates just about everything. That object will contain objects such as:

  • The scene controller, which runs the main FSM and provides access to the selected scene object. (private)
  • A "system" object, which holds things like the window handle or other platform-dependent things which may need interacted with. (access varies by project)
  • A utility object container, which holds the timer, randomizer, and other objects that provide "global" services. (publicly accessible)
  • The message dispatcher / scheduler. (publicly accessible)
  • The state object, which encapsulates all persistent state and may have load/save functions. (publicly accessible)

Note that the state object is usually a composition of several different parts of logically organized data. It can hold a vector of player objects, the board object, etc, etc. Don't be shy about composition.

In main() I create and configure an instance of Game and then call its run function. Game creates the initial scene object and passes in a pointer to itself. Using the run-and-return-successor strategy, each new scene is created by the currently running one, which simply passes its copy of the Game pointer into the new object. In this way the functions within the scene can access the persistent state by something like:

game->statePackage->board.freeParkingMoney;

or roll some dice like:

int resultOfTwoDice = game->utility->rollDice(2);

If you're writing some function that needs to use a persistent state member frequently then you can just make a local reference to it (in the scope of the function) so you don't have to type the long trail every time.

That's the the general idea that I use, anyway. I'm interested to see what other people do.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

Thanks for your post, it was very helpful. Always good to see how other people approach a problem.

In this way the functions within the scene can access the persistent state by something like:

game->statePackage->board.freeParkingMoney;

Ah, so in this case, board and freeParkingMoney would be public members? To me that seems fine, though I keep reading how having public variables / getters in a class would be bad design.

(I am not sure what terminology to use in C++ for composition... ie. if A has a member B, how to describe their relationship? All the suitable words like parent, child, root, base, sub-object, they all seem to be used to imply inheritance, haha. I'll say B is a sub-object and A is the root object).

Anyway, it seems perfectly fine for me to allow GameData to publically access its sub-objects, and their sub-objects, or indeed any data contained within it, directly. Though it definitely seems to be frowned upon. It would just be so much easier. I mean, the whole buyDeed function could be written like:


void buyDeed(ID deedID, GameData gameData) {
    auto &activePlayer = gameData.players.find(gameData.activePlayer);
    auto &playerDeeds = activePlayer.ownedDeeds;
    auto &bankDeeds = gameData.board.bank.ownedDeeds;
    auto &deedToMove = bankDeeds.find(deedID);
    auto &playerMoney = activePlayer.money;
    
    if ( playerMoney > deedToMove.cost() ) {
	transferDeed(deedID, playerDeeds, bankDeeds);
	playerMoney -= deedToMove.cost();
    }
}

I can only imagine what a mess that would be using best-practices and private data, if each sub-object could only access its own data. I'd need to write a load of member functions in each sub-class and pass through them all. And I'd need to pass as a parameter the required data so the function at the bottom has it. TBH there'd be a load of getters/setters anyway, so the members may aswell be all public. They are all sub-objects related to the root class of GameData anyway, it's not like i'm exposing them to the rest of the game code.

What does everyone else think?

Your design looks pretty good to me!

1. When you have a group of interacting 'things', like you say, you have to either make them accessible to each other or tell each one about the other ones it needs to talk to. No easy 3rd way unfortunately!

2. I'd put the function in the place where it needs most access to, then you reduce the above inter-dependency problem, but it can't be completely avoided.

I'd guess that most released games are nothing like as tidy as your design, with plenty of globally-accessible stuff ;-)

When you use composition, you can make the members public without sacrificing the restrictions on access - if you have the appropriate access level for the Board class, making Board a public member of GameData gives no further access to Board, but can mean less boilerplate to access it (depending on your answer to point 1 above).

Thanks for the reassurance! I guess when designing a modular composition-hierarchy for a complex object, there sometimes is no easy answer like you say. The contained sub-objects and functions will just have too many intertwined dependencies to split them in a nice way. Broadening the access is never good but can't really be helped. To me it's still better than storing references to the required data or passing lots of parameters around. I guess the most important thing is that the code works and is reasonably clean, not that it's a flawless example of OOP design ;D


(I am not sure what terminology to use in C++ for composition... ie. if A has a member B, how to describe their relationship? All the suitable words like parent, child, root, base, sub-object, they all seem to be used to imply inheritance, haha. I'll say B is a sub-object and A is the root object).

B is a component of A and A has a component B. A is composed out of B, C, etc.


B is a component of A and A has a component B. A is composed out of B, C, etc.

True, that's a pretty good description. I asked that question on SO and got a pretty good answer as well: http://stackoverflow.com/questions/34022697/terminology-for-class-objects-related-by-composition

Thanks for your post, it was very helpful. Always good to see how other people approach a problem.

In this way the functions within the scene can access the persistent state by something like:

game->statePackage->board.freeParkingMoney;


Ah, so in this case, board and freeParkingMoney would be public members? To me that seems fine, though I keep reading how having public variables / getters in a class would be bad design.


Firstly, not everything needs to be a class. You can use structs for composition. (And nobody will argue against it... Hopefully...)

Frankly though, if you disallow public state and getters like some people suggest (stares pointedly at Herb and Bjarne) then you're just making your life far more difficult without an equally valuable payoff.

The reasoning that people use to discourage public data members in classes is that they may invalidate the class invariants and that they may make it harder to refactor if you decide to change the way the class works.

In the first case, learn what invariants are and why they're important and then don't invalidate your invariants through public members. If you understand invariants then it's common sense, and the rule about public member data is really erring too far on the side of caution in my opinion.

In the second case, modern IDEs will let you chase down references to a member variable, so it's really not hard to change at all.

There's an additional argument if you're writing an interface or something similar, but in that case you should just use trivial getters/setters. You're not writing an interface here, so it's not really relevant. The concern in that case is that you'd be violating the open/closed rule (open for extension / closed for modification), which means that the people using your interface may write code that uses the public members, then if you changed them to be member functions (getters/setters with some logic) instead your clients may have to re-write a bunch of their code.

Anyway, it seems perfectly fine for me to allow GameData to publically access its sub-objects, and their sub-objects, or indeed any data contained within it, directly. Though it definitely seems to be frowned upon. It would just be so much easier. I mean, the whole buyDeed function could be written like:


void buyDeed(ID deedID, GameData gameData) {
    auto &activePlayer = gameData.players.find(gameData.activePlayer);
    auto &playerDeeds = activePlayer.ownedDeeds;
    auto &bankDeeds = gameData.board.bank.ownedDeeds;
    auto &deedToMove = bankDeeds.find(deedID);
    auto &playerMoney = activePlayer.money;
    
    if ( playerMoney > deedToMove.cost() ) {
	transferDeed(deedID, playerDeeds, bankDeeds);
	playerMoney -= deedToMove.cost();
    }
}
I can only imagine what a mess that would be using best-practices and private data, if each sub-object could only access its own data. I'd need to write a load of member functions in each sub-class and pass through them all. And I'd need to pass as a parameter the required data so the function at the bottom has it. TBH there'd be a load of getters/setters anyway, so the members may aswell be all public. They are all sub-objects related to the root class of GameData anyway, it's not like i'm exposing them to the rest of the game code.

What does everyone else think?


That's a little overboard with the references there. The idea I was communicating about references is that you can use them to avoid having to type out the full chain for some variable that you use several times in the same function. In the case shown you're just creating a reference for everything, which is actually more typing rather than less, lol.


void buyDeed(ID deedID, GameData gameData) {
    auto &activePlayer = gameData.players.find(gameData.activePlayer);
    auto &bankDeeds = gameData.board.bank.ownedDeeds;
    
    if ( playerMoney > deedToMove.cost() ) {
	transferDeed(deedID, activePlayer.ownedDeeds, bankDeeds);
	activePlayer.money -= bankDeeds.find(deedID).cost();
    }
}

Anyway, these classes/structs are intended to store state for access by the program. The program has to be able to access that state in some way or else they fail their job.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

That's a little overboard with the references there

Heh, yeh I just sketched that out quickly...was just revelling in the ease of access to those nested variables, y'know?


Firstly, not everything needs to be a class. You can use structs for composition

True, I was thinking about using structs here. Since I've decided to place the functions elsewhere GameData may aswell be a struct. I'll likely make a GameEngine class which owns the GameData and operates on it, and owns some other helpful objects of its own.


learn what invariants are and why they're important and then don't invalidate your invariants through public members.

Good tip... I'd always try to make sure to be very careful to preserve the invariants on a case-by-case basis within whatever function, like making sure a player's money doesn't go below zero etc.


The reasoning that people use to discourage public data members in classes is that they may invalidate the class invariants and that they may make it harder to refactor if you decide to change the way the class works.

I guess another reason people advise against public variables because it will be harder to track bugs, because there are more access points to the data. While this is true, in this case the game logic & data will be all contained in GameEngine and I can restrict access to that from the rest of the game code, so bugs in the logic shouldn't be too hard to track IMO.

Thanks again for taking the time to give input, it's very helpful.

The reasoning that people use to discourage public data members in classes is that they may invalidate the class invariants and that they may make it harder to refactor if you decide to change the way the class works.


I guess another reason people advise against public variables because it will be harder to track bugs, because there are more access points to the data. While this is true, in this case the game logic & data will be all contained in GameEngine and I can restrict access to that from the rest of the game code, so bugs in the logic shouldn't be too hard to track IMO.


More that it's harder to create bugs in the first place. You gave the example of not letting a player have negative money. That could be made an invariant for the player class, in which case the variable holding player's money should definitely be private, and functions that change the amount of money should observe the limits and somehow indicate a problem if you try to remove too much.
void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

This topic is closed to new replies.

Advertisement