particles as ECS entities, proper tilesets/maps (WIP)

This commit is contained in:
Lephenixnoir 2021-12-27 21:54:55 +01:00
parent 8ca3d6f22f
commit d942e1ffa7
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
27 changed files with 431 additions and 310 deletions

View File

@ -23,7 +23,6 @@ set(SOURCES
src/level.c
src/main.c
src/map.c
src/particles.c
src/pathfinding.c
src/render.c
src/util.c
@ -31,6 +30,7 @@ set(SOURCES
src/comp/entity.c
src/comp/fighter.c
src/comp/mechanical.c
src/comp/particle.c
src/comp/physical.c
src/comp/visible.c
)
@ -40,9 +40,10 @@ set(ASSETS
assets-cg/tileset_decor.png
assets-cg/tileset_base_2.png
assets-cg/tileset_decor_2.png
assets-cg/tilesets/cavern.tsx
assets-cg/tilesets/lab.tsx
# Levels
assets-cg/levels/demo.txt
assets-cg/levels/1.txt
assets-cg/levels/lv1.tmx
# HUD
assets-cg/hud.png
assets-cg/hud_life.png

View File

@ -1,6 +1,7 @@
import fxconv
import re
import os.path
import xml.etree.ElementTree
from PIL import Image, ImageChops
@ -13,6 +14,10 @@ def convert(input, output, params, target):
o = convert_animation(input, params)
elif params["custom-type"] == "aseprite-anim":
o = convert_aseprite_anim(input, output, params)
elif params["custom-type"] == "tiled-tileset":
o = convert_tiled_tileset(input, output, params)
elif params["custom-type"] == "tiled-map":
o = convert_tiled_map(input, output, params)
else:
recognized = False
@ -280,6 +285,7 @@ def convert_aseprite_anim(input, output, params):
for i in range(from_, to+1):
bbox = ImageChops.difference(pil_frames[i], bg).getbbox()
# pil_frames[i] becomes (offset_x, offset_y, cropped image)
if bbox:
pil_frames[i] = (bbox[0], bbox[1], pil_frames[i].crop(bbox))
else:
@ -322,3 +328,97 @@ def convert_aseprite_anim(input, output, params):
})
return o
def print_xml_tree(node, indent=0):
print(indent*" " + f"<{node.tag}> {node.attrib}")
for child in node:
print_xml_tree(child, indent+2)
def convert_tiled_tileset(input, output, params):
tree = xml.etree.ElementTree.parse(input)
tileset = tree.getroot()
assert tileset.tag == "tileset"
# We only support single-source tilesets
images = tileset.findall("image")
assert len(images) == 1
image = images[0]
tilewidth = int(tileset.attrib["tilewidth"])
tileheight = int(tileset.attrib["tileheight"])
# 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)
source = os.path.join(os.path.dirname(input), image.attrib["source"])
return fxconv.convert_bopti_cg(source, params)
def convert_tiled_map(input, output, params):
tree = xml.etree.ElementTree.parse(input)
map = tree.getroot()
assert map.tag == "map"
width = int(map.attrib["width"])
height = int(map.attrib["height"])
tilesets = map.findall("tileset")
assert len(tilesets) == 1
tileset = tilesets[0]
tileset_base = int(tileset.attrib["firstgid"])
# Grab tileset variable name from file path
tileset_var = os.path.basename(tileset.attrib["source"])
tileset_var = "tileset_" + os.path.splitext(tileset_var)[0]
layers = map.findall("layer")
assert len(layers) == 2
assert all(l.find("data").attrib["encoding"] == "csv" for l in layers)
data1 = [int(x) for x in layers[0].find("data").text.split(",")]
data2 = [int(x) for x in layers[1].find("data").text.split(",")]
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["type"]
value = prop.attrib["value"]
if type == "bool":
value = (value == "true")
else:
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
t1 = 0 if t1 < tileset_base else t1 - tileset_base
t2 = 0 if t2 < tileset_base else t2 - tileset_base
solid = 0
if t1 in tileprops and tileprops[t1].get("solid", False):
solid = 1
tiles += bytes([solid, t1, t2])
o = fxconv.Structure()
o += fxconv.u16(width)
o += fxconv.u16(height)
o += fxconv.ptr(tileset_var)
o += fxconv.ptr(tiles)
return o

View File

@ -1,3 +1,7 @@
*.txt:
custom-type: level_map
name_regex: (.*)\.txt lv_map_\1
*.tmx:
custom-type: tiled-map
name_regex: (.*)\.tmx map_\1

34
assets-cg/levels/lv1.tmx Normal file
View File

@ -0,0 +1,34 @@
<?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">
<tileset firstgid="1" source="../tilesets/cavern.tsx"/>
<layer id="4" name="Ground" width="24" height="11">
<data encoding="csv">
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
2,2,2,2,7,7,7,7,2,2,2,7,7,7,7,7,7,7,7,2,2,2,2,2,
2,2,7,7,3,3,5,3,7,2,2,4,4,4,4,4,4,3,3,7,7,2,2,2,
2,2,3,3,5,3,3,3,3,7,2,4,7,2,2,7,3,5,6,5,3,7,2,2,
2,2,5,3,3,3,3,5,3,3,7,5,4,7,7,3,5,6,3,6,5,3,2,2,
2,2,3,3,7,7,7,3,3,3,5,5,5,5,5,3,6,3,3,3,6,3,2,2,
2,2,3,5,3,6,3,3,5,2,2,2,4,5,2,3,5,6,3,6,5,3,2,2,
2,2,2,3,6,3,6,3,2,2,7,7,7,4,2,2,3,5,6,5,3,2,2,2,
2,2,2,3,5,6,3,3,7,7,4,4,4,4,2,2,2,3,3,3,2,2,2,2,
2,2,2,2,2,3,3,5,5,5,5,4,4,2,2,2,2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
</data>
</layer>
<layer id="2" name="Decorations" width="24" height="11">
<data encoding="csv">
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,9,0,0,0,0,0,0,9,0,10,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,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,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,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
</data>
</layer>
</map>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

View File

@ -0,0 +1,14 @@
<?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"/>
<tile id="1">
<properties>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="6">
<properties>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
</tileset>

View File

@ -0,0 +1,3 @@
*.tsx:
custom-type: tiled-tileset
name_regex: (.*)\.tsx tileset_\1

BIN
assets-cg/tilesets/lab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.2" name="lab" tilewidth="16" tileheight="16" tilecount="32" columns="16">
<image source="lab.png" width="256" height="32"/>
</tileset>

View File

@ -2,6 +2,7 @@
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/fighter.h"
#include "comp/particle.h"
#include "aoe.h"
#include "game.h"
#include "enemies.h"
@ -186,13 +187,9 @@ static bool attack_apply(game_t *game, aoe_t *aoe, entity_t *target)
}
/* Spawn damage particle */
particle_damage_t *p = malloc(sizeof *p);
p->particle.type = PARTICLE_DAMAGE;
p->particle.age = 0;
p->particle.pos = (vec2){ target_p->x, target_p->y - fix(0.5) };
p->damage = damage;
p->color = (target_f->identity == 0) ? C_RED : C_WHITE;
game_add_particle(game, &p->particle);
entity_t *particle = particle_make_damage(target, damage,
(target_f->identity == 0) ? C_RED : C_WHITE);
game_add_entity(game, particle);
return true;
}
@ -250,7 +247,7 @@ void aoe_update(game_t *game, entity_t *entity, fixed_t dt)
rect small_box = { 0, 0, 0, 0 };
small_box = rect_translate(small_box, physical_pos(entity));
if(map_collides(&game->map, small_box))
if(map_collides(game->map, small_box))
aoe->lifetime = 0;
}
}

View File

@ -3,6 +3,7 @@
#include "comp/visible.h"
#include "comp/mechanical.h"
#include "comp/fighter.h"
#include "comp/particle.h"
#include "aoe.h"
/* Align up to native pointer size */
@ -36,6 +37,10 @@ entity_t *(entity_make)(uint32_t comps)
offsets[n++] = size;
size = ALIGN(size + sizeof(aoe_t));
}
if(comps & ENTITY_COMP_particle) {
offsets[n++] = size;
size = ALIGN(size + sizeof(particle_t));
}
/* Allocate all components together */
entity_t *e;

View File

@ -36,6 +36,7 @@ typedef struct
#define ENTITY_COMP_mechanical 0x00000004
#define ENTITY_COMP_fighter 0x00000008
#define ENTITY_COMP_aoe 0x00000010
#define ENTITY_COMP_particle 0x00000020
/* Maximum number of components */
#define ENTITY_COMP_CAPACITY 31

118
src/comp/particle.c Normal file
View File

@ -0,0 +1,118 @@
#include "comp/entity.h"
#include "comp/physical.h"
#include "comp/particle.h"
#include "geometry.h"
#include <gint/defs/attributes.h>
#include <gint/display.h>
#include <stdio.h>
entity_t *particle_make_damage(entity_t *target, int damage, int color)
{
entity_t *e = entity_make(particle);
particle_t *p = getcomp(e, particle);
physical_t *target_p = getcomp(target, physical);
p->type = PARTICLE_DAMAGE;
p->plane = CEILING;
p->x = target_p->x;
p->y = target_p->y - fix(0.5);
p->z = 0;
p->age = 0;
p->DAMAGE.damage = damage;
p->DAMAGE.color = color;
return e;
}
entity_t *particle_make_dash(entity_t *target)
{
entity_t *e = entity_make(particle);
particle_t *p = getcomp(e, particle);
physical_t *target_p = getcomp(target, physical);
p->type = PARTICLE_DASH;
p->plane = HORIZONTAL;
p->x = target_p->x;
p->y = target_p->y;
p->z = 0;
p->age = 0;
return e;
}
static bool damage_update(particle_t *p, GUNUSED fixed_t dt)
{
return p->age >= 300;
}
static void damage_render(int x, int y, particle_t const *p)
{
extern bopti_image_t img_font_damage_white;
extern bopti_image_t img_font_damage_red;
bopti_image_t *img = (p->DAMAGE.color == C_RED) ? &img_font_damage_red :
&img_font_damage_white;
int char_w = img->width / 10;
int char_h = img->height;
/* Determine number of characters */
char str[16];
int n = snprintf(str, 16, "%d", p->DAMAGE.damage);
y -= char_h / 2;
x -= (char_w * n + 1) / 2;
for(int i = 0; i < n; i++) {
int offset = (char_w + 1) * (str[i] - '0');
dsubimage(x, y, img, offset, 0, char_w, char_h, DIMAGE_NONE);
x += char_w;
}
}
static bool dash_update(particle_t *p, GUNUSED fixed_t dt)
{
return p->age >= 256;
}
static void dash_render(int x, int y, particle_t const *p)
{
/* 32 * (1 - age/256) */
int radius = 32 - (p->age >> 3);
for(int dx = -5; dx <= +5; dx++)
for(int dy = -5; dy <= +5; dy++) {
if(dx * dx + dy * dy <= radius) {
int index = 396 * (y + dy) + (x + dx);
gint_vram[index] = ~((~gint_vram[index] & 0xf7de) >> 1);
}
}
}
//---
// Generic functions
//---
bool particle_update(particle_t *p, fixed_t dt)
{
p->age += fround(dt * 1000);
if(p->type == PARTICLE_DAMAGE)
return damage_update(p, dt);
if(p->type == PARTICLE_DASH)
return dash_update(p, dt);
return true;
}
void particle_render(int x, int y, particle_t const *p)
{
if(p->type == PARTICLE_DAMAGE)
return damage_render(x, y, p);
if(p->type == PARTICLE_DASH)
return dash_render(x, y, p);
}

56
src/comp/particle.h Normal file
View File

@ -0,0 +1,56 @@
//---
// particle: Lightweight, standalone component for particles
//
// This component if for basic visual particles with simple rendering. The
// particles can carry extra data and might be rendered above the rest of the
// scene.
//---
#pragma once
#include "comp/entity.h"
#include "fixed.h"
/* Particle types */
enum {
PARTICLE_GENERIC = 0,
PARTICLE_DAMAGE,
PARTICLE_DASH,
PARTICLE_HPBAR,
};
typedef struct
{
/* Type of particle */
uint8_t type;
/* Plane of rendering */
uint8_t plane;
/* Location on screen and in the air */
fixed_t x, y, z;
/* Age and lifetime */
fixed_t age;
/* 8 bytes of data */
union {
/* Generic particles or with a lot of extra data */
struct { void *data1, *data2; } GENERIC;
/* PARTICLE_DAMAGE: Value and color (can be C_WHITE or C_RED only) */
struct { int damage; uint16_t color; } DAMAGE;
/* PARTICLE_DASH: No extra data */
/* PARTICLE_HPBAR: Pointer to entity */
struct { entity_t *entity; } HPBAR;
};
} particle_t;
/* Make a damage particle for the specified target */
entity_t *particle_make_damage(entity_t *target, int damage, int color);
/* Make a dashing particle at the specified target's position */
entity_t *particle_make_dash(entity_t *target);
/* Update time and layout for a particle (type-generic). */
bool particle_update(particle_t *p, fixed_t dt);
/* Render a particle through the map and camera. */
void particle_render(int x, int y, particle_t const *p);

View File

@ -5,6 +5,7 @@
#include "comp/fighter.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/particle.h"
#include "aoe.h"
#include <stdlib.h>
@ -13,26 +14,12 @@ bool game_load(game_t *g, level_t *level)
{
game_unload(g);
map_t *m = &g->map;
m->width = level->map->width;
m->height = level->map->height;
m->tileset_base = level->map->tileset_base;
m->tileset_decor = level->map->tileset_decor;
m->tiles = malloc(m->width * m->height * sizeof *m->tiles);
if(!m->tiles) {
game_unload(g);
return false;
}
g->map = level->map;
for(int y = 0; y < m->height; y++)
for(int x = 0; x < m->width; x++) {
tile_t *t = map_tile(m, x, y);
t->base = level->map->tiles[level->map->width * y + x].base;
t->decor = level->map->tiles[level->map->width * y + x].decor;
t->solid = (t->base == 0) || (t->base >= 16);
}
// TODO: Tiled converter: specify solid tiles
// t->solid = (t->base == 0) || (t->base >= 16);
camera_init(&g->camera, m);
camera_init(&g->camera, g->map);
g->time_total = fix(0);
g->time_victory = fix(0);
@ -40,8 +27,6 @@ bool game_load(game_t *g, level_t *level)
g->entities = NULL;
g->entity_count = 0;
g->particles = NULL;
g->particle_count = 0;
g->level = level;
g->wave = 1;
@ -53,9 +38,7 @@ bool game_load(game_t *g, level_t *level)
void game_unload(game_t *g)
{
g->map.width = 0;
g->map.height = 0;
free(g->map.tiles);
g->map = NULL;
for(int i = 0; i < g->entity_count; i++)
entity_destroy(g->entities[i]);
@ -63,14 +46,6 @@ void game_unload(game_t *g)
g->entities = NULL;
g->entity_count = 0;
// TODo ECS: Remove particles
for(int i = 0; i < g->particle_count; i++)
free(g->particles[i]);
free(g->particles);
g->particles = NULL;
g->particle_count = 0;
}
level_wave_t const *game_current_wave(game_t const *g)
@ -182,28 +157,6 @@ void game_remove_dead_entities(game_t *g)
}
}
void game_add_particle(game_t *g, particle_t *p)
{
size_t new_size = (g->particle_count + 1) * sizeof *g->particles;
particle_t **new_particles = realloc(g->particles, new_size);
if(!new_particles) return;
g->particles = new_particles;
g->particles[g->particle_count] = p;
g->particle_count++;
}
/* Remove a particle and rearrange the array. */
static void game_remove_particle(game_t *g, int i)
{
if(i < 0 || i >= g->particle_count) return;
free(g->particles[i]);
g->particles[i] = g->particles[--g->particle_count];
/* Again, don't realloc to save on useless heap movement */
}
//---
// Generic entity functions
//---
@ -355,12 +308,14 @@ void game_update_aoes(game_t *g, fixed_t dt)
void game_update_particles(game_t *g, fixed_t dt)
{
int i = 0;
for(int i = 0; i < g->entity_count; i++) {
entity_t *e = g->entities[i];
particle_t *p = getcomp(e, particle);
if(p == NULL)
continue;
while(i < g->particle_count) {
bool remove = particle_update(g->particles[i], dt);
if(remove) game_remove_particle(g, i);
else i++;
bool remove = particle_update(p, dt);
if(remove) entity_mark_to_delete(e);
}
/* Spawn dash particles */
@ -369,26 +324,8 @@ void game_update_particles(game_t *g, fixed_t dt)
mechanical_t *m = getcomp(e, mechanical);
if(m && mechanical_dashing(e)) {
particle_dash_t *p = malloc(sizeof *p);
p->particle.type = PARTICLE_DASH;
p->particle.age = 0;
p->particle.pos = physical_pos(e);
game_add_particle(g, &p->particle);
entity_t *p = particle_make_dash(e);
game_add_entity(g, p);
}
}
}
/* Compare the y values of two particles. */
static int game_sort_particles_compare(void const *v1, void const *v2)
{
particle_t const *p1 = *(particle_t const **)v1;
particle_t const *p2 = *(particle_t const **)v2;
return p1->pos.y - p2->pos.y;
}
void game_sort_particles(game_t *g)
{
heap_sort(g->particles, g->particle_count, sizeof *g->particles,
game_sort_particles_compare);
}

View File

@ -8,14 +8,13 @@
#include "render.h"
#include "level.h"
#include "pathfinding.h"
#include "particles.h"
#include "comp/entity.h"
typedef struct game {
/* The map's coordinate system is the primary coordinate system in all of
this game's code */
map_t map;
map_t const *map;
/* User's camera */
camera_t camera;
/* Time played */
@ -28,9 +27,6 @@ typedef struct game {
int entity_count;
/* Player; this must be one of the entities loaded in the game */
entity_t *player;
/* List of particles */
particle_t **particles;
int particle_count;
/* Field of movement to reach the player (used by most enemy AIs) */
pfg_all2one_t paths_to_player;
@ -62,9 +58,6 @@ void game_next_wave(game_t *g);
/* Add an entity to the game (takes ownership; e will be freed). */
void game_add_entity(game_t *g, entity_t *e);
/* Add a particle to the game (takes ownership; p will be freed) */
void game_add_particle(game_t *g, particle_t *p);
/* Like game_add_entity(), but with a visual effect */
void game_spawn_entity(game_t *g, entity_t *e);

View File

@ -12,7 +12,7 @@
/* Directions */
enum { UP=0, RIGHT=1, DOWN=2, LEFT=3 };
/* Orientations */
enum { VERTICAL=0, HORIZONTAL=1 };
enum { VERTICAL=0, HORIZONTAL=1, CEILING=2 };
//---
// Geometric primitives; the engine types are the fixed-point versions, but the

View File

@ -2,12 +2,9 @@
#include "enemies.h"
/* List of maps */
extern level_map_t lv_map_demo;
extern level_map_t lv_map_1;
level_t lv_demo = {
.map = &lv_map_demo,
};
extern map_t const map_lv1;
// extern level_map_t lv_map_demo;
// extern level_map_t lv_map_1;
/* Notation that would be very cool:
@ -24,7 +21,7 @@ level_t lv_demo = {
increments of 0.5 unless specified otherwise or @<time> */
level_t lv_1 = {
.map = &lv_map_1,
.map = &map_lv1,
.wave_count = 5,
.waves = (level_wave_t []){
/* Wave 1: Just some slimes */

View File

@ -10,6 +10,7 @@
#pragma once
#include "fixed.h"
#include "map.h"
#include <stdint.h>
#include <stdbool.h>
#include <gint/display.h>
@ -35,20 +36,7 @@ typedef struct {
} level_wave_t;
typedef struct {
/* Dimensions of the level (yeah no surprise right) */
uint16_t width, height;
/* Pointers to tileset */
bopti_image_t *tileset_base, *tileset_decor;
/* Tile definitions; each tile is a dual-layer base/decoration */
struct {
uint8_t base;
uint8_t decor;
} tiles[];
} level_map_t;
typedef struct {
level_map_t *map;
map_t const *map;
/* Waves */
int wave_count;

View File

@ -3,6 +3,7 @@
#include "comp/visible.h"
#include "comp/mechanical.h"
#include "comp/fighter.h"
#include "comp/particle.h"
#include "anim.h"
#include "aoe.h"
@ -11,7 +12,6 @@
#include "geometry.h"
#include "level.h"
#include "map.h"
#include "particles.h"
#include "pathfinding.h"
#include "render.h"
@ -45,7 +45,6 @@ int main(void)
usb_open(interfaces, GINT_CALL_NULL);
game_t game = { 0 };
map_t *map = &game.map;
camera_t *c = &game.camera;
game_load(&game, &lv_1);
@ -193,7 +192,7 @@ int main(void)
vec2 p = physical_pos(player);
vec2 q = vec_i2f_center((ivec2){ 6, 9 });
bool clear = raycast_clear_hitbox(map, p, q, player_p->hitbox);
bool clear = raycast_clear_hitbox(game.map,p,q,player_p->hitbox);
ivec2 j = camera_map2screen(c, p);
ivec2 k = camera_map2screen(c, q);
@ -256,7 +255,6 @@ int main(void)
prof_enter(perf_simul);
game_spawn_enemies(&game);
game_sort_particles(&game);
key_event_t ev;
while((ev = pollevent()).type != KEYEV_NONE) {
@ -357,7 +355,7 @@ int main(void)
int dash_dir = (dir >= 0) ? dir : player_p->facing;
mechanical_dash(player, dash_dir);
}
mechanical_move4(player, dir, dt, map);
mechanical_move4(player, dir, dt, game.map);
if(dir >= 0)
visible_set_anim(player, &anims_player_Walking, 1);
@ -367,9 +365,10 @@ int main(void)
/* Directions to reach the player from anywhere on the grid */
pfg_all2one_free(&game.paths_to_player);
game.paths_to_player = pfg_bfs(map, vec_f2i(physical_pos(player)));
game.paths_to_player = pfg_bfs(game.map,vec_f2i(physical_pos(player)));
/* Enemy AI */
#if 0
if(player_f->HP > 0)
for(int i = 0; i < game.entity_count; i++) {
entity_t *e = game.entities[i];
@ -425,6 +424,7 @@ int main(void)
visible_set_anim(e, enemies[f->identity]->anim_attack, 2);
}
}
#endif
/* Player attack */
if(player_f->HP > 0 && attack && !player_f->current_attack) {
@ -483,7 +483,7 @@ int main(void)
- Barrier around player
- Teleport
- Time manipulation
- Player buuffs (short but strong) or wide area debuggs
- Player buffs (short but strong) or wide area debuggs
Ideas for items:
- Healing potion
- Equipment

View File

@ -15,7 +15,7 @@ rect tile_shape(tile_t const *tile)
tile_t *map_tile(map_t const *m, int x, int y)
{
if((unsigned)x >= (unsigned)m->width || (unsigned)y >= (unsigned)m->height)
if((unsigned)x >= m->width || (unsigned)y >= m->height)
return NULL;
return &m->tiles[y * m->width + x];

View File

@ -46,9 +46,9 @@ rect tile_shape(tile_t const *tile);
typedef struct
{
/* Dimensions, columns are 0 to width-1, rows are 0 to height-1 */
int width, height;
uint16_t width, height;
/* Tileset base (first layer), tileset decor (second layer) */
bopti_image_t *tileset_base, *tileset_decor;
bopti_image_t *tileset;
/* All tiles */
tile_t *tiles;

View File

@ -1,82 +0,0 @@
#include "particles.h"
#include <gint/display.h>
#include <stdio.h>
static bool damage_update(particle_damage_t *p, GUNUSED fixed_t dt)
{
return p->particle.age >= 500;
}
static void damage_render(int x, int y, particle_damage_t *p)
{
extern bopti_image_t img_font_damage_white;
extern bopti_image_t img_font_damage_red;
bopti_image_t *img = (p->color == C_RED) ? &img_font_damage_red :
&img_font_damage_white;
int char_w = img->width / 10;
int char_h = img->height;
/* Determine number of characters */
char str[16];
int n = snprintf(str, 16, "%d", p->damage);
y -= char_h / 2;
x -= (char_w * n + 1) / 2;
for(int i = 0; i < n; i++) {
int offset = (char_w + 1) * (str[i] - '0');
dsubimage(x, y, img, offset, 0, char_w, char_h, DIMAGE_NONE);
x += char_w;
}
}
static bool dash_update(particle_dash_t *p, GUNUSED fixed_t dt)
{
return p->particle.age >= 256;
}
static void dash_render(int x, int y, particle_dash_t *p)
{
/* 32 * (1 - age/256) */
int radius = 32 - (p->particle.age >> 3);
for(int dx = -5; dx <= +5; dx++)
for(int dy = -5; dy <= +5; dy++) {
if(dx * dx + dy * dy <= radius) {
int index = 396 * (y + dy) + (x + dx);
gint_vram[index] = ~((~gint_vram[index] & 0xf7de) >> 1);
}
}
}
//---
// Generic functions
//---
bool particle_update(particle_t *p, fixed_t dt)
{
p->age += fround(dt * 1000);
if(p->type == PARTICLE_DAMAGE)
return damage_update((void *)p, dt);
if(p->type == PARTICLE_DASH)
return dash_update((void *)p, dt);
return true;
}
void particle_render(int x, int y, particle_t const *p)
{
if(p->type == PARTICLE_DAMAGE)
return damage_render(x, y, (void *)p);
if(p->type == PARTICLE_DASH)
return dash_render(x, y, (void *)p);
}
bool particle_is_background(particle_t const *p)
{
return p->type == PARTICLE_DASH;
}

View File

@ -1,57 +0,0 @@
//---
// particles: Visual objects with no interactions
//---
#pragma once
#include "fixed.h"
#include "geometry.h"
#include "anim.h"
#include <gint/defs/types.h>
/* Particles with no interaction but visual effects */
typedef struct {
/* Particle type */
uint16_t type;
/* Time lived (milliseconds) */
uint16_t age;
/* Position on the map (used for sorting and layered rendering) */
vec2 pos;
} particle_t;
/* Particle types */
enum {
PARTICLE_DAMAGE = 0,
PARTICLE_DASH = 1,
};
/* Update time and layout for a particle (type-generic). */
bool particle_update(particle_t *p, fixed_t dt);
/* Render a particle through the map and camera. */
void particle_render(int x, int y, particle_t const *p);
/* Whether a particle needs to be rendered as background. */
bool particle_is_background(particle_t const *p);
//---
// Damage particles (text)
//---
/* Damage text particles */
typedef struct {
particle_t particle;
/* Damage value */
int damage;
/* Color (accepts C_RED; everything else is white) */
uint16_t color;
} particle_damage_t;
/* Dash trail particles */
typedef struct {
particle_t particle;
} particle_dash_t;

View File

@ -47,13 +47,13 @@ void pfg_path_free(pfg_path_t *grid_path);
/* pfg_bfs(): Breadth-First Search to generate all paths to a center node
This function runs a BFS and determines the shortest path from all points of
the map to the specified center point. The returned structure shoud be freed
by a call to pfg_all2one_free(). */
the map to the specified center point. The returned structure should be
freed by a call to pfg_all2one_free(). */
pfg_all2one_t pfg_bfs(map_t const *map, ivec2 center);
/* pfg_bfs_inwards(), pfg_bfs_outwards(): Determine paths after BFS
Thiese functions simply use the result of pfg_bfs() to generate a path in
These functions simply use the result of pfg_bfs() to generate a path in
the grid. Inwards paths lead from the source point to the center. Outwards
paths lead from the center to the destination point. */
pfg_path_t pfg_bfs_inwards(pfg_all2one_t const *field, ivec2 from);
@ -68,7 +68,7 @@ pfg_path_t pfg_bfs_outwards(pfg_all2one_t const *field, ivec2 to);
This function checks if the straight-line path from [start] to [end] is
clear of collisions with walls. It checks all tile transitions to ensure
that no solid tile is crossed. This function checks movement only for a ray
of no width, which is suitable for objects tht do not collide with walls.
of no width, which is suitable for objects that do not collide with walls.
TODO: Use tile hitboxes rather than simply checking if the tile is solid.
@ -100,7 +100,7 @@ bool raycast_clear_hitbox(map_t const *map, vec2 start, vec2 end,
// Pathfinding in continuous space (pfc)
//---
/* Path in continous space. */
/* Path in continuous space. */
typedef struct {
/* Map in which this path is located */
map_t const *map;

View File

@ -3,6 +3,7 @@
#include "comp/visible.h"
#include "comp/mechanical.h"
#include "comp/fighter.h"
#include "comp/particle.h"
#include "render.h"
#include "game.h"
#include "anim.h"
@ -131,28 +132,29 @@ static void render_map_layer(map_t const *m, camera_t const *c, int layer)
continue;
}
bool will_draw = false;
if(layer == 0) will_draw = (t->base < 16); // Floor
if(layer == 1) will_draw = (t->base >= 16); // Walls
if(!will_draw)
continue;
dsubimage(p.x, p.y, m->tileset_base,
TILE_WIDTH * (t->base % 16), TILE_HEIGHT * (t->base / 16),
if(layer == 0) dsubimage(p.x, p.y, m->tileset,
TILE_WIDTH * (t->base % 8), TILE_HEIGHT * (t->base / 8),
TILE_WIDTH, TILE_HEIGHT, DIMAGE_NOCLIP);
/* Decoration layer */
if(t->decor) dsubimage(p.x, p.y, m->tileset_decor,
TILE_WIDTH * (t->decor % 16), TILE_HEIGHT * (t->decor / 16),
if(layer == 1 && 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);
}
}
static int depth_measure(entity_t const *e, int direction)
{
particle_t *p = getcomp(e, particle);
if(p) {
return (p->plane != direction) ? -1 : p->y;
}
visible_t *v = getcomp(e, visible);
if(v == NULL || v->sprite_plane != direction)
return -1;
return getcomp(e, physical)->y;
if(v) {
return (v->sprite_plane != direction) ? -1 : getcomp(e, physical)->y;
}
return -1;
}
static int floor_depth_measure(entity_t const *e)
{
@ -162,6 +164,10 @@ static int wall_depth_measure(entity_t const *e)
{
return depth_measure(e, VERTICAL);
}
static int ceiling_depth_measure(entity_t const *e)
{
return depth_measure(e, CEILING);
}
static void render_shadow(int cx, int cy, int shadow_size)
{
@ -208,21 +214,33 @@ static void render_entities(game_t const *g, camera_t const *camera,
entity_t *e = g->entities[rendering_order[i]];
physical_t *p = getcomp(e, physical);
visible_t *v = getcomp(e, visible);
particle_t *pt = getcomp(e, particle);
ivec2 scr = camera_map2screen(camera, physical_pos(e));
int elevated_y = scr.y - fround(16 * v->z);
vec2 xy;
fixed_t z;
if(pt) {
xy = (vec2) { pt->x, pt->y };
z = pt->z;
}
else {
xy = physical_pos(e);
z = v->z;
}
ivec2 scr = camera_map2screen(camera, xy);
int elevated_y = scr.y - fround(16 * z);
/* Show shadow */
if(v->shadow_size) {
if(v && v->shadow_size) {
render_shadow(scr.x, scr.y, v->shadow_size);
}
/* Show entity sprite */
if(v->anim.frame) {
if(v && v->anim.frame) {
anim_frame_render(scr.x, elevated_y, v->anim.frame);
}
/* Show entity hitbox in the map coordinate system */
if(!v->anim.frame || show_hitboxes) {
if(v && (!v->anim.frame || show_hitboxes)) {
rect r = p->hitbox;
r = rect_scale(r, camera_ppu(camera));
r = rect_translate(r, vec_i2f(scr));
@ -232,6 +250,11 @@ static void render_entities(game_t const *g, camera_t const *camera,
dline(scr.x-1, scr.y, scr.x+1, scr.y, C_BLUE);
dline(scr.x, scr.y-1, scr.x, scr.y+1, C_BLUE);
}
/* Show particle */
if(pt) {
particle_render(scr.x, scr.y, pt);
}
}
free(rendering_order);
@ -242,35 +265,20 @@ void render_game(game_t const *g, bool show_hitboxes)
camera_t const *camera = &g->camera;
/* Render map floor */
render_map_layer(&g->map, camera, 0);
/* Render background particles
TODO ECS: Use ECS entities for particles */
for(int i = 0; i < g->particle_count; i++) {
particle_t *p = g->particles[i];
if(!particle_is_background(p)) continue;
ivec2 center = camera_map2screen(camera, p->pos);
particle_render(center.x, center.y, p);
}
render_map_layer(g->map, camera, 0);
/* Render floor entities */
render_entities(g, camera, floor_depth_measure, show_hitboxes);
/* Render map walls */
render_map_layer(&g->map, camera, 1);
render_map_layer(g->map, camera, 1);
/* Render wall entities
TODO ECS: Sort walls along wall entities! */
render_entities(g, camera, wall_depth_measure, show_hitboxes);
/* Render foreground particles
TODO ECS: Use ECS entities for particles */
for(int i = 0; i < g->particle_count; i++) {
particle_t *p = g->particles[i];
if(particle_is_background(p)) continue;
ivec2 center = camera_map2screen(camera, p->pos);
particle_render(center.x, center.y, p);
}
/* Render ceiling entities */
render_entities(g, camera, ceiling_depth_measure, show_hitboxes);
extern font_t font_rogue;
font_t const *old_font = dfont(&font_rogue);