With x = 0 and z = 0, any grid won’t be able to go into the negative numbers. That means when you are constructing a level, you need to be careful that you don’t build where the grid can’t go.
That is correct. The same issue applies if you exceed the width and height of the LevelGrid. Everything should be within these bounds (though you can put decorative elements outside of these bounds, just not units or locations you want to travel on/through)
So how do games work around this?
According to the devs of Project Zomboid, for example, this was limiting their map size to a quarter of what the engine could handle (it was not built using Unity though).
Is there some kind of mathematical trick to avoid this? or can you iterate starting from a negative number and going up in a for-loop?
The reason here is that the grid is positioned at the origin, so when you calculate the grid position from world position, your world position needs to be in the positive range. You could introduce an ‘offset’ that is added or subtracted to and from the world position to ‘translate’ the coordinates back and forth.
For example, you could have a 10x10 grid with an offset of (-5, -5). This essentially then means the grid is centered on the origin (somewhat) of the world, and all world positions from (-5,-5) to (5,5) would be valid (barring some rounding errors). You’d also need to take this into account when checking positions, etc. because, while the grid’s width and height may be 10, a world position of (8, 8) will no longer be valid in this scenario. A grid position of (0, 0) will now be at (-5, -5) in the world as opposed to the (0, 0) world position in the current system.
For the GridSystem
you may have an overloaded constructor that takes an offset (optional) and store it (All this code is adapted from the Turn-Based Strategy course’s code)
private Vector3 offset;
public GridSystem(int width, int height, float cellSize, Func<GridSystem<TGridObject>, GridPosition, TGridObject> createGridObject)
: this(width, height, cellSize, Vector3.zero, createGridObject) { } // pass Vector3.zero as the defaul offset
public GridSystem(int width, int height, float cellSize, Vector3 offset, Func<GridSystem<TGridObject>, GridPosition, TGridObject> createGridObject)
{
this.offset = offset;
// all the normal grid constructor code
}
This offset adjusts the calculation only - the grid’s indices still start at 0. What those indices are in relation to the world is just calculated differently now.
public Vector3 GetWorldPosition(GridPosition gridPosition)
{
// calculate as normal, then translate to 'offset'
return new Vector3(gridPosition.x, 0, gridPosition.z) * cellSize + offset;
}
public GridPosition GetGridPosition(Vector3 worldPosition)
{
// translate back to 0 (remove offset) before calculating as normal
Vector3 translated = worldPosition - offset;
return new GridPosition(
Mathf.RoundToInt(translated.x / cellSize),
Mathf.RoundToInt(translated.z / cellSize)
);
}
Alternatively, you could just set your starting position to be width/2,height/2. There is absolutely no reason it should limit your map size beyond the limits of performance issues (larger maps with pathfinding often == less performance).