From de522a1e64f0c9319d46038a86cb4825c919b2ee Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Fri, 3 Apr 2026 09:13:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor(animation):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E6=96=B9=E5=90=91=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将翻转逻辑集中到Animation类中处理 - 添加spriteFrameOffsets_存储帧偏移量 - 改进角色动画方向切换时的表现 - 移除CharacterAnimation中的ApplyFlipRecursive方法 - 优化动画帧位置和旋转的计算方式 --- .../include/frostbite2D/animation/animation.h | 9 ++ .../src/frostbite2D/animation/animation.cpp | 115 ++++++++++++++---- Game/include/character/CharacterAnimation.h | 1 - Game/src/character/CharacterActionLibrary.cpp | 61 +++++----- Game/src/character/CharacterAnimation.cpp | 20 ++- Game/src/character/CharacterDataLoader.cpp | 2 +- Game/src/character/CharacterObject.cpp | 2 +- Game/src/character/CharacterStateMachine.cpp | 23 +++- 8 files changed, 156 insertions(+), 77 deletions(-) diff --git a/Frostbite2D/include/frostbite2D/animation/animation.h b/Frostbite2D/include/frostbite2D/animation/animation.h index 350fd45..bca0c99 100644 --- a/Frostbite2D/include/frostbite2D/animation/animation.h +++ b/Frostbite2D/include/frostbite2D/animation/animation.h @@ -40,6 +40,7 @@ public: void Reset(); animation::AniFrame GetCurrentFrameInfo(); void SetFrameIndex(int index); + void SetDirection(int direction); void InterpolationLogic(); Vec2 GetMaxSize() const; @@ -65,6 +66,7 @@ public: std::vector frames_; std::vector> spriteFrames_; + std::vector spriteFrameOffsets_; std::unordered_map animationFlag_; @@ -75,8 +77,15 @@ public: ReplaceData additionalOptionsData_; Vec2 maxSize_ = Vec2::Zero(); + int direction_ = 1; std::vector interpolationData_; + +private: + void ApplyFramePresentation(const Vec2& framePos, + const Vec2& imageRate, + float rotation, + BlendMode blendMode); }; } // namespace frostbite2D diff --git a/Frostbite2D/src/frostbite2D/animation/animation.cpp b/Frostbite2D/src/frostbite2D/animation/animation.cpp index 3f9412d..1547ca9 100644 --- a/Frostbite2D/src/frostbite2D/animation/animation.cpp +++ b/Frostbite2D/src/frostbite2D/animation/animation.cpp @@ -5,10 +5,39 @@ #include #include #include +#include using namespace frostbite2D::animation; namespace frostbite2D { +namespace { + +Vec2 resolveFramePosition(const AniFrame& frameInfo, const Vec2& imageRate) { + return Vec2(frameInfo.imgPos.x * imageRate.x, frameInfo.imgPos.y * imageRate.y); +} + +float resolveFrameRotation(const AniFrame& frameInfo) { + if (frameInfo.flag.count("IMAGE_ROTATE")) { + return std::get(frameInfo.flag.at("IMAGE_ROTATE")); + } + return 0.0f; +} + +Vec2 resolveImageRate(const AniFrame& frameInfo) { + if (frameInfo.flag.count("IMAGE_RATE")) { + return std::get(frameInfo.flag.at("IMAGE_RATE")); + } + return Vec2::One(); +} + +BlendMode resolveBlendMode(const AniFrame& frameInfo) { + if (frameInfo.flag.count("GRAPHIC_EFFECT_LINEARDODGE")) { + return BlendMode::Additive; + } + return BlendMode::Normal; +} + +} // namespace Animation::Animation() { } @@ -39,6 +68,8 @@ void Animation::Init(const std::string& aniPath) { aniPath_ = aniPath; animationFlag_ = info->flag; frames_ = std::move(info->frames); + spriteFrames_.clear(); + spriteFrameOffsets_.clear(); if (animationFlag_.count("LOOP")) { isLooping_ = true; @@ -69,6 +100,7 @@ void Animation::Init(const std::string& aniPath) { } spriteFrames_.push_back(spriteObj); + spriteFrameOffsets_.push_back(spriteObj->GetOffset()); auto spriteSize = spriteObj->GetSize(); if (maxSize_.x < spriteSize.x) maxSize_.x = spriteSize.x; @@ -102,6 +134,7 @@ void Animation::Init(const std::string& aniPath) { if (ani.second.layer.size() >= 2) { subAni->SetZOrder(ani.second.layer[1]); } + subAni->SetDirection(direction_); AddChild(subAni); } } @@ -188,20 +221,11 @@ void Animation::FlushFrame(int index) { if (flagBuf.count("PLAY_SOUND")) { } - if (flagBuf.count("IMAGE_RATE")) { - auto rate = std::get(flagBuf.at("IMAGE_RATE")); - currentFrame_->SetScale(rate); - currentFrame_->SetPosition(frameInfo.imgPos.x * rate.x, frameInfo.imgPos.y * rate.y); - } - - if (flagBuf.count("GRAPHIC_EFFECT_LINEARDODGE")) { - currentFrame_->SetBlendMode(BlendMode::Additive); - } - - if (flagBuf.count("IMAGE_ROTATE")) { - auto rotation = std::get(flagBuf.at("IMAGE_ROTATE")); - currentFrame_->SetRotation(rotation); - } + Vec2 imageRate = resolveImageRate(frameInfo); + float rotation = resolveFrameRotation(frameInfo); + BlendMode blendMode = resolveBlendMode(frameInfo); + Vec2 framePos = resolveFramePosition(frameInfo, imageRate); + ApplyFramePresentation(framePos, imageRate, rotation, blendMode); if (flagBuf.count("INTERPOLATION")) { if (interpolationData_.empty()) { @@ -234,6 +258,27 @@ void Animation::SetFrameIndex(int index) { currentFrameTime_ = 0.0f; } +void Animation::SetDirection(int direction) { + direction_ = direction >= 0 ? 1 : -1; + + if (currentFrame_ && currentFrameIndex_ >= 0 && + currentFrameIndex_ < static_cast(frames_.size())) { + const AniFrame& frameInfo = frames_[currentFrameIndex_]; + Vec2 imageRate = resolveImageRate(frameInfo); + float rotation = resolveFrameRotation(frameInfo); + BlendMode blendMode = resolveBlendMode(frameInfo); + Vec2 framePos = resolveFramePosition(frameInfo, imageRate); + ApplyFramePresentation(framePos, imageRate, rotation, blendMode); + } + + for (const auto& child : GetChildren()) { + auto* subAnimation = dynamic_cast(child.Get()); + if (subAnimation) { + subAnimation->SetDirection(direction_); + } + } +} + void Animation::InterpolationLogic() { if (interpolationData_.empty()) { return; @@ -268,11 +313,6 @@ void Animation::InterpolationLogic() { static_cast(rgbaData[3]) / 255.0f); } - { - Vec2 posData = Vec2::lerp(oldData.imgPos, newData.imgPos, interRate); - currentFrame_->SetPosition(posData); - } - { Vec2 oldRateData = Vec2::One(); Vec2 newRateData = Vec2::One(); @@ -285,10 +325,6 @@ void Animation::InterpolationLogic() { } Vec2 rateData = Vec2::lerp(oldRateData, newRateData, interRate); - currentFrame_->SetScale(rateData); - } - - { float oldAngleData = 0.0f; float newAngleData = 0.0f; @@ -300,7 +336,9 @@ void Animation::InterpolationLogic() { } float angleData = math::lerp(oldAngleData, newAngleData, interRate); - currentFrame_->SetRotation(angleData); + Vec2 posData = Vec2::lerp(oldData.imgPos, newData.imgPos, interRate); + Vec2 framePos(posData.x * rateData.x, posData.y * rateData.y); + ApplyFramePresentation(framePos, rateData, angleData, currentFrame_->GetBlendMode()); } } @@ -308,4 +346,33 @@ Vec2 Animation::GetMaxSize() const { return maxSize_; } +void Animation::ApplyFramePresentation(const Vec2& framePos, + const Vec2& imageRate, + float rotation, + BlendMode blendMode) { + if (!currentFrame_ || currentFrameIndex_ < 0 || + currentFrameIndex_ >= static_cast(spriteFrameOffsets_.size())) { + return; + } + + currentFrame_->SetScale(imageRate); + currentFrame_->SetBlendMode(blendMode); + currentFrame_->SetFlippedX(direction_ < 0); + + Vec2 offset = spriteFrameOffsets_[currentFrameIndex_]; + Vec2 position = framePos; + float mirroredRotation = rotation; + if (direction_ < 0) { + // 朝左时要同时镜像帧位置和素材锚点偏移,避免多层时装各自翻转后错位。 + float visualWidth = currentFrame_->GetSize().x * std::abs(imageRate.x); + position.x = -position.x; + offset.x = -offset.x - visualWidth; + mirroredRotation = -rotation; + } + + currentFrame_->SetOffset(offset); + currentFrame_->SetPosition(position); + currentFrame_->SetRotation(mirroredRotation); +} + } // namespace frostbite2D diff --git a/Game/include/character/CharacterAnimation.h b/Game/include/character/CharacterAnimation.h index 6b8a5df..3246693 100644 --- a/Game/include/character/CharacterAnimation.h +++ b/Game/include/character/CharacterAnimation.h @@ -36,7 +36,6 @@ private: const std::string& actionPath, const character::CharacterConfig& config, const CharacterEquipmentManager& equipmentManager); - void ApplyFlipRecursive(Actor* actor, bool flipped) const; CharacterObject* parent_ = nullptr; ActionAnimationList actionAnimations_; diff --git a/Game/src/character/CharacterActionLibrary.cpp b/Game/src/character/CharacterActionLibrary.cpp index 5fb5ca3..e669a97 100644 --- a/Game/src/character/CharacterActionLibrary.cpp +++ b/Game/src/character/CharacterActionLibrary.cpp @@ -50,14 +50,6 @@ std::string trim(const std::string& value) { return value.substr(begin, end - begin); } -std::string trimBackticks(const std::string& value) { - std::string result = trim(value); - if (result.size() >= 2 && result.front() == '`' && result.back() == '`') { - return result.substr(1, result.size() - 2); - } - return result; -} - std::string fileStem(const std::string& path) { size_t slashPos = path.find_last_of("/\\"); size_t begin = slashPos == std::string::npos ? 0 : slashPos + 1; @@ -108,10 +100,18 @@ private: // action_list.lst 同时兼容两种格式: // 1. 标准 PVF #PVF_File + 索引 + `path` // 2. 早期临时版的 actionId + path 成对写法 +bool isNewActionListPath(const std::string& path) { + return path.find("action_logic") != std::string::npos + && path.size() >= 4 + && path.substr(path.size() - 4) == ".act"; +} + +// action_list.lst only accepts #PVF_File entries like `swordman/action_logic/idle.act`. std::vector> parseActionListEntries( ScriptTokenStream& listStream) { std::vector> entries; std::vector tokens; + auto& pvf = PvfArchive::get(); while (!listStream.IsEnd()) { std::string token = trim(listStream.Next()); if (!token.empty()) { @@ -119,23 +119,13 @@ std::vector> parseActionListEntries( } } - if (tokens.empty()) { - return entries; - } - - if (tokens.front() == "#PVF_File") { - for (size_t i = 1; i + 1 < tokens.size(); i += 2) { - std::string actionPath = trimBackticks(tokens[i + 1]); - if (actionPath.empty()) { - continue; - } - entries.push_back({toLower(fileStem(actionPath)), actionPath}); - } - return entries; - } - for (size_t i = 0; i + 1 < tokens.size(); i += 2) { - entries.push_back({toLower(tokens[i]), trimBackticks(tokens[i + 1])}); + std::string rawActionPath = trim(tokens[i + 1]); + std::string actionPath = pvf.normalizePath(rawActionPath); + if (actionPath.empty() || !isNewActionListPath(actionPath)) { + continue; + } + entries.push_back({toLower(fileStem(actionPath)), actionPath}); } return entries; } @@ -357,13 +347,20 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts( } auto entries = parseActionListEntries(listStream); + if (entries.empty()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterActionLibrary: action_list parsed 0 entries from %s", + listPath.c_str()); + } for (const auto& [actionId, relativePath] : entries) { if (actionId.empty() || relativePath.empty()) { continue; } - ScriptTokenStream actionStream( - "character/" + config.jobTag + "/action_logic/" + relativePath); + std::string fullActPath = relativePath.rfind("character/", 0) == 0 + ? relativePath + : "character/" + relativePath; + ScriptTokenStream actionStream(fullActPath); if (!actionStream.IsValid()) { continue; } @@ -377,9 +374,9 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts( } if (token == "[action id]") { - definition.actionId = toLower(actionStream.Next()); + definition.actionId = toLower(trim(actionStream.Next())); } else if (token == "[animation tag]") { - definition.animationTag = actionStream.Next(); + definition.animationTag = trim(actionStream.Next()); } else if (token == "[total frames]") { definition.totalFrames = toInt(actionStream.Next(), definition.totalFrames); } else if (token == "[loop]") { @@ -394,7 +391,7 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts( definition.moveSpeedScale = toFloat(actionStream.Next(), definition.moveSpeedScale); } else if (token == "[cancel rule]") { CharacterCancelRuleDefinition rule; - rule.targetAction = toLower(actionStream.Next()); + rule.targetAction = toLower(trim(actionStream.Next())); rule.beginFrame = toInt(actionStream.Next(), 0); rule.endFrame = toInt(actionStream.Next(), rule.beginFrame); rule.requireGrounded = isTrueToken(actionStream.Next()); @@ -403,14 +400,14 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts( } else if (token == "[frame event]") { CharacterFrameEventDefinition frameEvent; frameEvent.frame = toInt(actionStream.Next(), 0); - frameEvent.type = parseFrameEventType(actionStream.Next()); + frameEvent.type = parseFrameEventType(trim(actionStream.Next())); if (frameEvent.type == CharacterFrameEventType::SetVelocityXY) { frameEvent.velocityXY.x = toFloat(actionStream.Next(), 0.0f); frameEvent.velocityXY.y = toFloat(actionStream.Next(), 0.0f); } else if (frameEvent.type == CharacterFrameEventType::SetVelocityZ) { frameEvent.velocityZ = toFloat(actionStream.Next(), 0.0f); } else { - frameEvent.stringValue = actionStream.Next(); + frameEvent.stringValue = trim(actionStream.Next()); } definition.frameEvents.push_back(frameEvent); } @@ -425,8 +422,6 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts( addOrReplaceAction(actions_, std::move(definition)); } - SDL_Log("CharacterActionLibrary: loaded %d actions for job %d", - static_cast(actions_.size()), config.jobId); return true; } diff --git a/Game/src/character/CharacterAnimation.cpp b/Game/src/character/CharacterAnimation.cpp index c7dc6b3..ce6b8b9 100644 --- a/Game/src/character/CharacterAnimation.cpp +++ b/Game/src/character/CharacterAnimation.cpp @@ -1,8 +1,6 @@ -#include "character/CharacterAnimation.h" +#include "character/CharacterAnimation.h" #include "character/CharacterObject.h" #include -#include -#include #include #include @@ -160,20 +158,16 @@ void CharacterAnimation::SetAction(const std::string& actionName) { void CharacterAnimation::SetDirection(int direction) { direction_ = direction >= 0 ? 1 : -1; - ApplyFlipRecursive(this, direction_ < 0); -} -void CharacterAnimation::ApplyFlipRecursive(Actor* actor, bool flipped) const { - if (!actor) { + auto currentIt = actionAnimations_.find(currentActionTag_); + if (currentIt == actionAnimations_.end()) { return; } - if (auto* sprite = dynamic_cast(actor)) { - sprite->SetFlippedX(flipped); - } - - for (const auto& child : actor->GetChildren()) { - ApplyFlipRecursive(child.Get(), flipped); + for (auto& animation : currentIt->second) { + if (animation) { + animation->SetDirection(direction_); + } } } diff --git a/Game/src/character/CharacterDataLoader.cpp b/Game/src/character/CharacterDataLoader.cpp index 20da9a0..9a658b3 100644 --- a/Game/src/character/CharacterDataLoader.cpp +++ b/Game/src/character/CharacterDataLoader.cpp @@ -354,6 +354,7 @@ std::optional loadCharacterConfig(int jobId) { return std::nullopt; } + characterConfigCache()[jobId] = config; return config; } @@ -388,7 +389,6 @@ std::optional loadEquipmentConfig(int equipmentId) { EquipmentConfig config; config.id = equipmentId; config.path = indexIt->second; - SDL_Log("CharacterDataLoader: equipment %d -> %s", equipmentId, config.path.c_str()); while (!stream.isEnd()) { std::string segment = stream.get(); diff --git a/Game/src/character/CharacterObject.cpp b/Game/src/character/CharacterObject.cpp index 736ad7f..578879b 100644 --- a/Game/src/character/CharacterObject.cpp +++ b/Game/src/character/CharacterObject.cpp @@ -106,7 +106,7 @@ void CharacterObject::ApplyHit(const HitContext& hit) { } stateMachine_.ForceHurt(hurtAction); PlayAnimationTag(hurtAction ? hurtAction->animationTag - : ResolveAnimationTag("damage", "rest")); + : ResolveAnimationTag("hurt_light", "rest")); } void CharacterObject::OnUpdate(float deltaTime) { diff --git a/Game/src/character/CharacterStateMachine.cpp b/Game/src/character/CharacterStateMachine.cpp index b14163a..51e640e 100644 --- a/Game/src/character/CharacterStateMachine.cpp +++ b/Game/src/character/CharacterStateMachine.cpp @@ -13,6 +13,19 @@ bool hasMoveIntent(const CharacterIntent& intent) { return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f; } +void playActionById(CharacterObject& owner, + const std::string& actionId, + const std::string& fallbackTag = "rest") { + if (const auto* action = owner.FindAction(actionId)) { + if (!action->animationTag.empty()) { + owner.PlayAnimationTag(action->animationTag); + return; + } + } + + owner.PlayAnimationTag(owner.ResolveAnimationTag(actionId, fallbackTag)); +} + } // namespace void CharacterStateMachine::Reset() { @@ -100,18 +113,20 @@ void CharacterStateMachine::ChangeState(CharacterObject& owner, switch (nextState) { case CharacterStateId::Idle: - owner.PlayAnimationTag(owner.ResolveAnimationTag("rest", "rest")); + playActionById(owner, "idle"); break; case CharacterStateId::Move: - owner.PlayAnimationTag(owner.ResolveAnimationTag("run", "walk")); + playActionById(owner, "move"); break; case CharacterStateId::Jump: + playActionById(owner, "jump"); + break; case CharacterStateId::Fall: - owner.PlayAnimationTag(owner.ResolveAnimationTag("jump", "rest")); + playActionById(owner, "fall"); break; case CharacterStateId::Landing: landingTimer_ = 0.0f; - owner.PlayAnimationTag(owner.ResolveAnimationTag("rest", "rest")); + playActionById(owner, "landing"); break; case CharacterStateId::Action: case CharacterStateId::Hurt: