mirror of
https://github.com/luksamuk/engine-psx.git
synced 2025-04-28 13:28:02 +03:00
Add terrain angles and consider it on movement
This commit is contained in:
parent
a6a683ffff
commit
2b3a516e4b
14 changed files with 382 additions and 157 deletions
Binary file not shown.
Binary file not shown.
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
20
src/level.c
20
src/level.c
|
@ -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); */
|
||||
|
|
21
src/main.c
21
src/main.c
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
51
src/player.c
51
src/player.c
|
@ -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) {
|
||||
|
|
29
src/util.c
29
src/util.c
|
@ -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
2
tools/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
venv/
|
||||
.mypy_cache/
|
|
@ -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
144
tools/guide.org
Normal 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~
|
||||
|
105
tools/guide.txt
105
tools/guide.txt
|
@ -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`
|
||||
|
|
@ -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
7
tools/requirements.txt
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue