How to create an infinitely scrolling map in Unity (C#)
A project I’ve recently been working on is an endlessly scrolling game based on the hit indie game Vampire Survivors. The core gameplay loop of the title is that only the player character’s movement is controlled. No inputs for attacking, no inputs for using items, just the player their ability to navigate around, and a host of crazy passively used items.
I created my project in Unity’s 3D engine. The player-character has a camera that follows its movement and in order to program the level scrolling, I created a script called WorldScrolling
. In my game, the starting position of the player-character is (0, 0). The size of premade terrain tiles that inhabit the world is 20 units by 20 units and the one at the origin is the tile with the player-character.
This means that the visible tiles are the 8 terrain tiles generated in a square centered on the starting position of the character at the start of the game.
The script operates based on this tile-based approach, dividing the game world into a grid of equally sized tiles. Each tile is represented as a GameObject
in Unity.
The script keeps track of the player's position in the world using their Unity transform component. It then converts the player's position into tile coordinates, helping determine which tiles are currently visible to the player.
To optimize performance and resource usage, a "field of vision" is defined, limiting the number of tiles actively processed and displayed on the screen. This is crucial for larger game worlds to prevent lag and excessive memory consumption.
The Awake()
method is called when the script's GameObject
is initialized, even before the Start()
method. It's primarily used for setting up initial references and variables. Here, it initializes a 2D array called terrainTiles
. This array is used to store references to GameObjects representing the tiles in the game world grid. The dimensions of this array are determined by the terrainTileHorizontalCount
and terrainTileVerticalCount
variables, which represent the number of tiles horizontally and vertically in the game world.
The Update()
method is a standard Unity lifecycle method that gets called once per frame. Here, it updates the position of the player within the tile grid. It calculates the player's position in terms of tile coordinates (playerTilePosition
) based on their current position in world coordinates (playerTransform.position
) and the size of each tile (tileSize
).
It also adjusts the player's tile position to account for negative values. If the player's position is negative, it subtracts 1 from the corresponding tile coordinate.
The if(currentTilePosition != playerTilePosition)
condition checks if the player has moved to a different tile since the last frame. If they have, it updates the currentTilePosition
.
It then calculates the player's position within the current tile (onTileGridPlayerPosition
) using the CalculatePositionOnAxis
method, which we'll explain shortly.
Finally, it calls the UpdateTilesOnScreen()
:
The UpdateTilesOnScreen()
method is responsible for updating the visible tiles on the screen based on the player's position and field of vision.
It uses nested loops to iterate through a portion of the tile grid centered around the player's position. This portion is defined by the fieldOfVisionWidth
and fieldOfVisionHeight
. For each tile within this field of vision, it calculates the tile's position in the grid (tileToUpdate_x
and tileToUpdate_y
) using the helper CalculatePositionOnAxis
method. It then calculates the new position for the tile in world coordinates using the CalculateTilePosition
method. If the new position is different from the current position of the tile, it updates the tile's position and spawns the terrain associated with that tile using the Spawn()
method.
This method calculates the world position (in Unity's 3D space) of a tile based on its grid coordinates (x
and y
) and is referenced in UpdateTilesOnScreen()
. It takes two integer parameters (x
and y
) representing the grid coordinates of the tile. The method then uses these coordinates to calculate the actual position of the tile in world space. The tileSize
variable is used to determine the distance between each tile. The method returns a Vector3
representing the world position of the tile. The 0f
in the Vector3
is used for the z-coordinate, which is typically 0 in 2D games.
The CalculatePositionOnAxis
method is responsible for calculating the position of an object on a grid axis while considering the potential wrapping when the object's position goes below zero.
float currentValue
: This parameter represents the current position value on the axis that you want to calculate.
bool horizontal
: This parameter is a boolean flag indicating whether the calculation is for the horizontal axis (true
) or the vertical axis (false
).
Inside the method, there's an if-else block that handles the calculation based on the provided
horizontal
flag:If
horizontal
istrue
, it means you're calculating the position on the horizontal axis. The following steps are performed:It checks if
currentValue
is greater than or equal to zero. If it is, the position is within the grid boundaries. In this case, it calculates the modulus (%
) ofcurrentValue
byterrainTileHorizontalCount
. This ensures that the position remains within the grid bounds.If
currentValue
is negative, it means the position is outside the grid to the left. In this case, it adds 1 tocurrentValue
(to shift it one step to the right) and then calculates the modulus. This is done to handle wrapping around when the object goes below zero. For example, if the grid size is 10, andcurrentValue
is -2, it should wrap around to 7 (i.e., 10 - 1 - 2 % 10 = 7).
If
horizontal
isfalse
, it means you're calculating the position on the vertical axis, and similar steps are performed to handle wrapping based onterrainTileVerticalCount
.
Finally, the method returns the resulting position as an integer. The
(int)
cast is used to ensure that the position is returned as an integer value.
The final Add
method is a public method with a purpose to add a tile GameObject to the grid of tiles, referred to as terrainTiles
, at the grid position specified by the Vector2Int
value.
To recap:
The script maintains a grid of tiles and tracks the player's position within this grid.
It loads and updates visible tiles based on the player's position, ensuring an illusion of an infinitely scrolling 2D world.
The
fieldOfVisionHeight
andfieldOfVisionWidth
limit the number of tiles processed, optimizing performance.Tiles are added and removed from the grid dynamically using the
Add
method.The script uses helper methods to handle tile positions, axis calculations, and tile positioning within the world.
Overall this script took me around two to three days of work to figure out, but the results speak for themselves: