Objective
In this article, we explain how to create a screen using prefabricated tiles and how to move a sprite within this map by detecting collisions with tiles that are not passable.
We will only use two types of tiles, passable or not, inspired by the patterns of Usas, a platform game by Konami for MSX2 and one of my favorites. I extracted the patterns by loading the game with openMSX, pausing with the debugger, opening the VRAM viewer, and dumping the image of the pattern page to a file. This BMP-format file was then converted to MSX format as explained in the article Loading Images on MSX.
The program only uses two types of tiles for a simple introduction. To create a larger map, I recommend using the Tiled software, which is cross-platform and allows exporting the map to CSV, easily copyable into a C file as an array.
What is a tile map?
A tile map is the creation of a map where each tile has a code in memory indicating what type of tile it is and how it should be displayed. We will use 8x8 tiles in screen 5, so our screen will be formed by 32x26.5 tiles, losing the last 4 pixels of the last row. By knowing the content of each tile in memory, we can determine whether a sprite is in a restricted position or not.
To detect if there is a collision between the sprite and the map, we will use the technique explained in this article, which involves checking whether the next tile the sprite is supposed to move to is accessible or not.
Code
In this program, we load a tile image and, based on a map, paint these tiles. Then, we create a sprite and control its movement on the map without allowing it to pass over non-passable tiles.
For the sprite movement, we use a more advanced interrupt routine than the one in Fusion-C 1.3. The old function was `InitVDPInterruptHandler` (you can see its usage in the example `Interrupt-vdp.c`), but it had the drawback that if the function executed during the interrupt was in page 2 of memory, it would hang. Oduvaldo Pavan kindly developed functions to make the interrupt work on any page, and these functions are what you'll find in this code.
We have done this in the following program, which can be found in the MoltSXalats Gitlab:
The first part of the code calls the includes that will be used in the program, defines the variables for loading the images (file, BUFFER_SIZE_SC5, LDbuffer, mypalette), and the variable `map1` that will contain information about whether a tile can be crossed or not. This will be the scheme that we will later transform into the screen by painting 8x8 pixels differently depending on whether the map1 tile is a 0 or a 1.
In the variable `sprite`, we define a simple sprite that we will use to move around the screen.
Next come the functions for loading files from MSX-DOS: `FT_SetName`, `FT_errorHandler`, `FT_LoadSc5Image`, `FT_LoadPalette`, and `FT_LoadPaletteMSXViewer`, which have already been explained in the posts on Loading Images on MSX, and which we will use to load the 8x8 tiles into the video memory of the MSX.
After the functions for loading data into VRAM, we begin to define the variables we will use to control the hook of the Video interrupts. The address 0xFD9F is the one the Z80 reads every time there is a video interrupt, and we rewrite it so that it executes our function.
The function `InterruptHandlerHelper` is a small assembly function that will be used to jump to our function.
`InitializeMyInterruptHandler` is responsible for replacing the address of the interrupt hook with the address of our function, which we pass as the first parameter, `myInterruptHandlerFunction`. The second parameter, `isVdpInterrupt`, is set to 1 to indicate that it is the VDP interrupt.
Why do we use a VDP interrupt? We use it to establish a tempo in the game, a rhythm, where in each interrupt we update all the game data: player position, enemy position, music, sound effects, etc. Using the VDP interrupt, the option to detect sprite collisions is lost. But you have the advantage of maintaining a tempo, for example, to keep the rhythm of the background music.
In this case, we will use the interrupt to control the movement of the sprite.
The function `EndMyInterruptHandler` is used to return the VDP interrupt to its previous routine before we put ours.
The variable `copsVsync` is a counter of the times we enter the interrupt. It serves to handle different rhythms; perhaps the enemies are updated every 2 interrupts, the music every interrupt, etc. The variable `processar_moviment` is a flag to indicate that I can now move the sprite. This flag allows me to separate tasks performed during the interrupt because it will indicate to another function in the main loop that it can now work on processing the sprite's movement. This way, if the movement calculation were too heavy and took longer than the time between interrupts, we wouldn't interfere with other tasks that would also be updated.
The `main_loop` function is executed at each VDP interrupt. At the moment, we only have a counter of interrupt times, and every two interrupts, we update the flag to process the sprite's position. You can try other values; higher values will make the sprite less manageable, and smaller values will make it move very fast.
The next function initializes the game screen. Between lines 283 and 286, it loads the pattern image into a hidden page of the VRAM. Then, it has two loops to iterate through the matrix of tile maps, starting with rows and then columns. For each position in the matrix, it checks what type of tile it is. In this case, we only have 2 types; depending on the number, it copies the 8x8 tile from the page where we loaded the image and places it in the corresponding position. This way, the screen is drawn. The HMMM functions have the first two parameters as the origin coordinates of the 8x8 tile to copy. If we change these two parameters, we will have a different drawing for the forbidden and allowed tiles.
In the next lines (299 to 303), we create a simple sprite to move around the screen. The following lines are variables that we will use as coordinates of the sprite and to convert these coordinates to text to display them on the screen.
Here we have the `moviment_sprite` function, which is an infinite loop until the ESC key is pressed (line 311). Inside this loop, we first check if we are in an interrupt where we need to calculate movement (activated by the `main_loop` function of the VDP interrupt). If we need to find the sprite's positions, we first check if any arrow key (or joystick in the first port, as it reads the same when passing the parameter 0 to `JoystickRead`) is pressed and save it to the `stick` variable. Lines 317 to 321 write these coordinates to the screen; first, we convert the number to a character string (`sprintf`) and then display them on the screen using `PutText`. This makes debugging easier as we are visualizing the sprite's coordinates.
Then we do all the key control to move the sprite. We start with the up arrow (stick==1). First, we set the position where the sprite would go one pixel up and check which tile we would be in with this new location. Since they are 8x8 blocks, I need to convert the coordinates to tiles. To do this, we divide by 8; for simplicity, we shift 3 bits to the right (`>>` bitwise operation), which approximates a division by 8, rounding down to the nearest integer. Now we need to check what type of tile this new position is, so we look up the value in our variable containing the map by translating the tiles into an array. Since it is arranged by rows, the position of tile X is as it is, and we need to increase each Y position by 32 because the map has 32 horizontal tiles. This is line 331.
Additionally, we also need to look at the tile one position to the right of the map, which is line 332. I added this second line empirically; theoretically, it would only be necessary to check the future tile where we are going to see if it can be accessed or not. However, if I didn't add it, it would access part of the forbidden tile. However, as I have been writing this, I think the problem might come when rounding down to an integer, which always rounds down, and that's why the next tile needs to be taken. Well, if anyone has a better explanation, feel free to let us know.
Since it is a simple case and we only have 2 types of tiles, we check if either of the two inspected tiles is forbidden (of type 1 in our case). If it is forbidden, we revert the sprite's position that we had changed; in this case, y=y+1. If there were more types of forbidden tiles, we would need to add them in this if statement.
Finally, we relocate the sprite with the new value of the coordinates.
The rest of the cases have the same structure; the sprite's position changes, and it checks if the new tile (and the next one) is allowed or not.
Finally, we have the main function that starts by configuring the screen (lines 378 and 379). Then, it changes color 15, which in the palette of the loaded image had a dark color, to white so it can be read on the screen.
We initialize the variables and set the `main_loop` function in the VDP interrupt hook (line 385). We indicate the position where the sprite will start and call the function with an infinite loop until the ESC key is pressed. This function is `moviment_sprite` at line 389.
When we exit this loop, we restore the VDP interrupt hook with `EndMyInterruptHandler` and return to screen 0 with the original colors. Finally, we end the function and return to DOS with `Exit`.
Conclusion
In this article, we have constructed a simple grid map and moved a sprite across the screen without allowing the sprite to move onto forbidden tiles. The map only has two types of tiles, and if we want a more colorful and varied map, we would need to create different types of tiles. We would need to control these types when painting by adding more conditionals in lines 291. Knowing these types, we would also need to know their coordinates in VRAM, where they are stored, to copy them correctly using the HMMM function.
The MSX2 has vertical scrolling, and the MSX2+ also has horizontal scrolling. What if we have a map of tiles larger than what is displayed on the screen? Can we use scroll functions to move the entire screen and continue detecting collisions? These topics will be addressed in future articles.
Now that we have also seen the video interrupt, we could try generating an MSX-Music (OPLL) replayer that updates the playing notes with each interrupt. We will work on this functionality in upcoming articles.
Click here to see the example in action.
Comments