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:
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?
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.
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!