ZX Spectrum Next - Using Tiled
Welcome back for another blog edition!
This time I look at using the popular map editing program; Tiled, to create level maps for the ZX Spectrum Next.
In the last post, we already had a glimpse into this with the script that allowed exporting to a file directly from within Tiled, in a format that can be read by the Speccy Next with something like Nextbuild. This allowed creating maps with up to 256 tiles, so you could create a map from a selection of 64, 16x16 pixel tiles or, 256 8x8 pixel tiles for example.
I have now expanded on this and created 3 further scripts, so now have 4 in total. This allows exporting maps in various formats to cover all other areas of tiling....... well apart from the bathroom. Can't help you there!
The additional scripts are firstly, up to 65536 tiles, using 2 bytes to store the ID's instead of 1 like in the 256 mode . Second up to 256 tiles with the use of the attribute byte for palette offset, mirroring on the X/Y, rotating and with the facility to have the tile on top or below the ULA display and finally, 512 tiles using the attribute byte without ULA placing, but including the palette offset, mirroring and rotation..... Hopefully this covers most cases and would be of use to everyone!
Before we go further, you will need the Tiled software to create your level maps and an image for your tiles to build the levels.
I'll show all the scripts on this page further down so you can view them first; however, there is no need to create the script files yourself now, as in the other posts. I thought it about time I created a GitHub repository where you can just download them, and place them in the correct folder..... It is 2025 I guess and I was a bit behind! 😜
Here is my blog repository, where you will find things like this from now on, for all my future blog posts. Just match up the folder to the blog title.
Setting up the scripts in Tiled
Install and open Tiled, select preferences and plugins, then open the folder location below. (This will be where you save the scripts.)
Download my files, "NextMap256.js", "NextMap65536.js", "NextMap256AttributeULA.js" & "NextMap512Attribute.js" from the repository, and place them in this folder above. You can see the code for these plugins below.
You will also need the PNG images I created for numbers 0-15, to use as reference tiles.
NextMap256.js
/* NextMap256.js** A Tiled plugin to export a tile map as a binary file with hex values.* Blank tiles with a value of -1 are replaced with 00.* Supports tile ID's up to 256 (Examples 64 tiles for 16x16 pixels and 256 for 8x8 pixels)** By Paul Spectre Harthen**/var customZXNextBinaryExport256 = {name: "ZXNext Map 256 tile mode",extension: "map",write: function(p_map, p_fileName) {var outputFile = new BinaryFile(p_fileName, BinaryFile.WriteOnly);var bytes = [];for (let i = 0; i < p_map.layerCount; ++i) {let currentLayer = p_map.layerAt(i);if (currentLayer.isTileLayer) {for (let y = 0; y < currentLayer.height; ++y) {for (let x = 0; x < currentLayer.width; ++x) {let currentTile = currentLayer.cellAt(x, y);let currentTileID = currentTile.tileId;let byteValue = currentTileID === -1 ? 0x00 : currentTileID & 0xFF;bytes.push(byteValue);}}}}let byteArray = new Uint8Array(bytes);outputFile.write(byteArray.buffer);outputFile.commit();}};tiled.registerMapFormat("zxnextBinaryExport256", customZXNextBinaryExport256);
NextMap65536.js
/* NextMap65536.js** A Tiled plugin to export a tile map as a binary file with hex values.* Blank tiles with a value of -1 are replaced with 00 00.* Supports tile ID's up to 65536 (For users thinking outside the box.)** By Paul Spectre Harthen**/var customZXNextBinaryExport65536 = {name: "ZXNext Map 65536 tile mode",extension: "map",write: function(p_map, p_fileName) {var outputFile = new BinaryFile(p_fileName, BinaryFile.WriteOnly);var bytes = [];for (let i = 0; i < p_map.layerCount; ++i) {let currentLayer = p_map.layerAt(i);if (currentLayer.isTileLayer) {for (let y = 0; y < currentLayer.height; ++y) {for (let x = 0; x < currentLayer.width; ++x) {let currentTile = currentLayer.cellAt(x, y);let currentTileID = currentTile.tileId;if (currentTileID === -1) {bytes.push(0x00, 0x00);} else {let highByte = (currentTileID >> 8) & 0xFF;let lowByte = currentTileID & 0xFF;bytes.push(lowByte, highByte);}}}}}let byteArray = new Uint8Array(bytes);outputFile.write(byteArray.buffer);outputFile.commit();}};tiled.registerMapFormat("zxnextBinaryExport65536", customZXNextBinaryExport65536);
NextMap256AttributeULA.js
/* NextMap256AttributeULA.js** A Tiled plugin to export a tile map as a binary file with hex values.* Blank tiles with a value of -1 are replaced with 00.* Supports tile ID's up to 256 and using the attribute byte.* Includes Rotate, Flip both ways and Palette Offset using the same tile.* Also includes support of bit 8 of the high byte for tile on top or below the ULA.** By Paul Spectre Harthen**/var customZXNextBinaryExport256AttributeULA = {name: "ZXNext Map 256 tile mode with Attribute ULA",extension: "map",write: function(p_map, p_fileName) {// Process map tilesvar bytes = [];mapLayer = p_map.layerAt(0);offsetLayer = p_map.layerAt(1);if (mapLayer.isTileLayer && offsetLayer.isTileLayer) {for (let y = 0; y < mapLayer.height; ++y) {for (let x = 0; x < mapLayer.width; ++x) {let mapTile = mapLayer.cellAt(x, y);let mapTileID = mapTile.tileId;let D = mapTile.flippedAntiDiagonally ? 1 : 0;let V = mapTile.flippedVertically ? 1 : 0;let H = mapTile.flippedHorizontally ? 1 : 0;if (D == 1 && V == 0 && H == 1) {xMirrorBit = 0;yMirrorBit = 0;rotateBit = 1;} else if (D == 1 && V == 1 && H == 0) {xMirrorBit = 1;yMirrorBit = 1;rotateBit = 1;} else if (D == 0 && V == 1 && H == 1) {xMirrorBit = 1;yMirrorBit = 1;rotateBit = 0;} else{xMirrorBit = H;yMirrorBit = V;rotateBit = D;}let lowByte = mapTileID === -1 ? 0x00 : mapTileID & 0xFF;// Lookup PaletteOffsetlet offsetTile = offsetLayer.cellAt(x, y);let offsetTileID = (offsetTile.tileId === -1) ? 0 : offsetTile.tileId;let ULABit = (offsetTileID > 15) ? 1 : 0;//let paletteBits = offsetTileID & 0x0F;let paletteBits = (offsetTileID > 15) ? (offsetTileID-16) & 0x0F : offsetTileID & 0x0F;let highByte = (paletteBits << 4)| (xMirrorBit << 3)| (yMirrorBit << 2)| (rotateBit << 1)| (ULABit << 0);bytes.push(lowByte);bytes.push(highByte);}}}// Write to output filelet byteArray = new Uint8Array(bytes);let outputFile = new BinaryFile(p_fileName, BinaryFile.WriteOnly);outputFile.write(byteArray.buffer);outputFile.commit();}};tiled.registerMapFormat("zxnextBinaryExport256AttributeULA", customZXNextBinaryExport256AttributeULA);
NextMap512Attribute.js
/* NextMap512Attribute.js** A Tiled plugin to export a tile map as a binary file with hex values.* Blank tiles with a value of -1 are replaced with 00.* Supports tile ID's up to 512 and using the attribute byte.* Includes Rotate, Flip both ways and Palette Offset using the same tile.** By Paul Spectre Harthen**/var customZXNextBinaryExport512Attribute = {name: "ZXNext Map 512 tile mode with Attribute",extension: "map",write: function(p_map, p_fileName) {// Process map tilesvar bytes = [];mapLayer = p_map.layerAt(0);offsetLayer = p_map.layerAt(1);if (mapLayer.isTileLayer && offsetLayer.isTileLayer) {for (let y = 0; y < mapLayer.height; ++y) {for (let x = 0; x < mapLayer.width; ++x) {let mapTile = mapLayer.cellAt(x, y);let mapTileID = mapTile.tileId;if (mapTileID === -1) {bytes.push(0x00); // Represent blank tile as 0x00bytes.push(0x00); // Add a second 0x00 for the high bytecontinue;}let D = mapTile.flippedAntiDiagonally ? 1 : 0;let V = mapTile.flippedVertically ? 1 : 0;let H = mapTile.flippedHorizontally ? 1 : 0;if (D == 1 && V == 0 && H == 1) {xMirrorBit = 0;yMirrorBit = 0;rotateBit = 1;} else if (D == 1 && V == 1 && H == 0) {xMirrorBit = 1;yMirrorBit = 1;rotateBit = 1;} else if (D == 0 && V == 1 && H == 1) {xMirrorBit = 1;yMirrorBit = 1;rotateBit = 0;} else{xMirrorBit = H;yMirrorBit = V;rotateBit = D;}let baseTileID = mapTileID & 0x1FF;let lowByte = baseTileID & 0xFF;let tileRangeBit = (baseTileID > 255) ? 1 : 0;// Lookup PaletteOffsetlet offsetTile = offsetLayer.cellAt(x, y);let offsetTileID = (offsetTile.tileId === -1) ? 0 : offsetTile.tileId;let paletteBits = offsetTileID & 0x0F;let highByte = (paletteBits << 4)| (xMirrorBit << 3)| (yMirrorBit << 2)| (rotateBit << 1)| (tileRangeBit << 0);bytes.push(lowByte);bytes.push(highByte);}}}// Write to output filelet byteArray = new Uint8Array(bytes);let outputFile = new BinaryFile(p_fileName, BinaryFile.WriteOnly);outputFile.write(byteArray.buffer);outputFile.commit();}};tiled.registerMapFormat("zxnextBinaryExport512Attribute", customZXNextBinaryExport512Attribute);
When the files are saved in the plugins folder, you'll know if it's worked, as you should just be able to select Export from the file option, (You may have to restart Tiled, although I found I didn't need to.) and from the file type dropdown, you will see 4 export options for ".map"
Creating the maps
Creating a map using the 256 & 65536 exports, is quite straight forward. These are for the Layer 2 mode on the Speccy Next.
Here create your map size with either 8x8 or 16x16 pixel tiles and load your tileset image in to be chopped up by Tiled, then export with either option, creating a ".map" file with all your level data. For 256, this will be saved as 2 digit Hex values, in a binary file format for each tile ID and 65536, using more tile ID's, saved as 2 bytes of 2 digit hex codes.
These save just the tile ID references of each tile used on your map in its location, from top left to bottom right.
From here you can load a set of ".spr" tiles into something like Nextbuild, load the ".map" file and draw out your level, in code. (If you want a future post showing Nextbuild code to actually get the level on screen, then let me know and I will look at creating one soon.)
256 & 512 with attribute export options require a further step to use, but this allows all the extras which I will explain here. These are for hardware layer 3 mode or Tile Layer on the Speccy Next.
256 & 512 tile mode with attribute byte
The way these scripts create the output files, allows you to draw your level in a normal fashion with 8x8 pixel tiles, using the standard Tiled features like horizontal & vertical mirror and also rotate. (This is created on Tile Layer 1 in Tiled.)
You then use Tile Layer 2 in Tiled to create your palette offset, stamping a set of tiles with numbers from 0 to 15 over the level tiles. This denotes what palette offset bank should be used for that tile underneath it on Tile Layer 1.
In my repository I have created the number sets to be used as the reference tiles. These are nothing special, just PNG files that can be loaded into Tiled, and cut up as separate 8x8 tiles, with the numbers being used as a reference, so you know which palette offset you are using for each tile you have just stamped it on. (Of note, these tiles are loaded in as a separate set to your map tiles, as the ID's for these need to match the numbers 0-15.)
This is the set for 512 export version, the 256 version has 2 sets of 0-15 numbers which would use tile ID's 0-31 and are Red & Green. I will explain these later.
As an example of how the 512 export works for using different colour banks for the same tile, you may have a row all containing 16 of the same tile, lets say tile 321 for arguments sake (Anyone remember Dusty Bin?!?). Each of these tiles could then have a different palette offset number tile stamped on it from 1 to 15. In this mode you don't actually have to stamp the zero tiles down as I've made it so it defaults to zero anyway.
Then when you reference your tiles in code from the output file, each instance of tile 321 in that position on the map, would use the palette offset from 0 to 15 to display the same tile in a different colour offset.
Here you can see palette offset's set for the tiles as 9, 5, 5, 7, 5....etc. Again of note, you wouldn't need to put the 2 zero tiles down as they would be set to zero if left blank anyway, I am showing these as reference.
To make things easier for you when stamping the number tiles over the map on Tile Layer 2, you can reduce the opacity of Tile Layer 1.
To give an idea of how the file saves the map references with the attribute byte for 512 export, I need to explain how the information is split across the high and low bytes.
Here is an example of the hex values within the exported map file.
High byte:
Bits 15 - 12 contain the palette offset reference value from 0-15
Bit 11 a value of 0 or 1 for the X mirror value
Bit 10 a value of 0 or 1 for the Y mirror value
Bit 9 for 90 degree rotate with value 0 or 1
Bit 8 in this 512 tile mode, is the index bit, being 0 for tiles 0-255 and 1 for tiles 256-511
Low byte:
Bits 7 - 0 are all used to store a value from 0-255 for the tile ID
So if you are following along with this, in the example above where 4c90 has been saved for the first tile reference on the map, 4c is the low byte returning the tile ID reference from Tiled and 90 is the high byte with all bits 15 - 8 combined into one 2 digit hex code.
In order to sort this high byte, I've written the script to check each tile on the map and see if it is flipped or rotated and also if it has a palette offset reference tile stamped on top of it in layer 2 of Tiled. Once all checked, I have assigned all the values and created the hex codes for the output file.
Now I know this post has been going on for a while, but we are getting near the end. Maybe grab a coffee and just digest what has been said for a minute............... All refreshed?
Right the 256 attribute export is very similar to this 512 one above so won't need much more explaining, however there is a slight difference. Because we are now counting half the tiles, bit 8 is not required as the tile ID index, and is now used for the tile placement, above or below the ULA display.
I have set this up so it still uses Tile Layer 1 & 2 in Tiled as before, but this time the set of reference tiles with 2 sets of 0-15 numbers are used instead. These are the ones with red and green numbers and are loaded in as separate 8x8 tiles, using tile ID's 0-31 (Red 0-15 using ID's 0-15 and Green 0-15 using ID's 16-31.)
Everything is the same as before for mirror, rotate and palette offset, but this time having in mind 0-15 in either red or green represent a palette offset of 0-15 for the tile under it.
The difference now is that if you use a red number, the tile under it will be below the ULA display and if a green number the tile will be on top of the ULA display.
The default here is again a red 0, so if you place no numbered reference tiles at all, then all tiles on Tile Layer 1 of Tiled would be set as 0, meaning a palette offset of 0, and below the ULA display. However, if you wanted to use a palette offset of 0 and have the tile on top of the ULA display, then you WOULD have to put a green 0 tile down. Hope this all makes sense.
This shows the palette offset tile for 256 export, notice tile ID 19 is showing the green number 3.
And how you would use the tiles.
As before the file is exported in a similar way with only bit 8 being different.
High byte:
Bits 15 - 12 contain the palette offset reference value from 0-15
Bit 11 a value of 0 or 1 for the X mirror value
Bit 10 a value of 0 or 1 for the Y mirror value
Bit 9 for 90 degree rotate with value 0 or 1
Bit 8 in this 256 tile mode, is the ULA reference of 0 for below the ULA display and 1 for on top.
Low byte:
Bits 7 - 0 are all used to store a value from 0-255 for the tile ID
For more information regarding tile modes and how the bits are stored, there are plenty of sites out there worth a visit, with detailed reference material for this, however this one contains a great book which is free to download in PDF, or you can buy a printed copy like myself.
This will definitely help you to understand this subject further.
So now, hopefully you have found this interesting and of use, but more importantly, you should be all ready to go and get some levels designed for your games!
Still here?................. Well I know I've pretty much explained everything, but I have one last bonus bit which might be of use still, although it's not included in my repository.
So when testing the 512 attribute export and before I came up with the idea of using numbered tile references, I played around with detecting the palette offset, by setting a custom property on any of the main tile ID's I wanted. This only used one Tile Layer in Tiled when drawing the maps, however, the downside was that you couldn't have multiple palette offsets on a specific tile ID, any particular tile could only have 1 palette offset assigned to it.
I am including it here though in the off chance that you might want to see how to detect the custom property of a tile in Tiled when exporting, as it is quite tricky, and may save you some time.
With this, you might come up with a great idea for some other feature on a Tiled export, for your game which uses it. If you do, then please let the Speccy Next community know.
The Speccy Next has a great user base, and the more we share these things, our coding workflow and knowledge can only get better....and as always............. Have fun 👾
Custom property is set on the tile set and not the map and is case sensitive for the property name.
The code.
/* NextMap512CustomProperty.js** A Tiled plugin to export a tile map as a binary file with hex values.* Blank tiles with a value of -1 are replaced with 00.* Supports tile ID up to 512 for layer 3 (8x8 pixel 16 colour tiles)* Includes Rotate, Flip both ways and Palette Offset with custom tile property** Custom tile property name - PaletteOffset** By Paul Spectre Harthen**/var customZXNextBinaryExport512CP = {name: "ZXNext Map 512 Custom Property",extension: "map",write: function(p_map, p_fileName) {// Create PaletteOffset lookup arraylet tilesets = p_map.tilesets;let paletteOffsetLookup = new Array(512).fill(0); // Initialize with 0for (let t = 0; t < tilesets.length; ++t) {let tileset = tilesets[t];for (let tileId = 0; tileId < tileset.tileCount; ++tileId) {let tilesetTile = tileset.tile(tileId);if (tilesetTile) {let paletteOffset = tilesetTile.property("PaletteOffset");let localId = tileId; // LocalID is simply the tileId in this loopif (paletteOffset !== undefined && paletteOffset !== "No PaletteOffset") {let offsetValue = parseInt(paletteOffset, 10);if (!isNaN(offsetValue) && offsetValue >= 0 && offsetValue <= 15) {paletteOffsetLookup[localId] = offsetValue;}}}}}// Process map tilesvar bytes = [];for (let i = 0; i < p_map.layerCount; ++i) {let currentLayer = p_map.layerAt(i);if (currentLayer.isTileLayer) {for (let y = 0; y < currentLayer.height; ++y) {for (let x = 0; x < currentLayer.width; ++x) {let currentTile = currentLayer.cellAt(x, y);let currentTileID = currentTile.tileId;if (currentTileID === -1) {bytes.push(0x00); // Represent blank tile as 0x00bytes.push(0x00); // Add a second 0x00 for the high bytecontinue;}let rotateBit = currentTile.flippedAntiDiagonally ? 1 : 0;let yMirrorBit = currentTile.flippedVertically ? 1 : 0;let xMirrorBit = currentTile.flippedHorizontally ? 1 : 0;let baseTileID = currentTileID & 0x1FF;let lowByte = baseTileID & 0xFF;let tileRangeBit = (baseTileID > 255) ? 1 : 0;// Lookup PaletteOffset from arraylet paletteOffset = paletteOffsetLookup[currentTileID];let paletteBits = paletteOffset & 0x0F;let highByte = (paletteBits << 4)| (xMirrorBit << 3)| (yMirrorBit << 2)| (rotateBit << 1)| (tileRangeBit << 0);bytes.push(lowByte);bytes.push(highByte);}}}}// Write to output filelet byteArray = new Uint8Array(bytes);let outputFile = new BinaryFile(p_fileName, BinaryFile.WriteOnly);outputFile.write(byteArray.buffer);outputFile.commit();}};tiled.registerMapFormat("zxnextBinaryExport512CP", customZXNextBinaryExport512CP);
Comments
Post a Comment