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

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