1
\$\begingroup\$

I want to implement a toroidal world (all edges "wrap", the map repeats infinitely across its edges). My world is basically a 2D rectangle (it's a 2D game) divided into a grid.

The part I'm having trouble understanding and implementing is this: How should I implement/handle/draw a 2D grid in a toroidal world? The camera always follows the player, so visually you wouldn't notice when you crossed the edge.

It doesn't matter the language (I use .NET).

\$\endgroup\$
1

3 Answers 3

2
\$\begingroup\$

Older games often implemented wrap-around worlds "accidentally" by exploiting integer overflow behavior. If you have a 256x256 tile world, your coordinates are an 8 bit unsigned integer, you are at x = 255, and you add 1, you get back to 0.

If you are willing to stick to a map size of 256 x 256 (or alternatively 65,536 if you use 16 bit integers), and you are using a programming language that doesn't throw exceptions on integer overflows, you can still do that today.

But a more modern game engine should offer the flexibility of arbitrary map sizes that are dictated by game design considerations and not technical limitations. If you are developing such a modern game engine, then you can solve this by doing all access to map tiles and object coordinates through abstraction layers that implement wrap-around by themself:

x = x % MAP_WIDTH; if (x < 0) x = MAP_WIDTH + x; y = y % MAP_HEIGHT; if (y < 0) y = MAP_HEIGHT + y; 
\$\endgroup\$
2
\$\begingroup\$

Wraparound chunks and player jumps

Let's consider the problem in 1D (1 axis): Say I want to make an infinite runner game that consists of 3 chunks, looped over and over again. Boring game, but it serves to explain how we make a game look like we are moving in a straight line forever. Consider chunks A, B and C:

ABC ^ 

I start in chunk B. As I move forward, I leave B and enter C:

ABC ^ 

Oops, there is nothing in front of me. So at the moment that I cross the boundary between B and C, I grab chunk A, and move it ahead of chunk C:

BCA ^ 

As I move out of C into A, I do the same thing again, this time moving B in front:

CAB ^ 

And moving back into B, we move C ahead (aaand we've completed a full cycle):

ABC ^ 

What matters is that there is always something in front of me, and behind me. If each chunk were represented by a class instance, the code would look roughly like:

//1. declarations and initialisations class Chunk { ...chunk data... } Chunk[] chunks = new Chunk[3]; Chunk tmp = null; //player position and speed float x = 0; float vx = 1.4; //2. each frame / game loop iteration x += vx; //update position by speed int pxi = (floor(px) - 1) % 3; //chunk behind player (previous) int cxi = (floor(px) ) % 3; //chunk where player is (current) int nxi = (floor(px) + 1) % 3; //chunk ahead of player (next) Chunk chunkCurrent = chunks[cxi]; //...do jump collision detection based on current chunk... etc. 

Rendering

It's important to understand that you can either do this using modulo wrapping as above, OR by literally cycling the Chunks inside the array, every time you move forward one chunk; this also means you can assume the player is always at chunks[1], so there is always something behind and ahead of you. You don't need to move the player's index, just the chunks it refers to.

The reason I mention this is that you will need to do exactly this for the rendering side of things. If you have each of the 3 chunks displayed by an individual canvas or texture, you are going to need to swap these _in front of the player each time as they move forward.

About the math

In terms of the math, Philipp's answer is roughly correct. Performance-wise however you may prefer bitwise logic than the (relatively expensive) modulo operator and branching conditionals (if), provided you can use powers of 2 and unsigned integers (8, 16, 32 or 64 bit).

Unoptimised:

//each frame x = x % MAP_WIDTH; //costly modulo operator if (x < 0) x = MAP_WIDTH + x; //possibly costly conditional (depends on compiler) y = y % MAP_HEIGHT; //costly modulo operator if (y < 0) y = MAP_HEIGHT + y; //possibly costly conditional (depends on compiler) 

Optimised:

//initialisation int x = ...; int y = ...; const int MAP_WIDTH = pow(2, 8); //256 const int MAP_WIDTH_MOD = MAP_WIDTH - 1; //will set all 8 bits to 1 //each frame x = x & MAP_WIDTH_MOD; //same as 'x % MAP_WIDTH' y = y & MAP_WIDTH_MOD; //same as 'y % MAP_WIDTH' 

When running these for thousands of entities per frame, it can make a considerable difference.

The exact same math as the infinite runner example applies in 2D, we've just added a y axis.

\$\endgroup\$
1
  • \$\begingroup\$ @agone, with an unsigned input, % 3 has three possible outputs: 0, 1, and 2. & 2 has only two possible outputs: 0 and 2 (no 1), so these expressions are not equivalent. Maybe you meant & 3? You'll notice the latter part of Engineer's answer specifically demonstrates using bitwise AND for efficiency where it's appropriate, as well as showing how to calculate the correct bitmask value automatically so we can't accidentally typo it. \$\endgroup\$ Commented Oct 10, 2024 at 13:16
-1
\$\begingroup\$

This is a basic "get it working now" solution, which I recommend changing before shipping if your development cycle is longer than a month or two.

Use nine cameras

Instead of having one camera, have nine. One which follows the player, and eight more which are offset from the first camera by the size of the world in each dimension.

When you draw, first clear the screen to a fill color, then draw what each of the nine cameras sees (in any order). Cameras which do not see the world should not draw anything.

For example, if we consider only the x dimension, you have a camera that follows the player, a camera that is one world-size to the left of the player, and a camera that is one world-size to the right of the player. Most of the time, these supplemental cameras will be pointing at empty space, and so will not draw anything. However, when the player gets close to the edge of the world, one of these cameras will get equally close to the opposite edge.

With nine cameras, if the player moves to the bottom left corner of the map, the three cameras in the top right will get close to the top right corner of the map, and draw it connected to the bottom and left corners.

\$\endgroup\$
4
  • \$\begingroup\$ I think this answer assumes a specific game engine, it's clever but not necessarily helpful to OP if they're not using an engine where "cameras" can be layered and rendered to the viewport at the same time (like in Game Maker Studio?). \$\endgroup\$ Commented Oct 9, 2024 at 20:40
  • \$\begingroup\$ @Roman - I wrote it with the assumption that the asker was not using an engine, since they mentioned ".NET" where you'd expect to see an engine. Cameras are a useful abstraction even in 2D, whenever the player has a mobile viewport that shows only part of the world at a time - so I've usually had some kind of camera object in every game I've made. The asker mentioned "the camera" in their original post and used the [camera] tag, so I think they would agree. However, it is true that some engines will prevent people from implementing multiple cameras. \$\endgroup\$ Commented Oct 10, 2024 at 16:52
  • \$\begingroup\$ "Cameras which do not see the world should not draw anything" -- This is a very hard thing to pull off with the built-in "camera" functionality in most game engines. You can't take it for granted that this technique will just work without explaining how the custom compositing and multiple rendering passes are needed. It's likely OP will need to avoid using built-in "cameras" from the engine; If it's even possible for them to play around with the framebuffer directly in their engine... \$\endgroup\$ Commented Oct 11, 2024 at 14:22
  • 1
    \$\begingroup\$ @Romen - The only non-custom engine I've worked with is Unity, and in Unity, what I'm suggesting can be accomplished by setting the Camera's clearFlags to CameraClearFlags.Nothing to prevent it from clearing the screen before drawing. After that, just make sure no geometry is outside the world. Cameras which don't see anything don't draw anything. I assumed it would be similar in other engines. \$\endgroup\$ Commented Oct 11, 2024 at 16:04

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.