animated tileset

This commit is contained in:
Lephenixnoir 2022-02-11 20:42:20 +01:00
parent 5d6b313d96
commit 3c3883b076
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
9 changed files with 255 additions and 97 deletions

View File

@ -460,14 +460,64 @@ def convert_tiled_tileset(input, output, params):
tilewidth = int(tileset.attrib["tilewidth"])
tileheight = int(tileset.attrib["tileheight"])
tilecount = int(tileset.attrib["tilecount"])
# In Rogue Life there is only 16x16 :)
assert tilewidth == 16 and tileheight == 16
# Current we just convert the image, but we could do more later (especially
# if this converter gets reused in other projects)
def plane_id(plane_str):
return ["WALL", "FLOOR", "CEILING"].index(plane_str)
o = fxconv.Structure()
info = fxconv.Structure()
frames = fxconv.Structure()
frames_start = 0
# Tile sheet
source = os.path.join(os.path.dirname(input), image.attrib["source"])
return fxconv.convert_bopti_cg(source, params)
sheet = Image.open(source)
o += fxconv.u32(sheet.width // 16)
o += fxconv.u32(sheet.height // 16)
o += fxconv.ptr(fxconv.convert_bopti_cg(sheet, params))
# Analyze tile metadata and animation
for i in range(tilecount):
tile = tileset.findall(f".//tile[@id='{i}']")
assert len(tile) in [0,1]
plane = "FLOOR"
anim_length = 0
anim_start = frames_start
solid = 0
if tile:
for prop in tile[0].findall("./properties/property"):
name = prop.attrib["name"]
type = prop.attrib.get("type", "string")
value = prop.attrib["value"]
if name == "plane" and type == "string":
plane = value
elif name == "solid" and type == "bool":
solid = int(value == "true")
else:
raise fxconv.FxconvError(
f"Unknown tile property '{name}' with type '{type}'")
for frame in tile[0].findall("./animation/frame"):
frames += fxconv.u16(int(frame.attrib["tileid"]))
frames += fxconv.u16(int(frame.attrib["duration"]))
frames_start += 1
anim_length += 1
info += fxconv.u8(plane_id(plane))
info += fxconv.u8(anim_length)
info += fxconv.u8(anim_start)
info += fxconv.u8(solid)
o += fxconv.ptr(info)
o += fxconv.ptr(frames)
return o
def convert_tiled_map(input, output, params):
tree = xml.etree.ElementTree.parse(input)
@ -495,51 +545,16 @@ def convert_tiled_map(input, output, params):
assert len(data1) == width * height
assert len(data2) == width * height
#---
tileset = xml.etree.ElementTree.parse(
os.path.join(os.path.dirname(input), tileset.attrib["source"]))
tileprops = dict()
for tile in tileset.findall("tile"):
tile_id = int(tile.attrib["id"])
p = dict()
for prop in tile.find("properties").findall("property"):
name = prop.attrib["name"]
type = prop.attrib.get("type", "string")
value = prop.attrib["value"]
if type == "bool":
value = (value == "true")
elif type == "float":
value = float(value)
elif type == "file":
pass
elif type == "int":
value = int(value)
elif type == "string":
pass
else: # including "color" and "object"
raise Exception(f"unknown tile property type {type}")
p[name] = value
tileprops[tile_id] = p
#---
tiles = bytes()
for i in range(width * height):
t1 = data1[i] & 0x0fffffff
t2 = data2[i] & 0x0fffffff
# We expect tileset index 0 to be empty
# We turn empty tiles into index 0
t1 = 0 if t1 < tileset_base else t1 - tileset_base
t2 = 0 if t2 < tileset_base else t2 - tileset_base
solid = 0
plane = "FLOOR"
if t1 in tileprops:
solid = (tileprops[t1].get("solid", False) == True)
plane = tileprops[t1].get("plane", "FLOOR")
plane = ["WALL", "FLOOR", "CEILING"].index(plane)
tiles += bytes([solid, plane, t1, t2])
tiles += fxconv.u8(t1)
tiles += fxconv.u8(t2)
o = fxconv.Structure()
o += fxconv.u16(width)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.2" orientation="orthogonal" renderorder="right-down" width="24" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="1">
<map version="1.8" tiledversion="1.8.0" orientation="orthogonal" renderorder="right-down" width="24" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="1">
<tileset firstgid="1" source="../tilesets/cavern.tsx"/>
<layer id="4" name="Ground" width="24" height="11">
<data encoding="csv">
@ -23,9 +23,9 @@
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,10,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,
0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,11,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,20,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.2" name="cavern" tilewidth="16" tileheight="16" tilecount="16" columns="8">
<image source="cavern.png" width="128" height="32"/>
<tileset version="1.8" tiledversion="1.8.0" name="cavern" tilewidth="16" tileheight="16" tilecount="24" columns="8">
<image source="cavern.png" width="128" height="48"/>
<tile id="1">
<properties>
<property name="plane" value="CEILING"/>
@ -23,22 +23,54 @@
<properties>
<property name="plane" value="WALL"/>
</properties>
<animation>
<frame tileid="8" duration="3000"/>
<frame tileid="16" duration="100"/>
<frame tileid="17" duration="100"/>
<frame tileid="18" duration="100"/>
</animation>
</tile>
<tile id="9">
<properties>
<property name="plane" value="WALL"/>
</properties>
<animation>
<frame tileid="9" duration="300"/>
<frame tileid="10" duration="300"/>
<frame tileid="11" duration="300"/>
<frame tileid="12" duration="300"/>
</animation>
</tile>
<tile id="14">
<properties>
<property name="plane" value="WALL"/>
<property name="solid" type="bool" value="true"/>
</properties>
<animation>
<frame tileid="14" duration="3000"/>
<frame tileid="22" duration="300"/>
<frame tileid="14" duration="70"/>
<frame tileid="22" duration="70"/>
</animation>
</tile>
<tile id="15">
<properties>
<property name="plane" value="WALL"/>
<property name="solid" type="bool" value="true"/>
</properties>
<animation>
<frame tileid="15" duration="3000"/>
<frame tileid="23" duration="300"/>
<frame tileid="15" duration="70"/>
<frame tileid="23" duration="70"/>
</animation>
</tile>
<tile id="19">
<animation>
<frame tileid="19" duration="1000"/>
<frame tileid="20" duration="1000"/>
<frame tileid="21" duration="1000"/>
<frame tileid="20" duration="1000"/>
</animation>
</tile>
</tileset>

View File

@ -20,6 +20,11 @@ bool game_load(game_t *g, level_t const *level)
g->map = level->map;
g->occupation = malloc(g->map->width * g->map->height *
sizeof *g->occupation);
g->map_anim = malloc(g->map->width * g->map->height *
sizeof *g->map_anim);
for(int i = 0; i < g->map->width * g->map->height; i++)
g->map_anim[i] = rand() & 4095;
camera_init(&g->camera, g->map);
@ -46,6 +51,8 @@ void game_unload(game_t *g)
g->map = NULL;
free(g->occupation);
g->occupation = NULL;
free(g->map_anim);
g->map_anim = NULL;
for(int i = 0; i < g->entity_count; i++)
entity_destroy(g->entities[i]);
@ -373,6 +380,9 @@ void game_update_animations(game_t *g, fixed_t dt)
visible_update(e, dt);
}
for(int i = 0; i < g->map->width * g->map->height; i++)
g->map_anim[i] += fround(dt * 1000);
anim_state_update(&g->hud_xp_anim, dt);
}

View File

@ -19,8 +19,13 @@ typedef struct game {
map_t const *map;
/* Number of entities currently on each cell of the map */
uint8_t *occupation;
/* Position of each map cell in its animation cycle, in ms. Cells have
random offsets so animations are not all synchronized. We ignore the
wrapping effect entirely. */
uint16_t *map_anim;
/* User's camera */
camera_t camera;
/* Time played */
fixed_t time_total;
/* Time when victory was reached or defeat was dealt */

View File

@ -1,9 +1,10 @@
#include "map.h"
#include <stdlib.h>
rect tile_shape(tile_t const *tile)
rect tile_shape(tileset_t const *tileset, int id)
{
if(!tile->solid) return (rect){ 0 };
if(!tileset->tiles[id].solid)
return (rect){ 0 };
return (rect){
.l = -fix(0.5),
@ -13,12 +14,12 @@ rect tile_shape(tile_t const *tile)
};
}
tile_t *map_tile(map_t const *m, int x, int y)
map_cell_t *map_cell(map_t const *m, int x, int y)
{
if((unsigned)x >= m->width || (unsigned)y >= m->height)
return NULL;
return &m->tiles[y * m->width + x];
return &m->cells[y * m->width + x];
}
bool map_collides(map_t const *m, rect hitbox)
@ -31,14 +32,22 @@ bool map_collides(map_t const *m, rect hitbox)
/* Collisions against walls and static objects */
for(int y = y_min; y < y_max; y++)
for(int x = x_min; x < x_max; x++) {
tile_t *t = map_tile(m, x, y);
if(!t || !t->solid) continue;
map_cell_t *t = map_cell(m, x, y);
if(!t) continue;
vec2 center = { fix(x) + fix(0.5), fix(y) + fix(0.5) };
rect tile_hitbox = rect_translate(tile_shape(t), center);
vec2 c = { fix(x) + fix(0.5), fix(y) + fix(0.5) };
rect tile_hitbox;
if(rect_collide(hitbox, tile_hitbox))
return true;
if(m->tileset->tiles[t->base].solid) {
tile_hitbox = rect_translate(tile_shape(m->tileset, t->base), c);
if(rect_collide(hitbox, tile_hitbox))
return true;
}
if(m->tileset->tiles[t->decor].solid) {
tile_hitbox = rect_translate(tile_shape(m->tileset, t->decor), c);
if(rect_collide(hitbox, tile_hitbox))
return true;
}
}
return false;

View File

@ -18,28 +18,65 @@
#include <stdbool.h>
#include <stddef.h>
//---
// Tileset
//---
typedef struct
{
/* Rendering plane */
uint8_t plane;
/* Length of animation cycle. This is only defined for the first tile of
each cycle, because the cycle can use the same tile several times. If
anim_length=0 there is no animation; anim_length=1 is useless. */
uint8_t anim_length;
/* Index in the tileset's animation array where this tile's animation cycle
starts; it spans the range [anim_start .. anim_start+anim_length). */
uint8_t anim_start;
/* Hitbox. TODO: Allow any collision rectangle for tiles, for decor */
bool solid;
} tile_info_t;
typedef struct
{
/* ID of the tile visible during this frame */
uint16_t tile_id;
/* Duration before next frame, in ms */
uint16_t duration_ms;
} tile_animation_frame_t;
typedef struct
{
int width, height;
bopti_image_t const *sheet;
/* Tile information */
tile_info_t *tiles;
/* Animations */
tile_animation_frame_t *anim;
} tileset_t;
/* Shape for a tile; this lives in a coordinate system whose (0,0) ends up at
the middle of the tile in the map space (which is a point with half-integer
coordinates) */
rect tile_shape(tileset_t const *tileset, int id);
//---
// Tiles
//---
typedef struct
{
/* TODO: Layers of objects, stuff, dynamic elements, etc? */
/* TODO: Allow any collision shape for the tile! */
bool solid;
/* Rendering plane for that tile */
uint8_t plane;
/* Base layer: floor/wall pattern */
uint8_t base;
/* Decoration layer */
uint8_t decor;
} tile_t;
/* Shape for a tile; this lives in a coordinate system whose (0,0) ends up at
the middle of the tile in the map space (which is a point with half-integer
coordinates) */
rect tile_shape(tile_t const *tile);
} map_cell_t;
//---
// Map grid, tiles location in space, and entities
@ -50,14 +87,14 @@ typedef struct
/* Dimensions, columns are 0 to width-1, rows are 0 to height-1 */
uint16_t width, height;
/* Tileset base (first layer), tileset decor (second layer) */
bopti_image_t *tileset;
/* All tiles */
tile_t *tiles;
tileset_t *tileset;
/* All cells */
map_cell_t *cells;
} map_t;
/* Get a pointer to the tile at (x,y) in map; NULL if out of bounds. */
tile_t *map_tile(map_t const *m, int x, int y);
/* Get a pointer to the cell at (x,y) in map; NULL if out of bounds. */
map_cell_t *map_cell(map_t const *m, int x, int y);
/* Check whether a hitbox collides with the map. */
bool map_collides(map_t const *m, rect hitbox);

View File

@ -77,8 +77,13 @@ pfg_all2one_t pfg_dijkstra(map_t const *map, ivec2 center, uint8_t *occupation)
int dx = dx_array[dir];
int dy = dy_array[dir];
tile_t *tile = map_tile(map, point.x+dx, point.y+dy);
if(!tile || tile->solid) continue;
map_cell_t *cell = map_cell(map, point.x+dx, point.y+dy);
if(!cell) continue;
if(map->tileset->tiles[cell->base].solid)
continue;
if(cell->decor && map->tileset->tiles[cell->decor].solid)
continue;
int next_i = idx(point.x+dx, point.y+dy);
int occ = occupation ? occupation[next_i] : 0;
@ -170,8 +175,14 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end)
perfectly) */
int current_x = ffloor(x-(u.x < 0));
int current_y = ffloor(y-(u.y < 0));
tile_t *tile = map_tile(map, current_x, current_y);
if(tile && tile->solid) return false;
map_cell_t *cell = map_cell(map, current_x, current_y);
if(!cell)
return false;
if(map->tileset->tiles[cell->base].solid)
return false;
if(cell->decor && map->tileset->tiles[cell->decor].solid)
return false;
/* Distance to the next horizontal, and vertical line */
fixed_t dist_y = (u.y >= 0) ? fix(1) - fdec(y) : -(fdec(y-1) + 1);
@ -196,8 +207,13 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end)
}
if(t > fix(1)) break;
tile = map_tile(map, next_x, next_y);
if(tile && tile->solid) return false;
cell = map_cell(map, next_x, next_y);
if(!cell)
return false;
if(map->tileset->tiles[cell->base].solid)
return false;
if(cell->decor && map->tileset->tiles[cell->decor].solid)
return false;
}
return true;

View File

@ -122,31 +122,60 @@ fixed_t camera_ppu(camera_t const *c)
// Rendering
//---
static void render_map_layer(map_t const *m, camera_t const *c, int ss_x,
static inline void render_tile(int x, int y, tileset_t const *tileset,
int tile_id, int time_ms)
{
/* If the tile is animated, find the position in the cycle */
if(tileset->tiles[tile_id].anim_length > 0) {
int start = tileset->tiles[tile_id].anim_start;
tile_animation_frame_t *frames = &tileset->anim[start];
int cycle_duration_ms = 0;
for(int i = 0; i < tileset->tiles[tile_id].anim_length; i++)
cycle_duration_ms += frames[i].duration_ms;
time_ms %= cycle_duration_ms;
int i = 0;
while(time_ms >= 0) {
time_ms -= frames[i].duration_ms;
i++;
}
tile_id = frames[i-1].tile_id;
}
dsubimage(x, y, tileset->sheet,
TILE_WIDTH * (tile_id % tileset->width),
TILE_HEIGHT * (tile_id / tileset->width),
TILE_WIDTH, TILE_HEIGHT, DIMAGE_NOCLIP);
}
static void render_map_layer(game_t const *game, camera_t const *c, int ss_x,
int ss_y, int layer)
{
map_t const *map = game->map;
/* Render floor and walls */
for(int row = -2; row < m->height + 2; row++)
for(int col = -1; col < m->width + 1; col++) {
tile_t *t = map_tile(m, col, row);
for(int row = -2; row < map->height + 2; row++)
for(int col = -1; col < map->width + 1; col++) {
map_cell_t *cell = map_cell(map, col, row);
vec2 tile_pos = { fix(col), fix(row) };
ivec2 p = camera_map2screen(c, tile_pos);
p.x += ss_x;
p.y += ss_y;
if(!t && layer == CEILING) {
drect(p.x, p.y, p.x+15, p.y+15, C_BLACK);
if(!cell) {
if(layer == CEILING)
drect(p.x, p.y, p.x+15, p.y+15, C_BLACK);
continue;
}
if(t->plane != layer)
continue;
dsubimage(p.x, p.y, m->tileset,
TILE_WIDTH * (t->base % 8), TILE_HEIGHT * (t->base / 8),
TILE_WIDTH, TILE_HEIGHT, DIMAGE_NOCLIP);
if(t->decor) dsubimage(p.x, p.y, m->tileset,
TILE_WIDTH * (t->decor % 8), TILE_HEIGHT * (t->decor / 8),
TILE_WIDTH, TILE_HEIGHT, DIMAGE_NOCLIP);
int time_ms = game->map_anim[map->width * row + col];
if(map->tileset->tiles[cell->base].plane == layer)
render_tile(p.x, p.y, map->tileset, cell->base, time_ms);
if(cell->decor && map->tileset->tiles[cell->decor].plane == layer)
render_tile(p.x, p.y, map->tileset, cell->decor, time_ms);
}
}
@ -368,16 +397,16 @@ void render_game(game_t const *g, bool show_hitboxes)
prof_enter(ctx);
/* Render map floor and floor entities */
render_map_layer(g->map, camera, ss_x, ss_y, HORIZONTAL);
render_map_layer(g, camera, ss_x, ss_y, HORIZONTAL);
render_entities(g, camera, floor_depth_measure, ss_x, ss_y, show_hitboxes);
/* Render map walls and vertical entities
TODO ECS: Sort walls and wall entities together for proper ordering!*/
render_map_layer(g->map, camera, ss_x, ss_y, VERTICAL);
render_map_layer(g, camera, ss_x, ss_y, VERTICAL);
render_entities(g, camera, wall_depth_measure, ss_x, ss_y, show_hitboxes);
/* Render ceiling tiles (including out of bounds) and ceiling entities */
render_map_layer(g->map, camera, ss_x, ss_y, CEILING);
render_map_layer(g, camera, ss_x, ss_y, CEILING);
render_entities(g, camera, ceiling_depth_measure, ss_x,ss_y,show_hitboxes);
prof_leave(ctx);
@ -470,8 +499,13 @@ void render_pfg_all2one(pfg_all2one_t const *paths, camera_t const *c,
{
for(int row = 0; row < paths->map->height; row++)
for(int col = 0; col < paths->map->width; col++) {
tile_t *tile = map_tile(paths->map, col, row);
if(!tile || tile->solid) continue;
map_cell_t *cell = map_cell(paths->map, col, row);
if(!cell)
continue;
if(paths->map->tileset->tiles[cell->base].solid)
continue;
if(cell->decor && paths->map->tileset->tiles[cell->decor].solid)
continue;
vec2 fp = vec_i2f_center((ivec2){ col, row });
ivec2 p = camera_map2screen(c, fp);