I’ve found that in more complex 2D games, I end up maintaining several “boards” containing 2D arrays of objects. Generally I have a base board to hold the floor, a terrain board for walls and other structural objects, a feature or object board for smaller items, and an actor board. The feature and actor boards are generally kept in parallel with flat feature and actors arrays, as a convenience to either access them with an iteration or by board position.
This can make pathfinding and drawing to the screen become cumbersome, so I’ve begun implementing a Layer class to help. The layer holds a basic board, a link to its parent (if any), and an array of children. Children may be added with different scale from the parent board, so for instance a 25×25 terrain grid may actually be broken down into a 100×100 grid to contain the actor positions.
One advantage of this breakdown is that multi-pass pathfinding can be seamlessly integrated. This means I can run my initial pathfinding algorithm against the topmost layer to get a basic path, then drill down to find the detail. Since the basic path will usually be representative of the final travel, this means the bulk of the work will be done against the smaller grid.
To allow for data stored at this level, the layers will additionally need to be composed of a MapNode class, to support additional functionality beyond simply holding a variable. These nodes need to be aware of which neighboring nodes they have an accessible edge to, because even if they are both empty a child layer’s node may block the path between them. By caching this data in the topmost layer, we prevent too much backtracking when sharpening our route.
Another advantage is that the base MapLayer may itself be treated as a MapNode, and vise-versa. This means that the world can be fractally expanded to organically grow the map as needed. Simply add the current topmost layer to a newly created layer which contains the rest of the world, and procedurally generate its contents at that time.