refactor(animation): 重构动画方向处理逻辑

- 将翻转逻辑集中到Animation类中处理
- 添加spriteFrameOffsets_存储帧偏移量
- 改进角色动画方向切换时的表现
- 移除CharacterAnimation中的ApplyFlipRecursive方法
- 优化动画帧位置和旋转的计算方式
This commit is contained in:
2026-04-03 09:13:50 +08:00
parent bb75a57afb
commit de522a1e64
8 changed files with 156 additions and 77 deletions

View File

@@ -40,6 +40,7 @@ public:
void Reset(); void Reset();
animation::AniFrame GetCurrentFrameInfo(); animation::AniFrame GetCurrentFrameInfo();
void SetFrameIndex(int index); void SetFrameIndex(int index);
void SetDirection(int direction);
void InterpolationLogic(); void InterpolationLogic();
Vec2 GetMaxSize() const; Vec2 GetMaxSize() const;
@@ -65,6 +66,7 @@ public:
std::vector<animation::AniFrame> frames_; std::vector<animation::AniFrame> frames_;
std::vector<Ptr<Sprite>> spriteFrames_; std::vector<Ptr<Sprite>> spriteFrames_;
std::vector<Vec2> spriteFrameOffsets_;
std::unordered_map<std::string, animation::AniFlag> animationFlag_; std::unordered_map<std::string, animation::AniFlag> animationFlag_;
@@ -75,8 +77,15 @@ public:
ReplaceData additionalOptionsData_; ReplaceData additionalOptionsData_;
Vec2 maxSize_ = Vec2::Zero(); Vec2 maxSize_ = Vec2::Zero();
int direction_ = 1;
std::vector<animation::AniFrame> interpolationData_; std::vector<animation::AniFrame> interpolationData_;
private:
void ApplyFramePresentation(const Vec2& framePos,
const Vec2& imageRate,
float rotation,
BlendMode blendMode);
}; };
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -5,10 +5,39 @@
#include <frostbite2D/types/type_math.h> #include <frostbite2D/types/type_math.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <algorithm> #include <algorithm>
#include <cmath>
using namespace frostbite2D::animation; using namespace frostbite2D::animation;
namespace frostbite2D { 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<float>(frameInfo.flag.at("IMAGE_ROTATE"));
}
return 0.0f;
}
Vec2 resolveImageRate(const AniFrame& frameInfo) {
if (frameInfo.flag.count("IMAGE_RATE")) {
return std::get<Vec2>(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() { Animation::Animation() {
} }
@@ -39,6 +68,8 @@ void Animation::Init(const std::string& aniPath) {
aniPath_ = aniPath; aniPath_ = aniPath;
animationFlag_ = info->flag; animationFlag_ = info->flag;
frames_ = std::move(info->frames); frames_ = std::move(info->frames);
spriteFrames_.clear();
spriteFrameOffsets_.clear();
if (animationFlag_.count("LOOP")) { if (animationFlag_.count("LOOP")) {
isLooping_ = true; isLooping_ = true;
@@ -69,6 +100,7 @@ void Animation::Init(const std::string& aniPath) {
} }
spriteFrames_.push_back(spriteObj); spriteFrames_.push_back(spriteObj);
spriteFrameOffsets_.push_back(spriteObj->GetOffset());
auto spriteSize = spriteObj->GetSize(); auto spriteSize = spriteObj->GetSize();
if (maxSize_.x < spriteSize.x) maxSize_.x = spriteSize.x; 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) { if (ani.second.layer.size() >= 2) {
subAni->SetZOrder(ani.second.layer[1]); subAni->SetZOrder(ani.second.layer[1]);
} }
subAni->SetDirection(direction_);
AddChild(subAni); AddChild(subAni);
} }
} }
@@ -188,20 +221,11 @@ void Animation::FlushFrame(int index) {
if (flagBuf.count("PLAY_SOUND")) { if (flagBuf.count("PLAY_SOUND")) {
} }
if (flagBuf.count("IMAGE_RATE")) { Vec2 imageRate = resolveImageRate(frameInfo);
auto rate = std::get<Vec2>(flagBuf.at("IMAGE_RATE")); float rotation = resolveFrameRotation(frameInfo);
currentFrame_->SetScale(rate); BlendMode blendMode = resolveBlendMode(frameInfo);
currentFrame_->SetPosition(frameInfo.imgPos.x * rate.x, frameInfo.imgPos.y * rate.y); Vec2 framePos = resolveFramePosition(frameInfo, imageRate);
} ApplyFramePresentation(framePos, imageRate, rotation, blendMode);
if (flagBuf.count("GRAPHIC_EFFECT_LINEARDODGE")) {
currentFrame_->SetBlendMode(BlendMode::Additive);
}
if (flagBuf.count("IMAGE_ROTATE")) {
auto rotation = std::get<float>(flagBuf.at("IMAGE_ROTATE"));
currentFrame_->SetRotation(rotation);
}
if (flagBuf.count("INTERPOLATION")) { if (flagBuf.count("INTERPOLATION")) {
if (interpolationData_.empty()) { if (interpolationData_.empty()) {
@@ -234,6 +258,27 @@ void Animation::SetFrameIndex(int index) {
currentFrameTime_ = 0.0f; currentFrameTime_ = 0.0f;
} }
void Animation::SetDirection(int direction) {
direction_ = direction >= 0 ? 1 : -1;
if (currentFrame_ && currentFrameIndex_ >= 0 &&
currentFrameIndex_ < static_cast<int>(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<Animation*>(child.Get());
if (subAnimation) {
subAnimation->SetDirection(direction_);
}
}
}
void Animation::InterpolationLogic() { void Animation::InterpolationLogic() {
if (interpolationData_.empty()) { if (interpolationData_.empty()) {
return; return;
@@ -268,11 +313,6 @@ void Animation::InterpolationLogic() {
static_cast<float>(rgbaData[3]) / 255.0f); static_cast<float>(rgbaData[3]) / 255.0f);
} }
{
Vec2 posData = Vec2::lerp(oldData.imgPos, newData.imgPos, interRate);
currentFrame_->SetPosition(posData);
}
{ {
Vec2 oldRateData = Vec2::One(); Vec2 oldRateData = Vec2::One();
Vec2 newRateData = Vec2::One(); Vec2 newRateData = Vec2::One();
@@ -285,10 +325,6 @@ void Animation::InterpolationLogic() {
} }
Vec2 rateData = Vec2::lerp(oldRateData, newRateData, interRate); Vec2 rateData = Vec2::lerp(oldRateData, newRateData, interRate);
currentFrame_->SetScale(rateData);
}
{
float oldAngleData = 0.0f; float oldAngleData = 0.0f;
float newAngleData = 0.0f; float newAngleData = 0.0f;
@@ -300,7 +336,9 @@ void Animation::InterpolationLogic() {
} }
float angleData = math::lerp(oldAngleData, newAngleData, interRate); 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_; return maxSize_;
} }
void Animation::ApplyFramePresentation(const Vec2& framePos,
const Vec2& imageRate,
float rotation,
BlendMode blendMode) {
if (!currentFrame_ || currentFrameIndex_ < 0 ||
currentFrameIndex_ >= static_cast<int>(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 } // namespace frostbite2D

View File

@@ -36,7 +36,6 @@ private:
const std::string& actionPath, const std::string& actionPath,
const character::CharacterConfig& config, const character::CharacterConfig& config,
const CharacterEquipmentManager& equipmentManager); const CharacterEquipmentManager& equipmentManager);
void ApplyFlipRecursive(Actor* actor, bool flipped) const;
CharacterObject* parent_ = nullptr; CharacterObject* parent_ = nullptr;
ActionAnimationList actionAnimations_; ActionAnimationList actionAnimations_;

View File

@@ -50,14 +50,6 @@ std::string trim(const std::string& value) {
return value.substr(begin, end - begin); 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) { std::string fileStem(const std::string& path) {
size_t slashPos = path.find_last_of("/\\"); size_t slashPos = path.find_last_of("/\\");
size_t begin = slashPos == std::string::npos ? 0 : slashPos + 1; size_t begin = slashPos == std::string::npos ? 0 : slashPos + 1;
@@ -108,10 +100,18 @@ private:
// action_list.lst 同时兼容两种格式: // action_list.lst 同时兼容两种格式:
// 1. 标准 PVF #PVF_File + 索引 + `path` // 1. 标准 PVF #PVF_File + 索引 + `path`
// 2. 早期临时版的 actionId + 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<std::pair<std::string, std::string>> parseActionListEntries( std::vector<std::pair<std::string, std::string>> parseActionListEntries(
ScriptTokenStream& listStream) { ScriptTokenStream& listStream) {
std::vector<std::pair<std::string, std::string>> entries; std::vector<std::pair<std::string, std::string>> entries;
std::vector<std::string> tokens; std::vector<std::string> tokens;
auto& pvf = PvfArchive::get();
while (!listStream.IsEnd()) { while (!listStream.IsEnd()) {
std::string token = trim(listStream.Next()); std::string token = trim(listStream.Next());
if (!token.empty()) { if (!token.empty()) {
@@ -119,23 +119,13 @@ std::vector<std::pair<std::string, std::string>> 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) { 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; return entries;
} }
@@ -357,13 +347,20 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts(
} }
auto entries = parseActionListEntries(listStream); 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) { for (const auto& [actionId, relativePath] : entries) {
if (actionId.empty() || relativePath.empty()) { if (actionId.empty() || relativePath.empty()) {
continue; continue;
} }
ScriptTokenStream actionStream( std::string fullActPath = relativePath.rfind("character/", 0) == 0
"character/" + config.jobTag + "/action_logic/" + relativePath); ? relativePath
: "character/" + relativePath;
ScriptTokenStream actionStream(fullActPath);
if (!actionStream.IsValid()) { if (!actionStream.IsValid()) {
continue; continue;
} }
@@ -377,9 +374,9 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts(
} }
if (token == "[action id]") { if (token == "[action id]") {
definition.actionId = toLower(actionStream.Next()); definition.actionId = toLower(trim(actionStream.Next()));
} else if (token == "[animation tag]") { } else if (token == "[animation tag]") {
definition.animationTag = actionStream.Next(); definition.animationTag = trim(actionStream.Next());
} else if (token == "[total frames]") { } else if (token == "[total frames]") {
definition.totalFrames = toInt(actionStream.Next(), definition.totalFrames); definition.totalFrames = toInt(actionStream.Next(), definition.totalFrames);
} else if (token == "[loop]") { } else if (token == "[loop]") {
@@ -394,7 +391,7 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts(
definition.moveSpeedScale = toFloat(actionStream.Next(), definition.moveSpeedScale); definition.moveSpeedScale = toFloat(actionStream.Next(), definition.moveSpeedScale);
} else if (token == "[cancel rule]") { } else if (token == "[cancel rule]") {
CharacterCancelRuleDefinition rule; CharacterCancelRuleDefinition rule;
rule.targetAction = toLower(actionStream.Next()); rule.targetAction = toLower(trim(actionStream.Next()));
rule.beginFrame = toInt(actionStream.Next(), 0); rule.beginFrame = toInt(actionStream.Next(), 0);
rule.endFrame = toInt(actionStream.Next(), rule.beginFrame); rule.endFrame = toInt(actionStream.Next(), rule.beginFrame);
rule.requireGrounded = isTrueToken(actionStream.Next()); rule.requireGrounded = isTrueToken(actionStream.Next());
@@ -403,14 +400,14 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts(
} else if (token == "[frame event]") { } else if (token == "[frame event]") {
CharacterFrameEventDefinition frameEvent; CharacterFrameEventDefinition frameEvent;
frameEvent.frame = toInt(actionStream.Next(), 0); frameEvent.frame = toInt(actionStream.Next(), 0);
frameEvent.type = parseFrameEventType(actionStream.Next()); frameEvent.type = parseFrameEventType(trim(actionStream.Next()));
if (frameEvent.type == CharacterFrameEventType::SetVelocityXY) { if (frameEvent.type == CharacterFrameEventType::SetVelocityXY) {
frameEvent.velocityXY.x = toFloat(actionStream.Next(), 0.0f); frameEvent.velocityXY.x = toFloat(actionStream.Next(), 0.0f);
frameEvent.velocityXY.y = toFloat(actionStream.Next(), 0.0f); frameEvent.velocityXY.y = toFloat(actionStream.Next(), 0.0f);
} else if (frameEvent.type == CharacterFrameEventType::SetVelocityZ) { } else if (frameEvent.type == CharacterFrameEventType::SetVelocityZ) {
frameEvent.velocityZ = toFloat(actionStream.Next(), 0.0f); frameEvent.velocityZ = toFloat(actionStream.Next(), 0.0f);
} else { } else {
frameEvent.stringValue = actionStream.Next(); frameEvent.stringValue = trim(actionStream.Next());
} }
definition.frameEvents.push_back(frameEvent); definition.frameEvents.push_back(frameEvent);
} }
@@ -425,8 +422,6 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts(
addOrReplaceAction(actions_, std::move(definition)); addOrReplaceAction(actions_, std::move(definition));
} }
SDL_Log("CharacterActionLibrary: loaded %d actions for job %d",
static_cast<int>(actions_.size()), config.jobId);
return true; return true;
} }

View File

@@ -1,8 +1,6 @@
#include "character/CharacterAnimation.h" #include "character/CharacterAnimation.h"
#include "character/CharacterObject.h" #include "character/CharacterObject.h"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <frostbite2D/2d/sprite.h>
#include <algorithm>
#include <array> #include <array>
#include <cstdio> #include <cstdio>
@@ -160,20 +158,16 @@ void CharacterAnimation::SetAction(const std::string& actionName) {
void CharacterAnimation::SetDirection(int direction) { void CharacterAnimation::SetDirection(int direction) {
direction_ = direction >= 0 ? 1 : -1; direction_ = direction >= 0 ? 1 : -1;
ApplyFlipRecursive(this, direction_ < 0);
}
void CharacterAnimation::ApplyFlipRecursive(Actor* actor, bool flipped) const { auto currentIt = actionAnimations_.find(currentActionTag_);
if (!actor) { if (currentIt == actionAnimations_.end()) {
return; return;
} }
if (auto* sprite = dynamic_cast<Sprite*>(actor)) { for (auto& animation : currentIt->second) {
sprite->SetFlippedX(flipped); if (animation) {
} animation->SetDirection(direction_);
}
for (const auto& child : actor->GetChildren()) {
ApplyFlipRecursive(child.Get(), flipped);
} }
} }

View File

@@ -354,6 +354,7 @@ std::optional<CharacterConfig> loadCharacterConfig(int jobId) {
return std::nullopt; return std::nullopt;
} }
characterConfigCache()[jobId] = config; characterConfigCache()[jobId] = config;
return config; return config;
} }
@@ -388,7 +389,6 @@ std::optional<EquipmentConfig> loadEquipmentConfig(int equipmentId) {
EquipmentConfig config; EquipmentConfig config;
config.id = equipmentId; config.id = equipmentId;
config.path = indexIt->second; config.path = indexIt->second;
SDL_Log("CharacterDataLoader: equipment %d -> %s", equipmentId, config.path.c_str());
while (!stream.isEnd()) { while (!stream.isEnd()) {
std::string segment = stream.get(); std::string segment = stream.get();

View File

@@ -106,7 +106,7 @@ void CharacterObject::ApplyHit(const HitContext& hit) {
} }
stateMachine_.ForceHurt(hurtAction); stateMachine_.ForceHurt(hurtAction);
PlayAnimationTag(hurtAction ? hurtAction->animationTag PlayAnimationTag(hurtAction ? hurtAction->animationTag
: ResolveAnimationTag("damage", "rest")); : ResolveAnimationTag("hurt_light", "rest"));
} }
void CharacterObject::OnUpdate(float deltaTime) { void CharacterObject::OnUpdate(float deltaTime) {

View File

@@ -13,6 +13,19 @@ bool hasMoveIntent(const CharacterIntent& intent) {
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f; 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 } // namespace
void CharacterStateMachine::Reset() { void CharacterStateMachine::Reset() {
@@ -100,18 +113,20 @@ void CharacterStateMachine::ChangeState(CharacterObject& owner,
switch (nextState) { switch (nextState) {
case CharacterStateId::Idle: case CharacterStateId::Idle:
owner.PlayAnimationTag(owner.ResolveAnimationTag("rest", "rest")); playActionById(owner, "idle");
break; break;
case CharacterStateId::Move: case CharacterStateId::Move:
owner.PlayAnimationTag(owner.ResolveAnimationTag("run", "walk")); playActionById(owner, "move");
break; break;
case CharacterStateId::Jump: case CharacterStateId::Jump:
playActionById(owner, "jump");
break;
case CharacterStateId::Fall: case CharacterStateId::Fall:
owner.PlayAnimationTag(owner.ResolveAnimationTag("jump", "rest")); playActionById(owner, "fall");
break; break;
case CharacterStateId::Landing: case CharacterStateId::Landing:
landingTimer_ = 0.0f; landingTimer_ = 0.0f;
owner.PlayAnimationTag(owner.ResolveAnimationTag("rest", "rest")); playActionById(owner, "landing");
break; break;
case CharacterStateId::Action: case CharacterStateId::Action:
case CharacterStateId::Hurt: case CharacterStateId::Hurt: