From c7b9ffec1f1ddd95d92fc1bc0bac8258bf7ebfe4 Mon Sep 17 00:00:00 2001 From: obligaron Date: Mon, 12 Apr 2021 12:14:12 +0200 Subject: [PATCH] Decouple Animations from Gamelogi (Smooth Animations for skipped Frames). NewPlrAnim: Use default arguments instead of overloads StartPlrHit: Fix skippedAnimationFrames - Frames starts with 1 Add missing comment for StartPlrHit Fix GotHit-Animation: Skipping Frames corrected and adjusted _pAnimGameTicksSinceSequenceStarted for animations that don't start with a additional tick. Thanks @StephenCWills for the gothit skipping frame calculation logic :-) Update StartWalk: calculated numSkippedFrames in own line. Co-authored-by: Anders Jenbo StartPlrHit: always initialize skippedAnimationFrames Co-authored-by: Anders Jenbo Update nthread_GetProgressToNextGameTick comment Co-authored-by: Anders Jenbo fix spelling "lenght" instead of "length" Update NewPlrAnim comment Co-authored-by: Anders Jenbo GetFrameToUseForPlayerRendering: avoid one "else" --- Source/items.cpp | 9 +--- Source/nthread.cpp | 17 +++++++ Source/nthread.h | 5 ++ Source/player.cpp | 121 +++++++++++++++++++++++++++++++++++++------- Source/player.h | 24 ++++++++- Source/scrollrt.cpp | 12 +++-- 6 files changed, 159 insertions(+), 29 deletions(-) diff --git a/Source/items.cpp b/Source/items.cpp index 039e648bc..640f9d351 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -1075,14 +1075,7 @@ void CalcPlrItemVals(int p, bool Loadgfx) d = plr[p]._pdir; assert(plr[p]._pNAnim[d]); - plr[p]._pAnimData = plr[p]._pNAnim[d]; - - plr[p]._pAnimLen = plr[p]._pNFrames; - plr[p]._pAnimFrame = 1; - plr[p]._pAnimCnt = 0; - plr[p]._pAnimDelay = 3; - plr[p]._pAnimWidth = plr[p]._pNWidth; - plr[p]._pAnimWidth2 = (plr[p]._pNWidth - 64) >> 1; + NewPlrAnim(p, plr[p]._pNAnim[d], plr[p]._pNFrames, 0, plr[p]._pNWidth); } else { plr[p]._pgfxnum = g; } diff --git a/Source/nthread.cpp b/Source/nthread.cpp index 3fa8bf9b3..20ccf76c8 100644 --- a/Source/nthread.cpp +++ b/Source/nthread.cpp @@ -242,4 +242,21 @@ bool nthread_has_500ms_passed() return ticksElapsed >= 0; } +float nthread_GetProgressToNextGameTick() +{ + if (!gbRunGame || PauseMode || (!gbIsMultiplayer && gmenu_is_active())) // if game is not running or paused there is no next gametick in the near future + return 0.0f; + int currentTickCount = SDL_GetTicks(); + int ticksElapsed = last_tick - currentTickCount; + if (ticksElapsed <= 0) + return 1.0f; // game tick is due + int ticksAdvanced = gnTickDelay - ticksElapsed; + float percent = (float)ticksAdvanced / (float)gnTickDelay; + if (percent > 1.0f) + return 1.0f; + if (percent < 0.0f) + return 0.0f; + return percent; +} + } // namespace devilution diff --git a/Source/nthread.h b/Source/nthread.h index 326dd7f91..5631da1e3 100644 --- a/Source/nthread.h +++ b/Source/nthread.h @@ -23,5 +23,10 @@ void nthread_start(bool set_turn_upper_bit); void nthread_cleanup(); void nthread_ignore_mutex(bool bStart); bool nthread_has_500ms_passed(); +/** + * @brief Calculates the progress in time to the next game tick + * @return Progress as a fraction (0.0f to 1.0f) + */ +float nthread_GetProgressToNextGameTick(); } diff --git a/Source/player.cpp b/Source/player.cpp index ceb9b1ad6..ffb21d0d3 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -458,7 +458,7 @@ void FreePlayerGFX(int pnum) plr[pnum]._pGFXLoad = 0; } -void NewPlrAnim(int pnum, BYTE *Peq, int numFrames, int Delay, int width) +void NewPlrAnim(int pnum, BYTE *Peq, int numFrames, int Delay, int width, int numSkippedFrames /*= 0*/, bool processAnimationPending /*= false*/, int stopDistributingAfterFrame /*= 0*/) { if ((DWORD)pnum >= MAX_PLRS) { app_fatal("NewPlrAnim: illegal player %d", pnum); @@ -471,6 +471,9 @@ void NewPlrAnim(int pnum, BYTE *Peq, int numFrames, int Delay, int width) plr[pnum]._pAnimDelay = Delay; plr[pnum]._pAnimWidth = width; plr[pnum]._pAnimWidth2 = (width - 64) >> 1; + plr[pnum]._pAnimNumSkippedFrames = numSkippedFrames; + plr[pnum]._pAnimGameTicksSinceSequenceStarted = processAnimationPending ? -1 : 0; + plr[pnum]._pAnimStopDistributingAfterFrame = stopDistributingAfterFrame; } void ClearPlrPVars(int pnum) @@ -1404,7 +1407,8 @@ void StartWalk(int pnum, int xvel, int yvel, int xoff, int yoff, int xadd, int y } //Start walk animation - NewPlrAnim(pnum, plr[pnum]._pWAnim[EndDir], plr[pnum]._pWFrames, 0, plr[pnum]._pWWidth); + int numSkippedFrames = (currlevel == 0 && sgGameInitInfo.bRunInTown) ? (plr[pnum]._pWFrames / 2) : 0; + NewPlrAnim(pnum, plr[pnum]._pWAnim[EndDir], plr[pnum]._pWFrames, 0, plr[pnum]._pWWidth, numSkippedFrames, true); plr[pnum]._pdir = EndDir; plr[pnum]._pVar8 = 0; @@ -1441,7 +1445,18 @@ void StartAttack(int pnum, direction d) LoadPlrGFX(pnum, PFILE_ATTACK); } - NewPlrAnim(pnum, plr[pnum]._pAAnim[d], plr[pnum]._pAFrames, 0, plr[pnum]._pAWidth); + int skippedAnimationFrames = 1; // Every Attack start with Frame 2. Because ProcessPlayerAnimation is called after StartAttack and its increases the AnimationFrame. + if (plr[pnum]._pIFlags & ISPL_FASTATTACK) { + skippedAnimationFrames += 1; + } + if (plr[pnum]._pIFlags & ISPL_FASTERATTACK) { + skippedAnimationFrames += 2; + } + if (plr[pnum]._pIFlags & ISPL_FASTESTATTACK) { + skippedAnimationFrames += 2; + } + + NewPlrAnim(pnum, plr[pnum]._pAAnim[d], plr[pnum]._pAFrames, 0, plr[pnum]._pAWidth, skippedAnimationFrames, true, plr[pnum]._pAFNum); plr[pnum]._pmode = PM_ATTACK; FixPlayerLocation(pnum, d); SetPlayerOld(pnum); @@ -1461,7 +1476,15 @@ void StartRangeAttack(int pnum, direction d, int cx, int cy) if (!(plr[pnum]._pGFXLoad & PFILE_ATTACK)) { LoadPlrGFX(pnum, PFILE_ATTACK); } - NewPlrAnim(pnum, plr[pnum]._pAAnim[d], plr[pnum]._pAFrames, 0, plr[pnum]._pAWidth); + + int skippedAnimationFrames = 1; // Every Attack start with Frame 2. Because ProcessPlayerAnimation is called after StartRangeAttack and its increases the AnimationFrame. + if (!gbIsHellfire) { + if (plr[pnum]._pIFlags & ISPL_FASTATTACK) { + skippedAnimationFrames += 1; + } + } + + NewPlrAnim(pnum, plr[pnum]._pAAnim[d], plr[pnum]._pAFrames, 0, plr[pnum]._pAWidth, skippedAnimationFrames, true, plr[pnum]._pAFNum); plr[pnum]._pmode = PM_RATTACK; FixPlayerLocation(pnum, d); @@ -1486,7 +1509,13 @@ void StartPlrBlock(int pnum, direction dir) if (!(plr[pnum]._pGFXLoad & PFILE_BLOCK)) { LoadPlrGFX(pnum, PFILE_BLOCK); } - NewPlrAnim(pnum, plr[pnum]._pBAnim[dir], plr[pnum]._pBFrames, 2, plr[pnum]._pBWidth); + + int skippedAnimationFrames = 0; // Block can start with Frame 1 if Player 2 hits Player 1. In this case Player 1 will not call again ProcessPlayerAnimation. + if (plr[pnum]._pIFlags & ISPL_FASTBLOCK) { + skippedAnimationFrames = (plr[pnum]._pBFrames - 1); // ISPL_FASTBLOCK means there is only one AnimationFrame. + } + + NewPlrAnim(pnum, plr[pnum]._pBAnim[dir], plr[pnum]._pBFrames, 2, plr[pnum]._pBWidth, skippedAnimationFrames); plr[pnum]._pmode = PM_BLOCK; FixPlayerLocation(pnum, dir); @@ -1509,19 +1538,19 @@ void StartSpell(int pnum, direction d, int cx, int cy) if (!(plr[pnum]._pGFXLoad & PFILE_FIRE)) { LoadPlrGFX(pnum, PFILE_FIRE); } - NewPlrAnim(pnum, plr[pnum]._pFAnim[d], plr[pnum]._pSFrames, 0, plr[pnum]._pSWidth); + NewPlrAnim(pnum, plr[pnum]._pFAnim[d], plr[pnum]._pSFrames, 0, plr[pnum]._pSWidth, 1, true); break; case STYPE_LIGHTNING: if (!(plr[pnum]._pGFXLoad & PFILE_LIGHTNING)) { LoadPlrGFX(pnum, PFILE_LIGHTNING); } - NewPlrAnim(pnum, plr[pnum]._pLAnim[d], plr[pnum]._pSFrames, 0, plr[pnum]._pSWidth); + NewPlrAnim(pnum, plr[pnum]._pLAnim[d], plr[pnum]._pSFrames, 0, plr[pnum]._pSWidth, 1, true); break; case STYPE_MAGIC: if (!(plr[pnum]._pGFXLoad & PFILE_MAGIC)) { LoadPlrGFX(pnum, PFILE_MAGIC); } - NewPlrAnim(pnum, plr[pnum]._pTAnim[d], plr[pnum]._pSFrames, 0, plr[pnum]._pSWidth); + NewPlrAnim(pnum, plr[pnum]._pTAnim[d], plr[pnum]._pSFrames, 0, plr[pnum]._pSWidth, 1, true); break; } } @@ -1625,7 +1654,22 @@ void StartPlrHit(int pnum, int dam, bool forcehit) if (!(plr[pnum]._pGFXLoad & PFILE_HIT)) { LoadPlrGFX(pnum, PFILE_HIT); } - NewPlrAnim(pnum, plr[pnum]._pHAnim[pd], plr[pnum]._pHFrames, 0, plr[pnum]._pHWidth); + + int skippedAnimationFrames = 0; // GotHit can start with Frame 1. GotHit can for example be called in ProcessMonsters() and this is after ProcessPlayers(). + const int ZenFlags = ISPL_FASTRECOVER | ISPL_FASTERRECOVER | ISPL_FASTESTRECOVER; + if ((plr[pnum]._pIFlags & ZenFlags) == ZenFlags) { // if multiple hitrecovery modes are present the skipping of frames can go so far, that they skip frames that would skip. so the additional skipping thats skipped. that means we can't add the different modes together. + skippedAnimationFrames = 4; + } else if (plr[pnum]._pIFlags & ISPL_FASTESTRECOVER) { + skippedAnimationFrames = 3; + } else if (plr[pnum]._pIFlags & ISPL_FASTERRECOVER) { + skippedAnimationFrames = 2; + } else if (plr[pnum]._pIFlags & ISPL_FASTRECOVER) { + skippedAnimationFrames = 1; + } else { + skippedAnimationFrames = 0; + } + + NewPlrAnim(pnum, plr[pnum]._pHAnim[pd], plr[pnum]._pHFrames, 0, plr[pnum]._pHWidth, skippedAnimationFrames); plr[pnum]._pmode = PM_GOTHIT; FixPlayerLocation(pnum, pd); @@ -3598,18 +3642,61 @@ void ProcessPlayers() CheckNewPath(pnum); } while (tplayer); - plr[pnum]._pAnimCnt++; - if (plr[pnum]._pAnimCnt > plr[pnum]._pAnimDelay) { - plr[pnum]._pAnimCnt = 0; - plr[pnum]._pAnimFrame++; - if (plr[pnum]._pAnimFrame > plr[pnum]._pAnimLen) { - plr[pnum]._pAnimFrame = 1; - } - } + ProcessPlayerAnimation(pnum); + } + } +} + +void ProcessPlayerAnimation(int pnum) +{ + plr[pnum]._pAnimCnt++; + plr[pnum]._pAnimGameTicksSinceSequenceStarted++; + if (plr[pnum]._pAnimCnt > plr[pnum]._pAnimDelay) { + plr[pnum]._pAnimCnt = 0; + plr[pnum]._pAnimFrame++; + if (plr[pnum]._pAnimFrame > plr[pnum]._pAnimLen) { + plr[pnum]._pAnimFrame = 1; + plr[pnum]._pAnimGameTicksSinceSequenceStarted = 0; } } } +Sint32 GetFrameToUseForPlayerRendering(const PlayerStruct* pPlayer) +{ + // Normal logic is used, + // - if no frame-skipping is required and so we have exactly one Animationframe per GameTick (_pAnimUsedNumFrames = 0) + // or + // - if we load from a savegame where the new variables are not stored (we don't want to break savegame compatiblity because of smoother rendering of one animation) + if (pPlayer->_pAnimNumSkippedFrames <= 0) + return pPlayer->_pAnimFrame; + // After an attack hits (_pAFNum or _pSFNum) it can be canceled or another attack can be queued and this means the animation is canceled. + // In normal attacks frame skipping always happens before the attack actual hit. + // This has the advantage that the sword or bow always points to the enemy when the hit happens (_pAFNum or _pSFNum). + // Our distribution logic must also regard this behaviour, so we are not allowed to distribute the skipped animations after the actual hit (_pAnimStopDistributingAfterFrame). + int relevantAnimationLength; + if (pPlayer->_pAnimStopDistributingAfterFrame != 0) { + if (pPlayer->_pAnimFrame >= pPlayer->_pAnimStopDistributingAfterFrame) + return pPlayer->_pAnimFrame; + relevantAnimationLength = pPlayer->_pAnimStopDistributingAfterFrame - 1; + } else { + relevantAnimationLength = pPlayer->_pAnimLen; + } + float progressToNextGameTick = nthread_GetProgressToNextGameTick(); + float totalGameTicksForCurrentAnimationSequence = progressToNextGameTick + (float)pPlayer->_pAnimGameTicksSinceSequenceStarted; // we don't use the processed game ticks alone but also the fragtion of the next game tick (if a rendering happens between game ticks). This helps to smooth the animations. + int animationMaxGameTickets = relevantAnimationLength; + if (pPlayer->_pAnimDelay > 1) + animationMaxGameTickets = (relevantAnimationLength * pPlayer->_pAnimDelay); + float gameTickModifier = (float)animationMaxGameTickets / (float)(relevantAnimationLength - pPlayer->_pAnimNumSkippedFrames); // if we skipped Frames we need to expand the GameTicks to make one GameTick for this Animation "faster" + int absolutAnimationFrame = 1 + (int)(totalGameTicksForCurrentAnimationSequence * gameTickModifier); // 1 added for rounding reasons. float to int cast always truncate. + if (absolutAnimationFrame > relevantAnimationLength) // this can happen if we are at the last frame and the next game tick is due (nthread_GetProgressToNextGameTick returns 1.0f) + return relevantAnimationLength; + if (absolutAnimationFrame <= 0) { + SDL_Log("GetFrameToUseForPlayerRendering: Calculated an invalid Animation Frame"); + return 1; + } + return absolutAnimationFrame; +} + void ClrPlrPath(int pnum) { if ((DWORD)pnum >= MAX_PLRS) { diff --git a/Source/player.h b/Source/player.h index ab483378a..7a5535648 100644 --- a/Source/player.h +++ b/Source/player.h @@ -163,6 +163,9 @@ struct PlayerStruct { Sint32 _pAnimFrame; // Current frame of animation. Sint32 _pAnimWidth; Sint32 _pAnimWidth2; + Sint32 _pAnimNumSkippedFrames; // Number of Frames that will be skipped (for example with modifier "faster attack") + Sint32 _pAnimGameTicksSinceSequenceStarted; // Number of GameTicks after the current animation sequence started + Sint32 _pAnimStopDistributingAfterFrame; // Distribute the NumSkippedFrames only before this frame Sint32 _plid; Sint32 _pvid; spell_id _pSpell; @@ -329,8 +332,27 @@ void LoadPlrGFX(int pnum, player_graphic gfxflag); void InitPlayerGFX(int pnum); void InitPlrGFXMem(int pnum); void FreePlayerGFX(int pnum); -void NewPlrAnim(int pnum, BYTE *Peq, int numFrames, int Delay, int width); +/** + * @brief Sets the new Player Animation with all relevant information for rendering + + * @param pnum Player Id + * @param Peq Pointer to Animation Data + * @param numFrames Number of Frames in Animation + * @param Delay Delay after each Animation sequence + * @param width Width of sprite + * @param numSkippedFrames Number of Frames that will be skipped (for example with modifier "faster attack") + * @param processAnimationPending true if first ProcessAnimation will be called in same gametick after NewPlrAnim + * @param stopDistributingAfterFrame Distribute the NumSkippedFrames only before this frame + */ +void NewPlrAnim(int pnum, BYTE *Peq, int numFrames, int Delay, int width, int numSkippedFrames = 0, bool processAnimationPending = false, int stopDistributingAfterFrame = 0); void SetPlrAnims(int pnum); +void ProcessPlayerAnimation(int pnum); +/** + * @brief Calculates the Frame to use for the Animation rendering + * @param pPlayer Player + * @return The Frame to use for rendering + */ +Sint32 GetFrameToUseForPlayerRendering(const PlayerStruct *pPlayer); void CreatePlayer(int pnum, HeroClass c); int CalcStatDiff(int pnum); #ifdef _DEBUG diff --git a/Source/scrollrt.cpp b/Source/scrollrt.cpp index 4875b5f46..d823e24ce 100644 --- a/Source/scrollrt.cpp +++ b/Source/scrollrt.cpp @@ -387,12 +387,18 @@ static void DrawManaShield(CelOutputBuffer out, int pnum, int x, int y, bool lig * @param nCel frame * @param nWidth width */ -static void DrawPlayer(CelOutputBuffer out, int pnum, int x, int y, int px, int py, BYTE *pCelBuff, int nCel, int nWidth) +static void DrawPlayer(CelOutputBuffer out, int pnum, int x, int y, int px, int py) { if ((dFlags[x][y] & BFLAG_LIT) == 0 && !plr[myplr]._pInfraFlag && leveltype != DTYPE_TOWN) { return; } + PlayerStruct *pPlayer = &plr[pnum]; + + BYTE *pCelBuff = pPlayer->_pAnimData; + int nCel = GetFrameToUseForPlayerRendering(pPlayer); + int nWidth = pPlayer->_pAnimWidth; + if (pCelBuff == NULL) { SDL_Log("Drawing player %d \"%s\": NULL Cel Buffer", pnum, plr[pnum]._pName); return; @@ -462,7 +468,7 @@ void DrawDeadPlayer(CelOutputBuffer out, int x, int y, int sx, int sy) dFlags[x][y] |= BFLAG_DEAD_PLAYER; px = sx + p->_pxoff - p->_pAnimWidth2; py = sy + p->_pyoff; - DrawPlayer(out, i, x, y, px, py, p->_pAnimData, p->_pAnimFrame, p->_pAnimWidth); + DrawPlayer(out, i, x, y, px, py); } } } @@ -697,7 +703,7 @@ static void DrawPlayerHelper(CelOutputBuffer out, int x, int y, int sx, int sy) int px = sx + pPlayer->_pxoff - pPlayer->_pAnimWidth2; int py = sy + pPlayer->_pyoff; - DrawPlayer(out, p, x, y, px, py, pPlayer->_pAnimData, pPlayer->_pAnimFrame, pPlayer->_pAnimWidth); + DrawPlayer(out, p, x, y, px, py); } /**