Advertisement

isometric game tiling

Started by September 22, 2023 09:30 PM
46 comments, last by pbivens67 1 year, 1 month ago

Warp9 said:

pbivens67 said:

I am going back to my isometric game , my question is should I use a vector to store tiling sprites data?

Based on the statement that we are dealing with “tiling sprites” we're talking about putting them out as some kind of regularly spaced isometric game grid?

yes you are correct, there has to be to do this in an efficient manner.

My suggestion is that you should place the elements in the vector in a manner which relates to where they are in the grid then.

If you know where a sprite is at on your isometric grid, then you know where it is in the vector, and vice versa.

Also, if you know where an item is in your vector, and whence where it is on your grid, you don't actually need to store its location as a Tile class variable. You can just figure that out, based on the data you already have.

Advertisement

my question is how do I draw over 100 sprites in a unique fashion

pbivens67 said:

my question is how do I draw over 100 sprites in a unique fashion

Define “unique."

Do you mean that each one would be in a different location? Or is there some other way that they should all be unique?

each one should be in a different location

Focus Phil…these are the points Warp9 has introduced in the last couple posts.

  1. visualize what your regular grid is. (how to describe the rows and columns)
  2. layout your data in a similar fashion represented in code the way you see your grid.

    This allows you to give an isometric cell a row/column location which could also be used to index your flat or multidimensional vector.
    Another way would be to manage references to adjacent cells but it's your design.
  3. once you can represent your grid, you can store your grid.
    each unique cell can be given a number or enumeration for the specific kind it is.

Now that you know the layout and what you're drawing, figure out what you can see (if you're scrolling your world) and loop through the appropriate portion of your data. That means figuring out a world coordinate system instead of screen coordinates. But basically, it's just a pair of offsets. The real fun begins when you want to come up with clever ways of populating that grid in a procedural way. But hand assigning each cell an identifier gives some satisfaction the first time at least.

Lastly a reminder to make your draw function generic that accepts the cell id and location as arguments to know which and where to draw.
The name of the game is, once you figure out how to make a function do what you want and if other things want to do the same but with a different image and location, you work that into the same function to avoid unnecessary code duplication and will simplify your draw loop code.

artist pretending to be a programmer, pretending to be an artist.

Advertisement
class Tile
{
public:
	float tile_x;
	float tile_y;
	float tile_width;
	float tile_height;
};

	vector<Tile> tiles;
	Tile mountain;

	mountain.tile_x = -140.0f;
	mountain.tile_y = 100.0f;
	mountain.tile_width = 40.0f;
	mountain.tile_height = 20.0f;
	tiles.push_back(mountain);

This only makes sense if you want your tiles to be of irregular size, and not fixed to a grid. Since I doubt you want to get into that headache, you don't need x, y, width, or height properties. If you have a multidimensional array (or vector) all you need to do is fill it with a Tile number, where ‘1’ means mountain etc.

vector<vector<int>> vect
   {
       {1, 2, 3},
       {4, 5, 6},
       {7, 8, 9}
   };
for (int i = 0; i < vect.size(); i++)
    {
        for (int j = 0; j < vect[i].size(); j++)
        {
            cout << vect[i][j] << " ";
        }    
        cout << endl;
    }

vect[0][0] will be ‘1’ so refer to a mountain. To place tiles just multiply the tile_width/height with the index (and then convert cartesian to isometric).

can you explain the following

To place tiles just multiply the tile_width/height with the index (and then convert cartesian to isometric).

From the tutorial you said you were following:

for(i, loop through rows)
  for(j, loop through columns)
    x = j * tile width
    y = i * tile height
    tileType = levelData[i][j]
    placetile(tileType, twoDToIso(new Point(x, y)))
function twoDToIso(pt:Point):Point{
  var tempPt:Point = new Point(0,0);
  tempPt.x = pt.x - pt.y;
  tempPt.y = (pt.x + pt.y) / 2;
  return(tempPt);
}

pbivens67 said:
can you explain the following To place tiles just multiply the tile_width/height with the index (and then convert cartesian to isometric).

Lets go through different variants of storing a W * H sized regular grid in memory, and how to index a cell (or tile) from given grid coordinates (X,Y) to set it to 1.
I'll also comment on downsides of the variants.

Most intuitive is maybe to use a 2D array:

int grid[H][W];
grid[Y][X] = 1;

However, using 2D arrays is not recommended for performance reasons. (Although i'm not sure this concern is still upright with modern compilers, i never use 2D arrays at all)
So we usually use a 1D array and care about indexing math ourselves:

int grid[W*H];
grid[Y*W + X] = 1; // we multiply Y with the width of the grid to address a vertical row of cells, and then add X to adress the proper cell within that row

Basically we replace 2D indices with a multiplication and addition, which is still simple to use and becomes second nature quickly.

If your grid is large like games tend to be, we need to allocate the memory on the heap, because if done like above it may eat too much from our stack memory, which could even crash the application:

int *grid = new int [W*H]; // allocate
if (!grid) PanicAndExit(); // if pointer is zero, allocation has failed and we might need to close the app
grid[Y*W + X] = 1; // otherwise work as usual
delete [] grid; // we delete the memory when we no longer need it, eventually before closing the app or if the level has been finished and we want a new one

That's all fine.

But we can eventually use a std::vector instead, making some things easier.
So let's go through the same variants using std::vector…

std::vector< std::vector<int> > grid; // we nest two vectors to get two dimensions. at this point no memory has been allocated yet
grid.resize(H); // now we have allocated H rows, but each row is still empty
for (int y=0; y<H; y++) grid[y].resize(W); // we set the size of each row to W

grid[Y][X] = 1; // finally indexing the grid works the same as with 2D arrays

You see that's not only more complicated, it's also inefficient.
The problem is that now each row is it's own allocation, so the rows may be fragmented across memory. Looping over a region of our grid spanning multiple rows will have to jump around in memory, causing more cache misses than needed.
This problem also persists if we use different code a s shown above:

vector<vector<int>> vect
   {
       {1, 2, 3},
       {4, 5, 6},
       {7, 8, 9}
   };

Looks elegant, but still the same problem under the hood.
In our case each row has the same size, so there is no reason to use nested vectors. It's simply bad practice and inefficient.
The only reason we might want to do this is when our rows would have different sizes, e.g. if we used some sparse grid.

Now let's look at using the 1D approach with std::vector:

{
	std::vector<int> grid(W*H);
	grid[Y*W + X] = 1;
}
// here the grid went out of scope, so the memory will be released automatically in the destructor of our std::vector

No problem here, technically equivalent to the 1D array, and we do not even need to worry about deallocation, which is done automatically for us.

Conclusion:

You should to use either 1D array or 1D std::vector, but 2D or nested variants only if you have a really good reason to so. For a regular grid there is no such reason, so use 1D and handle indexing math yourself.
If the latter feels cumbersome, use a class to manage memory allocation and provide tooling functions to implement the indexing math.
E.g.:

struct TileMap
{
	std::vector<int> grid;
	int W,H;
	
	void Allocate (int w, int h)
	{
		W=w; H=h; grid.resize(W*H);
	}
	
	int TileIndex (int x, int y)
	{
		return y*W + x;
	}
	
	void SetTile (int x, int y, int value)
	{
		grid[TileIndex(x,y)] = value;
	}
	
	int GetTile (int x, int y)
	{
		return grid[TileIndex(x,y)];
	}
};

Because grid indexing math is so simple, i'm not sure if that's needed. A matter of taste, but it's an example of using data structures to write code once and use many times.
When working with 3D grids, i do use such things. But for 2D grids i usually just write the indexing math in place.

This topic is closed to new replies.

Advertisement