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

View File

@@ -5,10 +5,39 @@
#include <frostbite2D/types/type_math.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
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<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() {
}
@@ -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<Vec2>(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<float>(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<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() {
if (interpolationData_.empty()) {
return;
@@ -268,11 +313,6 @@ void Animation::InterpolationLogic() {
static_cast<float>(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<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

View File

@@ -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_;

View File

@@ -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<std::pair<std::string, std::string>> parseActionListEntries(
ScriptTokenStream& listStream) {
std::vector<std::pair<std::string, std::string>> entries;
std::vector<std::string> tokens;
auto& pvf = PvfArchive::get();
while (!listStream.IsEnd()) {
std::string token = trim(listStream.Next());
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) {
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<int>(actions_.size()), config.jobId);
return true;
}

View File

@@ -1,8 +1,6 @@
#include "character/CharacterAnimation.h"
#include "character/CharacterAnimation.h"
#include "character/CharacterObject.h"
#include <SDL2/SDL.h>
#include <frostbite2D/2d/sprite.h>
#include <algorithm>
#include <array>
#include <cstdio>
@@ -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<Sprite*>(actor)) {
sprite->SetFlippedX(flipped);
}
for (const auto& child : actor->GetChildren()) {
ApplyFlipRecursive(child.Get(), flipped);
for (auto& animation : currentIt->second) {
if (animation) {
animation->SetDirection(direction_);
}
}
}

View File

@@ -354,6 +354,7 @@ std::optional<CharacterConfig> loadCharacterConfig(int jobId) {
return std::nullopt;
}
characterConfigCache()[jobId] = config;
return config;
}
@@ -388,7 +389,6 @@ std::optional<EquipmentConfig> 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();

View File

@@ -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) {

View File

@@ -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: