fp: Start decoupling action from animation

This commit is contained in:
doyle 2023-09-18 00:12:58 +10:00
parent f847f5b0d0
commit f60099e7a3
3 changed files with 113 additions and 86 deletions

2
External/tely vendored

@ -1 +1 @@
Subproject commit 5bba4cddbc9352be4083fb1ec02dfbab2de0c115
Subproject commit b45db1a82cf5fb7f32d298c71b7dc0ac806a6f5f

View File

@ -214,40 +214,6 @@ void TELY_DLL_Init(void *user_data)
game->test_audio = platform->func_load_audio(assets, DQN_STRING8("Test Audio"), DQN_STRING8("Data/Audio/Purrple Cat - Moonwinds.qoa"));
}
void FP_Game_EntityChangeState(FP_GameEntity *entity, FP_GameEntityState state)
{
if (entity->state == state)
return;
entity->state = state;
entity->anim.frame = 0;
entity->anim.ticks = 0;
// decouple the state from the animation, e.g. the wall doesn't have an animation
uint16_t desired_sprite_anim_index = 0;
switch (state) {
case FP_GameEntityState_Nil: {
} break;
case FP_GameEntityState_Idle: {
desired_sprite_anim_index = TELY_Asset_GetSpriteAnim(entity->sprite_anims, DQN_STRING8("Idle"));
} break;
case FP_GameEntityState_Attack: {
desired_sprite_anim_index = TELY_Asset_GetSpriteAnim(entity->sprite_anims, DQN_STRING8("Attack A"));
} break;
case FP_GameEntityState_Run: {
desired_sprite_anim_index = TELY_Asset_GetSpriteAnim(entity->sprite_anims, DQN_STRING8("Running"));
} break;
}
if (entity->sprite_sheet && entity->sprite_anims.size) {
TELY_AssetSpriteAnimation const *sprite_anim = entity->sprite_anims.data + entity->anim.index;
entity->anim.index = desired_sprite_anim_index;
}
}
struct AStarNode
{
Dqn_usize cost;
@ -373,21 +339,6 @@ void FP_GameUpdate(TELY_Platform *platform, FP_Game *game, TELY_Renderer *render
if (game->clicked_entity.id) {
if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Delete))
FP_Game_DeleteEntity(game, game->clicked_entity);
FP_GameEntity *player = FP_Game_GetEntity(game, game->clicked_entity);
if (player) {
if (TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) {
FP_Game_EntityChangeState(player, FP_GameEntityState_Attack);
}
if (player->state != FP_GameEntityState_Attack) {
if (dir_vector.x || dir_vector.y) {
FP_Game_EntityChangeState(player, FP_GameEntityState_Run);
} else {
FP_Game_EntityChangeState(player, FP_GameEntityState_Idle);
}
}
}
} else {
game->camera.world_pos += dir_vector * 5.f;
}
@ -573,41 +524,107 @@ void FP_GameUpdate(TELY_Platform *platform, FP_Game *game, TELY_Renderer *render
entity->local_hit_box_size = padded_bbox.size;
}
// NOTE: Tick entity action ================================================================
// NOTE: Handle input on entity ============================================================
FP_GameEntityAction *action = &entity->action;
{
bool action_is_done = false;
TELY_AssetSpriteAnimation const *sprite_anim = entity->sprite_anims.data + entity->anim.index;
if (sprite_anim) {
if (entity->anim.frame >= sprite_anim->count) { // NOTE: Animation is finished
entity->anim.frame = 0;
entity->anim.ticks = 0;
action_is_done = true;
} else {
if (entity->anim.ticks++ > 4 /*ticks_per_anim_frame*/) {
entity->anim.frame++;
entity->anim.ticks = 0;
}
bool we_are_clicked_entity = entity->handle == game->clicked_entity;
if (action->state == FP_GameEntityState_Nil) {
action->state = FP_GameEntityState_Idle;
action->dirty = true;
}
if (action->state == FP_GameEntityState_Idle) {
if (we_are_clicked_entity && TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) {
action->state = FP_GameEntityState_AttackA;
action->dirty = true;
} else if (we_are_clicked_entity && (dir_vector.x || dir_vector.y)) {
action->state = FP_GameEntityState_Run;
action->dirty = true;
} else if (action->dirty) {
action->anim = {};
action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Idle")).index;
action->timer_s = FP_GAME_ENTITY_ACTION_INFINITE_TIMER;
action->dirty = false;
}
}
if (action_is_done) {
switch (entity->state) {
case FP_GameEntityState_Nil: break;
case FP_GameEntityState_Idle: break;
case FP_GameEntityState_Attack: {
FP_Game_EntityChangeState(entity, FP_GameEntityState_Idle);
} break;
case FP_GameEntityState_Run: {
if (dir_vector.x == 0 && dir_vector.y == 0)
FP_Game_EntityChangeState(entity, FP_GameEntityState_Idle);
} break;
if (action->state == FP_GameEntityState_AttackA) {
if (action->dirty) {
action->anim = {};
action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack A")).index;
action->timer_s = action->anim.asset->count * 0.016f;
action->dirty = false;
} else if (action->timer_s <= 0.f) {
action->state = FP_GameEntityState_Idle;
action->dirty = true;
} else if (we_are_clicked_entity && TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) {
// NOTE: Trigger AttackB if action is at the correct time
action->state = FP_GameEntityState_AttackB;
action->dirty = true;
}
}
if (action->state == FP_GameEntityState_AttackB) {
if (action->dirty) {
action->anim = {};
action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack B")).index;
action->timer_s = action->anim.asset->count * 0.016f;
action->dirty = false;
} else if (action->timer_s <= 0.f) {
action->state = FP_GameEntityState_Idle;
action->dirty = true;
} else if (we_are_clicked_entity && TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_J)) {
// NOTE: Trigger AttackC if action is at the correct time
action->state = FP_GameEntityState_AttackC;
action->dirty = true;
}
}
if (action->state == FP_GameEntityState_AttackC) {
if (action->dirty) {
action->anim = {};
action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Attack C")).index;
action->timer_s = action->anim.asset->count * 0.016f;
action->dirty = false;
} else if (action->timer_s <= 0.f) {
action->state = FP_GameEntityState_Idle;
action->dirty = true;
}
}
if (action->state == FP_GameEntityState_Run) {
if (action->dirty) {
action->anim = {};
action->anim.asset = entity->sprite_anims.data + TELY_Asset_GetSpriteAnimation(entity->sprite_anims, DQN_STRING8("Run")).index;
action->timer_s = FP_GAME_ENTITY_ACTION_INFINITE_TIMER;
action->dirty = false;
}
if (we_are_clicked_entity && (dir_vector.x == 0.f && dir_vector.y == 0.f)) {
action->state = FP_GameEntityState_Idle;
action->dirty = true;
}
}
// NOTE: Tick entity action ================================================================
TELY_AssetSpriteAnimation const *anim = action->anim.asset;
if (anim) {
if (action->anim.frame >= anim->count) { // NOTE: Animation is finished
action->anim.frame = 0;
action->anim.ticks = 0;
} else if (action->anim.ticks++ > 4 /*ticks_per_anim_frame*/) {
action->anim.frame++;
action->anim.ticks = 0;
}
}
if (action->timer_s != FP_GAME_ENTITY_ACTION_INFINITE_TIMER) {
action->timer_s = DQN_MAX(action->timer_s - DQN_CAST(Dqn_f32)input->delta_s, 0.f);
}
}
// NOTE: Calculate entity attack box =======================================================
if (entity->state == FP_GameEntityState_Attack) {
if (action->state == FP_GameEntityState_AttackA) {
entity->attack_box_size = entity->local_hit_box_size;
TELY_AssetSpriteSheet const *sprite_sheet = entity->sprite_sheet;
@ -720,11 +737,11 @@ void FP_GameRender(FP_Game *game, TELY_Platform *platform, TELY_Renderer *render
}
// NOTE: Render entity sprites =============================================================
if (entity->sprite_sheet && entity->sprite_anims.size) {
if (entity->sprite_sheet && entity->action.anim.asset) {
TELY_AssetSpriteSheet const *sprite_sheet = entity->sprite_sheet;
TELY_AssetSpriteAnimation const *sprite_anim = entity->sprite_anims.data + entity->anim.index;
TELY_AssetSpriteAnimation const *sprite_anim = entity->action.anim.asset;
Dqn_usize sprite_index = (sprite_anim->index + (entity->anim.frame % sprite_anim->count)) % sprite_sheet->sprite_count;
Dqn_usize sprite_index = (sprite_anim->index + (entity->action.anim.frame % sprite_anim->count)) % sprite_sheet->sprite_count;
Dqn_usize sprite_sheet_row = sprite_index / sprite_sheet->sprites_per_row;
Dqn_usize sprite_sheet_column = sprite_index % sprite_sheet->sprites_per_row;

View File

@ -46,16 +46,18 @@ enum FP_GameEntityState
{
FP_GameEntityState_Nil,
FP_GameEntityState_Idle,
FP_GameEntityState_Attack,
FP_GameEntityState_AttackA,
FP_GameEntityState_AttackB,
FP_GameEntityState_AttackC,
FP_GameEntityState_Run,
};
struct FP_GameEntityAnimation
{
uint16_t index;
uint16_t frame;
uint16_t ticks;
uint16_t ticks_per_frame;
TELY_AssetSpriteAnimation *asset;
uint16_t frame;
uint16_t ticks;
uint16_t ticks_per_frame;
};
struct FP_GameWaypoint
@ -65,6 +67,15 @@ struct FP_GameWaypoint
FP_GameWaypoint *prev;
};
Dqn_f32 const FP_GAME_ENTITY_ACTION_INFINITE_TIMER = -1.f;
struct FP_GameEntityAction
{
FP_GameEntityState state;
bool dirty;
FP_GameEntityAnimation anim;
Dqn_f32 timer_s;
};
struct FP_GameEntity
{
Dqn_String8 name;
@ -72,8 +83,7 @@ struct FP_GameEntity
TELY_AssetSpriteSheet *sprite_sheet;
Dqn_Slice<TELY_AssetSpriteAnimation> sprite_anims;
Dqn_V2 size_scale;
FP_GameEntityAnimation anim;
FP_GameEntityState state;
FP_GameEntityAction action;
Dqn_V2 velocity;
FP_GameEntityHandle stalk_entity;