Ashley
Ashley

Reputation: 497

What data structure to use for storing a 2D cell-based map of GameObjects?

I know you're probably thinking 2D array or 2D vector, but hear me out.

I'm actually already using a 2D array for tilemaps, which works great. However, I'm trying to develop a data structure that can store game objects positioned on top of such a tilemap.

My requirements are as follows:

I have drafted up a basic data structure that uses two STL containers:

Given my requirements, are there more suitable containers available than the ones I've used?

If it helps, I've pasted my code for the ObjectMap below:

#pragma once
#include <vector>
#include <map>

template<typename T>
struct Vec2 {
    Vec2() { x = 0; y = 0; }
    Vec2(T xVal, T yVal) : x(xVal), y(yVal) {}
    void Set(T xVal, T yVal) { x = xVal; y = yVal; }
    T x, y;
    Vec2& operator+=(const Vec2& rhs) { x += rhs.x; y += rhs.y; return *this; }
    Vec2 operator+(const Vec2& rhs) { return Vec2<T>(x + rhs.x, y + rhs.y); }
};

/// <summary>
/// Represents a map of objects that can be layered on top of a cell-based map
/// Allows for multiple objects per map cell
/// </summary>
template <typename Object>
class ObjectMap {
public:
    /// <summary>
    /// Gets the objects located at the given map cell
    /// </summary>
    /// <param name="row">The row of the cell to inspect</param>
    /// <param name="column">The column of the cell to inspect</param>
    /// <returns>
    /// A pointer to a vector of objects residing at the given cell.
    /// Returns a nullptr if there are no objects at the cell.
    /// </returns>
    std::vector<Object*>* At(int row, int column);

    /// <summary>
    /// Checks whether the ObjectMap contains the given object
    /// </summary>
    /// <param name="object">A pointer to the object to check for</param>
    /// <returns>True if the ObjectMap contains the object</returns>
    bool Contains(Object* object);

    /// <summary>
    /// Adds the given object to the ObjectMap at the given cell
    /// </summary>
    /// <param name="object">The object to add to the map</param>
    /// <param name="row">The row of the cell to add the object to</param>
    /// <param name="column">The column of the cell to add the object to</param>
    /// <returns>True if successful, false if the object is already in the ObjectMap</returns>
    bool Add(Object* object, int row, int column);

    /// <summary>
    /// Moves the given object by some number of rows and columns
    /// </summary>
    /// <param name="object">The object to move</param>
    /// <param name="rows">The number of rows to move the object by</param>
    /// <param name="columns">The number of columns to move the object by</param>
    /// <returns>True if successful, false if the object does not exist in the ObjectMap</returns>
    bool MoveBy(Object* object, int rows, int columns);

    /// <summary>
    /// Moves the given object to the given cell
    /// </summary>
    /// <param name="object">The object to move</param>
    /// <param name="row">The row of the cell to move the object to</param>
    /// <param name="column">The column of the cell to move the object to</param>
    /// <returns>True if successful, false if the object does not exist in the ObjectMap</returns>
    bool MoveTo(Object* object, int row, int column);

    /// <summary>
    /// Gets the position of the given object
    /// </summary>
    /// <param name="object">A pointer to the object to check the position of</param>
    /// <returns>
    /// A pointer to the position of the object.
    /// Returns a nullptr if the object does not exist in the ObjectMap.
    /// </returns>
    Vec2<int>* GetPosition(Object* object);
private:
    /// <summary>
    /// A 3D container allowing object access via cell positions
    /// Provides the ability to iterate across sections of the map
    /// Useful for object culling and rendering
    /// Useful for object lookup when the position is known
    /// Example: _map[a][b] is a vector objects positioned at the map cell (x=a,y=b)
    /// </summary>
    std::map<int, std::map<int, std::vector<Object*>>> _map;

    /// <summary>
    /// A 1D container of all objects and pointers to their positions
    /// Useful for quickly checking whether an object exists
    /// Useful for quickly getting the location of an object
    /// </summary>
    std::map<Object*, Vec2<int>*> _objects;
};

/// 
/// ObjectMap.tpp
/// The implementation has not been separated into a .cpp file because templated 
/// functions must be implemented in header files.
/// 
/// See http://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file
/// 
#include <algorithm>

template <typename Object>
std::vector<Object*>* ObjectMap<Object>::At(int column, int row) {
    // Checks whether a key exists for the given column
    if (_map.find(column) != _map.end()) {
        // Checks whether a key exists for the given row
        if (_map.at(column).find(row) != _map.at(column).end()) {
            // Return the objects residing in the cell
            return &_map.at(column).at(row);
        }
    }
    return nullptr;
}

template <typename Object>
bool ObjectMap<Object>::Contains(Object* object) {
    return _objects.find(object) != _objects.end();
}

template <typename Object>
bool ObjectMap<Object>::Add(Object* object, int column, int row) {
    if (!Contains(object)) {
        _objects[object] = new Vec2<int>(column, row);
        _map[column][row].push_back(object);
        return true;
    }
    return false;
}

template <typename Object>
bool ObjectMap<Object>::MoveBy(Object* object, int columns, int rows) {
    Vec2<int> newPosition = *_objects[object] + Vec2<int>(columns, rows);
    return MoveTo(object, newPosition.x, newPosition.y);
}

template <typename Object>
bool ObjectMap<Object>::MoveTo(Object* object, int column, int row) {
    if (Contains(object)) {
        // Get the position reference of the object
        Vec2<int>* position = _objects[object];

        // Erase the object from its current position in the map
        auto *oldTile = &_map[position->x][position->y];
        oldTile->erase(std::remove(oldTile->begin(), oldTile->end(), object), oldTile->end());

        // Erase any newly-empty keys from the map
        if (oldTile->size() == 0) {
            _map[position->x].erase(_map[position->x].find(position->y));
            if (_map[position->x].size() == 0) {
                _map.erase(_map.find(position->x));
            }
        }

        // Add the object to its new position on the map
        _map[column][row].push_back(object);

        // Set the position of the object
        position->Set(column, row);

        return true;
    }

    return false;
}

template <typename Object>
Vec2<int>* ObjectMap<Object>::GetPosition(Object * object) {
    if (Contains(object)) {
        return _objects[object];
    }
    return nullptr;
}

Upvotes: 2

Views: 1707

Answers (2)

You left one important part of the question unspecified: What percentage of your map cells won't ever get to hold an object?

  • If that percentage is very high (95% at the very least), your map<int, map<int, vector<>>> approach looks fine.

  • If that percentage is just high, a vector<map<int, vector<>>> would be better.

  • If that percentage is modest (anywhere in the proximity of 50% or lower), you should go for a vector<vector<vector<>>>.

The reasoning behind this is, that std::vector<> is vastly more efficient than std::map<int, > in the case that most elements are actually used. Indexing into an std::vector<> means just a bit of pointer arithmetic and a single memory access, whereas indexing into an std::map<> means O(log(n)) memory accesses. That's already a factor of 7 for just 128 entries. The std::map<> only has the advantage of reducing memory consumption in the presence of unused indices, which may bloat a sparsely used std::vector<>.

Now, if your count of unused cells is not very high, you have to expect pretty much every line of your 2D array being filled with something. Accordingly, the container holding the lines should be a vector<>. The same argument goes for using vector<> for the lines themselves if you expect a decent density of entries.


If your objects can only ever be at a single position within the map at any given time, you might also consider of just linking them up in an intrusive linked list. In that case, you would drop your map container from 3D to 2D, and then iterate through a linked list of objects that happen to be within the same bin.

Of course, that is only a valid option as long as the linked lists of objects are expected to be very small on average.

Upvotes: 2

Jay
Jay

Reputation: 656

You might want to look into Binary Space Partitions, which offer very fast lookup times eg for objects on screen.

For a grid, a good spacial structure is QuadTrees.

Upvotes: 3

Related Questions