From ac8ca29d0e4dec7b621225457f6eb5179655ea14 Mon Sep 17 00:00:00 2001 From: Holger Schemel Date: Sun, 16 Jun 2024 10:28:12 +0200 Subject: [PATCH] improved smooth movement animation for BD engine (complete rewrite) This is a complete rewrite of the smooth movement animation code for the BD game and graphics engine. The previous code was a mess, and still did not cover all cases (like game elements leaving a tile while another game element is entering the same tile in the same cycle). The new code only redraws a single playfield tile (without also trying to redraw the corresponding adjacent tile related to element movement) by drawing the tile background, the part of the element leaving that tile and the part of the element entering that tile (without redrawing the neighboring tile where the movement starts or stops, which is now redrawn completely independently, allowing for correctly handling all special cases in a much more clean way). --- src/game_bd/bd_caveengine.c | 16 +++- src/game_bd/bd_elements.h | 3 +- src/game_bd/bd_gameplay.c | 37 +++++--- src/game_bd/bd_gameplay.h | 3 +- src/game_bd/bd_graphics.c | 185 +++++++++++++++++------------------- src/game_bd/export_bd.h | 3 +- src/game_bd/main_bd.c | 9 +- 7 files changed, 140 insertions(+), 116 deletions(-) diff --git a/src/game_bd/bd_caveengine.c b/src/game_bd/bd_caveengine.c index 5b2bfcdf..9df249eb 100644 --- a/src/game_bd/bd_caveengine.c +++ b/src/game_bd/bd_caveengine.c @@ -543,6 +543,9 @@ static inline boolean is_space_dir(const GdCave *cave, const int x, const int y, static inline void store_dir_buffer(GdCave *cave, const int x, const int y, const GdDirection dir) { + int old_x = x; + int old_y = y; + // raw values without range correction int raw_x = x + gd_dx[dir]; int raw_y = y + gd_dy[dir]; @@ -552,7 +555,18 @@ static inline void store_dir_buffer(GdCave *cave, const int x, const int y, cons int new_y = gety(cave, raw_x, raw_y); int new_dir = (dir > GD_MV_TWICE ? dir - GD_MV_TWICE : dir); - game_bd.game->dir_buffer[new_y][new_x] = new_dir; + // if tile is moving two steps at once, correct old position + if (dir > GD_MV_TWICE) + { + raw_x = x + gd_dx[new_dir]; + raw_y = y + gd_dy[new_dir]; + + old_x = getx(cave, raw_x, raw_y); + old_y = gety(cave, raw_x, raw_y); + } + + game_bd.game->dir_buffer_from[old_y][old_x] = new_dir; + game_bd.game->dir_buffer_to[new_y][new_x] = new_dir; } // store an element at the given position diff --git a/src/game_bd/bd_elements.h b/src/game_bd/bd_elements.h index dd36b1ee..446dfc2d 100644 --- a/src/game_bd/bd_elements.h +++ b/src/game_bd/bd_elements.h @@ -319,10 +319,9 @@ typedef enum _element SCANNED = 0x100, COVERED = 0x200, - SKIPPED = 0x400, // binary AND this to elements to get rid of properties above. - O_MASK = ~(SCANNED | COVERED | SKIPPED) + O_MASK = ~(SCANNED | COVERED) } GdElement; typedef enum _sound diff --git a/src/game_bd/bd_gameplay.c b/src/game_bd/bd_gameplay.c index 085dad08..0b4f48cd 100644 --- a/src/game_bd/bd_gameplay.c +++ b/src/game_bd/bd_gameplay.c @@ -26,8 +26,10 @@ void gd_game_free(GdGame *game) gd_cave_map_free(game->element_buffer); if (game->last_element_buffer) gd_cave_map_free(game->last_element_buffer); - if (game->dir_buffer) - gd_cave_map_free(game->dir_buffer); + if (game->dir_buffer_from) + gd_cave_map_free(game->dir_buffer_from); + if (game->dir_buffer_to) + gd_cave_map_free(game->dir_buffer_to); if (game->gfx_buffer) gd_cave_map_free(game->gfx_buffer); @@ -89,10 +91,15 @@ static void load_cave(GdGame *game) gd_cave_map_free(game->last_element_buffer); game->last_element_buffer = NULL; - // delete direction buffer - if (game->dir_buffer) - gd_cave_map_free(game->dir_buffer); - game->dir_buffer = NULL; + // delete direction buffer (from) + if (game->dir_buffer_from) + gd_cave_map_free(game->dir_buffer_from); + game->dir_buffer_from = NULL; + + // delete direction buffer (to) + if (game->dir_buffer_to) + gd_cave_map_free(game->dir_buffer_to); + game->dir_buffer_to = NULL; // delete gfx buffer if (game->gfx_buffer) @@ -138,12 +145,19 @@ static void load_cave(GdGame *game) for (x = 0; x < game->cave->w; x++) game->last_element_buffer[y][x] = O_NONE; - // create new direction buffer - game->dir_buffer = gd_cave_map_new(game->cave, int); + // create new direction buffer (from) + game->dir_buffer_from = gd_cave_map_new(game->cave, int); + + for (y = 0; y < game->cave->h; y++) + for (x = 0; x < game->cave->w; x++) + game->dir_buffer_from[y][x] = GD_MV_STILL; + + // create new direction buffer (to) + game->dir_buffer_to = gd_cave_map_new(game->cave, int); for (y = 0; y < game->cave->h; y++) for (x = 0; x < game->cave->w; x++) - game->dir_buffer[y][x] = GD_MV_STILL; + game->dir_buffer_to[y][x] = GD_MV_STILL; // create new gfx buffer game->gfx_buffer = gd_cave_map_new(game->cave, int); @@ -392,8 +406,9 @@ static GdGameState gd_game_main_int(GdGame *game, boolean allow_iterate, boolean { for (x = 0; x < game->cave->w; x++) { - game->last_element_buffer[y][x] = game->element_buffer[y][x] & ~SKIPPED; - game->dir_buffer[y][x] = GD_MV_STILL; + game->last_element_buffer[y][x] = game->element_buffer[y][x]; + game->dir_buffer_from[y][x] = GD_MV_STILL; + game->dir_buffer_to[y][x] = GD_MV_STILL; } } diff --git a/src/game_bd/bd_gameplay.h b/src/game_bd/bd_gameplay.h index 999c5e84..2ea5d8c4 100644 --- a/src/game_bd/bd_gameplay.h +++ b/src/game_bd/bd_gameplay.h @@ -77,7 +77,8 @@ typedef struct _gd_game int state_counter; // counter used to control the game flow, rendering of caves int **element_buffer; int **last_element_buffer; - int **dir_buffer; + int **dir_buffer_from; + int **dir_buffer_to; int **gfx_buffer; // contains the indexes to the cells; // created by *start_level, deleted by *stop_game int itercycle; diff --git a/src/game_bd/bd_graphics.c b/src/game_bd/bd_graphics.c index 5da166f2..fa139d0c 100644 --- a/src/game_bd/bd_graphics.c +++ b/src/game_bd/bd_graphics.c @@ -561,7 +561,6 @@ static inline boolean el_collectible(const int element) { return (gd_elements[element & O_MASK].properties & P_COLLECTIBLE) != 0; } -#endif // returns true if the element is pushable static inline boolean el_pushable(const int element) @@ -580,6 +579,7 @@ static inline boolean el_falling(const int element) { return (gd_elements[element & O_MASK].properties & P_FALLING) != 0; } +#endif // returns true if the element is growing static inline boolean el_growing(const int element) @@ -600,55 +600,57 @@ static void gd_drawcave_tile(Bitmap *dest, GdGame *game, int x, int y, boolean d GdCave *cave = game->cave; int sx = x * cell_size - scroll_x; int sy = y * cell_size - scroll_y; - int dir = game->dir_buffer[y][x]; + int dir_from = game->dir_buffer_from[y][x]; + int dir_to = game->dir_buffer_to[y][x]; int tile = game->element_buffer[y][x]; + int tile_last = game->last_element_buffer[y][x]; + int tile_from = O_NONE; // source element if element is moving (will be set later) + int tile_to = tile; // target element if element is moving int frame = game->animcycle; - struct GraphicInfo_BD *g = &graphic_info_bd_object[tile][frame]; - Bitmap *tile_bitmap = gd_get_tile_bitmap(g->bitmap); - boolean is_movable = (el_can_move(tile) || el_falling(tile) || el_pushable(tile) || - el_player(tile)); - boolean is_movable_or_diggable = (is_movable || el_diggable(game->last_element_buffer[y][x])); - boolean is_moving = (is_movable_or_diggable && dir != GD_MV_STILL); + boolean is_moving_from = (dir_from != GD_MV_STILL); + boolean is_moving_to = (dir_to != GD_MV_STILL); + boolean is_moving = (is_moving_from || is_moving_to); boolean use_smooth_movements = use_bd_smooth_movements(); - // do not use smooth movement animation for growing or exploding game elements - if ((el_growing(tile) || el_explosion(tile)) && dir != GD_MV_STILL) + // if element is moving away from this tile, determine element that is moving + if (is_moving_from) { - int dx = (dir == GD_MV_LEFT ? +1 : dir == GD_MV_RIGHT ? -1 : 0); - int dy = (dir == GD_MV_UP ? +1 : dir == GD_MV_DOWN ? -1 : 0); - int old_x = cave->getx(cave, x + dx, y + dy); - int old_y = cave->gety(cave, x + dx, y + dy); - int last_tile_from = game->last_element_buffer[old_y][old_x] & ~SKIPPED; - boolean old_is_player = el_player(last_tile_from); - - // check special case of player running into enemy from top or left side - if (old_is_player) - { - game->element_buffer[y][x] = (dir == GD_MV_LEFT ? O_PLAYER_LEFT : - dir == GD_MV_RIGHT ? O_PLAYER_RIGHT : - dir == GD_MV_UP ? O_PLAYER_UP : - dir == GD_MV_DOWN ? O_PLAYER_DOWN : O_PLAYER); + int dx = (dir_from == GD_MV_LEFT ? -1 : dir_from == GD_MV_RIGHT ? +1 : 0); + int dy = (dir_from == GD_MV_UP ? -1 : dir_from == GD_MV_DOWN ? +1 : 0); + int new_x = cave->getx(cave, x + dx, y + dy); + int new_y = cave->gety(cave, x + dx, y + dy); - // draw player running into explosion (else player would disappear immediately) - gd_drawcave_tile(dest, game, x, y, draw_masked); + tile_from = game->element_buffer[new_y][new_x]; - game->element_buffer[y][x] = tile; - } - - use_smooth_movements = FALSE; + // handle special case of player running into enemy/explosion from top or left side + if ((el_growing(tile_from) || el_explosion(tile_from)) && el_player(tile_last)) + tile_from = tile_last; } + // -------------------------------------------------------------------------------- + // check if we should use smooth movement animations or not + // -------------------------------------------------------------------------------- + // do not use smooth movement animation for player entering exit (engine stopped) if (cave->player_state == GD_PL_EXITED) use_smooth_movements = FALSE; + // never treat empty space as "moving" (source tile if player is snapping) + if (tile_from == O_SPACE) + use_smooth_movements = FALSE; + // do not use smooth movement animation for player stirring the pot - if (tile == O_PLAYER_STIRRING) + if (tile_from == O_PLAYER_STIRRING || tile_to == O_PLAYER_STIRRING) + use_smooth_movements = FALSE; + + // do not use smooth movement animation for growing or exploding game elements + if (el_growing(tile) || el_explosion(tile)) use_smooth_movements = FALSE; #if DO_GFX_SANITY_CHECK if (use_native_bd_graphics_engine() && !setup.small_game_graphics && !program.headless) { + struct GraphicInfo_BD *g = &graphic_info_bd_object[tile][frame]; int old_x = (game->gfx_buffer[y][x] % GD_NUM_OF_CELLS) % GD_NUM_OF_CELLS_X; int old_y = (game->gfx_buffer[y][x] % GD_NUM_OF_CELLS) / GD_NUM_OF_CELLS_X; int new_x = g->src_x / g->width; @@ -668,85 +670,77 @@ static void gd_drawcave_tile(Bitmap *dest, GdGame *game, int x, int y, boolean d // if game element not moving (or no smooth movements requested), simply draw tile if (!is_moving || !use_smooth_movements) { + struct GraphicInfo_BD *g = &graphic_info_bd_object[tile][frame]; + Bitmap *tile_bitmap = gd_get_tile_bitmap(g->bitmap); + blit_bitmap(tile_bitmap, dest, g->src_x, g->src_y, cell_size, cell_size, sx, sy); return; } + // -------------------------------------------------------------------------------- // draw smooth animation for game element moving between two cave tiles + // -------------------------------------------------------------------------------- - if (!(game->last_element_buffer[y][x] & SKIPPED)) + // ---------- 1st step: draw background element for this tile ---------- { - // redraw previous game element on the cave field the new element is moving to - int tile_last = game->last_element_buffer[y][x]; - - // only redraw previous game element if it is diggable (like dirt etc.) - if (!el_diggable(tile_last)) - tile_last = O_SPACE; - - struct GraphicInfo_BD *g_old = &graphic_info_bd_object[tile_last][frame]; - Bitmap *tile_bitmap_old = gd_get_tile_bitmap(g_old->bitmap); + int tile_back = (!is_moving_to ? tile : el_diggable(tile_last) ? tile_last : O_SPACE); + struct GraphicInfo_BD *g = &graphic_info_bd_object[tile_back][frame]; + Bitmap *tile_bitmap = gd_get_tile_bitmap(g->bitmap); - blit_bitmap(tile_bitmap_old, dest, g_old->src_x, g_old->src_y, cell_size, cell_size, sx, sy); - } - - // get cave field position the game element is moving from - int dx = (dir == GD_MV_LEFT ? +1 : dir == GD_MV_RIGHT ? -1 : 0); - int dy = (dir == GD_MV_UP ? +1 : dir == GD_MV_DOWN ? -1 : 0); - int old_x = cave->getx(cave, x + dx, y + dy); - int old_y = cave->gety(cave, x + dx, y + dy); - int tile_from = game->element_buffer[old_y][old_x] & ~SKIPPED; // should never be skipped - int tile_last = game->last_element_buffer[y][x] & ~SKIPPED; - struct GraphicInfo_BD *g_from = &graphic_info_bd_object[tile_from][frame]; - Bitmap *tile_bitmap_from = gd_get_tile_bitmap(g_from->bitmap); - boolean old_is_player = el_player(tile_from); - boolean old_is_moving = (game->dir_buffer[old_y][old_x] != GD_MV_STILL); - boolean old_is_visible = (old_x >= cave->x1 && - old_x <= cave->x2 && - old_y >= cave->y1 && - old_y <= cave->y2); - - // never treat empty space as "moving" (may happen if player is snap-pushing element) - if (tile_from == O_SPACE) - old_is_moving = FALSE; - - if (old_is_visible) - { - if (!old_is_moving && !old_is_player) - { - // redraw game element on the cave field the element is moving from - blit_bitmap(tile_bitmap_from, dest, g_from->src_x, g_from->src_y, cell_size, cell_size, - sx + dx * cell_size, sy + dy * cell_size); - - game->element_buffer[old_y][old_x] |= SKIPPED; - } - else - { - // if old tile also moving (like pushing player), do not redraw tile background - // (but redraw if tile and old tile are moving/falling into different directions) - if (game->dir_buffer[old_y][old_x] == game->dir_buffer[y][x]) - game->last_element_buffer[old_y][old_x] |= SKIPPED; - } + blit_bitmap(tile_bitmap, dest, g->src_x, g->src_y, cell_size, cell_size, sx, sy); } // get shifted position between cave fields the game element is moving from/to int itercycle = MIN(MAX(0, game->itermax - game->itercycle - 1), game->itermax); int shift = cell_size * itercycle / game->itermax; - // when drawing player over walkable elements, always use masked drawing - // (does not use masking if moving from walkable to diggable tiles etc.) - if (el_player(tile) && el_walkable(tile_from) && el_walkable(tile_last)) - blit_bitmap = BlitBitmapMasked; + // ---------- 2nd step: draw element that is moving away from this tile ---------- - blit_bitmap(tile_bitmap, dest, g->src_x, g->src_y, cell_size, cell_size, - sx + dx * shift, sy + dy * shift); + if (is_moving_from) + { + struct GraphicInfo_BD *g = &graphic_info_bd_object[tile_from][frame]; + Bitmap *tile_bitmap = gd_get_tile_bitmap(g->bitmap); + int dx = (dir_from == GD_MV_LEFT ? -1 : dir_from == GD_MV_RIGHT ? +1 : 0); + int dy = (dir_from == GD_MV_UP ? -1 : dir_from == GD_MV_DOWN ? +1 : 0); + int xsize = (dx != 0 ? shift : cell_size); + int ysize = (dy != 0 ? shift : cell_size); + int gx = g->src_x + (dx < 0 ? cell_size - shift : 0); + int gy = g->src_y + (dy < 0 ? cell_size - shift : 0); + int tx = sx + (dx < 0 ? 0 : dx > 0 ? cell_size - shift : 0); + int ty = sy + (dy < 0 ? 0 : dy > 0 ? cell_size - shift : 0); + + if (el_walkable(tile)) + blit_bitmap = BlitBitmapMasked; + + blit_bitmap(tile_bitmap, dest, gx, gy, xsize, ysize, tx, ty); + + // when using dynamic scheduling (mainly BD1 levels), redraw tile in next frame + game->gfx_buffer[y][x] |= GD_REDRAW; + } - // special case: redraw player snapping a game element - if (old_is_visible && old_is_player && !old_is_moving) + // ---------- 3rd step: draw element that is moving towards this tile ---------- + + if (is_moving_to) { - // redraw game element on the cave field the element is moving from - blit_bitmap(tile_bitmap_from, dest, g_from->src_x, g_from->src_y, cell_size, cell_size, - sx + dx * cell_size, sy + dy * cell_size); + struct GraphicInfo_BD *g = &graphic_info_bd_object[tile_to][frame]; + Bitmap *tile_bitmap = gd_get_tile_bitmap(g->bitmap); + int dx = (dir_to == GD_MV_LEFT ? +1 : dir_to == GD_MV_RIGHT ? -1 : 0); + int dy = (dir_to == GD_MV_UP ? +1 : dir_to == GD_MV_DOWN ? -1 : 0); + int xsize = (dx != 0 ? cell_size - shift : cell_size); + int ysize = (dy != 0 ? cell_size - shift : cell_size); + int gx = g->src_x + (dx < 0 ? shift : 0); + int gy = g->src_y + (dy < 0 ? shift : 0); + int tx = sx + (dx < 0 ? 0 : dx > 0 ? shift : 0); + int ty = sy + (dy < 0 ? 0 : dy > 0 ? shift : 0); + + if (is_moving_from) + blit_bitmap = BlitBitmapMasked; + + blit_bitmap(tile_bitmap, dest, gx, gy, xsize, ysize, tx, ty); + + // when using dynamic scheduling (mainly BD1 levels), redraw tile in next frame + game->gfx_buffer[y][x] |= GD_REDRAW; } } @@ -791,12 +785,9 @@ int gd_drawcave(Bitmap *dest, GdGame *game, boolean force_redraw) { if (redraw_all || game->gfx_buffer[y][x] & GD_REDRAW || - game->dir_buffer[y][x] != GD_MV_STILL) + game->dir_buffer_from[y][x] != GD_MV_STILL || + game->dir_buffer_to[y][x] != GD_MV_STILL) { - // skip redrawing already drawn element with movement - if (game->element_buffer[y][x] & SKIPPED) - continue; - // now we have drawn it game->gfx_buffer[y][x] = game->gfx_buffer[y][x] & ~GD_REDRAW; diff --git a/src/game_bd/export_bd.h b/src/game_bd/export_bd.h index 883d4ace..7778b066 100644 --- a/src/game_bd/export_bd.h +++ b/src/game_bd/export_bd.h @@ -83,7 +83,8 @@ struct EngineSnapshotInfo_BD // data from pointers in game structure int element_buffer[MAX_PLAYFIELD_WIDTH][MAX_PLAYFIELD_HEIGHT]; int last_element_buffer[MAX_PLAYFIELD_WIDTH][MAX_PLAYFIELD_HEIGHT]; - int dir_buffer[MAX_PLAYFIELD_WIDTH][MAX_PLAYFIELD_HEIGHT]; + int dir_buffer_from[MAX_PLAYFIELD_WIDTH][MAX_PLAYFIELD_HEIGHT]; + int dir_buffer_to[MAX_PLAYFIELD_WIDTH][MAX_PLAYFIELD_HEIGHT]; int gfx_buffer[MAX_PLAYFIELD_WIDTH][MAX_PLAYFIELD_HEIGHT]; GdCave cave; diff --git a/src/game_bd/main_bd.c b/src/game_bd/main_bd.c index 34a75074..4404d0ab 100644 --- a/src/game_bd/main_bd.c +++ b/src/game_bd/main_bd.c @@ -574,7 +574,8 @@ void SaveEngineSnapshotValues_BD(void) { engine_snapshot_bd.element_buffer[x][y] = game->element_buffer[y][x]; engine_snapshot_bd.last_element_buffer[x][y] = game->last_element_buffer[y][x]; - engine_snapshot_bd.dir_buffer[x][y] = game->dir_buffer[y][x]; + engine_snapshot_bd.dir_buffer_from[x][y] = game->dir_buffer_from[y][x]; + engine_snapshot_bd.dir_buffer_to[x][y] = game->dir_buffer_to[y][x]; engine_snapshot_bd.gfx_buffer[x][y] = game->gfx_buffer[y][x]; } } @@ -605,7 +606,8 @@ void LoadEngineSnapshotValues_BD(void) engine_snapshot_bd.game.element_buffer = game->element_buffer; engine_snapshot_bd.game.last_element_buffer = game->last_element_buffer; - engine_snapshot_bd.game.dir_buffer = game->dir_buffer; + engine_snapshot_bd.game.dir_buffer_from = game->dir_buffer_from; + engine_snapshot_bd.game.dir_buffer_to = game->dir_buffer_to; engine_snapshot_bd.game.gfx_buffer = game->gfx_buffer; *game = engine_snapshot_bd.game; @@ -616,7 +618,8 @@ void LoadEngineSnapshotValues_BD(void) { game->element_buffer[y][x] = engine_snapshot_bd.element_buffer[x][y]; game->last_element_buffer[y][x] = engine_snapshot_bd.last_element_buffer[x][y]; - game->dir_buffer[y][x] = engine_snapshot_bd.dir_buffer[x][y]; + game->dir_buffer_from[y][x] = engine_snapshot_bd.dir_buffer_from[x][y]; + game->dir_buffer_to[y][x] = engine_snapshot_bd.dir_buffer_to[x][y]; game->gfx_buffer[y][x] = engine_snapshot_bd.gfx_buffer[x][y]; } } -- 2.34.1