Hi all! At Eeems' suggestion, I am posting this work-in-progress code
I've been working on-and-off on my tilemap engine since my Pokemon Amber attempt. The current rewrite is slower, but a bit easier to work with on the my end. It also allows the surrounding map to still be animated if I need to pull up a menu or dialog! I've also added in map compression using modified code from my
LZD project, as well as event tiles!
In this version, I mess around with OS RAM to make larger chunks of contiguous non-user RAM, so it might cause crashes.
As of now, all you can do is walk around the map and enter/exit map editing mode. Since maps are currently stored in the app, map editing is not permanent. If you leave the map and come back, all changes are reverted.
If you want to edit the map:
Press [mode] to enter map editing mode
Use [+] and [-] to scroll through the available tiles for the map
Use [*] to toggle walkability. You'll see corner guards if the tile can't be walked on.
Use [Enter] to set a tile.
Use [stat] to exit map editing
When you hover over a tile that can't be walked over, a solid border will be drawn.
When you walk over an event tile, a dotted border will be drawn.
If you want to *actually* edit or create maps, you'll have to do that on your computer and recompile the app. You'll need
spasm (with app signing enabled!) and Python with numpy. The map structure is:
.db height,width
.db start_y,start_x ;The player will be drawn at y+5 and x+6 !
.db number_of_tiles
.dw tile_index_0
.dw tile_index_1
...
;map data
.db blah
.db blah
.db blah
...
;Event code
.db 0
The map data is actually stored transposed. So the tiles you see across the top row are actually stored in the left column of bytes! This makes it a headache to code.
You can use up to 64 different tiles in a map.
Each byte in the map has the index into the tileset stored in the lower six bits. Setting the top bit means it can't be walked on, and setting bit 6 means walking into it triggers an event.
When events are triggered, the map's event handler is parsed an executed. It is a very basic scripting language that currently handles addition, subtraction, =, <, &, warping, and conditional execution. Scripts end with a null byte (0x00) and are stored similarly to how they'd be parsed in RPN format, currently with the exception of conditional execution (since the condition is checked before executing). 'x' and 'y' can be used to read the player's current coordinates, 'i' evaluates a condition (like an If statement) and if it is true, it executes the next command, otherwise it skips. 'w' is used to warp and requires bytes in the form of `map_id,start_y-5,start_x-6`. Finally, 8-bit integers are prefixed by a `.db 1`. For a convoluted example, here is the event handler for inside the "house" :
;if (y == 3 or y == 4) and x == 7:
; warp(0,2-5,5-6)
.db "y",_num,3,"-",_num,2,"<x",_num,7,"=&iw",0,2-5,5-6
.db 0
Once you've made your map, you'll need to compress it. First you'll compile it to a binary, then you need to compress that data and convert the compressed data back to assembly. For example:
spasm maps/map9.z80 maps/map9.bin
python zlz.py maps/map9.bin maps/map9_comp.z80 map9
And finally, you need to add the map to the mapLUT found near the bottom of main.z80, just add the label name (in the above case, `.dw map9`) and include the compressed version of the file at the bottom.
The system is largely interrupt-based. A custom interrupt updates the current keypress, updates the tile animation, updates the LCD, and draws the tilemap. All you have to do is set the appropriate flag:
rpgflags = 33
updateLCD = 0 ;Tells the interrupts to update the LCD
drawmap = 1 ;Tells the interrupts to redraw the map
animate = 2 ;Tells the interrupts to animate tiles
noupdate = 3 ;Overrides the interrupt's LCD update and tilemap redraw
When a tile's animation counter runs out, it automatically sets the drawmap flag. When a map is redrawn, it automatically set the updateLCD flag and resets the drawmap flag (no need to keep doing it). And if the noupdate flag is set, then interrupt-based map drawing and LCD updating is disabled (but these can still be used manually).
There are three graphics buffers, one of which is quite non-standard. They are all vertically aligned instead of horizontally aligned (the OS' format is horizontal). That means the second byte corresponds to the 8-pixel chunk below the first byte, as opposed to adjacent to it.
gbuf : This is a 1120-byte buffer! It is 80 pixels tall and 112 pixels wide. Only the center part is shown. This is where the tiemap is drawn.
maskbuf : A 768 byte buffer. During LCD updates, the data here is ANDed on top of the gbuf data.
topbuf : A 768 byte buffer. During LCD updates, the data here is ORed on top of the previous data.
The latter two buffers are used for overworld graphics, like the player sprite, or in the future, dialog, menus, and NPCs, etc.