2024-08-12 22:28:34 -03:00
|
|
|
* Startup
|
|
|
|
|
|
|
|
Everything starts with generating 16x16 tiles. You are welcome to start with
|
|
|
|
128x128 tiles if you find it easier, but you'll have to control well your tiles
|
|
|
|
so they don't add up much. Remember that VRAM is limited on the PlayStation, so
|
|
|
|
your art is going to have to be cut into 8x8 pieces, and these pieces should
|
|
|
|
fill a 256x256 texture at max, at the end of these steps, and shouldn't have too
|
|
|
|
many colors, so let's say you're constrained to 1023 tiles of 8x8 pixels (tile 0
|
|
|
|
is always a blank tile).
|
|
|
|
|
|
|
|
Your first step is creating a 16x16.png file with your 16x16 tiles. If you
|
|
|
|
started working with 128x128 you could probably cut them up into a single sprite
|
|
|
|
sheet with the aid of Aseprite.
|
|
|
|
|
|
|
|
|
|
|
|
** Generating Python's virtualenv
|
|
|
|
|
|
|
|
This is how you can create a virtualenv with all Python pendencies to run the
|
|
|
|
tools, though you won't really need it in most cases, since there are no "weird"
|
|
|
|
packages being used:
|
|
|
|
|
|
|
|
#+begin_src bash
|
|
|
|
cd tools/
|
|
|
|
python -m venv ./venv
|
|
|
|
./venv/bin/pip install -r requirements.txt
|
|
|
|
#+end_src
|
|
|
|
|
|
|
|
To run any scripts with this venv, run ~./tools/venv/bin/python
|
|
|
|
./tools/script.py~.
|
|
|
|
|
2024-11-03 01:46:09 -03:00
|
|
|
* Generating a character sprite 8x8 tileset and mappings
|
|
|
|
|
2025-01-11 00:20:05 -03:00
|
|
|
** Exporting tilemap and tileset
|
|
|
|
|
2024-11-03 01:46:09 -03:00
|
|
|
For characters, we need to use the Aseprite tool for building sprites.
|
|
|
|
|
|
|
|
Import the character's sprites as a sprite sheet and into an .aseprite file (see
|
|
|
|
'assets/sprites/CHARA/SONIC.aseprite').
|
|
|
|
|
|
|
|
The character's sprites must be manipulated into individual frames on a tileset
|
|
|
|
layer, with 8x8 tiles. Plus, you'll have to organize the frames in such a way
|
|
|
|
that frames of the same animation are coupled together.
|
|
|
|
|
|
|
|
Finally, group frames into animations by creating tags with animation names
|
2025-01-11 00:20:05 -03:00
|
|
|
(again, see 'SONIC.ase' for valid animation names).
|
|
|
|
|
|
|
|
Finally, export the animation names and mappings using 'export_tilemap_psx.lua'.
|
|
|
|
|
|
|
|
Now to export the tileset itself, you'll need to File > Export > Export
|
|
|
|
Tileset. On this window, choose the following options:
|
2024-11-03 01:46:09 -03:00
|
|
|
|
2025-01-11 00:20:05 -03:00
|
|
|
- Sheet Type: By Rows;
|
|
|
|
- Constraints: Fixed Width;
|
|
|
|
- Merge Duplicates: Yes;
|
|
|
|
- Borders
|
|
|
|
- Spacing: 1;
|
|
|
|
- Output
|
|
|
|
- Output File: SONIC.png.
|
|
|
|
|
|
|
|
[[file:sprite_export_settings.png]]
|
2024-11-03 01:46:09 -03:00
|
|
|
|
|
|
|
This will create a 'SONIC.png' file and a 'SONIC.json' file.
|
|
|
|
|
2025-01-11 00:20:05 -03:00
|
|
|
** Tweaking the texture
|
|
|
|
|
|
|
|
This is not over yet -- You'll need to open up SONIC.png on a tool such as GIMP
|
|
|
|
and guarantee that no individual tile is being divided by the 256px line at the
|
|
|
|
bottom of the image.
|
|
|
|
|
|
|
|
If so, you'll need to take the row of tiles that is being cut in half and move
|
|
|
|
it downwards so the first pixel in Y coordinate aligns with position 256.
|
|
|
|
|
|
|
|
This is necessary since these tiles need to be within a 256x256 texture, and
|
|
|
|
when they can't, we simply snap them to the next texture page (in this case, at
|
|
|
|
the bottom). So the sprites use four texture pages (two horizontally since it
|
|
|
|
uses 8bpp color, and other two on the bottom of VRAM since they don't fit in a
|
|
|
|
single row), but the same CLUT.
|
|
|
|
|
|
|
|
** Produce a TIM texture and a CHARA file
|
|
|
|
|
2024-11-03 01:46:09 -03:00
|
|
|
Now, just pack the frame and tile data into a CHARA file, and generate a TIM
|
|
|
|
image with correct CLUT and TPAGE info:
|
|
|
|
|
|
|
|
#+begin_src bash :eval never
|
|
|
|
framepacker.py SONIC.json SONIC.CHARA
|
|
|
|
img2tim -usealpha -org 320 0 -plt 0 480 -bpp 8 -o SONIC.TIM SONIC.png
|
|
|
|
#+end_src
|
|
|
|
|
|
|
|
Notice that, different than other image tile data, character sprites rely on a
|
|
|
|
PNG's alpha channel to generate transparency bits, instead of using the full
|
|
|
|
black color as mask.
|
|
|
|
|
2025-01-11 00:20:05 -03:00
|
|
|
** Animation names and generating their Adler32 hashes
|
|
|
|
|
|
|
|
If you need to refer to an animation directly by name, the animations are
|
2024-11-03 01:46:09 -03:00
|
|
|
referred to by the engine by their Adler32 hash, so you might want to add a new
|
|
|
|
definition for that on ~player.c~.
|
|
|
|
|
|
|
|
The names used on tags are always converted to uppercase, with no spaces.
|
|
|
|
|
|
|
|
To calculate the Adler32 hash for a string, one may use Zlib through Python.
|
|
|
|
|
|
|
|
Here's a tool to generate hash definitions for some animations.
|
|
|
|
|
|
|
|
#+begin_src python :results output
|
|
|
|
import zlib
|
|
|
|
|
|
|
|
names = [
|
|
|
|
"STOPPED",
|
|
|
|
"IDLE",
|
|
|
|
"WALKING",
|
|
|
|
"RUNNING",
|
|
|
|
"ROLLING",
|
|
|
|
"SKIDDING",
|
|
|
|
"PEELOUT",
|
|
|
|
"PUSHING",
|
|
|
|
"CROUCHDOWN",
|
|
|
|
"LOOKUP",
|
|
|
|
"SPRING",
|
|
|
|
"HURT",
|
|
|
|
"DEATH",
|
2025-01-07 02:08:47 -03:00
|
|
|
"DROWN",
|
|
|
|
"GASP",
|
2025-01-07 10:48:10 -03:00
|
|
|
"WATERWALK",
|
|
|
|
"DROP",
|
|
|
|
"BALANCELIGHT",
|
|
|
|
"BALANCEHEAVY",
|
2024-11-03 01:46:09 -03:00
|
|
|
]
|
|
|
|
|
|
|
|
def get_hash(name):
|
|
|
|
hash = zlib.adler32(str.encode(name))
|
|
|
|
return f"0x{hash:08x}"
|
|
|
|
|
|
|
|
def print_hashes(names):
|
|
|
|
for name in names:
|
|
|
|
hash = get_hash(name)
|
|
|
|
print(f"#define ANIM_{name:16} {hash}")
|
|
|
|
|
|
|
|
print_hashes(names)
|
|
|
|
#+end_src
|
|
|
|
|
|
|
|
#+RESULTS:
|
|
|
|
#+begin_example
|
|
|
|
#define ANIM_STOPPED 0x08cd0220
|
|
|
|
#define ANIM_IDLE 0x02d1011f
|
|
|
|
#define ANIM_WALKING 0x0854020e
|
|
|
|
#define ANIM_RUNNING 0x08bf0222
|
|
|
|
#define ANIM_ROLLING 0x08890218
|
|
|
|
#define ANIM_SKIDDING 0x0a85024e
|
|
|
|
#define ANIM_PEELOUT 0x0849021f
|
|
|
|
#define ANIM_PUSHING 0x08b2021f
|
|
|
|
#define ANIM_CROUCHDOWN 0x104802fd
|
|
|
|
#define ANIM_LOOKUP 0x067001db
|
|
|
|
#define ANIM_SPRING 0x068e01d4
|
|
|
|
#define ANIM_HURT 0x031b0144
|
|
|
|
#define ANIM_DEATH 0x04200167
|
2025-01-07 02:08:47 -03:00
|
|
|
#define ANIM_DROWN 0x048a018b
|
|
|
|
#define ANIM_GASP 0x02d9012c
|
2025-01-07 10:48:10 -03:00
|
|
|
#define ANIM_WATERWALK 0x0da602b3
|
|
|
|
#define ANIM_DROP 0x02f80136
|
|
|
|
#define ANIM_BALANCELIGHT 0x156c035f
|
|
|
|
#define ANIM_BALANCEHEAVY 0x15570364
|
2024-11-03 01:46:09 -03:00
|
|
|
#+end_example
|
|
|
|
|
2024-08-12 22:28:34 -03:00
|
|
|
* Generating 8x8 tiles and their 16x16 mappings
|
|
|
|
|
|
|
|
The following steps will allow you to create intermediate files 'tiles.png',
|
|
|
|
'map16.json' and 'collision16.json'.
|
|
|
|
|
|
|
|
You will also be able to cook these files into PlayStation-only engine files
|
|
|
|
'TILES.TIM', 'MAP16.MAP' and 'MAP16.COL'. These are binary equivalents to the
|
|
|
|
files above, with only relevant information.
|
|
|
|
|
|
|
|
Extra files such as 'tiles16.tsx' will also be generated.
|
|
|
|
|
|
|
|
1. Create '16x16.png' tiles.
|
|
|
|
2. Import '16x16.png' tiles into a 'tiles16.tsx'.
|
|
|
|
3. Export 'tiles16.tsx' from Tiled as 'collision16.json'.
|
|
|
|
4. Copy '16x16.png' to '8x8.png'.
|
|
|
|
5. Open '8x8.png' (still 16x16 tiles) on Aseprite.
|
|
|
|
6. File > Import > Import Sprite Sheet. The single image will be used as
|
|
|
|
one. Make it a 16x16 grid.
|
|
|
|
7. Right click layer > Convert to > tilemap. Make it a 8x8 grid.
|
|
|
|
8. File > Scripts > export_tilemap_psx. This will create a '8x8.json'
|
|
|
|
file. Rename it to 'map16.json'.
|
|
|
|
9. File > Scripts > export_tileset_psx. Use a 8x8 grid. This will overwrite
|
|
|
|
'8x8.png'. Rename it to 'tiles.png'.
|
|
|
|
10. Open 'tiles.png' with your favorite editor and make sure that all
|
|
|
|
transparent pixels are set to color `#000000` (black).
|
|
|
|
11. Use TIMTOOL.EXE (preferably) from Psy-Q library to generate a .TIM for your
|
|
|
|
tiles. This will generate a 'TILES.TIM' file on the same directory of the
|
|
|
|
texture.
|
|
|
|
- Make sure you un-mark the "Set for Black" option in Semi Transparent
|
|
|
|
Information.
|
|
|
|
- Make sure your tileset is at 448x0 and that the CLUT information is 4-bit
|
2024-10-29 03:32:25 -03:00
|
|
|
depth and at 0x482. Notice that texture pages 8 and 24 are for level
|
2024-08-12 22:28:34 -03:00
|
|
|
tiles and CLUT information, respectively.
|
|
|
|
- *NOTE:* If you use another tool such as TIMEDIT, just make sure the black
|
|
|
|
color is accurately picked as transparent color, and that no
|
|
|
|
semi-transparency is enabled. Also ensure the positions for the texture
|
|
|
|
and the CLUT on proper texture pages.
|
|
|
|
12. Use the tool 'framepacker.py' to turn 'map16.json' into a 'MAP16.MAP' file:\
|
|
|
|
~framepacker.py --tilemap map16.json MAP16.MAP~
|
|
|
|
13. Use the tool 'cookcollision.py' to turn 'collision16.json' into a
|
|
|
|
'MAP16.COL' file:\
|
|
|
|
~cookcollision.py collision16.json MAP16.COL~
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* Generating 128x128 tiles and mappings
|
|
|
|
|
|
|
|
The following steps will allow you to generate a 'MAP128.MAP' file from a
|
2024-08-18 03:01:52 -03:00
|
|
|
'tilemap128.tmx'.
|
|
|
|
This 'tilemap128.tmx' tile is supposed to be a map comprised of 16x16 tiles,
|
|
|
|
created from the same '16x16.png' file we addressed earlier.
|
2024-08-12 22:28:34 -03:00
|
|
|
Each 128x128 tile is supposed to be equivalent to every eight rows and columns
|
|
|
|
on the .tmx map.
|
|
|
|
|
|
|
|
Please make sure that the first tile is COMPLETELY BLANK and mind the tile
|
|
|
|
sequence (tiles are counted first from left to right, then up to down).
|
|
|
|
|
2024-09-13 01:40:52 -03:00
|
|
|
1. Create a 'tiles16.tsx' map from '16x16.png', if you haven't already.
|
2024-10-29 03:32:25 -03:00
|
|
|
2. Create a 'tilemap128.tmx' map and use 'tiles16.tsx' as tileset. I recommend
|
|
|
|
this map to start with 32x112 dimensions, and 16x16 tiles, of course.
|
|
|
|
- Create layers called "none", "oneway" and "solid" (top to bottom), with
|
|
|
|
those specific names.
|
2024-08-12 22:28:34 -03:00
|
|
|
3. Create your tiles from left to right, and if you must, up to down. Be mindful
|
|
|
|
of tile order, and make sure that the first tile (first eight rows and
|
|
|
|
columns) are completely blank.
|
|
|
|
4. Once you're done with your map (you may save your project for later
|
|
|
|
manipulation), export your .tmx to a 'map128.csv'.
|
|
|
|
5. Use the tool 'chunkgen.py' to turn 'map128.csv' into a 'MAP128.MAP' file:\
|
|
|
|
~chunkgen.py map128.csv MAP128.MAP~
|
|
|
|
|
|
|
|
** Preparation for level map creation
|
|
|
|
|
|
|
|
Do this in preparation for creating your actual level map:
|
|
|
|
|
2024-09-13 01:40:52 -03:00
|
|
|
1. Go back to your 'map128.tmx' and export it to an image called '128.png'.
|
2024-08-12 22:28:34 -03:00
|
|
|
- Make sure you didn't mess up the tile mapping, and that the tile is
|
|
|
|
properly aligned with the upper left corner of your frame. You'll see that
|
|
|
|
by looking at the continuous line in your 128x128 infinite map.
|
|
|
|
- Make sure you didn't mess up the map size also. Generally speaking, extra
|
|
|
|
tiles on the right side are just as bad; use Map > Resize Map as needed to
|
|
|
|
ensure that there are no extra tiles to the right.
|
|
|
|
2. Create a '128x128.tsx' tileset and use image '128x128.png' as base.
|
|
|
|
- If you already created this file, once you re-export '128x128.png', it
|
|
|
|
should update with no extra effort needed, and so will your level maps that
|
|
|
|
use this tileset.
|
|
|
|
|
|
|
|
|
|
|
|
* Generating your level
|
|
|
|
|
|
|
|
The following steps will allow you to create level maps such as 'Z1.tmx' and
|
|
|
|
'Z2.tmx', and generate levels such as 'Z1.LVL' and 'Z1.LVL', in PlayStation
|
|
|
|
format.
|
|
|
|
|
2024-08-18 01:30:12 -03:00
|
|
|
This will also create intermediate files such as 'Z1.psxlvl' and
|
|
|
|
'Z2.psxlvl'. This intermediate representation is necessary because Tiled is
|
|
|
|
unable to export levels in binary format in one go, due to scripting
|
|
|
|
limitations.
|
2024-08-12 22:28:34 -03:00
|
|
|
|
|
|
|
You'll need to have Python scripting enabled in Tiled, and you'll also need to
|
|
|
|
have `lvlexporter.py` on your Tiled scripts directory (generally `~/.tiled` on
|
|
|
|
Linux).
|
|
|
|
|
|
|
|
1. Create a 'Z1.tmx' or 'Z2.tmx' file using '128x128.tsx' as tileset. The level
|
|
|
|
must be exacly 255x31 blocks long; block size must be 128x128.
|
|
|
|
2. Create a layer called 'LAYER0' and another one called 'LAYER1'. Make sure
|
|
|
|
that 'LAYER1' is above 'LAYER0'; level layers are exported from bottom to
|
|
|
|
top.
|
|
|
|
3. Draw your tiles preferably on 'LAYER0' (this part is still unfinished, but
|
|
|
|
this is the only layer where collision detection happens). Use 'LAYER1' to
|
|
|
|
draw tiles that should go on front of your character (this part is also a
|
|
|
|
work-in-progress).
|
|
|
|
4. Once you're done with your map, go to File > Export as..., pick the
|
2024-08-18 01:30:12 -03:00
|
|
|
"PlayStation proto map" format, and save it as 'Z1.psxlvl' or 'Z2.psxlvl'.
|
2024-08-12 22:28:34 -03:00
|
|
|
5. Use the tool 'cooklvl.py' to turn 'Z1.json' or 'Z2.json' into 'Z1.LVL' or
|
|
|
|
'Z2.LVL':\
|
2024-08-18 01:30:12 -03:00
|
|
|
~cooklvl.py Z1.psxlvl Z1.LVL~
|
2024-08-12 22:28:34 -03:00
|
|
|
|