improved smooth movement animation for BD engine (complete rewrite)
authorHolger Schemel <holger.schemel@virtion.de>
Sun, 16 Jun 2024 08:28:12 +0000 (10:28 +0200)
committerHolger Schemel <holger.schemel@virtion.de>
Sun, 16 Jun 2024 08:42:06 +0000 (10:42 +0200)
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
src/game_bd/bd_elements.h
src/game_bd/bd_gameplay.c
src/game_bd/bd_gameplay.h
src/game_bd/bd_graphics.c
src/game_bd/export_bd.h
src/game_bd/main_bd.c

index 5b2bfcdfcd1292268083f70c45a5fa64fd235469..9df249eba1fba020e9ef53063c92c0aed0b8dbe7 100644 (file)
@@ -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
index dd36b1ee0ff391b8661cac9c84cf8d333022567b..446dfc2d385c029ba2202b843a30ab6ad020d14b 100644 (file)
@@ -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
index 085dad08f6f0201c9aa6f65d79982df4ab521514..0b4f48cdac4eab23a22b7d4bae0f441309ca132d 100644 (file)
@@ -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;
        }
       }
 
index 999c5e845ccc824955cc188430978ad6e6347060..2ea5d8c4bfc1929f6cf9c0c3151a98211d5fc470 100644 (file)
@@ -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;
index 5da166f2f5bed8aaf28db0990fa03b181acf4782..fa139d0cec13b52f0f886f4165a9fe43df98b268 100644 (file)
@@ -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;
 
index 883d4ace79be743d1cfa4613bbc64a30fe0b199c..7778b066a4a8e37265111872ad1f0376d2283f5f 100644 (file)
@@ -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;
index 34a75074754a3dde0df08ddc1b5bbd93b02a5057..4404d0ab06d3765ffd1afb71ae93d1f3cdc04c36 100644 (file)
@@ -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];
     }
   }