I started studying simulation of moving agents ten years ago and I have come to realize that regular 2D Grids are extraordinary abstractions for the navigable space. In fact, regular 2D grids are very easy to encode and they offer an elegant framework for path planning and collision avoidance algorithms deployment.
Photo by Glenn Carstens-Peters on Unsplash
In this post, I am sharing an object oriented architecture — with its C++ implementation — that I am actually using in my personal projects when I need a 2D Grid. I hope that it could be an affordable starting point for anyone who is interested in the subject.
Architecture
To be very concrete, I am going to assume that I am building a simulation app — of moving agents — that uses a 2D grid as the navigable space model. The root object oriented architecture that I am using to tackle the implementation is outlined in Fig. 1. Basically, there is an application object called App and three other packages:
- env : for the navigable space representation and manipulation objects
- viewers : for visualisation purposes
- geometry : for geometrical abstraction
I am giving more details about App, env, viewers and geometry below.
Fig. 1. Architecture of our 2D Grid App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
sfml2dgrid
.
├── CMakeLists.txt
├── deps
│ └── sfml
│ └── CMakeLists.txt.in
└── sfml2dgrid
├── CMakeLists.txt
├── main.cpp
├── app
│ ├── include
│ │ └── App.h
│ └── src
│ └── App.cpp
├── env
│ ├── include
│ │ ├── Grid.h
│ │ └── IGrid.h
│ └── src
│ └── Grid.cpp
├── geometry
│ └── include
│ └── geometry.h
└── viewers
├── include
│ ├── AbstractViewer.h
│ └── GridViewer.h
└── src
├── AbstractViewer.cpp
└── GridViewer.cpp
The file tree of the project with the source (.cpp) and header (.h) files that I am going to discuss about in the rest of the post
App : the Application Object
The App object is the definition of our application — see Fig.2.
It holds a viewer (viewers::AbstractViewer) — that contains display instructions — and a reference to a 2D Grid (env::IGrid) — that will be used to manipulate the 2D grid. Note that the App object has an SFML window as a private attribute — this is where the rendering will occurs.
Fig. 2.
App
object is attached to a viewer (AbstractViewer
) and controls a 2D Grid (IGrid
)
App also defines two important methods :
- App::run : responsible of the simulation logic
- App::display : responsible of the display (what we see on the screen).
App.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#ifndef APP_H
#define APP_H
namespace sf
{
class RenderWindow;
};
namespace env
{
class IGrid;
};
namespace viewers
{
class AbstractViewer;
};
class App
{
private:
// sfml render window
sf::RenderWindow *_window = nullptr;
// the 2D grid pointer
env::IGrid *_grid = nullptr;
// a pointer to the viewer
// this could be a set of viewer actually
// if we consider component behavior
viewers::AbstractViewer *_viewer = nullptr;
public:
// theorical width of the environment
// will match the grid width in terms of number of cells.
static const int DEFAULT_WIDTH;
// theorical height of the environment.
static const int DEFAULT_HEIGHT;
// x-resolution of the grid i.e. the x-size of a cell
static const int DEFAULT_RESX;
// y-resolution of the grid i.e. the y-size of a cell
static const int DEFAULT_RESY;
// attach window to the app
void setWindow(sf::RenderWindow *);
// attach a specific viewer
void setViewer(viewers::AbstractViewer *);
// attach a grid (should have been initialized)
void setGrid(env::IGrid *);
// return the attached grid
env::IGrid *getGrid();
// return the attached window
sf::RenderWindow *getWindow();
// run the application (the logic)
void run();
// show content (display routines)
void display();
App() = default;
virtual ~App();
};
#endif // !APP_H
In the App.h file, I have defined static attributes — DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_RESX, DEFAULT_RESY — to set the default dimensions of a grid in the application.
AbstractViewer, GridViewer and the geometry Package
AbstractViewer is meant to be derivated according to what we want to show on the application window — for instance, GridViewer is a specific implementation of AbstractViewer that uses the geometry package to draw the grid lines.
AbstractViewer : The Generic Definition of a Viewer
AbstractViewer is always attached to an App object — see Fig.3 — and defines a protected abstract method called AbstractViewer::iDraw — implemented in its inherited classes. AbstractViewer::iDraw is called by the public method AbstractViewer::iDisplay when necessary — that will be in the App::display function.
Fig. 3.
GridViewer
is a specific viewer (i.e. inherits fromAbstractViewer
) in charge of drawing the lines (horizontal and vertical) of the grid. It uses thegeometry
package
AbstractViewer.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#ifndef ABSTRACTVIEWER_H
#define ABSTRACTVIEWER_H
class App;
namespace viewers
{
// AbstractViewer
class AbstractViewer
{
public:
// activate the viewer. If activated, it provides the desired view
virtual void iActivate();
// deactivate the viewer. Do not display anaything if deactivated
virtual void iDeactivate();
// return True if the viewer is active
virtual bool iIsActive() const;
// display function
virtual void iDisplay();
// attach the application object
virtual void iSetApp(App *);
virtual ~AbstractViewer() = default;
AbstractViewer() = default;
protected:
// specific draw method (to be concretized in child classes)
virtual void iDraw() = 0;
// if active the viewer is automatically activated
bool _active = false;
// reference to the attached app
App *_app;
};
} // namespace viewers
#endif // !ABSTRACTVIEWER_H
AbstractViewer is always attached to an App object and defines a protected abstract method called AbstractViewer::iDraw.
GridViewer : the Grid Lines Viewer
For the exercice, we will define GridViewer as our only viewer, reponsible of displaying the lines of the 2D Grid. The private methods defined in GridViewer are :
- initialize : to build the set of segments to be displayed
- drawLine : to call the graphic engine and draw every segment built during the initialization step initialize
- iDraw : to call the above methods in the right order.
GridViewer.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef GRIDVIEWER_H
#define GRIDVIEWER_H
#include "AbstractViewer.h"
#include "geometry.h"
#include <vector>
namespace viewers
{
class GridViewer : public AbstractViewer
{
public:
GridViewer() = default;
virtual void initialize();
virtual ~GridViewer() = default;
protected:
virtual void iDraw();
private:
void drawLines(geometry::ISegmentFunctor &) const;
std::vector<geometry::Segment> _lines;
};
} // namespace viewers
#endif // !GRIDVIEWER_H
We define GridViewer as our only viewer, reponsible of displaying the lines of the 2D Grid
The geometry Package
Here is the full description of our modest geometry package.
It contains the following objects :
- Point : a 2D point definition, actually a pair of integers
- Segment : the definition of a segment — a pair of Point
- ISegmentFunctor : an interface, meant to be realised by functors that applies on Segment
geometry.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef GEOMETRY_H
#define GEOMETRY_H
#include <utility>
namespace geometry
{
// definition of a point (typedef is sufficient)
typedef std::pair<int, int> Point;
// definition of a segment (typedef is sufficient)
typedef std::pair<Point, Point> Segment;
// Functor definition to apply on segment
// We can inherit from this to function
// to apply display instruction on segments
class ISegmentFunctor {
public:
// operator function to apply on each segment
// (should concretized according to the need)
virtual void operator()(
const Segment& // cell_id
) = 0;
};
}
#endif // !GEOMETRY_H
Our very modest geometry package contains geometry::Point, geometry::Segment and geometry::ISegmentFunctor.
The 2D Grid
The most important features of our 2D grid are defined in the IGrid interface — see Fig.4. IGrid is realized by the Grid object which is composed of grid cells — note that the CELL object is defined in IGrid.h.
Fig. 4.
Grid
realizes the interface of a 2D grid, defined inIGrid
IGrid.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#ifndef IGRID_H
#define IGRID_H
namespace env
{
struct CELL
{
int _id; // id of the cell
CELL() = default;
CELL(const CELL &) = default;
};
class IGrid
{
public:
virtual ~IGrid() = default;
// returns the width
virtual int iGetSizeX() const = 0;
// return the height
virtual int iGetSizeY() const = 0;
// return the number of cells in the grid
virtual int iGetNumberOfCells() const = 0;
// get the width of a cell (in terms of pixels)
virtual int iGetResolutionX() const = 0;
// get the height of a cell (in terms of pixels)
virtual int iGetResolutionY() const = 0;
//-- Test
// relative position of a cell according to its id
virtual bool iGetCellPosition(
const CELL &, // cell
int &, // posx
int &, // posy
) const = 0;
// coordinates of a cell accoring to its id
virtual bool iGetCellCoordinates(
const CELL &, // cell
int &, // row_number
int & // column_number
) const = 0;
// cell rank of the the cell according
// to its relative position in the grid
virtual bool iGetCellNumber(
int, // row_number
int, // column_number
CELL &) const = 0;
// the containing cell given the coordinates in the 2D space
virtual bool iGetContainingCell(
int, // posx
int, // posy
CELL & // cell
) const = 0;
// check if a given point is within a given cell
virtual bool iIsWithinCell(
int, // posx
int, // posy
const CELL & // cell
) const = 0;
// initialize the vector of cells, obstacle mask, etc.
virtual void iInitialize() = 0;
};
} // namespace env
#endif // !IGRID_H
The features of the 2D grid are defined in IGrid.h — IGrid.h also contains the definition of the CELL object.
Grid.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#ifndef GRID_H
#define GRID_H
#include "IGrid.h"
#include <vector>
namespace env
{
const int DEFAULT_GRID_SIZEX = 10;
const int DEFAULT_GRID_SIZEY = 10;
const int DEFAULT_RESOLUTIONX = 1;
const int DEFAULT_RESOLUTIONY = 1;
class Grid : public IGrid
{
public:
virtual ~Grid() = default;
Grid() = default;
//-- Getters
virtual int iGetSizeX() const;
virtual int iGetSizeY() const;
virtual int iGetNumberOfCells() const;
virtual int iGetResolutionX() const;
virtual int iGetResolutionY() const;
// Test
virtual bool iGetCellPosition(const CELL &, int &, int &) const;
virtual bool iGetCellCoordinates(const CELL &, int &, int &) const;
virtual bool iGetCellNumber(int, int, CELL &) const;
virtual bool iGetContainingCell(int, int, CELL &) const;
virtual bool iIsWithinCell(int, int, const CELL &) const;
virtual void iInitialize();
//-- Setters
void setSizeX(int);
void setSizeY(int);
void setResolutionX(int);
void setResolutionY(int);
private:
int _sizex = DEFAULT_GRID_SIZEX;
int _sizey = DEFAULT_GRID_SIZEY;
int _resolutionx = DEFAULT_RESOLUTIONX;
int _resolutiony = DEFAULT_RESOLUTIONY;
std::vector<CELL> _cells;
};
} // namespace env
#endif // !GRID_H
The Grid object realizes the IGrid interface.
Demo
Our 2D Grid app architecture is now complete! The build strategy is exactly the same as what we have presented in a previous post — we refer the reader to that post for more details. The main file for the demo contains the following :
main.cpp
Configure and Build
The interested reader can fork the complete source code from here and run the following in a terminal at the root of the project folder :
on windows
1
2
3
$ cmake -G "Visual Studio $(Version)" -S . -B ./build
$ cmake --build ./build --config Debug --target app
$ ./bin/Debug/app
on linux
1
2
3
4
5
$ mkdir build
$ cd build
$ cmake -G "Unix Makefiles" .. -DCMAKE_BUILD_TYPE=Debug
$ cmake --build ./ --target app --config Debug
$ ../bin/Debug/app
Step by step demo : launching the 2D Grid SFML App from the terminal (built on ubuntu 18.08 with gcc 7.5)
You should see a clickable window with a 2D-Grid displayed on it! Enjoy and feel free to send me your feedbacks!