0
\$\begingroup\$

Currently in the process of making a game like Dwarf Fortress, so without graphics. I'm mostly thinking about how I want to implement everything. While writing code I keep stumbling on general game implementation problems that are really annoying me. Assume this:

We have a Map class which stores Chunk classes which stores Tile classes which stores Players and Enemys currently on this tile.

class Map {
    public:
        std::vector<std::vector<Chunk>> chunks;
};

class Chunk {
    public:
        std::vector<std::vector<Tile>> tiles;
        std::vector<Player*> player_need_update;
        std::vector<Enemy*> needs_update;
};

class Tile {
    public:
        std::vector<Player*> players;
        std::vector<Enemy*> enemies;
};

Now let's say I want to move() a player to a different Tile. For this I created a needs_update vector storing entities that need to be updated on the game Map. Every game tick we loop all chunks and loop all entities needing an update.

I think (thought) this is a logical way of doing this because we can have a tile that is independent of a chunk that is independent of a map. Same for entities. So let's say we want to move an Entity:

void Entity::move(uint16_t move_x, uint16_t move_y) {
    x += move_x;
    y += move_y;
    
    changed() // Notify chunk that something changed about us
}

This is already where it is going wrong. To be able to notify the chunk we have moved, we need to know ourselves in which chunk we are Chunk *in_chunk; in class Entity. And this breaks the structure I wanted to achieve (independence across classes).

Another example, suppose we want to attack an Entity on a different Tile. We would need to know there is an entity in the first place, but we would also need to know things about the enemy to then decide whether to attack it or not.

Player::attack(Enemy &enemy /* ?? */) {
    // Attack
}

I think it is wrong to move a player by calling a function in Map:

void Map::movePlayer(Player &player) {
    // Do stuff to move player
}

So, how do you implement basic game structures like this, keeping things logically structured but while still being able to ie. move a player across the map?

\$\endgroup\$
7
  • \$\begingroup\$ It seems to me that you're shooting for independence across classes that don't have a clear hierarchical relationship. Like, wanting independence between Maps and Chunks and Tiles makes sense because they're just subcomponents of one another, but why shoot for independence between the world and the entities? \$\endgroup\$ Commented Jan 21, 2021 at 16:24
  • \$\begingroup\$ ^I should clarify; I'm aiming to understand why "independence" should generally imply "logical." The main example that confused me was the Player-Enemy example; why is it illogical for the Player to read data about an Enemy? \$\endgroup\$ Commented Jan 21, 2021 at 16:40
  • \$\begingroup\$ @FluffytheTogekiss it is not illogical, it is not possible. Being able to read other entities requires knowledge of other tiles thus knowledge of the Map. Edit: in my implementation, that's why I'm asking this question. \$\endgroup\$
    – Duco
    Commented Jan 21, 2021 at 16:56
  • \$\begingroup\$ OK good, so that confirms my suspicion that I was misunderstanding you. I read that as a statement of a separate issue from the map issue, which confused me. \$\endgroup\$ Commented Jan 21, 2021 at 16:58
  • \$\begingroup\$ Some do it by forgetting about OOP: the enemy doesn't move itself - this is a game about moving enemies. \$\endgroup\$ Commented Jan 21, 2021 at 18:28

1 Answer 1

1
\$\begingroup\$

The target implementation of your Map and entities is very much like a graph, or, more specifically, a tree:

Map Graph

Since the goal is to minimize references to parent nodes, we will call update methods from parents, as well as have parents pass data to those children that is necessary for them to update.

Since entities need to be able to move throughout the tree, it's inevitable that we will need some sort of function in the Map and Chunk to be able to move entities to different nodes. However, we can separate updating the graph from updating the state of the entities, which will keep them from storing references to one another.

Updating the Map Graph

Since your Entity::move method adjusts fields for x and y, it seems that you already locally store the coordinates of your entities. Thus, you have enough information to know when to delete references to entities in your Chunks and Tiles; specifically, when their coordinates are no longer within the bounds of that Chunk or Tile.

For example, in your Tile class (I'm doing this in Javaish-pseudocode since I'm clueless with C++ and don't want to cause confusion):

for(Player player in players) {
    if(player.coordinates != this.coordinates) {
        player.needsMove = true;
        /*Remove reference to player from vector*/;
    }
}

Then, in your Chunk class:

for(Player player in player_need_update) {
    if(player.needsMove) {
        if(inThisChunk(player.coordinates)) {
            /*Add a reference to this player in the appropriate Tile in this Chunk*/;
            player.needsMove = false;
        }
    }
}

And you'd have to do the same in your Map class if entities can move between Chunks.

Detection Among Entities

A similar approach could help the entities detect one another. For example, take the case of Players attacking Enemies. If a Player can only attack Enemies within the same Chunk, then we could do something like this in the Chunk class:

for(Player player in player_need_update) {
    for(Enemy enemy in enemies) {
        if(player.shouldAttack(enemy)) {
            player.attack(enemy);
        }
    }
}

From there, calls to Player::attack() can be passed references to enemies without storing them for long-term use.

\$\endgroup\$

You must log in to answer this question.

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