Data structure for tile map for use with artemis

2k views Asked by At

I've been working on a turn based tile map based game using artemis-odb and libGDX.

I wanted different terrain types for the map such as grass, sand, water, mountains and so forth, with each of these different terrain types having different movement costs to pass through and various additional properties pertinent to the game play.

I'm considering a couple different approaches currently:

  1. I could make the map a system GameMapSystem and have each type of terrain represented by an entity with the relevant components for each type of terrain (TerrainStats, and occasionally spell effect components Exploding for instance). My main concern is how to manage the mapping of tiles to terrain type entities. Conceptually that should be as easy as maintaining an int[][] with the values corresponding to the id of the terrain entity, however in that case the temporary marker components (Exploding) would be attached to all of a given terrain type at a time. This seems less than optimal. So then would I need to have a separate entity for each tile then? Aren't I creating additional overhead for the entity framework if I do that then?

  2. I've also considered making the game map and terrain types POJOS and then simply creating marker entities with the marker components for the special effects. Doing it this way however, it looks like I'd be passing the GameMap object around willy-nilly in order to have various systems be able to process on it (for rendering, collision, pathing, etc). In addition, wouldn't my game map then need to also track the entities that are ON the map at any given time with their positions in order to do my pathing logic? I'd prefer to keep the management of the entities completely under the domain of the entity framework if possible as it means slightly easier maintenance.

I'm curious if there are any approaches that I haven't examined yet. Otherwise I feel inclined towards method #2, unless there's some way to fix method #1 that I've overlooked.

2

There are 2 answers

0
MLevy On BEST ANSWER

I ended up using something from both methods. The following code snippets should help illustrate the method I undertook:

class TerrainType {
    public String displayName;
    public String regionName;
    public int movementCost;
    /* additional properties omitted */
    /* constructors omitted */
}

This structure holds the relevant information on a terrain type including movement cost and other gameplay related stats (I've omitted the rest for simplicity), the display name of the terrain type to show if inspected, and the name of the TextureRegion to pull from the TextureAtlas that my renderer is holding so kindly for me.

class GameMapSystem extends EntityProcessingSystem {
    @Mapper private ComponentMapper<MapPosition> pm;
    @Mapper private ComponentMapper<SolidObject> som;

    private ListMultimap<MapPosition, Entity> entityByLocation;

    private int[][] map;
    private int width, height;
    private Array<TerrainType> terrainTypes;

    /**
     * Accepts an Array of TerrainType objects and an 2d integer array with
     * values corresponding to indices into the array for the correct type.
     * 
     * In my case, these values are gleaned by reading a level description
     * file, but any source should be fine.
     */
    public GameMapSystem(Array<TerrainType> terrainTypes, int[][] map) {
        super(Aspect.getForAll(MapPosition.class));
        this.terrainTypes = terrainTypes;
        this.map = map;
        this.width = map.length;
        this.height = map[0].length;
        this.entityByLocation = ArrayListMultimap.create();
    }

    public boolean isOccupied(int x, int y) {
        List<Entity> entities = entityByLocation(new MapPosition(x, y));
        for(Entity e : entities) {
            if(som.has(e)) {
                return true;
            }
        }
        return false;
    }

    @Override
    protected void inserted(Entity e) {
        this.entityByLocation.put(pm.get(e), e);
    }

    @Override
    protected void removed(Entity e) {
        this.entityByLocation.remove(pm.get(e), e);
    }

    /* additional EntityProcessingSystem overrides omitted */
}

This EntityProcessingSystem is then attached to my world in passive mode. There shouldn't be any real reason to actually do any processing for my world within this system, what I really wanted was to be able to listen to inserted and removed events to put entities into the map. A manager would have been overkill in this situation just because it would have told me about EVERY entity being inserted or removed, and I only cared about the map related ones (or more specifically the map related ones with position). I then have some separate path finding logic that consumes additional (not seen here) methods to guide the AI around simply by requesting this passive system from the world object.

For completeness, the MapPosition class is shown below as well. Of importance is the inclusion of equals() and hashcode() to aid in the use of MapPosition as a key in a collection.

public class MapPosition extends Component
{
    public int x, y;

    public MapPosition(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object other) {
        if(!(other instanceof MapPosition)) {
            return false;
        }

        MapPosition pos = (MapPosition)other;

        return (pos.x == this.x && pos.y == this.y);
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 59 * hash + this.x;
        hash = 59 * hash + this.y;
        return hash;
    }
}

I will likely be trying to find a more convenient data structure than using the guava Multimap eventually, but it works for right now and I feel comfortable moving on to flesh out the rest of the public API for theses classes. If this answer does help anyone else, do keep in mind that the performance of the ArrayListMultimap for this implementation has not undergone rigorous testing!

1
Sergey Yakovlev On

I'm in a process of figuring out the same thing. Just wanted to share some answers from Artemis developer about this case which leaves us without answer actually but worth mentioning:

http://slick.ninjacave.com/forum/viewtopic.php?p=20125#p20125 http://slick.ninjacave.com/forum/viewtopic.php?p=20136#p20136

Honestly, Artemis is "still considered experimental". It's a new paradigm I wanted to look at, it is promising, but there are still those issues that I haven't really found answers to, how big a role do systems play, what you do not put into entities/components, etc. There's some battle going on inside my head about how big a role entities/components play when it comes to what appears to be a non-entity thing, like terrain, background music, etc.

Also another hint given by him is to distinguish like:

  • Systems "it can do" on/with entities. ("it can" AcquireEnemyTarget(System), "it can" SpawnNewBaddies(System))
  • Components are table rows in a corresponding component table, and contain data for specific functionality/state.

So looks like it is up to us to explore the actual solution as the paradigm isn`t mature enough to have a "right" answer