Hi there! In the previous posts, we dealt with static maps, and moving sprites… Now we want to “move the screen” too!
We suggest you to use the last program you wrote on the previous tutorial, so that we will have a common base. Furthermore, we need a bigger map, so that we can “move” around it. You can create your own map and graphics, but since this is a tutorial, we will use this one. Please refer to the first uSVC tutorial to see how to import a map.
If you have imported correctly (actually by overwriting the existing code), the gamemap.h file should show a 80×50 size.
To have scrolling, we need to modify the function draw map. In particular, in the first episode, the
drawMap() function had 3 parameters, and we did not use them. In particular, we did not use x and y. We are going to use these parameters now.
Let’s first consider how to have a tile-step scrolling: i.e. in multiple of 8 pixels (the tile size). The algorithm is simple: for each screen coordinate
(sx, sy), instead of copying the tile at coordinate
(sx, sy) in the map, we copy the tile at the position
(x / 8 +sx, y / 8 + sy). We use x / 8 and y / 8, because in drawMap we want x and y to represent the scroll offset in number of pixels, and not in number tiles.
drawMap() becomes now:
Optimization tip: the code above can be greatly optimized, but to make it more understandable we can keep it as it is.
Now, let’s add to the main loop a simple code snippet that allows us to move in x and y directions…
Now let’s build and upload in debug mode… As you can see, it scrolls, but it is not smooth at all, and actually this is rather unpleasing. Furthermore there is something wrong with the sprites. There is some graphic glitch under the sprites:
Let’s fix the glitch first.
This is due to how the function
If you recall tutorial 2, when you call draw sprites, the function
drawSprites() might reserve some temporary tiles for the sprites. In each one of these tiles,
drawSprites() will copy the tile referenced by vram, and then it will draw onto that temporary tile the part of the sprite which need to appear on that particular position. We didn’t say that the function also will remember which tile was stored on a particular vram location before exchanging it with a temporary tile. Furthermore, we didn’t tell you also that, before drawing any sprites,
drawSprites() will try to deallocate the previously allocated temporary tiles, and it will also restore the old tile that was present before the previous
- Checks if there are any allocated temporary tiles in the vram.
- If there are, it deallocates them, by restoring into vram, the previous tile reference, that was removed by the previous call
- It performs actual sprite drawing (with temporary tiles allocation).
As you saw, this might be an issue, if we change the vram content before actually calling
drawSprites(). On the other hand, the map must be drawn BEFORE
drawSprites() is called. To workaround this impasse, we must restore the old tiles before actually drawing the map: this is done by either calling
removeAllSprites() with a non-zero parameter, or by adding a call to
restoreBackgroundTiles() in the
Noticeably, we did not have this issue on the previous tutorial, as we never really updated the vram with new map data.
The most attentive of you might have recognized something that is not really optimized: why do we need to restore the old tiles in vram, if we need to change them anyway in the
drawMap(), which will modify vram? The reason will be clear later, and a slight improvement will be suggested. Still, this improvement will have only a minor impact on the performance.
In this tutorial we go for the second solution, we will have a call to
drawMap() as shown below:
With this modification, you should not see glitches anymore, as shown by the detailed grab below.
Still, we need to do something more, to enable smooth scrolling. To do this, we need first to increase the vram size, and the reason will be clear soon. First, open uSVC_config.h and change VRAMX and VRAMY according to the image below (41 and 26, respectively).
Now a bit of theory. What we are going to do is to move the visible area over the vram, which is now bigger than a screen, which is 320 x 200 pixels.
We already managed to scroll in terms of multiples of 8 pixels. We need to handle the case when the scroll is not a multiple of 8 pixels, i.e when the remainder of “scroll / 8” is 1 through 7. In other words, we need to add an offset to the 8-pixel scroll we achieved with the modified
Luckily, our VGA signal generation routine can draw lines even with a horizontal offset. This means that, instead of start drawing the line from the first horizontal pixel of the first tile, the signal generation routine can start from an arbitrary pixel, i.e. with a (positive) offset. We will see in an advanced tutorial that this horizontal offset can be defined per-screen, per-tile or even per single line. While per-screen offset is used for scrolling, per-tile and per-line can be used for other effects such as parallax or water distortion effects.
The vertical offset is even simpler to conceive, and it is done by the kernel as well: instead of telling to the routine that we are currently drawing the current line (called “
currentLineNumber”), we tell it that we are drawing the current line plus a vertical offset (“
currentLineNumber + verticalOffset”).
Now, it should be clear why we need a vram at least 1 tile larger in both directions. For instance, we scroll horizontally and vertically by half a tile, the screen visible area will overlap the tiles at coordinate having
xTile == 40 (41th horizontal tile), and/or having
yTile == 25 (26th vertical tile).
To set the vertical and horizontal (screen-wise) offsets, we need to call the functions
setYScroll(). These functions must be fed not with the entire scroll value, but only to the remainder of the operation “
x / 8” and “
y / 8”. Noticeably, a division by 8 is just a right shift of 3 positions, and the remainder of that division is a bitwise AND with 7. The compiler already detect this and you can safely use division and modulus operators if you want.
drawMap() function will be as follows:
Now, compile and upload. Wow, this is now a very smooth scrolling, right? As you can see from the frame below, the scroll is not only limited to 8-pixel multiples.
Warning! With the current
drawMap() function, you need to pay attention not to overflow, i.e. to keep the x and y scroll values so that the last visible pixel is within the map. If you don’t take care about this, uSVC might crash!
As we said before, the function is not well optimized. Among the things we can optimize:
- We always redraw the entire vram, (41 x 26 tiles) even in the case of 0 xScroll and yScroll. This is not a big deal, we are just adding at most 26+41 unnecessary operations. The computational effort for this is really negligible with respect to all the rest of our code and our Cortex M0+ can handle them decently.
- We always redraw the entire vram, even when actually we don’t have to, e.g. when we actually should only update xScroll and yScroll (e.g. when the both parameter x and y change from 0 to 1). This optimization can save us plenty of time.
- We restore the background tiles (by deallocating the temporary sprite tiles), even when we redraw the entire vram. This issue is less important than the second, but it could bring more significant benefits than issue 1.
The snippet below addresses issue 2 and 3.
Let’s analyze it.
We have created two static variables (i.e. their values will be “remembered” when the function exits, so that, next time the function is called, they will have the value that they had at the end of the previous call). These hold the last x and y offset, in terms of tiles. If there is a
(x, y) change, that does not affect
x / 8 and
y / 8 (integer division) result, then we do not actually have to redraw the map, i.e. change the vram content, we need at most to change x and y scroll (see last lines). In this case, we might need to restore also the background tiles if we drew sprites. Instead, if the change on the
(x, y) is so that either
x / 8 and/or
y / 8 is changed, then we need to scroll by one tile, i.e. the map must be redrawn. In such case, we don’t have to restore the background tiles (as the two nested “for” cycles would automatically write a new background), but we still have to signal that the sprite tiles have been removed by calling
freeSpriteTiles(). This latter API is faster than “
restoreBackgroundTiles()” as it simply deallocates all the tiles, and does not try to restore vram.
As a last remark about the snipped above, you might have noticed that we included “
|| forceRedraw” condition. This is required when you draw the frame the first time. If we didn’t included a “
forceRedraw” command, there could be a case in which the background is not drawn until we get a scroll of more than 8 pixel, with respect to the position
Sprites and Scrolling
What about sprites? Well, the sprite engine has been implemented to be as general as possible, this is why they have screen-relative coordinates. Noticeably, coordinates might be also negative, for instance if the sprite is partially off-screen.
To appreciate this, let’s remove the sprite movement routine, i.e. comment the highlighted block shown below:
As you can see, the sprites stay at the center of the screen, while the map is scrolling.
Instead, typically we want instead that the sprites move with the map. How to do this?
The solution is to use the “cameraX” and “cameraY” variables. These variables indicate where the screen, i.e. the visible area, is actually located in the map. If you want the sprites to stay at a map relative – and not screen relative – position, then you just only have to put the sprites with “
xSprite - cameraX”, and “
ySprite - cameraY” coordinates, where
ySprites are the absolute coordinates, i.e. in the map.
So, let’s try this, and for the putSprite() function, let’s write what we show below:
putSprite(numSprite, x - cameraX + xs * 16, y - cameraY + ys * 16, SPRITE_FLAGS_HANDLE_CENTER | SPRITE_FLAGS_ROTATE_90, spriteFrame);
You will notice that now the sprites won’t be always on the center of the screen, but they will stay at their positions, as if they were actually on the map.
In just three lessons you already learnt how to place sprites, draw map and get a smoot scrolling effect!
We need now to learn how to get the input from the USB peripherals, and then you’ll be ready to create some games! After these tutorials we will cover more advanced lessons for more interesting effects.