Add particles!

This commit is contained in:
2024-01-29 20:01:36 +01:00
parent a633c9cbdf
commit df581dcb9d
14 changed files with 354 additions and 2 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 KiB

After

Width:  |  Height:  |  Size: 441 KiB

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,11 @@ ECS_COMPONENT_DECLARE(Resource);
ECS_COMPONENT_DECLARE(Owner);
ECS_COMPONENT_DECLARE(EmitterAttachment);
ECS_COMPONENT_DECLARE(ParticleEmitter);
ECS_COMPONENT_DECLARE(ParticleLayer0);
ECS_COMPONENT_DECLARE(ParticleLayer1);
ECS_COMPONENT_DECLARE(SpatialGridID);
ECS_COMPONENT_DECLARE(Position);
ECS_COMPONENT_DECLARE(Size);
@@ -50,6 +55,11 @@ void initComponentIDs(ecs_world_t *ecs) {
ECS_COMPONENT_DEFINE(ecs, Owner);
ECS_COMPONENT_DEFINE(ecs, EmitterAttachment);
ECS_COMPONENT_DEFINE(ecs, ParticleEmitter);
ECS_COMPONENT_DEFINE(ecs, ParticleLayer0);
ECS_COMPONENT_DEFINE(ecs, ParticleLayer1);
ECS_COMPONENT_DEFINE(ecs, SpatialGridID);
ECS_COMPONENT_DEFINE(ecs, Position);
ECS_COMPONENT_DEFINE(ecs, Size);

View File

@@ -37,7 +37,69 @@ typedef struct Owner {
} Owner;
extern ECS_COMPONENT_DECLARE(Owner);
/**********************************************************
* Particle components
*********************************************************/
typedef struct EmitterAttachment {
ecs_entity_t baseEntity;
Vector2 offset;
} EmitterAttachment;
extern ECS_COMPONENT_DECLARE(EmitterAttachment);
typedef struct ParticleEmitter {
f32 emitterElapsed;
f32 emitterLifetime;
Vector2 pos;
struct ParticleEmitterData {
// offset
Vector2 minOffset, maxOffset;
// startVel
Vector2 minStartVel, maxStartVel;
// endVel
Vector2 minEndVel, maxEndVel;
// startSize
f32 minStartSize, maxStartSize;
// endSize
f32 minEndSize, maxEndSize;
// startRotSpeed
f32 minStartRotSpeed, maxStartRotSpeed;
// endRotSpeed
f32 minEndRotSpeed, maxEndRotSpeed;
// startColor
Color minStartColor, maxStartColor;
// endColor
Color minEndColor, maxEndColor;
// lifetime
f32 minLifetime, maxLifetime;
BlendMode blend;
BzTileID tileID;
} data;
ecs_entity_t targetParticles; // ParticleLayer0 / ParticleLayer1
} ParticleEmitter;
extern ECS_COMPONENT_DECLARE(ParticleEmitter);
typedef struct Particle {
Vector2 pos;
f32 rotation;
const Vector2 startVel, endVel;
const f32 startSize, endSize;
const f32 startRotSpeed, endRotSpeed;
const Color startColor, endColor;
const BlendMode blend: 8;
const BzTileID tileID: 24;
const f32 lifetime;
f32 elapsed;
} Particle;
static_assert(sizeof(Particle) == 64, "");
// 2 Separate components (Before entity, After entity), so we don't need to sort
typedef Particle ParticleLayer0, ParticleLayer1;
extern ECS_COMPONENT_DECLARE(ParticleLayer0);
extern ECS_COMPONENT_DECLARE(ParticleLayer1);
/**********************************************************
* Movement components

View File

@@ -94,6 +94,8 @@ typedef struct Game {
f32 elapsed;
ecs_query_t *drawQuery;
ecs_query_t *particles0Query;
ecs_query_t *particles1Query;
} Game;
extern ecs_world_t *ECS;

View File

@@ -34,6 +34,45 @@ static OwnerType getOwnerType(BzTileID tile) {
}
}
typedef enum ParticleType {
PARTICLE_NONE = -1,
PARTICLE_CIRCLE,
PARTICLE_SMALL_SQUARE,
PARTICLE_SQUARE,
PARTICLE_COUNT,
} ParticleType;
static BzTileID getParticleTypeTile(ParticleType type) {
switch (type) {
case PARTICLE_SQUARE: return 1559;
case PARTICLE_CIRCLE: return 1560;
case PARTICLE_SMALL_SQUARE: return 1561;
default: return -1;
}
}
static ParticleType getParticleType(BzTileID tile) {
switch (tile) {
case 1559:
return PARTICLE_SQUARE;
case 1560:
return PARTICLE_CIRCLE;
case 1561:
return PARTICLE_SMALL_SQUARE;
default:
return PARTICLE_COUNT;
}
}
static const char *getParticleTypeStr(ParticleType type) {
switch (type) {
case PARTICLE_SQUARE: return "square";
case PARTICLE_CIRCLE: return "circle";
case PARTICLE_SMALL_SQUARE: return "small_square";
default: return NULL;
}
}
typedef enum TerrainType {
TERRAIN_NONE = -1,
TERRAIN_GOLD_ORE,

View File

@@ -16,6 +16,7 @@
#include "pathfinding.h"
#include "sounds.h"
#include "entity_factory.h"
// Constants
const char *GAME_DATA_SAVE_PATH = "game_data.ini";
@@ -192,6 +193,12 @@ bool init(void *userData) {
{ ecs_id(TextureRegion) }
},
});
game->particles0Query = ecs_query(ECS, {
.filter.terms = { { ecs_id(ParticleLayer0) } }
});
game->particles1Query = ecs_query(ECS, {
.filter.terms = { { ecs_id(ParticleLayer1) } }
});
{
ECS_COMPONENT_DEFINE(ECS, InputState);
@@ -354,6 +361,8 @@ void deinit(void *userData) {
// Destroy queries
ecs_query_fini(input->queries.selected);
ecs_query_fini(game->drawQuery);
ecs_query_fini(game->particles0Query);
ecs_query_fini(game->particles1Query);
Game gameCopy = *game;
InputState inputCopy = *input;
@@ -530,6 +539,22 @@ static void renderGame(Game *game, float dt) {
DrawTexturePro(tex, draw.src, draw.dst, draw.origin, draw.rotation, c);
}
bzTileMapClearCollisionLayer(&game->map, COLL_LAYER_TRANSPARENCY);
// Draw particle layer 1
it = ecs_query_iter(ECS, game->particles1Query);
ecs_defer_begin(ECS);
while (ecs_iter_next(&it)) {
Particle *particle = ecs_field(&it, Particle, 1);
for (i32 i = 0; i < it.count; i++) {
DrawCircleV(particle[i].pos, 2.0f, RED);
if (updateParticle(tex, &particle[i], dt))
ecs_delete(ECS, it.entities[i]);
}
}
ecs_defer_end(ECS);
#if 0
Vector2 target = GetMousePosition();
target = GetScreenToWorld2D(target, game->camera);
static f32 elapsed = 0;
@@ -543,7 +568,6 @@ static void renderGame(Game *game, float dt) {
elapsed += dt * 2;
elapsed = Clamp(elapsed, 0, 1.0f);
attack = false;
#if 0
if (worker && false) {
Position *pos = ecs_get_mut(ECS, worker, Position);
DrawCircle(pos->x, pos->y, 2.0f, BLUE);
@@ -709,6 +733,58 @@ void imguiRender(float dt, void *userData) {
break;
}
igText("Input state: %s", inputState);
if (igCollapsingHeader_TreeNodeFlags("ParticleEditor", 0)) {
static ParticleEmitter emitter = {0.0f};
emitter.data.tileID = getParticleTypeTile(PARTICLE_CIRCLE);
emitter.data.blend = BLEND_ADDITIVE;
struct ParticleEmitterData *data = &emitter.data;
igSliderFloat2("Pos", &emitter.pos.x, 0.0f, 1000.0f, "%.2f", 0);
igSliderFloat2("minOffset", &data->minOffset.x, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat2("maxOffset", &data->maxOffset.x, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat2("minStartVel", &data->minStartVel.x, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat2("maxStartVel", &data->maxStartVel.x, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat2("minEndVel", &data->minEndVel.x, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat2("maxEndVel", &data->maxEndVel.x, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat("minStartSize", &data->minStartSize, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat("maxStartSize", &data->maxStartSize, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat("minEndSize", &data->minEndSize, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat("maxEndSize", &data->maxEndSize, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat("minStartRotSpeed", &data->minStartRotSpeed, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat("maxStartRotSpeed", &data->maxStartRotSpeed, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat("minEndRotSpeed", &data->minEndRotSpeed, -50.0f, 50.0f, "%.2f", 0);
igSliderFloat("maxEndRotSpeed", &data->maxEndRotSpeed, -50.0f, 50.0f, "%.2f", 0);
#define igColor(label, color) \
do { \
\
float cols[4] = { color.r / 255.0f, color.g / 255.0f, \
color.b / 255.0f, color.a / 255.0f }; \
igColorEdit4(label, cols, 0); \
color.r = cols[0] * 255.0f; \
color.g = cols[1] * 255.0f; \
color.b = cols[2] * 255.0f; \
color.a = cols[3] * 255.0f; \
} while (0)
igColor("minStartColor", data->minStartColor);
igColor("maxStartColor", data->maxStartColor);
igColor("minEndColor", data->minEndColor);
igColor("maxEndColor", data->maxEndColor);
#undef igColor
igSliderFloat("minLifetime", &data->minLifetime, 0.0f, 6.0f, "%.2f", 0);
igSliderFloat("maxLifetime", &data->maxLifetime, 0.0f, 6.0f, "%.2f", 0);
Particle particle = spawnParticle(&emitter);
ecs_entity_t e = entityCreateEmpty();
ecs_set_ptr(ECS, e, ParticleLayer1, &particle);
}
if (igCollapsingHeader_TreeNodeFlags("Selection", 0)) {
switch (input->state) {
case INPUT_SELECTED_UNITS:

View File

@@ -3,6 +3,109 @@
#include "../game_state.h"
#include <raymath.h>
#include <stdlib.h>
void updateParticleEmitter(ecs_iter_t *it) {
ParticleEmitter *emitter = ecs_field(it, ParticleEmitter, 1);
f32 dt = GetFrameTime();
for (i32 i = 0; i < it->count; i++) {
ecs_entity_t entity = it->entities[i];
if (ecs_has(ECS, entity, EmitterAttachment)) {
EmitterAttachment attachment = *ecs_get(ECS, entity, EmitterAttachment);
if (!ecs_is_alive(ECS, attachment.baseEntity)) {
ecs_delete(ECS, entity);
continue;
}
Vector2 pos = *ecs_get(ECS, entity, Position);
pos = Vector2Add(pos, attachment.offset);
emitter->pos = pos;
}
emitter[i].emitterElapsed += dt;
if (emitter[i].emitterElapsed >= emitter[i].emitterLifetime)
ecs_delete(ECS, entity);
}
}
static inline int lerpInt(int start, int end, int amount)
{
float result = start + amount*(end - start);
return result;
}
bool updateParticle(const Texture2D tex, Particle *particle, f32 dt) {
f32 alpha = particle->elapsed / particle->lifetime;
Vector2 vel = Vector2Lerp(particle->startVel, particle->endVel, alpha);
f32 size = Lerp(particle->startSize, particle->endSize, alpha);
f32 rot = Lerp(particle->startRotSpeed, particle->endRotSpeed, alpha);
Color startC = particle->startColor;
Color endC = particle->endColor;
Color color = {
.r = lerpInt(startC.r, endC.r, alpha),
.g = lerpInt(startC.g, endC.g, alpha),
.b = lerpInt(startC.b, endC.b, alpha),
.a = lerpInt(startC.a, endC.a, alpha)
};
//BeginBlendMode(particle->blend);
f32 hSize = size * 0.5f;
Vector2 center = particle->pos;
Rectangle src = getTextureRect(particle->tileID);
src.x += 0.1f;
src.y += 0.1f;
src.width -= 0.2f;
src.height -= 0.2f;
DrawTexturePro(tex, src, (Rectangle) {
center.x,
center.y,
size, size
}, (Vector2) {hSize, hSize}, rot, color);
//EndBlendMode();
particle->pos = Vector2Add(particle->pos, Vector2Scale(vel, dt));
particle->elapsed += dt;
return alpha >= 1.0f;
}
// https://stackoverflow.com/questions/13408990/how-to-generate-random-float-number-in-c
static inline f32 randFloatRange(f32 min, f32 max) {
float scale = rand() / (float) RAND_MAX; /* [0, 1.0] */
return min + scale * ( max - min ); /* [min, max] */
}
static inline Vector2 randVector2Range(Vector2 from, Vector2 to) {
return (Vector2) {
randFloatRange(from.x, to.x),
randFloatRange(from.y, to.y)
};
}
static inline Color randColorRange(Color from, Color to) {
return (Color) {
.r = GetRandomValue(from.r, to.r),
.g = GetRandomValue(from.g, to.g),
.b = GetRandomValue(from.b, to.b),
.a = GetRandomValue(from.a, to.a),
};
}
Particle spawnParticle(const ParticleEmitter *emitter) {
const struct ParticleEmitterData *data = &emitter->data;
return (Particle) {
.pos = Vector2Add(emitter->pos, randVector2Range(data->minOffset, data->maxOffset)),
.rotation = 0.0f,
.startVel = randVector2Range(data->minStartVel, data->maxStartVel),
.endVel = randVector2Range(data->minEndVel, data->maxEndVel),
.startSize = randFloatRange(data->minStartSize, data->maxStartSize),
.endSize = randFloatRange(data->minEndSize, data->maxEndSize),
.startRotSpeed = randFloatRange(data->minStartRotSpeed, data->maxStartRotSpeed),
.endRotSpeed = randFloatRange(data->minEndRotSpeed, data->maxEndRotSpeed),
.startColor = randColorRange(data->minStartColor, data->maxStartColor),
.endColor = randColorRange(data->minEndColor, data->maxEndColor),
.blend = data->blend,
.tileID = data->tileID,
.elapsed = 0.0f,
.lifetime = randFloatRange(data->minLifetime, data->maxLifetime),
};
}
void updateAnimationState(ecs_iter_t *it) {
Animation *anim = ecs_field(it, Animation, 1);

View File

@@ -32,6 +32,14 @@ void setAIBehaviour(ecs_entity_t entity, const BzBTNode *root,
* Animation Systems
**********************************/
/*
* 1: ParticleEmitter
*/
void updateParticleEmitter(ecs_iter_t *it);
bool updateParticle(const Texture2D tex, Particle *particle, f32 dt);
Particle spawnParticle(const ParticleEmitter *emitter);
/*
* 1: Animation

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 KiB

After

Width:  |  Height:  |  Size: 441 KiB

View File

@@ -2411,6 +2411,36 @@
"value":""
}]
},
{
"id":1559,
"properties":[
{
"name":"particle",
"type":"string",
"value":""
}],
"type":"square"
},
{
"id":1560,
"properties":[
{
"name":"particle",
"type":"string",
"value":""
}],
"type":"circle"
},
{
"id":1561,
"properties":[
{
"name":"particle",
"type":"string",
"value":""
}],
"type":"small_square"
},
{
"animation":[
{

View File

@@ -1108,6 +1108,21 @@
<property name="terrain" value=""/>
</properties>
</tile>
<tile id="1559" type="square">
<properties>
<property name="particle" value=""/>
</properties>
</tile>
<tile id="1560" type="circle">
<properties>
<property name="particle" value=""/>
</properties>
</tile>
<tile id="1561" type="small_square">
<properties>
<property name="particle" value=""/>
</properties>
</tile>
<tile id="1563" type="mage">
<properties>
<property name="animation" value="idle"/>

Binary file not shown.

View File

@@ -41,6 +41,7 @@ building_tiles = extract_by_property(all_tiles, "building")
entity_tiles = extract_by_property(all_tiles, "entity")
item_tiles = extract_by_property(all_tiles, "item")
ownership_tiles = extract_by_property(all_tiles, "ownership")
particle_tiles = extract_by_property(all_tiles, "particle")
writer.header_guard_start()
script_name = os.path.basename(__file__)
@@ -56,10 +57,16 @@ terrain_writer = EnumWriter(writer, terrain_tiles, "terrain")
building_writer = EnumWriter(writer, building_tiles, "building")
entity_writer = EnumWriter(writer, entity_tiles, "entity")
ownership_writer = EnumWriter(writer, ownership_tiles, "owner")
particle_write = EnumWriter(writer, particle_tiles, "particle")
ownership_writer.output_enum()
ownership_writer.output_tile_to_enum("getOwnerType")
particle_write.output_enum()
particle_write.output_enum_to_tile("getParticleTypeTile")
particle_write.output_tile_to_enum("getParticleType")
particle_write.output_enum_to_str("getParticleTypeStr")
terrain_writer.output_enum()
terrain_writer.output_tile_has_anim("terrainHasAnimation")
terrain_writer.output_tile_anim_sequence("terrainGetAnimationSequence")