engine-psx/tools/guide.org
Lucas S. Vieira b75c58a4d8 Add Knuckles
2025-04-08 01:46:50 -03:00

321 lines
12 KiB
Org Mode

* 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~.
* Generating a character sprite 8x8 tileset and mappings
** Exporting tilemap and tileset
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
(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:
- Sheet Type: By Rows;
- Constraints: Fixed Width;
- Merge Duplicates: Yes;
- Borders
- Spacing: 1;
- Output
- Output File: SONIC.png.
[[file:sprite_export_settings.png]]
This will create a 'SONIC.png' file and a 'SONIC.json' file.
** 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
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.
** Animation names and generating their Adler32 hashes
If you need to refer to an animation directly by name, the animations are
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",
"SPINDASH",
"SKIDDING",
"PEELOUT",
"PUSHING",
"CROUCHDOWN",
"LOOKUP",
"SPRING",
"HURT",
"DEATH",
"DROWN",
"GASP",
"WATERWALK",
"DROP",
"BALANCELIGHT",
"BALANCEHEAVY",
"FLYUP",
"FLYDOWN",
"FLYTIRED",
"SWIMMING",
"SWIMTIRED",
"TAILIDLE",
"TAILMOVE",
"TAILFLY",
"CLIMBSTOP",
"CLIMBUP",
"CLIMBDOWN",
"CLIMBRISE",
"CLIMBEND",
"GLIDE",
"GLIDETURNA",
"GLIDETURNB",
"GLIDECANCEL",
"GLIDELAND",
"GLIDERISE",
]
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_SPINDASH 0x0acd025b
#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
#define ANIM_DROWN 0x048a018b
#define ANIM_GASP 0x02d9012c
#define ANIM_WATERWALK 0x0da602b3
#define ANIM_DROP 0x02f80136
#define ANIM_BALANCELIGHT 0x156c035f
#define ANIM_BALANCEHEAVY 0x15570364
#define ANIM_FLYUP 0x04980191
#define ANIM_FLYDOWN 0x086f0224
#define ANIM_FLYTIRED 0x0aee0264
#define ANIM_SWIMMING 0x0b2a026c
#define ANIM_SWIMTIRED 0x0e0502b9
#define ANIM_TAILIDLE 0x0a6e0249
#define ANIM_TAILMOVE 0x0ab30262
#define ANIM_TAILFLY 0x08390216
#define ANIM_CLIMBSTOP 0x0d1102ae
#define ANIM_CLIMBUP 0x0805020d
#define ANIM_CLIMBDOWN 0x0cd402a0
#define ANIM_CLIMBRISE 0x0ce9029b
#define ANIM_CLIMBEND 0x0a22023f
#define ANIM_GLIDE 0x04400166
#define ANIM_GLIDETURNA 0x100902f0
#define ANIM_GLIDETURNB 0x100a02f1
#define ANIM_GLIDECANCEL 0x1252030c
#define ANIM_GLIDELAND 0x0cab0285
#define ANIM_GLIDERISE 0x0ce60299
#+end_example
* 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
depth and at 0x482. Notice that texture pages 8 and 24 are for level
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
'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.
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).
1. Create a 'tiles16.tsx' map from '16x16.png', if you haven't already.
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.
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:
1. Go back to your 'map128.tmx' and export it to an image called '128.png'.
- 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.
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.
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
"PlayStation proto map" format, and save it as 'Z1.psxlvl' or 'Z2.psxlvl'.
5. Use the tool 'cooklvl.py' to turn 'Z1.json' or 'Z2.json' into 'Z1.LVL' or
'Z2.LVL':\
~cooklvl.py Z1.psxlvl Z1.LVL~