Advertisement

Implementing 2D World Chunking and World Positions

Started by September 21, 2019 03:50 AM
5 comments, last by ItsQuesoTime 5 years, 2 months ago

Hello - 

 

I am currently working on adding a chunking system/scrolling to my 2D game, but I'm having some problems structuring things in an efficient way.

 

From my understanding, I need a world position and a camera position, and then I need to move the camera around the world and load/unload chunks as needed. I know that camera position is gained from the corners of the screen. I also know that the point of chunking is so you don't need a massive data structure to store the world in. What I don't understand is how to get world position if there isn't actually a world data structure, only chunks.

 

So basically, these are my questions:

How would I go about getting world position to implement chunking/scrolling?

How would I "connect" chunks together so that they line up with one another, while also allowing them to be loaded/unloaded independently?

 

Right now my rendering is simply taking in 2D array data (which is currently a single 50x50 chunk), iterating through it, and drawing the tiles equally spaced apart in a grid.

 

Anything I've tried either doesn't work or is extremely inefficient.

 

Thanks for any insight.

You need a consistent world information, no need to delete the information outside of the drawing chunk.
You just have to chunk the loading time. If you delete any of the world information,
it is lost forever.

Let world information stay in the database, and use a flexible loading time.

I'd give you example code in C++, but I really don't know which language/library you are using.

Advertisement

Here are my assumptions about your project:

  • Unit of measurement is in pixels
  • A tile is unit squared; 64x64px
  • Top left screen coord is 0,0 and bottom right is 1919,1079; 1080p monitor
  • Your 50x50 tilemap chunk is rendered from left to right, top to bottom

 

7 hours ago, ItsQuesoTime said:

How would I go about getting world position to implement chunking/scrolling?

Lets say your tiles are 64x64px and your chunk is 50x50 tiles, so a chunk would be 3200x3200px.
Also for the time being, lets say chunk instances are stored in a 2D array. If the chunk is not loaded then the instance is invalid.

Pseudo Code:


TILE_SIZE = 64
CHUNK_SIZE = TILE_SIZE * 50

chunk_x, chunk_y GetChunkCoord( pixel_x, pixel_y)
{
  chunk_x = pixel_x / CHUNK_SIZE
  chunk_y = pixel_y / CHUNK_SIZE
}

chunk GetChunk( chunk_x, chunk_y )
{
  chunk = chunks[chunk_x][chunk_y]
}

tile GetTile( pixel_x, pixel_y )
{
  chunk = GetChunk( GetChunkCoord( pixel_x, pixel_y ) )
  
  if chunk is valid
  {
    tile_x = (pixel_x % CHUNK_SIZE) / TILE_SIZE
    tile_y = (pixel_y % CHUNK_SIZE) / TILE_SIZE
    
    tile = tilemap[tile_x][tile_y]
  }
}


Now for figuring out what chunks the camera sees.
Since a chunk is 3200px squared, you can fit a 1920x1080 screen within a chunk.
The most chunks you can physically see is 4 when the screen is at one corner of a chunk.

 


SCREEN_X = 1920
SCREEN_Y = 1080
SCREEN_HX = SCREEN_X / 2
SCREEN_HY = SCREEN_Y / 2

chunk[4] GetCameraChunks( camera_x, camera_y )
{
  min_x  = camera_x - SCREEN_HX
  max_x = camera_x + SCREEN_HX
  min_y = camera_y - SCREEN_HY
  max_y = camera_y + SCREEN_HY

  chunk[0] = GetChunk( GetChunkCoord( min_x, min_y ) )
  chunk[1] = GetChunk( GetChunkCoord( max_x, min_y ) )
  chunk[2] = GetChunk( GetChunkCoord( min_x, max_y ) )
  chunk[3] = GetChunk( GetChunkCoord( max_x, max_y ) )
}

 

 

7 hours ago, ItsQuesoTime said:

How would I "connect" chunks together so that they line up with one another, while also allowing them to be loaded/unloaded independently? 

Chunks should be referenced by chunk coord. I don't know what language you are using, however I bet there is some form of a map class for the language you are using. The chunk coord pair would be the map key and the map value would be the instance of your chunk. If there is no entry for the map key, the chunk is not loaded. If not loaded, load the chunk and create a key for the map. One trick to unload chunks is to have a timer countdown for chunks that are not within the camera view. The countdown could be set for a few minutes and when a chunk has any tiles rendered just reset the countdown. If the countdown hits 0, unload the chunk.

I'm going to redefine function GetChunk and GetCameraChunks in my Pseudo Code


class Chunk
{
  id
  data
  cooldown
}

COOLDOWN_SECONDS = 180


map_chunks = map< ChunkID, Chunk >


chunk GetChunk( chunk_x, chunk_y )
{
  chunk_id = pair<chunk_x,chunk_y>
  
  if map_chunk.contains(chunk_id)
   chunk = map_chunk.get(chunk_id)
  else
   chunk = invalid
}


chunk LoadChunk( chunk_id )
{
  chunk = new Chunk
  {
    id = chunk_id
    data = Tile[50][50]
    cooldown = COOLDOWN_SECONDS
  }
    
  .. fill chunk.data
    
  map.insert(chunk_id, chunk)
}


chunk_id[4] GetCameraChunks( camera_x, camera_y )
{
  min_x = camera_x - SCREEN_HX
  max_x = camera_x + SCREEN_HX
  min_y = camera_y - SCREEN_HY
  max_y = camera_y + SCREEN_HY

  chunk_id[0] = pair< GetChunkCoord( min_x, min_y ) >
  chunk_id[1] = pair< GetChunkCoord( max_x, min_y ) >
  chunk_id[2] = pair< GetChunkCoord( min_x, max_y ) >
  chunk_id[3] = pair< GetChunkCoord( max_x, max_y ) >
}


void Render()
{
  .. Get camera position in pixels
  camera_x = ..
  camera_y = ..

  chunk_id[4] = GetCameraChunks( camera_x, camera_y )

  ..Note chunk_id may have duplicate IDs
  previous_chunk = invalid
  
  for i in range(0, 4)
  {
    chunk = GetChunk(chunk_id[i])
    
    if chunk not valid
    {
      chunk = LoadChunk(chunk_id[i])
    }
    
    if chunk != previous_chunk
    {
      ..Note I'm just going to render everything in the chunk
      for tile_y in range(0, 50)
      {
       for tile_x in range(0, 50)
       {
         tile = chunk.data[tile_x][tile_y]
         
         ..Tile's pixel location in world space
         world_px = (TILE_SIZE * tile_x) + (chunk.id[x] * CHUNK_SIZE)
         world_py = (TILE_SIZE * tile_y) + (chunk.id[y] * CHUNK_SIZE)
    
         ..Camera's top left px coord in world space
         camera_px = camera_x - SCREEN_HX
         camrea_py = camera_y - SCREEN_HY
      
         ..Tile's pixel location in screen space
         tile_px = world_px - camera_px
         tile_py = world_py - camera_py
         
         .. Render the tile
       }
      }
      
      ..Reset chunk's unload timer
      chunk.cooldown = COOLDOWN_SECONDS
    }
    
    previous_chunk = chunk
  }
}


void Update( delta_time )
{
  for chunk in map_chunks
  {
    chunk.cooldown = chunk.cooldown - delta_time
    
    if chunk.cooldown < 0
    {
      map_chunks.remove( chunk.id )
    }
  }
}


I hope this helps. Good luck ;)

BTW: I didn't check any of my math, so there may be errors.

 

9 hours ago, Acosix said:

You need a consistent world information, no need to delete the information outside of the drawing chunk.
You just have to chunk the loading time. If you delete any of the world information,
it is lost forever.

Let world information stay in the database, and use a flexible loading time.

I'd give you example code in C++, but I really don't know which language/library you are using.

I've been storing my world data in external files so that it doesn't get deleted. I'm using Haxe and OpenFL, sorry for not specifying.

 

 

 

6 hours ago, Neometron said:

Here are my assumptions about your project:

  • Unit of measurement is in pixels
  • A tile is unit squared; 64x64px
  • Top left screen coord is 0,0 and bottom right is 1919,1079; 1080p monitor
  • Your 50x50 tilemap chunk is rendered from left to right, top to bottom

 

Lets say your tiles are 64x64px and your chunk is 50x50 tiles, so a chunk would be 3200x3200px.
Also for the time being, lets say chunk instances are stored in a 2D array. If the chunk is not loaded then the instance is invalid.

Pseudo Code:



TILE_SIZE = 64
CHUNK_SIZE = TILE_SIZE * 50

chunk_x, chunk_y GetChunkCoord( pixel_x, pixel_y)
{
  chunk_x = pixel_x / CHUNK_SIZE
  chunk_y = pixel_y / CHUNK_SIZE
}

chunk GetChunk( chunk_x, chunk_y )
{
  chunk = chunks[chunk_x][chunk_y]
}

tile GetTile( pixel_x, pixel_y )
{
  chunk = GetChunk( GetChunkCoord( pixel_x, pixel_y ) )
  
  if chunk is valid
  {
    tile_x = (pixel_x % CHUNK_SIZE) / TILE_SIZE
    tile_y = (pixel_y % CHUNK_SIZE) / TILE_SIZE
    
    tile = tilemap[tile_x][tile_y]
  }
}


Now for figuring out what chunks the camera sees.
Since a chunk is 3200px squared, you can fit a 1920x1080 screen within a chunk.
The most chunks you can physically see is 4 when the screen is at one corner of a chunk.

 



SCREEN_X = 1920
SCREEN_Y = 1080
SCREEN_HX = SCREEN_X / 2
SCREEN_HY = SCREEN_Y / 2

chunk[4] GetCameraChunks( camera_x, camera_y )
{
  min_x  = camera_x - SCREEN_HX
  max_x = camera_x + SCREEN_HX
  min_y = camera_y - SCREEN_HY
  max_y = camera_y + SCREEN_HY

  chunk[0] = GetChunk( GetChunkCoord( min_x, min_y ) )
  chunk[1] = GetChunk( GetChunkCoord( max_x, min_y ) )
  chunk[2] = GetChunk( GetChunkCoord( min_x, max_y ) )
  chunk[3] = GetChunk( GetChunkCoord( max_x, max_y ) )
}

 

 

Chunks should be referenced by chunk coord. I don't know what language you are using, however I bet there is some form of a map class for the language you are using. The chunk coord pair would be the map key and the map value would be the instance of your chunk. If there is no entry for the map key, the chunk is not loaded. If not loaded, load the chunk and create a key for the map. One trick to unload chunks is to have a timer countdown for chunks that are not within the camera view. The countdown could be set for a few minutes and when a chunk has any tiles rendered just reset the countdown. If the countdown hits 0, unload the chunk.

I'm going to redefine function GetChunk and GetCameraChunks in my Pseudo Code



class Chunk
{
  id
  data
  cooldown
}

COOLDOWN_SECONDS = 180


map_chunks = map< ChunkID, Chunk >


chunk GetChunk( chunk_x, chunk_y )
{
  chunk_id = pair<chunk_x,chunk_y>
  
  if map_chunk.contains(chunk_id)
   chunk = map_chunk.get(chunk_id)
  else
   chunk = invalid
}


chunk LoadChunk( chunk_id )
{
  chunk = new Chunk
  {
    id = chunk_id
    data = Tile[50][50]
    cooldown = COOLDOWN_SECONDS
  }
    
  .. fill chunk.data
    
  map.insert(chunk_id, chunk)
}


chunk_id[4] GetCameraChunks( camera_x, camera_y )
{
  min_x = camera_x - SCREEN_HX
  max_x = camera_x + SCREEN_HX
  min_y = camera_y - SCREEN_HY
  max_y = camera_y + SCREEN_HY

  chunk_id[0] = pair< GetChunkCoord( min_x, min_y ) >
  chunk_id[1] = pair< GetChunkCoord( max_x, min_y ) >
  chunk_id[2] = pair< GetChunkCoord( min_x, max_y ) >
  chunk_id[3] = pair< GetChunkCoord( max_x, max_y ) >
}


void Render()
{
  .. Get camera position in pixels
  camera_x = ..
  camera_y = ..

  chunk_id[4] = GetCameraChunks( camera_x, camera_y )

  ..Note chunk_id may have duplicate IDs
  previous_chunk = invalid
  
  for i in range(0, 4)
  {
    chunk = GetChunk(chunk_id[i])
    
    if chunk not valid
    {
      chunk = LoadChunk(chunk_id[i])
    }
    
    if chunk != previous_chunk
    {
      ..Note I'm just going to render everything in the chunk
      for tile_y in range(0, 50)
      {
       for tile_x in range(0, 50)
       {
         tile = chunk.data[tile_x][tile_y]
         
         ..Tile's pixel location in world space
         world_px = (TILE_SIZE * tile_x) + (chunk.id[x] * CHUNK_SIZE)
         world_py = (TILE_SIZE * tile_y) + (chunk.id[y] * CHUNK_SIZE)
    
         ..Camera's top left px coord in world space
         camera_px = camera_x - SCREEN_HX
         camrea_py = camera_y - SCREEN_HY
      
         ..Tile's pixel location in screen space
         tile_px = world_px - camera_px
         tile_py = world_py - camera_py
         
         .. Render the tile
       }
      }
      
      ..Reset chunk's unload timer
      chunk.cooldown = COOLDOWN_SECONDS
    }
    
    previous_chunk = chunk
  }
}


void Update( delta_time )
{
  for chunk in map_chunks
  {
    chunk.cooldown = chunk.cooldown - delta_time
    
    if chunk.cooldown < 0
    {
      map_chunks.remove( chunk.id )
    }
  }
}


I hope this helps. Good luck ;)

BTW: I didn't check any of my math, so there may be errors.

 

So just to clarify: you're suggesting having a map (associative array) to store the entire world, where keys are chunk coordinates in the world, and values are the chunk data (2D Array of tiles). If there is not value with a coordinate, the chunk isn't loaded, and if there is, it is loaded.

 

If I got that right, I just have one more question:

How would I draw the tiles in terms of the world, not the camera? Because the way I currently have things is just me drawing the world at the top left corner of the camera, which won't work if I'm going to have scrolling.

 

Thanks for the help, it has given me a lot of ideas.

6 hours ago, ItsQuesoTime said:

How would I draw the tiles in terms of the world, not the camera? Because the way I currently have things is just me drawing the world at the top left corner of the camera, which won't work if I'm going to have scrolling. 

I'll try to clarify what I was doing in the Render pseudo code.
I'll do it twice to show how it works.

Round 1:


void Render()
{
  .. Get camera position in pixels
  camera_x = ..
  camera_y = ..
  
  chunk_id[4] = GetCameraChunks( camera_x, camera_y )

Let say the camera's origin is located at 960, 540.
camera_x = 960
camera_y = 540

We'll look at the GetCameraChunks. BTW, remember when I said I didn't check my math; I didn't account for off by 1 problem so I forgot to subtract 1 to the maximums.


chunk_id[4] GetCameraChunks( camera_x, camera_y )
{
  min_x = camera_x - SCREEN_HX
  max_x = camera_x + SCREEN_HX - 1
  min_y = camera_y - SCREEN_HY
  max_y = camera_y + SCREEN_HY - 1

  chunk_id[0] = pair< GetChunkCoord( min_x, min_y ) >
  chunk_id[1] = pair< GetChunkCoord( max_x, min_y ) >
  chunk_id[2] = pair< GetChunkCoord( min_x, max_y ) >
  chunk_id[3] = pair< GetChunkCoord( max_x, max_y ) >
}

min_x  = 960 - 960 = 0
max_x = 960 + 960 - 1 = 1919
min_y = 540 - 540 = 0
max_y = 540 + 540 - 1= 1079

We'll also look at what GetChunkCoord does with these values.


chunk_x, chunk_y GetChunkCoord( pixel_x, pixel_y)
{
  chunk_x = pixel_x / CHUNK_SIZE
  chunk_y = pixel_y / CHUNK_SIZE
}

chunk_x = pixel_x / CHUNK_SIZE  = 0 / 3200  = 0
chunk_y = pixel_y / CHUNK_SIZE  = 0 / 3200 = 0

chunk_x = pixel_x / CHUNK_SIZE  = 1919 / 3200  = 0
chunk_y = pixel_y / CHUNK_SIZE  = 0 / 3200 = 0

chunk_x = pixel_x / CHUNK_SIZE  = 0 / 3200  = 0
chunk_y = pixel_y / CHUNK_SIZE  = 1079 / 3200 = 0

chunk_x = pixel_x / CHUNK_SIZE  = 1919 / 3200  = 0
chunk_y = pixel_y / CHUNK_SIZE  = 1079 / 3200 = 0

Note that this is normal behavior for division for integer types.

So going back to GetCameraChunks:
chunk_id[0] = pair< GetChunkCoord( min_x, min_y ) >  =  pair< 0, 0>
chunk_id[1] = pair< GetChunkCoord( max_x, min_y ) >  =  pair< 0, 0>
chunk_id[2] = pair< GetChunkCoord( min_x, max_y ) >  = pair< 0, 0>
chunk_id[3] = pair< GetChunkCoord( max_x, max_y ) >  = pair< 0, 0>
 

I'll skip ahead to this section of the Render function:


..Tile's pixel location in world space
world_px = (TILE_SIZE * tile_x) + (chunk.id[x] * CHUNK_SIZE)
world_py = (TILE_SIZE * tile_y) + (chunk.id[y] * CHUNK_SIZE)
    
..Camera's top left px coord in world space
camera_px = camera_x - SCREEN_HX
camrea_py = camera_y - SCREEN_HY
      
..Tile's pixel location in screen space
tile_px = world_px - camera_px
tile_py = world_py - camera_py

world_px = (TILE_SIZE * tile_x) + (chunk.id[x] * CHUNK_SIZE) = ( 64 * 0 ) + ( 0 * 3200 ) = 0
world_py = (TILE_SIZE * tile_y) + (chunk.id[y] * CHUNK_SIZE) = ( 64 * 0 ) + ( 0 * 3200 ) = 0

camera_px = camera_x - SCREEN_HX  = 960 - 960 = 0
camrea_py = camera_y - SCREEN_HY  = 540 - 540 = 0

tile_px = world_px - camera_px  = 0 - 0 = 0
tile_py = world_py - camera_py = 0 - 0 = 0

At this point render the tile at tile_px and tile_py which is top left corner of that tile image.


Round 2: We moved the camera by 10px in the update function.

camera_x += 10  = 960 + 10 = 970
camera_y += 10  = 540 + 10 = 550

GetCameraChunks( 970, 550 ) will yield again 4x pair<0, 0>

world_px = (TILE_SIZE * tile_x) + (chunk.id[x] * CHUNK_SIZE) = ( 64 * 0 ) + ( 0 * 3200 ) = 0
world_py = (TILE_SIZE * tile_y) + (chunk.id[y] * CHUNK_SIZE) = ( 64 * 0 ) + ( 0 * 3200 ) = 0

camera_px = camera_x - SCREEN_HX  = 970 - 960 = 10
camrea_py = camera_y - SCREEN_HY  = 550 - 540 = 10

tile_px = world_px - camera_px  = 0 - 10 = -10
tile_py = world_py - camera_py = 0 - 10 = -10

At this point render the tile at tile_px and tile_py which is top left corner of that tile image but partially off the screen.

The conclusion is that horizontal and vertical scrolling is built into the formula. Just move the camera an the world will move with it.

22 hours ago, Neometron said:

I'll try to clarify what I was doing in the Render pseudo code.
I'll do it twice to show how it works.

Round 1:



void Render()
{
  .. Get camera position in pixels
  camera_x = ..
  camera_y = ..
  
  chunk_id[4] = GetCameraChunks( camera_x, camera_y )

Let say the camera's origin is located at 960, 540.
camera_x = 960
camera_y = 540

We'll look at the GetCameraChunks. BTW, remember when I said I didn't check my math; I didn't account for off by 1 problem so I forgot to subtract 1 to the maximums.



chunk_id[4] GetCameraChunks( camera_x, camera_y )
{
  min_x = camera_x - SCREEN_HX
  max_x = camera_x + SCREEN_HX - 1
  min_y = camera_y - SCREEN_HY
  max_y = camera_y + SCREEN_HY - 1

  chunk_id[0] = pair< GetChunkCoord( min_x, min_y ) >
  chunk_id[1] = pair< GetChunkCoord( max_x, min_y ) >
  chunk_id[2] = pair< GetChunkCoord( min_x, max_y ) >
  chunk_id[3] = pair< GetChunkCoord( max_x, max_y ) >
}

min_x  = 960 - 960 = 0
max_x = 960 + 960 - 1 = 1919
min_y = 540 - 540 = 0
max_y = 540 + 540 - 1= 1079

We'll also look at what GetChunkCoord does with these values.



chunk_x, chunk_y GetChunkCoord( pixel_x, pixel_y)
{
  chunk_x = pixel_x / CHUNK_SIZE
  chunk_y = pixel_y / CHUNK_SIZE
}

chunk_x = pixel_x / CHUNK_SIZE  = 0 / 3200  = 0
chunk_y = pixel_y / CHUNK_SIZE  = 0 / 3200 = 0

chunk_x = pixel_x / CHUNK_SIZE  = 1919 / 3200  = 0
chunk_y = pixel_y / CHUNK_SIZE  = 0 / 3200 = 0

chunk_x = pixel_x / CHUNK_SIZE  = 0 / 3200  = 0
chunk_y = pixel_y / CHUNK_SIZE  = 1079 / 3200 = 0

chunk_x = pixel_x / CHUNK_SIZE  = 1919 / 3200  = 0
chunk_y = pixel_y / CHUNK_SIZE  = 1079 / 3200 = 0

Note that this is normal behavior for division for integer types.

So going back to GetCameraChunks:
chunk_id[0] = pair< GetChunkCoord( min_x, min_y ) >  =  pair< 0, 0>
chunk_id[1] = pair< GetChunkCoord( max_x, min_y ) >  =  pair< 0, 0>
chunk_id[2] = pair< GetChunkCoord( min_x, max_y ) >  = pair< 0, 0>
chunk_id[3] = pair< GetChunkCoord( max_x, max_y ) >  = pair< 0, 0>
 

I'll skip ahead to this section of the Render function:



..Tile's pixel location in world space
world_px = (TILE_SIZE * tile_x) + (chunk.id[x] * CHUNK_SIZE)
world_py = (TILE_SIZE * tile_y) + (chunk.id[y] * CHUNK_SIZE)
    
..Camera's top left px coord in world space
camera_px = camera_x - SCREEN_HX
camrea_py = camera_y - SCREEN_HY
      
..Tile's pixel location in screen space
tile_px = world_px - camera_px
tile_py = world_py - camera_py

world_px = (TILE_SIZE * tile_x) + (chunk.id[x] * CHUNK_SIZE) = ( 64 * 0 ) + ( 0 * 3200 ) = 0
world_py = (TILE_SIZE * tile_y) + (chunk.id[y] * CHUNK_SIZE) = ( 64 * 0 ) + ( 0 * 3200 ) = 0

camera_px = camera_x - SCREEN_HX  = 960 - 960 = 0
camrea_py = camera_y - SCREEN_HY  = 540 - 540 = 0

tile_px = world_px - camera_px  = 0 - 0 = 0
tile_py = world_py - camera_py = 0 - 0 = 0

At this point render the tile at tile_px and tile_py which is top left corner of that tile image.


Round 2: We moved the camera by 10px in the update function.

camera_x += 10  = 960 + 10 = 970
camera_y += 10  = 540 + 10 = 550

GetCameraChunks( 970, 550 ) will yield again 4x pair<0, 0>

world_px = (TILE_SIZE * tile_x) + (chunk.id[x] * CHUNK_SIZE) = ( 64 * 0 ) + ( 0 * 3200 ) = 0
world_py = (TILE_SIZE * tile_y) + (chunk.id[y] * CHUNK_SIZE) = ( 64 * 0 ) + ( 0 * 3200 ) = 0

camera_px = camera_x - SCREEN_HX  = 970 - 960 = 10
camrea_py = camera_y - SCREEN_HY  = 550 - 540 = 10

tile_px = world_px - camera_px  = 0 - 10 = -10
tile_py = world_py - camera_py = 0 - 10 = -10

At this point render the tile at tile_px and tile_py which is top left corner of that tile image but partially off the screen.

The conclusion is that horizontal and vertical scrolling is built into the formula. Just move the camera an the world will move with it.

Thanks for the clarification, I think I know what to do now.

This topic is closed to new replies.

Advertisement