From 6c1d0dfdb2998a583f23fd50e21e391ad520a3ae Mon Sep 17 00:00:00 2001 From: Klemen Plestenjak Date: Wed, 7 Feb 2024 17:14:17 +0100 Subject: [PATCH] Add hurt, die animation when taking damage --- game/components.c | 3 ++ game/components.h | 19 +++++++++++++ game/entity_factory.c | 26 +++++++++++++++++ game/entity_factory.h | 1 + game/game_tileset.h | 58 +++++++++++++++++++++++++++++++++++++- game/systems/s_animation.c | 20 +++++++------ game/systems/s_entity.c | 41 ++++++++++++++++++++++++--- game/systems/s_event.c | 30 ++++++++++++++++++++ game/systems/s_ui.c | 4 +-- game/systems/systems.c | 19 +++++++++++-- game/systems/systems.h | 14 ++++++--- game/utils.h | 7 +++++ rawAssets/game.tsj | 2 +- rawAssets/game.tsx | 2 +- scripts/extract_common.py | 28 ++++++++++++++++++ scripts/extract_tileset.py | 1 + 16 files changed, 251 insertions(+), 24 deletions(-) diff --git a/game/components.c b/game/components.c index de5b56c..cc0d30b 100644 --- a/game/components.c +++ b/game/components.c @@ -48,6 +48,8 @@ ECS_TAG_DECLARE(Buildable); ECS_TAG_DECLARE(Workable); ECS_TAG_DECLARE(Attackable); +ECS_COMPONENT_DECLARE(DelayDelete); + void initComponentIDs(ecs_world_t *ecs) { ECS_TAG_DEFINE(ecs, GameEntity); @@ -95,6 +97,7 @@ void initComponentIDs(ecs_world_t *ecs) { ECS_TAG_DEFINE(ecs, Workable); ECS_TAG_DEFINE(ecs, Attackable); + ECS_COMPONENT_DEFINE(ecs, DelayDelete); } #include diff --git a/game/components.h b/game/components.h index 392a3e1..d2ee4d8 100644 --- a/game/components.h +++ b/game/components.h @@ -156,6 +156,8 @@ typedef struct Animation { i32 curFrame; f32 elapsed; + + bool playInFull; } Animation; extern ECS_COMPONENT_DECLARE(Animation); @@ -183,6 +185,10 @@ extern ECS_COMPONENT_DECLARE(Easing); /********************************************************** * Event components *********************************************************/ +typedef struct DamageEvent { + f32 amount; +} DamageEvent; + typedef struct HarvestEvent { ResourceType type; i32 amount; @@ -240,6 +246,10 @@ extern ECS_COMPONENT_DECLARE(Worker); // Unit can: // - Attack typedef struct Unit { + f32 attackCooldown; + f32 attackElapsed; + f32 minDamage; + f32 maxDamage; f32 maxSpeed; f32 acceleration; f32 deceleration; @@ -273,6 +283,15 @@ extern ECS_COMPONENT_DECLARE(Harvestable); extern ECS_TAG_DECLARE(Buildable); extern ECS_TAG_DECLARE(Attackable); +/********************************************************** + * DelayDelete components + *********************************************************/ +typedef struct DelayDelete { + f32 time; + f32 elapsed; +} DelayDelete; +extern ECS_COMPONENT_DECLARE(DelayDelete); + void initComponentIDs(ecs_world_t *ecs); void igTagCheckbox(const char *label, ecs_world_t *ecs, diff --git a/game/entity_factory.c b/game/entity_factory.c index 8291b1a..1101bd8 100644 --- a/game/entity_factory.c +++ b/game/entity_factory.c @@ -56,6 +56,10 @@ ecs_entity_t entityCreateBaseUnit(const Position position, f32 size, Player play ecs_set(ECS, e, AIBlackboard, {.entity = e}); ecs_add_id(ECS, e, Selectable); ecs_set(ECS, e, Unit, { + .attackElapsed = 0.0f, + .attackCooldown = 1.0f, + .minDamage = 1.0f, + .maxDamage = 2.0f, .acceleration = 80.0f, .maxSpeed = 15.0f, .deceleration = 0.1f, @@ -67,6 +71,28 @@ ecs_entity_t entityCreateBaseUnit(const Position position, f32 size, Player play ecs_entity_t entityCreateSoldier(const Position position, Player player, Game *game) { ecs_entity_t e = entityCreateBaseUnit(position, 10.0f, player, ENTITY_SOLDIER, ANIM_IDLE, game); + ecs_set(ECS, e, Health, { + .startHP = 40.0f, + .hp = 40.0f, + .lastChanged = -1.0f + }); + Unit *unit = ecs_get_mut(ECS, e, Unit); + unit->minDamage = 5.0f; + unit->maxDamage = 10.0f; + unit->attackCooldown = 1.0f; + return e; +} +ecs_entity_t entityCreateWarrior(const Position position, Player player, Game *game) { + ecs_entity_t e = entityCreateBaseUnit(position, 10.0f, player, ENTITY_WARRIOR, ANIM_IDLE, game); + ecs_set(ECS, e, Health, { + .startHP = 80.0f, + .hp = 80.0f, + .lastChanged = -1.0f + }); + Unit *unit = ecs_get_mut(ECS, e, Unit); + unit->minDamage = 8.0f; + unit->maxDamage = 22.0f; + unit->attackCooldown = 1.8f; return e; } diff --git a/game/entity_factory.h b/game/entity_factory.h index 16a3235..d4df203 100644 --- a/game/entity_factory.h +++ b/game/entity_factory.h @@ -9,6 +9,7 @@ ecs_entity_t entityCreateEmpty(); ecs_entity_t entityCreateBaseUnit(const Position position, f32 size, Player player, EntityType type, AnimType startAnim, Game *game); ecs_entity_t entityCreateSoldier(const Position position, Player player, Game *game); +ecs_entity_t entityCreateWarrior(const Position position, Player player, Game *game); ecs_entity_t entityCreateWorker(const Position position, Player player, Game *game); ecs_entity_t entityRecruit(EntityType type, Position position, Player player, Game *game); diff --git a/game/game_tileset.h b/game/game_tileset.h index a714a5a..16521e3 100644 --- a/game/game_tileset.h +++ b/game/game_tileset.h @@ -744,6 +744,62 @@ static bool entityHasAnimation(EntityType entity, AnimType type) { } } +static f32 entityGetAnimationLength(EntityType entity, AnimType type) { + switch (entity) { + case ENTITY_WORKER: + switch (type) { + case ANIM_IDLE: return 0.4f; + case ANIM_WALK: return 0.72f; + case ANIM_HURT: return 0.42000000000000004f; + case ANIM_DIE: return 0.9000000000000001f; + default: break; + } + case ENTITY_SOLDIER: + switch (type) { + case ANIM_IDLE: return 0.4f; + case ANIM_WALK: return 0.72f; + case ANIM_HURT: return 0.42000000000000004f; + case ANIM_DIE: return 0.7000000000000001f; + default: break; + } + case ENTITY_WARRIOR: + switch (type) { + case ANIM_IDLE: return 0.4f; + case ANIM_WALK: return 0.72f; + case ANIM_HURT: return 0.42000000000000004f; + case ANIM_DIE: return 0.7000000000000001f; + default: break; + } + case ENTITY_MAGE: + switch (type) { + case ANIM_IDLE: return 0.4f; + case ANIM_WALK: return 0.72f; + case ANIM_HURT: return 0.42000000000000004f; + case ANIM_DIE: return 0.7000000000000001f; + default: break; + } + case ENTITY_ORC: + switch (type) { + case ANIM_IDLE: return 0.4f; + case ANIM_WALK: return 0.72f; + case ANIM_HURT: return 0.42000000000000004f; + case ANIM_DIE: return 0.7000000000000001f; + default: break; + } + case ENTITY_GOBLIN: + switch (type) { + case ANIM_IDLE: return 0.4f; + case ANIM_WALK: return 0.72f; + case ANIM_HURT: return 0.42000000000000004f; + case ANIM_DIE: return 0.7000000000000001f; + default: break; + } + default: break; + } + BZ_ASSERT(0); + return 0.0f; +} + static AnimationSequence entityGetAnimationSequence(EntityType entity, AnimType type) { switch (entity) { case ENTITY_WORKER: @@ -807,7 +863,7 @@ static AnimationFrame entityGetAnimationFrame(EntityType entity, AnimType type, case ANIM_IDLE: return ((AnimationFrame []) {{27, 0.2000f}, {28, 0.2000f}}) [frameIdx]; case ANIM_WALK: return ((AnimationFrame []) {{29, 0.1800f}, {30, 0.1800f}, {31, 0.1800f}, {30, 0.1800f}}) [frameIdx]; case ANIM_HURT: return ((AnimationFrame []) {{32, 0.1400f}, {33, 0.1400f}, {34, 0.1400f}}) [frameIdx]; - case ANIM_DIE: return ((AnimationFrame []) {{32, 0.1400f}, {33, 0.1400f}, {34, 0.1400f}, {35, 0.1400f}, {36, 0.1400f}}) [frameIdx]; + case ANIM_DIE: return ((AnimationFrame []) {{32, 0.1400f}, {33, 0.1400f}, {34, 0.1400f}, {35, 0.1400f}, {36, 0.3400f}}) [frameIdx]; default: break; } case ENTITY_SOLDIER: diff --git a/game/systems/s_animation.c b/game/systems/s_animation.c index eb61a75..8dec256 100644 --- a/game/systems/s_animation.c +++ b/game/systems/s_animation.c @@ -1,6 +1,7 @@ #include "systems.h" #include "../game_state.h" +#include "../utils.h" #include #include @@ -68,11 +69,6 @@ bool updateParticle(const Texture2D tex, Particle *particle, f32 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), @@ -107,10 +103,18 @@ Particle spawnParticle(const ParticleEmitter *emitter) { }; } +void animationSetState(Animation *anim, AnimType type, bool playInFull) { + anim->animType = type; + anim->sequence = entityGetAnimationSequence(anim->entityType, type); + anim->curFrame = 0; + anim->elapsed = 0; + anim->playInFull = playInFull; +} void updateAnimationState(ecs_iter_t *it) { Animation *anim = ecs_field(it, Animation, 1); TextureRegion *text = ecs_field(it, TextureRegion, 2); for (i32 i = 0; i < it->count; i++) { + if (anim->playInFull) continue; ecs_entity_t entity = it->entities[i]; AnimType type = ANIM_IDLE; if (ecs_has(ECS, entity, Velocity)) { @@ -123,10 +127,7 @@ void updateAnimationState(ecs_iter_t *it) { } if (type != anim[i].animType) { - anim[i].animType = type; - anim[i].sequence = entityGetAnimationSequence(anim[i].entityType, type); - anim[i].curFrame = 0; - anim[i].elapsed = 0; + animationSetState(&anim[i], type, false); } } } @@ -145,6 +146,7 @@ void updateAnimation(ecs_iter_t *it) { if (anim[i].elapsed < frame.duration) continue; i32 nextFrame = (anim[i].curFrame + 1) % seq.frameCount; + if (nextFrame == 0) anim[i].playInFull = false; anim[i].curFrame = nextFrame; anim[i].frame = entityGetAnimationFrame(anim[i].entityType, anim[i].animType, nextFrame); anim[i].elapsed = 0.0f; diff --git a/game/systems/s_entity.c b/game/systems/s_entity.c index 8887f9c..32bcb8e 100644 --- a/game/systems/s_entity.c +++ b/game/systems/s_entity.c @@ -5,6 +5,7 @@ #include "../input.h" #include "../pathfinding.h" #include "../entity_factory.h" +#include "../utils.h" #include #include @@ -144,16 +145,22 @@ void entityUpdateKinematic(ecs_iter_t *it) { position[i] = Vector2Add(position[i], Vector2Scale(velocity[i], dt)); } } -void entityUpdatePhysics(ecs_iter_t *it) { +void entityUpdate(ecs_iter_t *it) { Game *game = ecs_singleton_get_mut(ECS, Game); Position *position = ecs_field(it, Position, 1); HitBox *hitbox = ecs_field(it, HitBox, 2); Velocity *velocity = ecs_field(it, Velocity, 3); - SpatialGridID *spatialID = ecs_field(it, SpatialGridID, 4); + Unit *unit = ecs_field(it, Unit, 4); + Owner *owner = ecs_field(it, Owner, 5); + SpatialGridID *spatialID = ecs_field(it, SpatialGridID, 6); f32 dt = it->delta_time; for (i32 i = 0; i < it->count; i++) { + // Attack thingies + unit[i].attackElapsed += dt; + bool canAttack = unit[i].attackElapsed > unit[i].attackCooldown; + // Only update "stationary" entities bool stationary = Vector2Length(velocity[i]) <= 0.2f; Rectangle bounds = entityTransformHitBox(position[i], hitbox[i]); @@ -173,9 +180,35 @@ void entityUpdatePhysics(ecs_iter_t *it) { if (!CheckCollisionRecs(bounds, otherBounds)) { continue; } + + // Attack update + if (canAttack && ecs_has(ECS, other, Health) && ecs_has(ECS, other, Owner)) { + Health *otherHealth = ecs_get_mut(ECS, other, Health); + Player otherPlayer = ecs_get(ECS, other, Owner)->player; + + if (otherPlayer != owner[i].player) { + Rectangle collisionRec = GetCollisionRec(bounds, otherBounds); + f32 percentageCovered = (collisionRec.width * collisionRec.height) / (bounds.width * bounds.height); + + f32 dealDmg = randFloatRange(unit[i].minDamage, unit[i].maxDamage); + f32 multiplier = 1.0f + percentageCovered; + multiplier = Clamp(multiplier, 0.8f, 1.6f); + + dealDmg *= multiplier; + + damageEvent(other, (DamageEvent) { + .amount = dealDmg + }); + + canAttack = false; + unit[i].attackElapsed = 0.0f; + } + + } + + // Physics update slowDown += 0.1f; Position dif = Vector2Subtract(otherPos, position[i]); - //dif = Vector2Normalize(dif); dir = Vector2Add(dir, dif); } @@ -270,7 +303,7 @@ void updateBuildingRecruitment(ecs_iter_t *it) { } } -void renderHealth(ecs_iter_t *it) { +void renderHealthBar(ecs_iter_t *it) { Position *pos = ecs_field(it, Position, 1); HitBox *hitbox = ecs_field(it, HitBox, 2); Health *health = ecs_field(it, Health, 3); diff --git a/game/systems/s_event.c b/game/systems/s_event.c index eb241a6..04ba4bf 100644 --- a/game/systems/s_event.c +++ b/game/systems/s_event.c @@ -3,6 +3,36 @@ #include "../game_state.h" #include "../sounds.h" +void damageEvent(ecs_entity_t entity, DamageEvent event) { + BZ_ASSERT(ecs_has(ECS, entity, Health)); + + Health *health = ecs_get_mut(ECS, entity, Health); + health->hp -= event.amount; + + bool hasAnimation = ecs_has(ECS, entity, Animation); + if (hasAnimation && health->hp > 0) { + // Still alive, just play hurt anim + Animation *animation = ecs_get_mut(ECS, entity, Animation); + animationSetState(animation, ANIM_HURT, true); + } else if (hasAnimation) { + // Delay delete + Animation *animation = ecs_get_mut(ECS, entity, Animation); + animationSetState(animation, ANIM_DIE, true); + ecs_set(ECS, entity, DelayDelete, { + .time = entityGetAnimationLength(animation->entityType, ANIM_DIE) + }); + // Remove, so it becomes inactive + ecs_remove_id(ECS, entity, Selectable); + ecs_remove(ECS, entity, Health); + ecs_remove(ECS, entity, Unit); + ecs_remove(ECS, entity, Building); + } else { + // No animation, delete right away + ecs_delete(ECS, entity); + } + health->lastChanged = GetTime(); +} + i32 harvestEvent(ecs_entity_t entity, HarvestEvent event) { BZ_ASSERT(ecs_has_id(ECS, entity, ecs_id(Harvestable))); BZ_ASSERT(ecs_has(ECS, entity, Resource)); diff --git a/game/systems/s_ui.c b/game/systems/s_ui.c index c23fd79..2469cdc 100644 --- a/game/systems/s_ui.c +++ b/game/systems/s_ui.c @@ -291,9 +291,9 @@ void drawMainMenuUI(Game *game, f32 dt) { if (uiMainMenuButton("Play", true)) { setScreen(game, SCREEN_GAME); unloadMap(game); - //loadMap(game, "assets/maps/tree_test.tmj"); + loadMap(game, "assets/maps/tree_test.tmj"); //loadMap(game, "assets/maps/entity_test.tmj"); - loadMap(game, "assets/maps/map_01.tmj"); + //loadMap(game, "assets/maps/map_01.tmj"); } if (uiMainMenuButton("Settings", true)) { setScreen(game, SCREEN_SETTINGS); diff --git a/game/systems/systems.c b/game/systems/systems.c index aebb9b5..f634c9e 100644 --- a/game/systems/systems.c +++ b/game/systems/systems.c @@ -73,6 +73,19 @@ ECS_MOVE(Building, dst, src, { *dst = *src; }) +void delayDeleteUpdate(ecs_iter_t *it) { + DelayDelete *delay = ecs_field(it, DelayDelete, 1); + + f32 dt = GetFrameTime(); + + for (i32 i = 0; i < it->count; i++) { + delay[i].elapsed += dt; + + if (delay[i].elapsed >= delay[i].time) + ecs_delete(ECS, it->entities[i]); + } +} + void setupSystems() { ecs_set_hooks(ECS, SpatialGridID, { .dtor = ecs_dtor(SpatialGridID), @@ -99,7 +112,7 @@ void setupSystems() { ECS_SYSTEM(ECS, entityUpdateSpatialID, EcsOnUpdate, Position, HitBox, Velocity, SpatialGridID); ECS_SYSTEM(ECS, entityUpdateKinematic, EcsOnUpdate, Position, Velocity, Steering, Unit); - ECS_SYSTEM(ECS, entityUpdatePhysics, EcsOnUpdate, Position, HitBox, Velocity, SpatialGridID); + ECS_SYSTEM(ECS, entityUpdate, EcsOnUpdate, Position, HitBox, Velocity, Unit, Owner, SpatialGridID); ECS_SYSTEM(ECS, entityMoveToTarget, EcsOnUpdate, Position, Velocity, TargetPosition, Steering); ECS_SYSTEM(ECS, entityFollowPath, EcsOnUpdate, Path); @@ -113,12 +126,14 @@ void setupSystems() { ECS_SYSTEM(ECS, updateAnimation, EcsOnUpdate, Animation, TextureRegion); ECS_SYSTEM(ECS, updateEasingSystem, EcsOnUpdate, Easing, Position, HitBox, Rotation); - ECS_SYSTEM(ECS, renderHealth, EcsOnUpdate, Position, HitBox, Health); + ECS_SYSTEM(ECS, renderHealthBar, EcsOnUpdate, Position, HitBox, Health); ECS_SYSTEM(ECS, renderDebugPath, EcsOnUpdate, Path); ECS_SYSTEM(ECS, renderColliders, EcsOnUpdate, Position, HitBox); ECS_SYSTEM(ECS, renderOrientationDirection, EcsOnUpdate, Position, Orientation); + ECS_SYSTEM(ECS, delayDeleteUpdate, EcsOnUpdate, DelayDelete); + renderDebugPathSystem = renderDebugPath; renderOrientDirSystem = renderOrientationDirection; renderCollidersSystem = renderColliders; diff --git a/game/systems/systems.h b/game/systems/systems.h index 2b60102..89cdc3c 100644 --- a/game/systems/systems.h +++ b/game/systems/systems.h @@ -46,6 +46,7 @@ bool updateParticle(const Texture2D tex, Particle *particle, f32 dt); Particle spawnParticle(const ParticleEmitter *emitter); +void animationSetState(Animation *anim, AnimType type, bool playInFull); /* * 1: Animation * 2: TextureRegion @@ -117,13 +118,16 @@ void entityUpdateSpatialID(ecs_iter_t *it); void entityUpdateKinematic(ecs_iter_t *it); /* + * Big system for updating physics and attacking * 0: Game (singleton) for collisions * 1: Position * 2: HitBox - * 2: Velocity - * 3: SpatialGridID + * 3: Velocity + * 4: Unit + * 5: Owner + * 6: SpatialGridID */ -void entityUpdatePhysics(ecs_iter_t *it); +void entityUpdate(ecs_iter_t *it); /* * 1: Position @@ -152,7 +156,7 @@ void updateBuildingRecruitment(ecs_iter_t *it); * 2: HitBox * 3: Health */ -void renderHealth(ecs_iter_t *it); +void renderHealthBar(ecs_iter_t *it); /* * 1: Position @@ -177,6 +181,8 @@ void renderDebugPath(ecs_iter_t *it); * Event Systems **********************************/ +void damageEvent(ecs_entity_t entity, DamageEvent event); + i32 harvestEvent(ecs_entity_t entity, HarvestEvent event); void depositEvent(ecs_entity_t entity, DepositEvent event); diff --git a/game/utils.h b/game/utils.h index d94b6a7..2a8e1df 100644 --- a/game/utils.h +++ b/game/utils.h @@ -3,6 +3,7 @@ #include #include +#include static Rectangle getCameraBounds(Camera2D camera) { Rectangle bounds = { @@ -18,6 +19,12 @@ static Rectangle getCameraBounds(Camera2D camera) { return bounds; } +// 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] */ +} + // Implemented in main.c bool serializeGameData(const char *path, const GameData *gameData); bool deserializeGameData(const char *path, GameData *gameData); diff --git a/rawAssets/game.tsj b/rawAssets/game.tsj index 89c18f5..089f18d 100644 --- a/rawAssets/game.tsj +++ b/rawAssets/game.tsj @@ -432,7 +432,7 @@ "tileid":35 }, { - "duration":140, + "duration":340, "tileid":36 }], "id":35, diff --git a/rawAssets/game.tsx b/rawAssets/game.tsx index d67c1fe..f1d302c 100644 --- a/rawAssets/game.tsx +++ b/rawAssets/game.tsx @@ -199,7 +199,7 @@ - + diff --git a/scripts/extract_common.py b/scripts/extract_common.py index a0061b5..d45ba70 100644 --- a/scripts/extract_common.py +++ b/scripts/extract_common.py @@ -381,6 +381,34 @@ class EnumWriter: writer.block_end() writer.empty_line() + + def output_anim_length(self, func_name): + writer = self.writer + writer.output(f"static f32 {func_name}({self.enum_type} entity, {self.anim_type} type) ") + writer.block_start() + + writer.output("switch (entity) ") + writer.block_start() + for entity, animation_types in self.anim_map.items(): + writer.output(f"case {entity}:\n") + writer.indent() + writer.output("switch (type) ") + writer.block_start() + for anim_type in animation_types: + anim = self.anim_map[entity][anim_type] + duration = sum([x['duration'] * 0.001 for x in anim]) + writer.output(f"case {anim_type}: return {duration}f;\n") + writer.output("default: break;\n") + writer.block_end() + writer.unindent() + writer.output("default: break;\n") + writer.block_end() + + writer.output("BZ_ASSERT(0);\n") + writer.output("return 0.0f;\n") + writer.block_end() + writer.empty_line() + def output_anim_sequence(self, func_name): writer = self.writer writer.output(f"static AnimationSequence {func_name}({self.enum_type} entity, {self.anim_type} type) ") diff --git a/scripts/extract_tileset.py b/scripts/extract_tileset.py index 91accb6..ed31e66 100644 --- a/scripts/extract_tileset.py +++ b/scripts/extract_tileset.py @@ -90,6 +90,7 @@ anim_writer.output_enum_to_tile("getEntityTile") anim_writer.output_enum_to_str("getEntityStr") anim_writer.output_anim_enum_to_str("getEntityAnimationStr") anim_writer.output_has_anim("entityHasAnimation") +anim_writer.output_anim_length("entityGetAnimationLength") anim_writer.output_anim_sequence("entityGetAnimationSequence") anim_writer.output_anim_frame("entityGetAnimationFrame")