MaxCodes.info - Scrolling Maps

Scrolling Maps


          At one point or another you'll get tired of having static maps that are no bigger than the window they're being displayed in.
After all, we want games that are engaging and interesting to the player right? Having a map larger than the screen has numerous
advantages, not the least of them being but when it comes time to implement them and it can be a little confusing. I liken it to
trying to cut your hair in the mirror: the principle is easily understood, but when you get down to putting it into practice your
brain gets thrown off by the change in orientation. I know i had a little difficulty getting it right at first, and based on a recent
thread on /r/RoguelikeDev others have as well. It's also one of those topics
that when you google it, theres alot of people talking about it, and not alot of practical examples of how to ACTUALLY do it.
          Whether you're using BearLibTerminal, libTCOD, or NCURSES, rendering grid based map from an array to the screen
is a straight foward and intuitive afair, seeing as almost EVERYONE uses a 2d array or vector as theire map representation, this article moves foward
under the assumption that you are using it as well. The basic map rendering function looks like this:

            void draw_map()
            {
                int x, y;
                for (y = 0; y < map_height; y++)
                {
                    for (x = 0; x < map_width; x++)
                    {
                        //yada yada, some logic
                        //and then
                        terminal_put(x,y,map_rep[x][y]);
                    }
                }
                //down here their might be a function call to render
                //other objects, or maybe even the code to render said objects
            }
        

A simple 2D array traversal is convienient in the case of a grid based map and it serves us well as a jumping off point. One of the major issues
that arrises from this is that it makes some assumptions, for example it assumes your map is at least no bigger than the window being rendered to. The first
change we want to make is to change the conditions of the loop, which will help us think out how to handle the rest of this task. If our goal is to use a map
that is larger then the screen, we either have to stretch the screen or shrink the map to fit it all on in one go, neither of which is practical or useful to us
So w'ere going to change the conditions of the loop from being constrained by the size of the map, to being constrained by the size of the screen:

            void draw_map()
            {
                int x, y;
                for (y = 0; y < screen_height; y++)
                {
                    for (x = 0; x < screen_width; x++)
                    {
                        
                        terminal_put(x,y,map_rep[x][y]);
                    }
                }
            } 
        
Now, regardless of how big the map is, the only thing being displayed is the same section of the map, the upper left corner. What we want is that when our
character starts moving, the area of the map being explored is what's rendered(obviously). Alot of people use the allegory of a "camera" to accomplish this
in that the player is focused in the center of the camera and regardless of the change in location the player stays in view, its a good analogy and explains nicely
how we go about accomplishing this. So we have a view variables we need to track in order to get things rendering properly:
  • Screen width and height
  • Map Width and Height
  • Camera X/Y
  • Player X/Y

With a little bit of math we can interpolate these values to get the representation of what we want. We're going to translate map and player x/y points
to camera X/Y points in order to take a "slice" of the map and render the proper section on the screen in relation to our player. We've already established
that we want our players character to be the main focus in the middle of the screen. which means our players x/y on the screen should be screen_width/2
and screen_height/2. And to keep the player centered on the screen, we should also keep the map centered around them which gives as the camera's X/Y value. How?
What we want, essentially, is for our for loops to do the equivelant of this:

                playerx = screen_width/2;
                playery = screen_height/2;
                for (y = playery/2; y < playery*2; y++)
                {
                   for (x = playerx/2; x < playerx*2; x++)
                   {
                        terminal_put(x,y,map[x][y]);
                   }
                }
            
The problem, is that we need x to be 0, if x were to start at playerx/2 instead, nothing would show up on the screen. So instead of changing the x/y values of the loop
were going to translate the screen values to map values so that map[playerx/2][playery/2] is displayed instead of map[0][0]. The following function takes care of that
of that translation for us:

            //Returns coordinates for the camera's upper left corner
            auto track_camera(int playerx, int playery) -> std::tuple
            {
                int map_width = 240;
                int map_height = 80;
                int scr_width = 100;
                int scr_height = 40;
	            int x = playerx - scr_width / 2; //Keep the player in the middle
	            int y = playery - scr_height / 2; //of the screen
	
                if (x < 0) 
                { 
                    x = 0; 
                }
                if (y < 0) 
                { y = 0; 
                }
                if (x > map_width - scr_width - 1) 
                { 
                    x = map_width - scr_width - 1; 
                }
                if (y > map_height - scr_height - 1) 
                {
                     y = map_height - scr_height - 1; 
                }
	            return std::make_tuple(x,y);
            }
        
Now at the beginning of our render function we make a call to track_camera with the players x/y position and it returns an x/y
value of where we should start displaying the map from, all wrapped up nicely as a tuple. Using this we can implement
the changes to our map rendering like so:
            void draw_map(map layout, player play)
            {
                int scr_width = 100; int scr_height = 40;
                int x, y;
                int camerax, cameray;
                int mapx, mapy;
                std::tie(camerax,cameray) = track_camera(playerx, playery);
                for (x = 0; x < scr_width; x++)
                {
                    for (y = 0; y < scr_height; y++)
                    {
                        mapx = camerax + x;
                        mapy = cameray + y;
                        terminal_put(x,y, layout[mapx][mapy].symbol);
                    }
                }
            }
        

Note in the above function that while we're outputting to an unchanged x/y value, were reading the array from the translated
mapx/mapy values, which is really the trick to this whole thing. You'll also notice that track_camera provides us our map offset
in relation to our playerx/playery values, so we need a way to translate those as well so that our character, and in fact any other object
being rendered is displayed properly in relation to the map:

            auto player_to_cam(int playerx, int playery, int camerax, int cameray) -> std::tuple
            {
	            int scr_width = 100, scr_height = 40;
                playerx = playerx - camerax;
                playery = playery - cameray;
 
	            if (playerx < 0 || playery < 0 || playerx >= scr_width || playery >= scr_height)
                { 
		            return std::make_tuple(0, 0);  //if it's outside the view, return nothing
                }
	            return std::make_tuple(playerx, playery);
            } 
        
Again we supply our players x/y, and with our new found camera x/y we are returned a tuple of x/y values with which to display
our character on the screen. Putting it all together our drawing function now looks like this:
            void draw_map(map layout, player play)
            {
                int scr_width = 100; int scr_height = 40;
                int x, y;
                int camerax, cameray;
                int mapx, mapy;
                std::tie(camerax,cameray) = track_camera(playerx, player.y);
                for (x = 0; x < scr_width; x++)
                {
                    for (y = 0; y < scr_height; y++)
                    {
                        mapx = camerax + x;
                        mapy = cameray + y;
                        curr = layout[mapx][mapy];
                        terminal_put(x,y, layout[mapx][mapy].symbol);
                        
                    }
                }
                int px, py;
                std::tie(px,py) = player_to_cam(play.x, play.y, camerax, cameray);
                terminal_color("green");
                play.draw(px,py);
            }
        

And there you have it! proper scrolling ability on maps that are larger than the view. I should mention that the above code takes care of the
edge cases where if your offest is less than zero or at the far end of the map, the camera "snaps" to the walls and the player moves about the screen
like normal.

To see this code fully integrated, code examples are available on my GitHub

Enjoy, and Feel free to comment below!



(c) 2020 Max Goren
MaxCodes.info