Skip to content

Commit

Permalink
Update animationinfo.cpp
Browse files Browse the repository at this point in the history
Deshitified the code abit...

Moved some repeated logic into a helper function (configureFrameDistribution) to handle frame distribution calculation.

Used std::max and std::clamp in places to ensure values stay within valid ranges.

Added validation for ticksPerFrame to avoid potential division by zero.

Added const where possible to indicate immutability.
  • Loading branch information
Nairdaa authored Nov 19, 2024
1 parent 8615d31 commit a6d1a48
Showing 1 changed file with 118 additions and 109 deletions.
227 changes: 118 additions & 109 deletions Source/engine/animationinfo.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @file animationinfo.cpp
*
* Contains the core animation information and related logic
* Contains the core animation information and related logic.
*/

#include "animationinfo.h"
Expand All @@ -13,90 +13,111 @@
#include "nthread.h"
#include "utils/log.hpp"

namespace devilution {
namespace devilution
{

int8_t AnimationInfo::getFrameToUseForRendering() const
{
// Normal logic is used,
// - if no frame-skipping is required and so we have exactly one Animationframe per game tick
// or
// - if we load from a savegame where the new variables are not stored (we don't want to break savegame compatibility because of smoother rendering of one animation)
// If no frame-skipping is required (exactly one Animation frame per game tick),
// or if loading from a save game where the new variables are not stored, use normal logic.
if (relevantFramesForDistributing_ <= 0)
return std::max<int8_t>(0, currentFrame);

// Handle cases where the current frame exceeds the number of relevant frames.
if (currentFrame >= relevantFramesForDistributing_)
return currentFrame;

int16_t ticksSinceSequenceStarted = ticksSinceSequenceStarted_;
if (ticksSinceSequenceStarted_ < 0) {
ticksSinceSequenceStarted = 0;
// Ensure ticksSinceSequenceStarted_ is non-negative for calculations.
int16_t ticksSinceSequenceStarted = std::max<int16_t>(0, ticksSinceSequenceStarted_);
if (ticksSinceSequenceStarted_ < 0)
Log("getFrameToUseForRendering: Invalid ticksSinceSequenceStarted_ {}", ticksSinceSequenceStarted_);
}

// we don't use the processed game ticks alone but also the fraction of the next game tick (if a rendering happens between game ticks). This helps to smooth the animations.
// Calculate total ticks considering progress to next game tick for smoother rendering.
int32_t totalTicksForCurrentAnimationSequence = getProgressToNextGameTick() + ticksSinceSequenceStarted;

int8_t absoluteAnimationFrame = static_cast<int8_t>(totalTicksForCurrentAnimationSequence * tickModifier_ / baseValueFraction / baseValueFraction);
if (skippedFramesFromPreviousAnimation_ > 0) {
// absoluteAnimationFrames contains also the Frames from the previous Animation, so if we want to get the current Frame we have to remove them
// Compute the absolute frame based on timing and modifiers.
int8_t absoluteAnimationFrame = static_cast<int8_t>((totalTicksForCurrentAnimationSequence * tickModifier_) / (baseValueFraction * baseValueFraction));

// Adjust for skipped frames from previous animations.
if (skippedFramesFromPreviousAnimation_ > 0)
{
absoluteAnimationFrame -= skippedFramesFromPreviousAnimation_;
if (absoluteAnimationFrame < 0) {
// We still display the remains of the previous Animation
if (absoluteAnimationFrame < 0)
{
absoluteAnimationFrame = numberOfFrames + absoluteAnimationFrame;
}
} else if (absoluteAnimationFrame >= relevantFramesForDistributing_) {
// this can happen if we are at the last frame and the next game tick is due
if (absoluteAnimationFrame >= (relevantFramesForDistributing_ + 1)) {
// we should never have +2 frames even if next game tick is due
Log("getFrameToUseForRendering: Calculated an invalid Animation Frame (Calculated {} MaxFrame {})", absoluteAnimationFrame, relevantFramesForDistributing_);
}
else if (absoluteAnimationFrame >= relevantFramesForDistributing_)
{
// Handle edge case where the calculated frame exceeds limits.
if (absoluteAnimationFrame >= (relevantFramesForDistributing_ + 1))
{
Log("getFrameToUseForRendering: Invalid Animation Frame (Calculated {} MaxFrame {})",
absoluteAnimationFrame, relevantFramesForDistributing_);
}

return relevantFramesForDistributing_ - 1;
}
if (absoluteAnimationFrame < 0) {
Log("getFrameToUseForRendering: Calculated an invalid Animation Frame (Calculated {})", absoluteAnimationFrame);

// Ensure the calculated frame is within valid bounds.
if (absoluteAnimationFrame < 0)
{
Log("getFrameToUseForRendering: Invalid Animation Frame (Calculated {})", absoluteAnimationFrame);

return 0;
}

return absoluteAnimationFrame;
}

uint8_t AnimationInfo::getAnimationProgress() const
{
// Ensure ticksSinceSequenceStarted_ is non-negative for calculations.
int16_t ticksSinceSequenceStarted = std::max<int16_t>(0, ticksSinceSequenceStarted_);
int32_t tickModifier = tickModifier_;

if (relevantFramesForDistributing_ <= 0) {
if (ticksPerFrame <= 0) {
// Handle case where animation distribution is not active.
if (relevantFramesForDistributing_ <= 0)
{
if (ticksPerFrame <= 0)
{
Log("getAnimationProgress: Invalid ticksPerFrame {}", ticksPerFrame);
return 0;
}
// This logic is used if animation distribution is not active (see getFrameToUseForRendering).
// In this case the variables calculated with animation distribution are not initialized and we have to calculate them on the fly with the given information.

ticksSinceSequenceStarted = ((currentFrame * ticksPerFrame) + tickCounterOfCurrentFrame) * baseValueFraction;
tickModifier = baseValueFraction / ticksPerFrame;
}

int32_t totalTicksForCurrentAnimationSequence = getProgressToNextGameTick() + ticksSinceSequenceStarted;
int32_t progressInAnimationFrames = totalTicksForCurrentAnimationSequence * tickModifier;
int32_t animationFraction = progressInAnimationFrames / numberOfFrames / baseValueFraction;
int32_t animationFraction = progressInAnimationFrames / (numberOfFrames * baseValueFraction);

assert(animationFraction <= baseValueFraction);

return static_cast<uint8_t>(animationFraction);
}

void AnimationInfo::setNewAnimation(OptionalClxSpriteList celSprite, int8_t numberOfFrames, int8_t ticksPerFrame, AnimationDistributionFlags flags /*= AnimationDistributionFlags::None*/, int8_t numSkippedFrames /*= 0*/, int8_t distributeFramesBeforeFrame /*= 0*/, uint8_t previewShownGameTickFragments /*= 0*/)
void AnimationInfo::setNewAnimation(OptionalClxSpriteList celSprite, int8_t numberOfFrames, int8_t ticksPerFrame, AnimationDistributionFlags flags /*= AnimationDistributionFlags::None*/, int8_t numSkippedFrames /*= 0*/, int8_t distributeFramesBeforeFrame /*= 0*/, uint8_t previewShownGameTickFragments /*= 0*/)
{
if ((flags & AnimationDistributionFlags::RepeatedAction) == AnimationDistributionFlags::RepeatedAction && distributeFramesBeforeFrame != 0 && this->numberOfFrames == numberOfFrames && currentFrame + 1 >= distributeFramesBeforeFrame && currentFrame != this->numberOfFrames - 1) {
// We showed the same Animation (for example a melee attack) before but truncated the Animation.
// So now we should add them back to the new Animation. This increases the speed of the current Animation but the game logic/ticks isn't affected.
if ((flags & AnimationDistributionFlags::RepeatedAction) == AnimationDistributionFlags::RepeatedAction && distributeFramesBeforeFrame != 0 && this->numberOfFrames == numberOfFrames && currentFrame + 1 >= distributeFramesBeforeFrame && currentFrame != this->numberOfFrames - 1)
{
// Restore truncated animation frames.
skippedFramesFromPreviousAnimation_ = this->numberOfFrames - currentFrame - 1;
} else {
}
else
{
skippedFramesFromPreviousAnimation_ = 0;
}

if (ticksPerFrame <= 0) {
// Ensure ticksPerFrame is valid.
if (ticksPerFrame <= 0)
{
Log("setNewAnimation: Invalid ticksPerFrame {}", ticksPerFrame);
ticksPerFrame = 1;
}

// Initialize animation state.
this->sprites = celSprite;
this->numberOfFrames = numberOfFrames;
currentFrame = numSkippedFrames;
Expand All @@ -107,113 +128,103 @@ void AnimationInfo::setNewAnimation(OptionalClxSpriteList celSprite, int8_t numb
tickModifier_ = 0;
isPetrified = false;

if (numSkippedFrames != 0 || flags != AnimationDistributionFlags::None) {
// Animation Frames that will be adjusted for the skipped Frames/game ticks
int8_t relevantAnimationFramesForDistributing = numberOfFrames;
if (distributeFramesBeforeFrame != 0) {
// 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).
relevantAnimationFramesForDistributing = distributeFramesBeforeFrame - 1;
}
// Only modify distribution if frames were skipped or specific flags are set.
if (numSkippedFrames != 0 || flags != AnimationDistributionFlags::None)
{
configureFrameDistribution(flags, numberOfFrames, distributeFramesBeforeFrame, numSkippedFrames, ticksPerFrame, previewShownGameTickFragments);
}
}

// Game ticks that will be adjusted for the skipped Frames/game ticks
int32_t relevantAnimationTicksForDistribution = relevantAnimationFramesForDistributing * ticksPerFrame;

// How many game ticks will the Animation be really shown (skipped Frames and game ticks removed)
int32_t relevantAnimationTicksWithSkipping = relevantAnimationTicksForDistribution - (numSkippedFrames * ticksPerFrame);

if ((flags & AnimationDistributionFlags::ProcessAnimationPending) == AnimationDistributionFlags::ProcessAnimationPending) {
// If processAnimation will be called after setNewAnimation (in same game tick as setNewAnimation), we increment the Animation-Counter.
// If no delay is specified, this will result in complete skipped frame (see processAnimation).
// But if we have a delay specified, this would only result in a reduced time the first frame is shown (one skipped delay).
// Because of that, we only the remove one game tick from the time the Animation is shown
relevantAnimationTicksWithSkipping -= 1;
// The Animation Distribution Logic needs to account how many game ticks passed since the Animation started.
// Because processAnimation will increase this later (in same game tick as setNewAnimation), we correct this upfront.
// This also means Rendering should never happen with ticksSinceSequenceStarted_ < 0.
ticksSinceSequenceStarted_ = -baseValueFraction;
}
void AnimationInfo::configureFrameDistribution(AnimationDistributionFlags flags, int8_t numberOfFrames, int8_t distributeFramesBeforeFrame, int8_t numSkippedFrames, int8_t ticksPerFrame, uint8_t previewShownGameTickFragments)
{
int8_t relevantAnimationFramesForDistributing = numberOfFrames;

if ((flags & AnimationDistributionFlags::SkipsDelayOfLastFrame) == AnimationDistributionFlags::SkipsDelayOfLastFrame) {
// The logic for player/monster/... (not processAnimation) only checks the frame not the delay.
// That means if a delay is specified, the last-frame is shown less than the other frames
// Example:
// If we have a animation with 3 frames and with a delay of 1 (ticksPerFrame = 2).
// The logic checks "if (frame == 3) { start_new_animation(); }"
// This will result that frame 4 is the last shown Animation Frame.
// GameTick Frame Cnt
// 1 1 0
// 2 1 1
// 3 2 0
// 3 2 1
// 4 3 0
// 5 - -
// in game tick 5 ProcessPlayer sees Frame = 3 and stops the animation.
// But Frame 3 is only shown 1 game tick and all other Frames are shown 2 game ticks.
// That's why we need to remove the Delay of the last Frame from the time (game ticks) the Animation is shown
relevantAnimationTicksWithSkipping -= (ticksPerFrame - 1);
}
// Adjust relevant frames based on pre-frame distribution.
if (distributeFramesBeforeFrame != 0)
{
relevantAnimationFramesForDistributing = distributeFramesBeforeFrame - 1;
}

// The truncated Frames from previous Animation will also be shown, so we also have to distribute them for the given time (game ticks)
relevantAnimationTicksForDistribution += (skippedFramesFromPreviousAnimation_ * ticksPerFrame);
int32_t relevantAnimationTicksForDistribution = relevantAnimationFramesForDistributing * ticksPerFrame;
int32_t relevantAnimationTicksWithSkipping = relevantAnimationTicksForDistribution - (numSkippedFrames * ticksPerFrame);

// At this point we use fixed point math for the fragment calculations
relevantAnimationTicksForDistribution *= baseValueFraction;
relevantAnimationTicksWithSkipping *= baseValueFraction;
if (flags & AnimationDistributionFlags::ProcessAnimationPending)
{
relevantAnimationTicksWithSkipping -= 1;
ticksSinceSequenceStarted_ = -baseValueFraction;
}

// The preview animation was shown some times (less than one game tick)
// So we overall have a longer time the animation is shown
ticksSinceSequenceStarted_ += previewShownGameTickFragments;
relevantAnimationTicksWithSkipping += previewShownGameTickFragments;
if (flags & AnimationDistributionFlags::SkipsDelayOfLastFrame)
{
relevantAnimationTicksWithSkipping -= (ticksPerFrame - 1);
}

// if we skipped Frames we need to expand the game ticks to make one game tick for this Animation "faster"
int32_t tickModifier = 0;
if (relevantAnimationTicksWithSkipping != 0)
tickModifier = baseValueFraction * relevantAnimationTicksForDistribution / relevantAnimationTicksWithSkipping;
relevantAnimationTicksForDistribution += (skippedFramesFromPreviousAnimation_ * ticksPerFrame);
relevantAnimationTicksForDistribution *= baseValueFraction;
relevantAnimationTicksWithSkipping *= baseValueFraction;

// tickModifier specifies the Animation fraction per game tick, so we have to remove the delay from the variable
tickModifier /= ticksPerFrame;
ticksSinceSequenceStarted_ += previewShownGameTickFragments;
relevantAnimationTicksWithSkipping += previewShownGameTickFragments;

relevantFramesForDistributing_ = relevantAnimationFramesForDistributing;
tickModifier_ = static_cast<uint16_t>(tickModifier);
// Calculate tick modifier for frame distribution.
int32_t tickModifier = 0;
if (relevantAnimationTicksWithSkipping != 0)
{
tickModifier = (baseValueFraction * relevantAnimationTicksForDistribution) / relevantAnimationTicksWithSkipping;
}

tickModifier /= ticksPerFrame;

// Set up calculated values for frame distribution.
relevantFramesForDistributing_ = relevantAnimationFramesForDistributing;
tickModifier_ = static_cast<uint16_t>(tickModifier);
}

void AnimationInfo::changeAnimationData(OptionalClxSpriteList celSprite, int8_t numberOfFrames, int8_t ticksPerFrame)
{
if (numberOfFrames != this->numberOfFrames || ticksPerFrame != this->ticksPerFrame) {
// Ensure that the currentFrame is still valid and that we disable ADL because the calculated values (for example tickModifier_) could be wrong
if (numberOfFrames >= 1)
if (numberOfFrames != this->numberOfFrames || ticksPerFrame != this->ticksPerFrame)
{
// Ensure current frame is valid and disable Animation Distribution Logic.
if (numberOfFrames >= 1)
{
currentFrame = std::clamp<int8_t>(currentFrame, 0, numberOfFrames - 1);
else
}
else
{
currentFrame = -1;
}

this->numberOfFrames = numberOfFrames;
this->ticksPerFrame = ticksPerFrame;
ticksSinceSequenceStarted_ = 0;
relevantFramesForDistributing_ = 0;
tickModifier_ = 0;
}

this->sprites = celSprite;
}

void AnimationInfo::processAnimation(bool reverseAnimation /*= false*/)
{
tickCounterOfCurrentFrame++;
ticksSinceSequenceStarted_ += baseValueFraction;
if (tickCounterOfCurrentFrame >= ticksPerFrame) {

if (tickCounterOfCurrentFrame >= ticksPerFrame)
{
tickCounterOfCurrentFrame = 0;
if (reverseAnimation) {
if (reverseAnimation)
{
--currentFrame;
if (currentFrame == -1) {
if (currentFrame < 0) {
currentFrame = numberOfFrames - 1;
ticksSinceSequenceStarted_ = 0;
}
} else {
}
else
{
++currentFrame;
if (currentFrame >= numberOfFrames) {
if (currentFrame >= numberOfFrames)
{
currentFrame = 0;
ticksSinceSequenceStarted_ = 0;
}
Expand All @@ -223,9 +234,7 @@ void AnimationInfo::processAnimation(bool reverseAnimation /*= false*/)

uint8_t AnimationInfo::getProgressToNextGameTick() const
{
if (isPetrified)
return 0;
return ProgressToNextGameTick;
return isPetrified ? 0 : ProgressToNextGameTick;
}

} // namespace devilution

0 comments on commit a6d1a48

Please sign in to comment.