top of page

Scrolling through a tiled screen

Objective

In a previous article (Moving across a tiled screen), we explored how to create a screen with a tile map and how to move a sprite that detects impassable tiles. But what if our tile map is larger than the screen space? A common solution on the MSX is to clear the screen and reload the new visible section. Another method is to scroll the screen. This article will focus on developing the latter technique, which involves scrolling. In this video we can watch the final result.


Types of scrolling in MSX

MSX1 computers use the Texas Instruments TMS9918 VDP (or a clone of it). This VDP does not have built-in scrolling capabilities, so scrolling had to be done through software. The next generation utilizes the Yamaha V9938, which includes vertical scrolling, and in the third generation, the MSX2+ introduces horizontal scrolling with the V9958.


So, how did MSX1 computers achieve scrolling, like in Knightmare? They had to implement scroll by software. Each screen refresh required redrawing it quickly with the desired displacement, and in most games, the scrolling appears to be somewhat jerky, as can be seen in this video of Knightmare by Araubi. This article by Grauw explains how software scrolling works on different MSX systems. We will explore the features provided by the MSX2+ to implement our scrolling.


Hardware scroll V9958 (MSX2+)

The video chip of the MSX2+ (and Turbo-R) adds 3 new registers to control horizontal scrolling, which are #25, #26, #27, as explained in the Yamaha documentation.


Register #25 configures the type of scroll in the lowest bits. Bit 0, called SP2, is set to 0 if the scroll is single-page or double-page. In a single-page scroll, the left part of the screen that is not visible appears on the right.If SP2 is set to 1, the scroll is double-page, and as the first page disappears, it is filled from the other side with the second page.


The other configuration bit, bit 1, called MSK, controls whether the 8 leftmost pixels are masked. When set to 0, the 8 pixels on the left are not masked and are visible. When set to 1, the 8 pixels are hidden. This is used because during scrolling, the 8 pixels on the left have an uncertain value, so masking them hides any artifacts.


In Fusion-C, the commands to perform scrolling are SetScrollDouble(char n), SetScrollMask(char n), SetScrollH(int n), SetScrollV(char n). The first two are used to configure register #25. Passing the value 1 to SetScrollDouble places the next VRAM page after the active page. With a value of 0, scrolling is done only on one page. With SetScrollMask, passing a 1 hides the 8 leftmost pixels, and passing a 0 displays them. The other two commands move the screen, scrolling the specified number of pixels from the original image.


To have a first contact with these functions, let's see with a small program that loads 4 images onto the 4 pages of screen 5 and tests the aforementioned functions. You can find this program on the MoltsXalats GitLab.



First of all, we start by writing the includes and defining the variables to load the images and palettes as explained in the article "Load images to MSX".


Next, we continue defining the functions to read files from the disk:



And we start defining the main function of the program:



Until line 156, we change the text color and switch to screen 5 to load the different images and palettes. The images we have on each page, as indicated by the debugger, are:




The first thing we'll try is to shift page 2 of the VRAM 125 pixels to the right and 15 pixels down, with the leftmost 8-pixel block hidden, and we'll wait for a key to be pressed. On the screen, we get:



Now we do the same thing, but with the 8-pixel block visible for page 0:



The fact of hiding or not hiding the 8 pixels in a spontaneous movement, without it varying, just means that we have 8 pixels less of image.


Let's continue with the program:



Now we perform a concatenated scroll (SetScrollDouble(1)) of page 2, lines 198-209:



And what happens if the visible page is page 3, what does it connect to at the back? Well, it connects to page 0 as can be seen in the following code example, resulting in this image:



The only difference we've noticed by hiding or not hiding the block of the first 8 pixels is that we lose part of the image that remains dark. But what happens if we scroll pixel by pixel? What do we see in this block of 8 pixels? This is what we have tried to solve with the lines of code from 228 to 243.


What happens in this block of 8 pixels is that they flicker every 8 pixels, moving along, but when they reach the end, they become black again.


While if we have the block hidden, as done in lines 246-254, we see that the visible part moves smoothly, without flickering on the leftmost side.



Finally, we are left wondering what happens if we instruct it to scroll more than 256 pixels? This is what we attempt to answer in the last activity (line 256), and as can be seen, it starts over again. I understand that the hardware accepts values up to 512, for example, if we scroll on screen 7, but when applied to 256, it simply starts over.



Finally, we return to Screen 0, restore the palette, and exit.


If you open the openMSX debugger during any of the examples, you will see the images as they are in memory, without any scrolling. This scrolling of the V9958 registers only affects the screen rendering; the positions in VRAM remain unchanged. Therefore, if we need to copy memory segments using coordinates, these coordinates must reference the original page, the one in the debugger, not the coordinates we see on the screen.


One last thing I would like to mention is that SpaceManbow uses the V9958. In this YouTube video, you can see the difference between the V9958 and the V9938 and appreciate the smooth scrolling of the V9958.


Scrolling screen by tiles

Once we've experimented with what the V9958 can do, let's try to apply it to the tile map we had in the previous article. What we want to achieve is that when the main character moves towards the edge of the screen, the next block of tiles appears by shifting all the others. At the same time, when it moves inside the allowed zone and encounters a tile that it cannot pass through, it should not cross it as it did in the tile map.


To achieve this, we've chosen to scroll within a single screen and draw new tiles as we move, utilizing the 8 pixels of the hidden block and the pixels beyond the line 212.


Let's explain the code in detail:



Let's start with the includes we'll be using and the variables for loading the images and palette. Then, we define two variables indicating the current tile of the map being painted at the top-left corner of the screen, map_tile_x and map_tile_y. The variables x and y represent the coordinates of the character, the sprite we control and move across the screen.


Next, we have constants for the keys and the coordinates of the VRAM memory page. After that, we define NOMBRE_RAJOLES_HOR and NOMBRE_RAJOLES_VER, which represent the dimensions of the tile map defined in map1. The constants NOMBRE_RAJOLES_HOR_ORIGEN_PATRONS represent the number of tiles that fit in one horizontal screen, and NOMBRE_RAJOLES_PANTALLA_HOR contains the number of tiles we will paint, excluding the hidden one that we are gradually revealing. The following constants are the equivalents for the vertical dimensions: NOMBRE_RAJOLES_PANTALLA_VER represents the visible tiles, and NOMBRE_RAJOLES_PANTALLA_VER_SCROLL represents the tiles we use for scrolling, those that fit on a full rotating page.


Finally, we have the constant variable map1, which contains the information about the type of each tile, similar to the tile map article, but this time much larger than what fits in a screen display. Here, I've defined it directly in the code, but if we were to paginate and wanted it to be independent of the code, we could define the variable at a memory address like char map __at 0x????, and then load the file to that memory address during loading. Since the variable is so long, it occupies many screenshots:



Once we've defined the entire map, we define a sprite that we'll move across the screen, followed by all the functions to load the images from the disk.



Next, we have the VDP interrupt, which, as in the tile map article, we'll use to trigger a flag indicating that we need to analyze all the movement logic.



Now we move on to painting the game screen, just as we did in the previous article: stamp_x and stamp_y will have the coordinates where we have the images of the tiles that will form the map. Depending on the type of tile, they will return certain coordinates to be painted.


The function obtenir_coordenades_rajola(map_x, map_yreturns the type of tile that should be placed at the given coordinates.


Then, we paint the game screen in init_pantalla_joc() by loading the image with the patterns and painting it entirely. We create the sprite and set the rest to coordinate 255 so they don't appear. I read this article on msx.org while writing this post and thought that instead of line 255, which has the problem that when the scroll goes up they appear, it's better to put them in the 8 hidden pixels of the horizontal scroll, which don't move.



Next, we have the variables that we'll use to control the scroll. pos_scroll_x and pos_scroll_y will store the position of the scroll on the horizontal and vertical axes, respectively. We'll use flags to determine if we still need to scroll or if we're already at the map's limit. These flags are fer_scroll_lateral and fer_scroll_vertical. At line 341, we have the variables desti_x and desti_y, which indicate the position where we need to paint the new line of 8x8 tiles. These variables are defined as char so that we can perform the operation always with modulo 256, since if we exceed the limit, it always takes the least significant 8 bits. Since both vertical and horizontal scroll (as we're not using the two linked pages) are also 256.


Now, let's begin with the first function to paint the scroll, which is the one for upward movement, scroll_amunt(). The first thing we do is check if we've reached the map's limit; if not, we can activate vertical scroll by setting the fer_scroll_vertical flag.


At line 348, we have the code for scrolling. First, we decrement the variable pos_scroll_y to move up one line and check if we're at the position to repaint a line on the screen, which is every 8 pixels. Therefore, we check if the 3 least significant bits are 0. If we need to paint a new line, we have to inform the map that we've changed position, so we decrement map_tile_y. Next, we need to determine the scroll position to know which coordinates of the page we're seeing. This is stored in desti_y, which is the value of the scroll minus the 8 pixels we're going to paint. The painting loop should be done horizontally, so desti_x will depend on the loop variable (n) and the scroll position. The first thing we do is convert the pixel coordinates to map coordinates (blocks of 8) by dividing by 8, taking the integer part, which is the same as shifting 3 bits to the right. Then, we add the loop variable that's in tiles and convert it back to coordinates, now by multiplying by 8, but it would be more efficient to shift 3 bits to the left. We obtain the coordinates of the map tiles and copy the pattern tile to the calculated positions of desti_x and desti_y.


At line 362, we subtract one position from the vertical scroll. I found this line empirically. If the main character stopped just at this line and didn't continue upward but changed direction, the next map to be painted wasn't done correctly.


After handling these cases, we proceed to scroll vertically by calling the function SetScrollV().


As the last part of the code, we check if with the new scroll coordinates we've exceeded the map. If so, we indicate to the flag that the scroll is no longer active and reduce the screen position to return to the previous one.



Now we move on to do the same for scrolling down. First, we detect if we're not already at the end of the map at line 371. If it's time to paint (lines 375-390), we only change the part of obtaining coordinates, since if we use the top-left part as indicators of where we are (map_x and map_y), we need to add an offset to know which is the bottom part, which we do with NOMBRE_RAJOLES_PANTALLA_VER - 1. Finally, at line 391, we check if we need to set the flag of scrolling to 0 (no scroll).



Now let's start with the first of the lateral scrolls, the one to the left, which follows the same structure as the previous one but with new conditions to detect limits and where to paint for the lateral scroll. Also, this time the loop is not done over the X coordinates but we paint an entire column.


At line 400, we detect if we can do a lateral scroll and activate the flag. Then, we update the scroll position and at line 406, we detect if we need to paint a new tile of the map. Since they are 8x8 tiles, we also do the same as in the vertical scroll and detect the least significant 3 bits when it's 0 (every 8 times). If it's time to paint a new column, we update the horizontal map position (map_tile_x) and calculate where we need to paint. desti_x is determined by the scroll, and desti_y is obtained by dividing the pixels by 8 (shifting 3 bits to the right).


Between lines 411 to 422, we have the loop for painting vertically. Here we have divided it into two parts: from the current position downwards, and then the remaining part that goes to the beginning of the screen.


Line 424 is the same as in the vertical scroll, which was found empirically that if it didn't paint correctly just when the character left this point and went to another place on the scroll.

At line 426, we perform the horizontal scroll with the function SetScrollH().


Finally, we check if we are at the beginning of the map, and if so, we revert the change in the scroll position and activate the flag to prevent further leftward movements.



But can't we use a single loop to paint vertically as we did in the vertical scrolls? Yes, we can. We can use the fact that the overflow of the character does not affect us and starts counting again. The loop would be like this:


for (int m = 0; m < NOMBRE_RAJOLES_PANTALLA_VER_SCROLL ; m++) {
        obtenir_coordenades_rajola(map_tile_x, m + map_tile_y);
   		desti_y = (m + (pos_scroll_y>>3)) * 8;
        HMMM(stamp_x, stamp_y, desti_x,
             OFFSET_COORDENADAY_PAGINA_ACTIVA_2 + desti_y, 8, 8);
      }

Now let's move on to implement the equivalent for the right scroll. The only changes will be in the map limit checks at lines 437 and 463; instead of decrementing map_tile_x and pos_scroll_x, they will be incremented; and the position to obtain the coordinates to paint will have an offset of 31 tiles.



This function could also be simplified by removing the two painting loops and replacing them with:


for (int m = 0; m < NOMBRE_RAJOLES_PANTALLA_VER_SCROLL; m++) {
        obtenir_coordenades_rajola(map_tile_x + 31, m + map_tile_y);
   		desti_y = (m + (pos_scroll_y>>3)) * 8;
        HMMM(stamp_x, stamp_y, desti_x,
             OFFSET_COORDENADAY_PAGINA_ACTIVA_2 + desti_y, 8, 8);
      }

Once we have defined the new scroll functions, we now have the functions to determine whether the tile we are going to is a passable tile or not. We discover this with the es_casellaParet() function, which returns a 1 if the tile cannot be passed through, depending on the type of tile.


The next function, calculem_tiles(), tells us which tile of the screen we are on depending on the scroll. It converts the coordinates to tiles. The character's coordinate is only affected by the 3 least significant bits of the scroll to determine if it has changed tiles. Therefore, we use a bitwise AND operation (`&`) to isolate those bits, add it to the coordinate, and then divide it by 8 to get the tile coordinate (shifting the 3 least significant bits to the right `>>3`).



The previous function is the one we will use in the different movement functions to determine where we are on the tile map. The first movement function we have is the moviment_amunt() function, where we decrement the vertical coordinate, determine the tile we are on, and check if the current or the previous tile (due to rounding issues, as explained in the article Moving across a tiled screen, we need to check two tiles) is passable or not. At line 506, we compare if the tiles we have checked are blocking tiles, and if so, we revert the position of the main character. Then, at line 509, we check if the character is above a threshold (in this case, 14 pixels), beyond which we will trigger the scroll and move the character back, as the screen advances.


The moviment_avall() function does the same but adds to the Y coordinate instead of subtracting. The position of the tiles also varies and has been adjusted empirically, following the trial and error method.



Now we need to take the same steps for the horizontal axis in the functions moviment_dreta() and moviment_esquerra(). Then we have the moviment_sprite() function that handles whether we press the ESC key to exit or if we are moving the joystick (or the arrow keys). Depending on the key pressed, it will execute certain movement functions or others:



And finally, the main() function starts by setting up the screen and the initial positions within the grid map. Then, at line 608, it calls the function to initialize the screen (load images and create the grid map). We finish initializing variables used to control the scroll and initialize the interrupt control to perform movements of the main character. At line 623, we draw the sprite of the main character and simply listen for the exit key press while processing the movement of the main character with the moviment_sprite() function.


If we exit with the ESC key, we stop the interrupts and return to the initial state. Then, between lines 628 and 645, we print debugging information. Finally, we return to DOS with Exit(0).



Conclusions

We've seen how to implement scrolling using the hardware functions of the V9958. The scrolling functions are all very similar and could be combined into a single function. This is what I attempted to do in scrfusfons.c. Unfortunately, I couldn't get it to work correctly. If anyone is able to fix it, please let us know.


Next, we could explore how to keep the top part of the screen stationary to display game information such as lives, points, weapons, etc. This could be done by copying a block or using line interrupts. We'll explore this in another blog post.


Click here for seeing the examples working.



Comments


bottom of page