Add terrain angles and consider it on movement

This commit is contained in:
Lucas S. Vieira 2024-08-12 22:28:34 -03:00
parent a6a683ffff
commit 2b3a516e4b
14 changed files with 382 additions and 157 deletions

Binary file not shown.

Binary file not shown.

View file

@ -9,9 +9,13 @@
#define LEVEL_ARENA_SIZE 65536
typedef struct {
int32_t floor_angle;
uint8_t floor[8];
int32_t rwall_angle;
uint8_t rwall[8];
int32_t ceiling_angle;
uint8_t ceiling[8];
int32_t lwall_angle;
uint8_t lwall[8];
} Collision;
@ -47,6 +51,7 @@ typedef struct {
uint8_t collided;
uint8_t direction; // horizontal/vertical
int16_t pushback;
int32_t angle;
} CollisionEvent;

View file

@ -10,6 +10,8 @@ int RotTransPers(SVECTOR *v, uint32_t *xy0);
int RotAverageNclip4(SVECTOR *a, SVECTOR *b, SVECTOR *c, SVECTOR *d,
uint32_t *xy0, uint32_t *xy1, uint32_t *xy2, uint32_t *xy3,
int *otz);
void CrossProduct0(VECTOR *v0, VECTOR *v1, VECTOR *out);
void CrossProduct12(VECTOR *v0, VECTOR *v1, VECTOR *out);
uint8_t *file_read(const char *filename, uint32_t *length);
void load_texture(uint8_t *data, TIM_IMAGE *tim);
@ -17,6 +19,7 @@ void load_texture(uint8_t *data, TIM_IMAGE *tim);
uint8_t get_byte(uint8_t *bytes, uint32_t *b);
uint16_t get_short_be(uint8_t *bytes, uint32_t *b);
uint16_t get_short_le(uint8_t *bytes, uint32_t *b);
uint32_t get_long_be(uint8_t *bytes, uint32_t *b);
uint32_t adler32(const char *s);

View file

@ -62,12 +62,19 @@ _load_collision(TileMap16 *mapping, const char *filename)
mapping->collision[tile_id] = alloc_arena_malloc(&_level_arena, sizeof(Collision));
Collision *collision = mapping->collision[tile_id];
collision->floor_angle = (int32_t)get_long_be(bytes, &b);
for(int j = 0; j < 8; j++)
collision->floor[j] = get_byte(bytes, &b);
collision->rwall_angle = (int32_t)get_long_be(bytes, &b);
for(int j = 0; j < 8; j++)
collision->rwall[j] = get_byte(bytes, &b);
collision->ceiling_angle = (int32_t)get_long_be(bytes, &b);
for(int j = 0; j < 8; j++)
collision->ceiling[j] = get_byte(bytes, &b);
collision->lwall_angle = (int32_t)get_long_be(bytes, &b);
for(int j = 0; j < 8; j++)
collision->lwall[j] = get_byte(bytes, &b);
}
@ -478,16 +485,21 @@ linecast(LevelData *lvl, TileMap128 *map128, TileMap16 *map16,
// to determine which height mask we are supposed to use.
uint8_t *mask0 = NULL, *mask1 = NULL;
uint8_t index = 0;
int32_t angle0, angle1;
if(direction == 0) { // horizontal
// index is related to y
if(magnitude < 0) {
mask0 = piece0 ? piece0->lwall : NULL;
angle0 = piece0 ? piece0->lwall_angle : 0;
mask1 = piece1 ? piece1->lwall : NULL;
angle1 = piece1 ? piece1->lwall_angle : 0;
// index starts at 0 from uppermost
index = vpiecey[0]; // piece 1 or 0, doesn't matter
} else {
mask0 = piece0 ? piece0->rwall : NULL;
angle0 = piece0 ? piece0->rwall_angle : 0;
mask1 = piece1 ? piece1->rwall : NULL;
angle1 = piece1 ? piece1->rwall_angle : 0;
// index starts at 15 from uppermost
index = 15 - vpiecey[0];
}
@ -495,12 +507,16 @@ linecast(LevelData *lvl, TileMap128 *map128, TileMap16 *map16,
// index is related to x
if(magnitude >= 0) {
mask0 = piece0 ? piece0->floor : NULL;
angle0 = piece0 ? piece0->floor_angle : 0;
mask1 = piece1 ? piece1->floor : NULL;
angle1 = piece1 ? piece1->floor_angle : 0;
// index starts at 0 from leftmost
index = vpiecex[0];
} else {
mask0 = piece0 ? piece0->ceiling : NULL;
angle0 = piece0 ? piece0->ceiling_angle : 0;
mask1 = piece1 ? piece1->ceiling : NULL;
angle1 = piece1 ? piece1->ceiling_angle : 0;
// index starts at 15 from rightmost
index = 15 - vpiecey[0];
}
@ -514,8 +530,10 @@ linecast(LevelData *lvl, TileMap128 *map128, TileMap16 *map16,
// But, if our height on heightmask has a 0 height, then we proceed.
uint8_t mask_byte = 0;
int16_t h;
int32_t angle = 0;
if(mask0) {
mask_byte = mask0[index >> 1];
angle = angle0;
mask_byte = mask_byte >> (((index & 0x1) ^ 0x1) << 2);
mask_byte = mask_byte & 0xf;
@ -529,6 +547,7 @@ linecast(LevelData *lvl, TileMap128 *map128, TileMap16 *map16,
// if there was no collision on that spot.
if((mask_byte == 0) && mask1) {
mask_byte = mask1[index >> 1];
angle = angle1;
mask_byte = mask_byte >> (((index & 0x1) ^ 0x1) << 2);
mask_byte = mask_byte & 0xf;
@ -578,6 +597,7 @@ linecast(LevelData *lvl, TileMap128 *map128, TileMap16 *map16,
if(ev.collided) {
ev.direction = direction;
ev.pushback = (int16_t)mask_byte - h - (magnitude < 0 ? 16 : 0);
ev.angle = angle;
/* if(ev.direction == 0) { // horizontal */
/* // TODO */
/* ev.pushback = (int16_t)mask_byte - h - (magnitude < 0 ? 16 : 0); */

View file

@ -330,21 +330,24 @@ engine_draw()
"VEL %08x %08x\n"
"GSP %08x\n"
"DIR %c\n"
"GRN %c(%2d) // %c(%2d)\n"
"CEI %c(%2d) // %c(%2d)\n"
"LEF %c(%2d)\n"
"RIG %c(%2d)\n",
"GR1 %c(%2d %5d)\n"
"GR2 %c(%2d %5d)\n"
/* "CEI %c(%2d) // %c(%2d)\n" */
/* "LEF %c(%2d)\n" */
/* "RIG %c(%2d)\n" */
,
/* cam_pos.vx, cam_pos.vy, */
player.pos.vx, player.pos.vy,
player.vel.vx, player.vel.vy,
player.vel.vz,
player.anim_dir >= 0 ? 'R' : 'L',
player.ev_grnd1.collided ? 'Y' : 'N', player.ev_grnd1.pushback,
player.ev_grnd2.collided ? 'Y' : 'N', player.ev_grnd2.pushback,
player.ev_ceil1.collided ? 'Y' : 'N', player.ev_ceil1.pushback,
player.ev_ceil2.collided ? 'Y' : 'N', player.ev_ceil2.pushback,
player.ev_left.collided ? 'Y' : 'N', player.ev_left.pushback,
player.ev_right.collided ? 'Y' : 'N', player.ev_right.pushback);
player.ev_grnd2.collided ? 'Y' : 'N', player.ev_grnd2.pushback
/* player.ev_ceil1.collided ? 'Y' : 'N', player.ev_ceil1.pushback, */
/* player.ev_ceil2.collided ? 'Y' : 'N', player.ev_ceil2.pushback, */
/* player.ev_left.collided ? 'Y' : 'N', player.ev_left.pushback, */
/* player.ev_right.collided ? 'Y' : 'N', player.ev_right.pushback */
);
draw_text(8, 12, 0, buffer);
}
}

View file

@ -241,8 +241,21 @@ _player_collision_detection(Player *player)
}
if(!player->grnd) {
player->angle = 0;
if((player->ev_grnd1.collided || player->ev_grnd2.collided) && (player->vel.vy >= 0)) {
player->vel.vy = 0;
// Set angle according to movement
if(player->ev_grnd1.collided && !player->ev_grnd2.collided)
player->angle = player->ev_grnd1.angle;
else if(!player->ev_grnd1.collided && player->ev_grnd2.collided)
player->angle = player->ev_grnd2.angle;
// In case both are available, get the angle on the left.
// This introduces certain collision bugs but let's leave it
// like this for now
else player->angle = player->ev_grnd1.angle;
// TODO: Set ground speed according to X and Y velocity
player->vel.vz = player->vel.vx;
int32_t pushback =
(player->ev_grnd1.pushback > player->ev_grnd2.pushback)
? player->ev_grnd1.pushback
@ -284,27 +297,43 @@ player_update(Player *player)
}
// X movement
/* Ground movement */
if(player->grnd) {
if(pad_pressing(PAD_RIGHT)) {
if(player->grnd && (player->vel.vx < 0)) {
player->vel.vx += X_DECEL;
if(player->vel.vz < 0) {
player->vel.vz += X_DECEL;
} else {
player->vel.vx += X_ACCEL;
player->vel.vz += X_ACCEL;
player->anim_dir = 1;
}
} else if(pad_pressing(PAD_LEFT)) {
if(player->grnd && (player->vel.vx > 0)) {
player->vel.vx -= X_DECEL;
if(player->vel.vz > 0) {
player->vel.vz -= X_DECEL;
} else {
player->vel.vx -= X_ACCEL;
player->vel.vz -= X_ACCEL;
player->anim_dir = -1;
}
} else {
player->vel.vx -= (player->vel.vx > 0 ? X_FRICTION : -X_FRICTION);
if(abs(player->vel.vx) <= X_FRICTION) player->vel.vx = 0;
player->vel.vz -= (player->vel.vz > 0 ? X_FRICTION : -X_FRICTION);
if(abs(player->vel.vz) <= X_FRICTION) player->vel.vz = 0;
}
if(player->vel.vx > X_TOP_SPD) player->vel.vx = X_TOP_SPD;
else if(player->vel.vx < -X_TOP_SPD) player->vel.vx = -X_TOP_SPD;
if(player->vel.vz > X_TOP_SPD) player->vel.vz = X_TOP_SPD;
else if(player->vel.vz < -X_TOP_SPD) player->vel.vz = -X_TOP_SPD;
// Distribute ground speed onto X and Y components
player->vel.vx = (player->vel.vz * rcos(player->angle)) >> 12;
player->vel.vy = (player->vel.vz * -rsin(player->angle)) >> 12;
} else {
// Air X movement
if(pad_pressing(PAD_RIGHT)) {
player->vel.vx += X_ACCEL;
player->anim_dir = 1;
} else if(pad_pressing(PAD_LEFT)) {
player->vel.vx -= X_ACCEL;
player->anim_dir = -1;
}
}
// Y movement
if(!player->grnd) {

View file

@ -42,6 +42,24 @@ RotTransPers(SVECTOR *v, uint32_t *xy0)
return otz;
}
void
CrossProduct0(VECTOR *v0, VECTOR *v1, VECTOR *out)
{
gte_ldopv1(v0);
gte_ldopv2(v1);
gte_op0();
gte_stlvnl(out);
}
void
CrossProduct12(VECTOR *v0, VECTOR *v1, VECTOR *out)
{
gte_ldopv1(v0);
gte_ldopv2(v1);
gte_op12();
gte_stlvnl(out);
}
uint8_t *
file_read(const char *filename, uint32_t *length)
{
@ -105,6 +123,17 @@ get_short_le(uint8_t *bytes, uint32_t *b)
return value;
}
uint32_t
get_long_be(uint8_t *bytes, uint32_t *b)
{
uint32_t value = 0;
value |= bytes[(*b)++] << 24;
value |= bytes[(*b)++] << 16;
value |= bytes[(*b)++] << 8;
value |= bytes[(*b)++];
return value;
}
uint32_t
adler32(const char *s)

2
tools/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
venv/
.mypy_cache/

View file

@ -5,13 +5,16 @@
import json
import sys
from ctypes import c_ushort, c_ubyte
import numpy as np
import math
from ctypes import c_ushort, c_ubyte, c_int32
from enum import Enum
from pprint import pp as pprint
from math import sqrt
# Ensure binary C types are encoded as big endian
c_ushort = c_ushort.__ctype_be__
c_int32 = c_int32.__ctype_be__
# This package depends on shapely because I'm fed up with attempting to code
# point and polygon checking myself. On arch linux, install python-shapely.
@ -32,6 +35,35 @@ def point_in_geometry(p, points):
return shape.contains(point) or shape.intersects(point)
def normalize(v):
norm = np.linalg.norm(v)
return [c / norm for c in v]
def fix_angle(x):
# This ensures that an angle in radians is always on their
# 1st or 4th quadrant equivalent, and also on the first lap.
fixed = x
if (x >= (np.pi / 2)) and (x < np.pi):
fixed = (2 * np.pi) - (np.pi - x)
if (x >= np.pi) and (x < (1.5 * np.pi)):
fixed = x - np.pi
return fixed % (2 * np.pi)
def to_psx_angle(a):
# PSX angles are given in degrees, ranged from 0.0 to 1.0 in 20.12
# fixed-point format (therefore from 0 to 4096).
# All we need to do is fix its quadrant and lap, convert it to a
# ratio [0..360], then multiply it by 4096. This is how we get
# our angle.
# Final gsp->(xsp, ysp) conversions in-game are given as
# {x: (gsp * cos(x) >> 12), y: (gsp * -sin(x)) >> 12}.
a = np.rad2deg(fix_angle(a))
rat = a / 360
return math.floor(rat * 4096)
def get_height_mask(d: Direction, points):
# Perform iterative linecast.
# Linecast checks for a point within a geometry starting at a height
@ -40,6 +72,7 @@ def get_height_mask(d: Direction, points):
# Of course, if pointing downwards, we go from left to right, top to bottom.
# If using any other direction... flip it accordingly.
heightmask = []
angle = 0
for pos in range(16):
found = False
for height in reversed(range(1, 16)):
@ -62,7 +95,39 @@ def get_height_mask(d: Direction, points):
break
if not found:
heightmask.append(0)
return heightmask
# Build vector according to direction
# and heightmask
# TODO: Maybe the referential dirvec is wrong?
vector = [0, 0]
dirvec = [0, 0]
dirv = 0
delta = heightmask[0] - heightmask[-1]
if d == Direction.DOWN:
# Vector points left to right
vector = [16, delta]
dirvec = [1, 0]
dirv = 0
elif d == Direction.UP:
# Vector points right to left
vector = [-16, -delta]
dirvec = [-1, 0]
dirv = 0
elif d == Direction.LEFT:
# Vector points top to bottom
vector = [-delta, 16]
dirvec = [0, 1]
dirv = 1
elif d == Direction.RIGHT:
# Vector points bottom to top
vector = [delta, -16]
dirvec = [0, -1]
dirv = 1
vector = normalize(vector)
angle = math.atan2(dirvec[1], dirvec[0]) - math.atan2(vector[1], vector[0])
angle = to_psx_angle(angle)
return (heightmask, angle)
def parse_masks(tiles):
@ -70,14 +135,19 @@ def parse_masks(tiles):
for tile in tiles:
points = tile.get("points")
id = tile.get("id")
(floor, floor_angle) = get_height_mask(Direction.DOWN, points)
(ceil, ceil_angle) = get_height_mask(Direction.UP, points)
(rwall, rwall_angle) = get_height_mask(Direction.RIGHT, points)
(lwall, lwall_angle) = get_height_mask(Direction.LEFT, points)
res.append(
{
"id": tile.get("id"),
"masks": {
"floor": get_height_mask(Direction.DOWN, points),
"ceiling": get_height_mask(Direction.UP, points),
"rwall": get_height_mask(Direction.RIGHT, points),
"lwall": get_height_mask(Direction.LEFT, points),
"floor": [floor_angle, floor],
"ceiling": [ceil_angle, ceil],
"rwall": [rwall_angle, rwall],
"lwall": [lwall_angle, lwall],
},
}
)
@ -153,20 +223,33 @@ def write_mask_data(f, mask_data):
# Binary layout:
# 1. Number of tiles (ushort, 2 bytes)
# 2. Tile data
# 2. Tile data [many]
# 2.1. tile id (ushort, 2 bytes)
# 2.2. Floor mode data (8 bytes)
# 2.3. Right wall mode data (8 bytes)
# 2.4. Ceiling mode data (8 bytes)
# 2.5. Left wall mode data (8 bytes)
# 2.2. Floor
# 2.2.1. Angle (4 bytes - PSX format)
# 2.2.2. Data (8 bytes)
# 2.3. Right wall
# 2.3.1. Angle (4 bytes - PSX format)
# 2.3.2. Data (8 bytes)
# 2.4. Ceiling
# 2.4.1. Angle (4 bytes - PSX format)
# 2.4.2. Data (8 bytes)
# 2.5. Left wall
# 2.5.1. Angle (4 bytes - PSX format)
# 2.5.2. Data (8 bytes)
def write_file(f, tile_data):
f.write(c_ushort(len(tile_data)))
for tile in tile_data:
f.write(c_ushort(tile.get("id")))
write_mask_data(f, tile.get("masks").get("floor"))
write_mask_data(f, tile.get("masks").get("rwall"))
write_mask_data(f, tile.get("masks").get("ceiling"))
write_mask_data(f, tile.get("masks").get("lwall"))
masks = tile.get("masks")
f.write(c_int32(masks.get("floor")[0]))
write_mask_data(f, masks.get("floor")[1])
f.write(c_int32(masks.get("rwall")[0]))
write_mask_data(f, masks.get("rwall")[1])
f.write(c_int32(masks.get("ceiling")[0]))
write_mask_data(f, masks.get("ceiling")[1])
f.write(c_int32(masks.get("lwall")[0]))
write_mask_data(f, masks.get("lwall")[1])
def main():

144
tools/guide.org Normal file
View file

@ -0,0 +1,144 @@
* 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 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 448x256. 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
'map128.tmx'.
This 'map128.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 'tileset16.tsx' map from '16x16.png', if you haven't already.
2. Create a 'map128.tmx' map and use 'tileset16.tsx' as tileset. This map must
have infinite dimensions.
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 '128x128.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.json' and 'Z1.json'. 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.json' or 'Z2.json'.
5. Use the tool 'cooklvl.py' to turn 'Z1.json' or 'Z2.json' into 'Z1.LVL' or
'Z2.LVL':\
~cooklvl.py Z1.json Z1.LVL~

View file

@ -1,105 +0,0 @@
* 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 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.
12.1 Make sure you un-mark the "Set for Black" option in Semi Transparent Information.
13.2 Make sure your tileset is at 448x0 and that the CLUT information is 4-bit depth and at 448x256.
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 'map128.tmx'.
This 'map128.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 'tileset16.tsx' map from '16x16.png', if you haven't already.
2. Create a 'map128.tmx' map and use 'tileset16.tsx' as tileset. This map must have infinite dimensions.
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'
Do this in preparation for creating your actual level map:
1. Go back to your 'map128.tmx' and export it to an image called '128x128.png'.
1.1. 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.
1.2. 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.
2.1. 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.json' and 'Z1.json'. 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.json' or 'Z2.json'.
5. Use the tool 'cooklvl.py' to turn 'Z1.json' or 'Z2.json' into 'Z1.LVL' or 'Z2.LVL':
`cooklvl.py Z1.json Z1.LVL`

View file

@ -1,6 +1,6 @@
// -*- mode: c; -*-
bitfield HeightMask {
bitfield HeightMaskData {
unsigned c0 : 4;
unsigned c1 : 4;
unsigned c2 : 4;
@ -19,16 +19,21 @@ bitfield HeightMask {
unsigned cF : 4;
};
struct HeightMask {
be s32 angle;
be HeightMaskData data;
};
struct Collision {
be u16 tile_id;
// Height mask downwards (left to right)
be HeightMask floor;
HeightMask floor;
// Height mask to right (bottom to top)
be HeightMask rwall;
HeightMask rwall;
// Height mask upwards (right to left)
be HeightMask ceiling;
HeightMask ceiling;
// Height mask to left (top to bottom)
be HeightMask lwall;
HeightMask lwall;
};
struct TileData {

7
tools/requirements.txt Normal file
View file

@ -0,0 +1,7 @@
numpy==2.0.1
pandas==2.2.2
python-dateutil==2.9.0.post0
pytz==2024.1
shapely==2.0.5
six==1.16.0
tzdata==2024.1