feat(动画): 添加角色动作合成纹理功能

实现角色动作的离屏渲染合成功能,支持获取合成纹理及其相关信息:
1. 新增CanvasActor用于离屏渲染
2. 新增RenderTexture封装FBO和纹理
3. 扩展Renderer支持离屏渲染到纹理
4. 为CharacterAnimation添加合成纹理生成逻辑
5. 在调试界面添加合成纹理预览功能
This commit is contained in:
2026-04-07 06:17:49 +08:00
parent 6684abd131
commit 808431f92c
14 changed files with 1207 additions and 22 deletions

View File

@@ -3,6 +3,7 @@
#include "character/CharacterDataLoader.h"
#include "character/CharacterEquipmentManager.h"
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/2d/canvas_actor.h>
#include <frostbite2D/animation/animation.h>
#include <frostbite2D/base/RefPtr.h>
#include <functional>
@@ -21,9 +22,20 @@ public:
using ActionFrameFlagCallback = std::function<void(int)>;
using ActionEndCallback = std::function<void()>;
struct CompositeFrameInfo {
bool valid = false;
Rect localBounds;
Vec2 originInTexture = Vec2::Zero();
Vec2 groundAnchorInTexture = Vec2::Zero();
int width = 1;
int height = 1;
};
bool Init(CharacterObject* parent,
const character::CharacterConfig& config,
const CharacterEquipmentManager& equipmentManager);
void Update(float deltaTime) override;
void Render() override;
bool SetAction(const std::string& actionName);
void SetDirection(int direction);
@@ -39,6 +51,12 @@ public:
animation::AniFrame GetCurrentFrameInfo() const;
void SetActionFrameFlagCallback(ActionFrameFlagCallback callback);
void SetActionEndCallback(ActionEndCallback callback);
bool HasCompositeTexture() const;
Ptr<Texture> GetCompositeTexture() const;
Vec2 GetCompositeTextureSize() const;
Vec2 GetCompositeOriginInTexture() const;
Vec2 GetCompositeGroundAnchorInTexture() const;
uint64 GetCompositeVersion() const { return compositeVersion_; }
private:
static std::string FormatImgPath(std::string path, Animation::ReplaceData data);
@@ -46,6 +64,15 @@ private:
Animation* GetCurrentPrimaryAnimation() const;
void RefreshRuntimeCallbacks();
bool shouldSkipMissingAnimation(const std::string& aniPath);
void BuildCompositeActionFrames();
CompositeFrameInfo ComputeCompositeFrameInfo(const std::vector<RefPtr<Animation>>& animations,
int direction) const;
CompositeFrameInfo GetCurrentCompositeFrameInfo() const;
uint64 CaptureCurrentRenderSignature() const;
void MarkCompositeDirty();
void UpdateCompositeCamera();
void RefreshCompositeTextureIfNeeded();
void RenderCurrentActionToCompositeCanvas();
void CreateAnimationBySlot(const std::string& actionName,
const std::string& slotName,
@@ -61,6 +88,11 @@ private:
ActionEndCallback actionEndCallback_;
std::unordered_set<std::string> missingAnimationPaths_;
size_t skippedMissingAnimationCount_ = 0;
RefPtr<CanvasActor> compositeCanvas_ = nullptr;
std::map<std::string, CompositeFrameInfo> compositeFrameInfoByAction_;
bool compositeDirty_ = true;
uint64 compositeVersion_ = 0;
uint64 lastCompositeSignature_ = 0;
};
} // namespace frostbite2D

View File

@@ -141,6 +141,12 @@ public:
int GetCurrentAnimationFrameCount() const;
float GetCurrentAnimationProgressNormalized() const;
animation::AniFrame GetCurrentAnimationFrameInfo() const;
bool HasCompositeTexture() const;
Ptr<Texture> GetCompositeTexture() const;
Vec2 GetCompositeTextureSize() const;
Vec2 GetCompositeOriginInTexture() const;
Vec2 GetCompositeGroundAnchorInTexture() const;
uint64 GetCompositeTextureVersion() const;
/// @brief 绑定当前动作主动画的运行时回调,供后续脚本系统接入。
void SetAnimationFrameFlagCallback(CharacterAnimation::ActionFrameFlagCallback callback);

View File

@@ -1,12 +1,14 @@
#pragma once
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/2d/sprite.h>
namespace frostbite2D {
class CharacterObject;
class GameMap;
class NineSliceActor;
class Sprite;
class TextSprite;
/**
@@ -33,6 +35,11 @@ public:
private:
void initOverlay();
void updateOverlay();
void updateMapDebugHighlight();
void updateCompositePreview();
Vec2 computeScreenMatchedPreviewSize(const Vec2& textureSize) const;
void updateCompositePreviewMarker(const Vec2& textureSize, const Vec2& previewSize,
const Vec2& originInTexture);
void setOverlayVisible(bool visible);
GameDebugActor();
@@ -42,6 +49,13 @@ private:
CharacterObject* trackedCharacter_ = nullptr;
RefPtr<NineSliceActor> background_;
RefPtr<TextSprite> coordText_;
RefPtr<TextSprite> actionText_;
RefPtr<TextSprite> compositeText_;
RefPtr<Sprite> compositePreview_;
RefPtr<Actor> compositeOriginMarker_;
CharacterObject* previewCharacter_ = nullptr;
uint64 previewCompositeVersion_ = 0;
bool previewTextureAvailable_ = false;
};
} // namespace frostbite2D

View File

@@ -1,9 +1,11 @@
#include "character/CharacterAnimation.h"
#include "character/CharacterObject.h"
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <sstream>
#include <utility>
@@ -32,6 +34,82 @@ std::string actionTail(const std::string& path) {
return path.substr(slashPos);
}
struct BoundsAccumulator {
bool valid = false;
float minX = 0.0f;
float minY = 0.0f;
float maxX = 0.0f;
float maxY = 0.0f;
void includeRect(const Rect& rect) {
if (rect.empty()) {
return;
}
if (!valid) {
minX = rect.left();
minY = rect.top();
maxX = rect.right();
maxY = rect.bottom();
valid = true;
return;
}
minX = std::min(minX, rect.left());
minY = std::min(minY, rect.top());
maxX = std::max(maxX, rect.right());
maxY = std::max(maxY, rect.bottom());
}
};
Rect transformRectBounds(const Rect& rect, const Transform2D& transform) {
Vec2 p0 = transform.transformPoint(Vec2(rect.left(), rect.top()));
Vec2 p1 = transform.transformPoint(Vec2(rect.right(), rect.top()));
Vec2 p2 = transform.transformPoint(Vec2(rect.left(), rect.bottom()));
Vec2 p3 = transform.transformPoint(Vec2(rect.right(), rect.bottom()));
float minX = std::min(std::min(p0.x, p1.x), std::min(p2.x, p3.x));
float minY = std::min(std::min(p0.y, p1.y), std::min(p2.y, p3.y));
float maxX = std::max(std::max(p0.x, p1.x), std::max(p2.x, p3.x));
float maxY = std::max(std::max(p0.y, p1.y), std::max(p2.y, p3.y));
return Rect(minX, minY, maxX - minX, maxY - minY);
}
void combineHash(uint64& seed, uint64 value) {
constexpr uint64 kOffset = 0x9e3779b97f4a7c15ULL;
seed ^= value + kOffset + (seed << 6) + (seed >> 2);
}
CharacterAnimation::CompositeFrameInfo combineCompositeFrameInfo(
const CharacterAnimation::CompositeFrameInfo& lhs,
const CharacterAnimation::CompositeFrameInfo& rhs) {
if (!lhs.valid) {
return rhs;
}
if (!rhs.valid) {
return lhs;
}
float left = std::floor(std::min(lhs.localBounds.left(), rhs.localBounds.left()));
float top = std::floor(std::min(lhs.localBounds.top(), rhs.localBounds.top()));
float right = std::ceil(std::max(lhs.localBounds.right(), rhs.localBounds.right()));
float bottom =
std::ceil(std::max(lhs.localBounds.bottom(), rhs.localBounds.bottom()));
CharacterAnimation::CompositeFrameInfo result;
result.valid = true;
result.localBounds =
Rect(left, top, std::max(right - left, 1.0f), std::max(bottom - top, 1.0f));
result.width =
std::max(static_cast<int>(std::lround(result.localBounds.width())), 1);
result.height =
std::max(static_cast<int>(std::lround(result.localBounds.height())), 1);
result.originInTexture =
Vec2(-result.localBounds.left(), -result.localBounds.top());
result.groundAnchorInTexture = result.originInTexture;
return result;
}
} // namespace
bool CharacterAnimation::Init(CharacterObject* parent,
@@ -45,6 +123,21 @@ bool CharacterAnimation::Init(CharacterObject* parent,
direction_ = 1;
actionFrameFlagCallback_ = nullptr;
actionEndCallback_ = nullptr;
compositeCanvas_ = MakePtr<CanvasActor>();
compositeFrameInfoByAction_.clear();
compositeDirty_ = true;
compositeVersion_ = 0;
lastCompositeSignature_ = 0;
if (!compositeCanvas_->Init(1, 1)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"CharacterAnimation: failed to initialize composite canvas");
compositeCanvas_.Reset();
} else {
compositeCanvas_->SetCustomDrawCallback([this]() {
RenderCurrentActionToCompositeCanvas();
});
}
for (const auto& [actionName, actionPath] : config.animationPath) {
for (const char* slotName : kAvatarParts) {
@@ -65,9 +158,27 @@ bool CharacterAnimation::Init(CharacterObject* parent,
config.jobId);
}
BuildCompositeActionFrames();
return true;
}
void CharacterAnimation::Update(float deltaTime) {
Actor::Update(deltaTime);
uint64 renderSignature = CaptureCurrentRenderSignature();
if (renderSignature != lastCompositeSignature_) {
compositeDirty_ = true;
}
}
void CharacterAnimation::Render() {
if (compositeDirty_ && compositeCanvas_ && Renderer::get().isFrameActive()) {
RefreshCompositeTextureIfNeeded();
}
Actor::Render();
}
std::string CharacterAnimation::FormatImgPath(std::string path, Animation::ReplaceData data) {
size_t pos = path.find("%04d");
if (pos != std::string::npos) {
@@ -262,6 +373,7 @@ bool CharacterAnimation::SetAction(const std::string& actionName) {
currentActionTag_ = actionName;
RefreshRuntimeCallbacks();
SetDirection(direction_);
MarkCompositeDirty();
return true;
}
@@ -278,6 +390,8 @@ void CharacterAnimation::SetDirection(int direction) {
animation->SetDirection(direction_);
}
}
MarkCompositeDirty();
}
bool CharacterAnimation::IsCurrentActionFinished() const {
@@ -327,4 +441,158 @@ void CharacterAnimation::SetActionEndCallback(ActionEndCallback callback) {
RefreshRuntimeCallbacks();
}
bool CharacterAnimation::HasCompositeTexture() const {
return compositeCanvas_ && compositeCanvas_->IsCanvasReady() &&
GetCurrentCompositeFrameInfo().valid;
}
Ptr<Texture> CharacterAnimation::GetCompositeTexture() const {
return compositeCanvas_ ? compositeCanvas_->GetOutputTexture() : nullptr;
}
Vec2 CharacterAnimation::GetCompositeTextureSize() const {
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
return Vec2(static_cast<float>(info.width), static_cast<float>(info.height));
}
Vec2 CharacterAnimation::GetCompositeOriginInTexture() const {
return GetCurrentCompositeFrameInfo().originInTexture;
}
Vec2 CharacterAnimation::GetCompositeGroundAnchorInTexture() const {
return GetCurrentCompositeFrameInfo().groundAnchorInTexture;
}
CharacterAnimation::CompositeFrameInfo CharacterAnimation::ComputeCompositeFrameInfo(
const std::vector<RefPtr<Animation>>& animations, int direction) const {
CompositeFrameInfo info;
BoundsAccumulator bounds;
for (const auto& animation : animations) {
if (!animation) {
continue;
}
Rect animationBounds;
if (!animation->GetStaticLocalBounds(direction, animationBounds)) {
continue;
}
bounds.includeRect(
transformRectBounds(animationBounds, animation->GetLocalTransform()));
}
if (!bounds.valid) {
return info;
}
float left = std::floor(bounds.minX);
float top = std::floor(bounds.minY);
float right = std::ceil(bounds.maxX);
float bottom = std::ceil(bounds.maxY);
info.valid = true;
info.localBounds = Rect(left, top, std::max(right - left, 1.0f),
std::max(bottom - top, 1.0f));
info.width = std::max(static_cast<int>(std::lround(info.localBounds.width())), 1);
info.height = std::max(static_cast<int>(std::lround(info.localBounds.height())), 1);
info.originInTexture = Vec2(-info.localBounds.left(), -info.localBounds.top());
info.groundAnchorInTexture = info.originInTexture;
return info;
}
void CharacterAnimation::BuildCompositeActionFrames() {
compositeFrameInfoByAction_.clear();
for (const auto& [actionName, animations] : actionAnimations_) {
CompositeFrameInfo rightInfo = ComputeCompositeFrameInfo(animations, 1);
CompositeFrameInfo leftInfo = ComputeCompositeFrameInfo(animations, -1);
compositeFrameInfoByAction_[actionName] =
combineCompositeFrameInfo(rightInfo, leftInfo);
}
}
CharacterAnimation::CompositeFrameInfo CharacterAnimation::GetCurrentCompositeFrameInfo() const {
auto it = compositeFrameInfoByAction_.find(currentActionTag_);
if (it == compositeFrameInfoByAction_.end()) {
return CompositeFrameInfo();
}
return it->second;
}
uint64 CharacterAnimation::CaptureCurrentRenderSignature() const {
uint64 signature = 1469598103934665603ULL;
combineHash(signature, static_cast<uint64>(direction_ >= 0 ? 1 : 0));
combineHash(signature, static_cast<uint64>(std::hash<std::string>{}(currentActionTag_)));
auto currentIt = actionAnimations_.find(currentActionTag_);
if (currentIt == actionAnimations_.end()) {
return signature;
}
for (const auto& animation : currentIt->second) {
if (!animation) {
continue;
}
combineHash(signature, animation->GetRenderSignature());
}
return signature;
}
void CharacterAnimation::MarkCompositeDirty() {
compositeDirty_ = true;
if (compositeCanvas_) {
compositeCanvas_->SetDirty();
}
}
void CharacterAnimation::UpdateCompositeCamera() {
if (!compositeCanvas_) {
return;
}
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
Camera& camera = compositeCanvas_->GetCanvasCamera();
camera.setViewport(std::max(info.width, 1), std::max(info.height, 1));
camera.setZoom(1.0f);
Vec2 cameraOrigin = GetWorldTransform().transformPoint(
Vec2(info.localBounds.left(), info.localBounds.top()));
camera.setPosition(cameraOrigin);
}
void CharacterAnimation::RefreshCompositeTextureIfNeeded() {
if (!compositeDirty_ || !compositeCanvas_) {
return;
}
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
if (!info.valid) {
compositeDirty_ = false;
lastCompositeSignature_ = CaptureCurrentRenderSignature();
return;
}
if (!compositeCanvas_->SetCanvasSize(std::max(info.width, 1),
std::max(info.height, 1))) {
return;
}
UpdateCompositeCamera();
if (!compositeCanvas_->Redraw()) {
return;
}
compositeDirty_ = false;
lastCompositeSignature_ = CaptureCurrentRenderSignature();
++compositeVersion_;
}
void CharacterAnimation::RenderCurrentActionToCompositeCanvas() {
if (actionAnimations_.find(currentActionTag_) == actionAnimations_.end()) {
return;
}
RenderChildren();
}
} // namespace frostbite2D

View File

@@ -404,6 +404,32 @@ animation::AniFrame CharacterObject::GetCurrentAnimationFrameInfo() const {
return animationManager_ ? animationManager_->GetCurrentFrameInfo() : animation::AniFrame();
}
bool CharacterObject::HasCompositeTexture() const {
return animationManager_ && animationManager_->HasCompositeTexture();
}
Ptr<Texture> CharacterObject::GetCompositeTexture() const {
return animationManager_ ? animationManager_->GetCompositeTexture() : nullptr;
}
Vec2 CharacterObject::GetCompositeTextureSize() const {
return animationManager_ ? animationManager_->GetCompositeTextureSize() : Vec2::Zero();
}
Vec2 CharacterObject::GetCompositeOriginInTexture() const {
return animationManager_ ? animationManager_->GetCompositeOriginInTexture()
: Vec2::Zero();
}
Vec2 CharacterObject::GetCompositeGroundAnchorInTexture() const {
return animationManager_ ? animationManager_->GetCompositeGroundAnchorInTexture()
: Vec2::Zero();
}
uint64 CharacterObject::GetCompositeTextureVersion() const {
return animationManager_ ? animationManager_->GetCompositeVersion() : 0;
}
void CharacterObject::SetAnimationFrameFlagCallback(
CharacterAnimation::ActionFrameFlagCallback callback) {
if (animationManager_) {

View File

@@ -1,9 +1,13 @@
#include "common/debug/GameDebugActor.h"
#include "common/debug/GameDebugActor.h"
#include "character/CharacterObject.h"
#include "map/GameMap.h"
#include "ui/NineSliceActor.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
#include <frostbite2D/2d/sprite.h>
#include <frostbite2D/2d/text_sprite.h>
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/scene/scene.h>
#include <limits>
@@ -15,8 +19,38 @@ constexpr float kDebugHudMarginX = 12.0f;
constexpr float kDebugHudMarginY = 12.0f;
constexpr float kDebugHudPaddingX = 8.0f;
constexpr float kDebugHudPaddingY = 6.0f;
constexpr float kDebugHudLineGap = 4.0f;
constexpr float kDebugHudPreviewGap = 8.0f;
constexpr float kDebugHudOriginMarkerHalfExtent = 3.0f;
constexpr char kDebugHudPopupImg[] = "sprite/interface/newstyle/windows/popup/popup.img";
class DebugCrosshairActor : public Actor {
public:
void Render() override {
if (!IsVisible()) {
return;
}
Renderer& renderer = Renderer::get();
Vec2 center = GetWorldTransform().transformPoint(Vec2::Zero());
Color color = Colors::Yellow;
color.a *= GetWorldOpacity();
if (color.a <= 0.0f) {
return;
}
float lineLength = kDebugHudOriginMarkerHalfExtent * 2.0f + 1.0f;
renderer.drawQuad(
Rect(center.x - kDebugHudOriginMarkerHalfExtent, center.y, lineLength,
1.0f),
color);
renderer.drawQuad(
Rect(center.x, center.y - kDebugHudOriginMarkerHalfExtent, 1.0f,
lineLength),
color);
}
};
void ConfigureTextLine(RefPtr<TextSprite> textSprite, const char* name,
int zOrder) {
if (!textSprite) {
@@ -83,6 +117,11 @@ void GameDebugActor::SetDebugMap(GameMap* map) {
}
void GameDebugActor::SetTrackedCharacter(CharacterObject* character) {
if (trackedCharacter_ != character) {
previewCharacter_ = nullptr;
previewCompositeVersion_ = 0;
previewTextureAvailable_ = false;
}
trackedCharacter_ = character;
updateOverlay();
}
@@ -93,6 +132,16 @@ void GameDebugActor::ClearDebugContext() {
}
debugMap_ = nullptr;
trackedCharacter_ = nullptr;
previewCharacter_ = nullptr;
previewCompositeVersion_ = 0;
previewTextureAvailable_ = false;
if (compositePreview_) {
compositePreview_->SetTexture(nullptr);
compositePreview_->SetSize(0.0f, 0.0f);
}
if (compositeOriginMarker_) {
compositeOriginMarker_->SetVisible(false);
}
setOverlayVisible(false);
}
@@ -102,7 +151,7 @@ void GameDebugActor::OnUpdate(float deltaTime) {
}
void GameDebugActor::initOverlay() {
if (background_ || coordText_) {
if (background_ || coordText_ || actionText_ || compositeText_ || compositePreview_) {
return;
}
@@ -120,43 +169,233 @@ void GameDebugActor::initOverlay() {
ConfigureTextLine(coordText_, "debugCoordText", 1);
AddChild(coordText_);
actionText_ = TextSprite::create();
ConfigureTextLine(actionText_, "debugActionText", 1);
AddChild(actionText_);
compositeText_ = TextSprite::create();
ConfigureTextLine(compositeText_, "debugCompositeText", 1);
AddChild(compositeText_);
compositePreview_ = MakePtr<Sprite>();
compositePreview_->SetName("debugCompositePreview");
compositePreview_->SetZOrder(1);
AddChild(compositePreview_);
compositeOriginMarker_ = MakePtr<DebugCrosshairActor>();
compositeOriginMarker_->SetName("debugCompositeOriginMarker");
compositeOriginMarker_->SetZOrder(2);
compositeOriginMarker_->SetVisible(false);
AddChild(compositeOriginMarker_);
setOverlayVisible(false);
}
void GameDebugActor::updateOverlay() {
if (!coordText_ || !debugMap_ || !trackedCharacter_ ||
!debugMap_->IsDebugModeEnabled()) {
if (debugMap_) {
debugMap_->SetDebugHighlightedMoveAreaIndex(GameMap::kInvalidMoveAreaIndex);
}
if (!coordText_ || !actionText_ || !compositeText_ || !compositePreview_) {
setOverlayVisible(false);
return;
}
updateMapDebugHighlight();
if (!trackedCharacter_) {
if (compositePreview_) {
compositePreview_->SetTexture(nullptr);
compositePreview_->SetSize(0.0f, 0.0f);
}
if (compositeOriginMarker_) {
compositeOriginMarker_->SetVisible(false);
}
previewCharacter_ = nullptr;
previewCompositeVersion_ = 0;
previewTextureAvailable_ = false;
setOverlayVisible(false);
return;
}
const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition();
char coordTextBuffer[96];
SDL_snprintf(coordTextBuffer, sizeof(coordTextBuffer), "角色坐标: (%d, %d, %d)",
worldPosition.x, worldPosition.y, worldPosition.z);
coordText_->SetText(coordTextBuffer);
int frameCount = std::max(trackedCharacter_->GetCurrentAnimationFrameCount(), 1);
int frameIndex = std::clamp(trackedCharacter_->GetCurrentAnimationFrameIndex(), 0,
frameCount - 1);
char actionTextBuffer[128];
SDL_snprintf(actionTextBuffer, sizeof(actionTextBuffer),
"Action: %s dir:%c frame:%d/%d",
trackedCharacter_->GetCurrentAction().empty()
? "<none>"
: trackedCharacter_->GetCurrentAction().c_str(),
trackedCharacter_->GetDirection() >= 0 ? 'R' : 'L', frameIndex + 1,
frameCount);
actionText_->SetText(actionTextBuffer);
updateCompositePreview();
Vec2 coordTextSize = coordText_->GetTextSize();
Vec2 actionTextSize = actionText_->GetTextSize();
Vec2 compositeTextSize = compositeText_->GetTextSize();
Vec2 previewSize = compositePreview_->IsVisible() ? compositePreview_->GetSize()
: Vec2::Zero();
float textBlockWidth = std::max(coordTextSize.x,
std::max(actionTextSize.x, compositeTextSize.x));
float panelWidth = std::max(textBlockWidth, previewSize.x) + kDebugHudPaddingX * 2.0f;
float contentHeight = coordTextSize.y;
if (actionText_->IsVisible()) {
contentHeight += kDebugHudLineGap + actionTextSize.y;
}
if (compositeText_->IsVisible()) {
contentHeight += kDebugHudLineGap + compositeTextSize.y;
}
if (compositePreview_->IsVisible()) {
contentHeight += kDebugHudPreviewGap + previewSize.y;
}
float panelHeight = contentHeight + kDebugHudPaddingY * 2.0f;
Vec2 panelSize(panelWidth, panelHeight);
if (background_) {
background_->SetSize(panelSize);
}
SetPosition(kDebugHudMarginX, kDebugHudMarginY);
SetScale(1.0f);
float currentY = kDebugHudPaddingY;
coordText_->SetPosition(kDebugHudPaddingX, currentY);
currentY += coordTextSize.y;
if (actionText_->IsVisible()) {
currentY += kDebugHudLineGap;
actionText_->SetPosition(kDebugHudPaddingX, currentY);
currentY += actionTextSize.y;
}
if (compositeText_->IsVisible()) {
currentY += kDebugHudLineGap;
compositeText_->SetPosition(kDebugHudPaddingX, currentY);
currentY += compositeTextSize.y;
}
if (compositePreview_->IsVisible()) {
currentY += kDebugHudPreviewGap;
compositePreview_->SetPosition(kDebugHudPaddingX, currentY);
updateCompositePreviewMarker(trackedCharacter_->GetCompositeTextureSize(),
compositePreview_->GetSize(),
trackedCharacter_->GetCompositeOriginInTexture());
}
setOverlayVisible(true);
}
void GameDebugActor::updateMapDebugHighlight() {
if (!debugMap_) {
return;
}
if (!trackedCharacter_ || !debugMap_->IsDebugModeEnabled()) {
debugMap_->SetDebugHighlightedMoveAreaIndex(GameMap::kInvalidMoveAreaIndex);
return;
}
const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition();
Vec3 currentWorldPos(static_cast<float>(worldPosition.x),
static_cast<float>(worldPosition.y),
static_cast<float>(worldPosition.z));
size_t moveAreaIndex = debugMap_->FindMoveAreaIndex(currentWorldPos);
debugMap_->SetDebugHighlightedMoveAreaIndex(moveAreaIndex);
}
char coordTextBuffer[96];
SDL_snprintf(coordTextBuffer, sizeof(coordTextBuffer), "角色坐标: (%d, %d, %d)",
worldPosition.x, worldPosition.y, worldPosition.z);
coordText_->SetText(coordTextBuffer);
Vec2 coordTextSize = coordText_->GetTextSize();
float panelWidth = coordTextSize.x + kDebugHudPaddingX * 2.0f;
float panelHeight = coordTextSize.y + kDebugHudPaddingY * 2.0f;
Vec2 panelSize(panelWidth, panelHeight);
if (background_) {
background_->SetSize(panelSize);
void GameDebugActor::updateCompositePreview() {
if (!trackedCharacter_ || !compositePreview_ || !compositeText_) {
return;
}
SetPosition(kDebugHudMarginX, kDebugHudMarginY);
SetScale(1.0f);
coordText_->SetPosition(kDebugHudPaddingX, kDebugHudPaddingY);
setOverlayVisible(true);
Ptr<Texture> compositeTexture = trackedCharacter_->GetCompositeTexture();
bool textureAvailable =
trackedCharacter_->HasCompositeTexture() && compositeTexture != nullptr;
uint64 compositeVersion =
textureAvailable ? trackedCharacter_->GetCompositeTextureVersion() : 0;
bool needsRefresh = previewCharacter_ != trackedCharacter_ ||
previewCompositeVersion_ != compositeVersion ||
previewTextureAvailable_ != textureAvailable;
if (needsRefresh) {
previewCharacter_ = trackedCharacter_;
previewCompositeVersion_ = compositeVersion;
previewTextureAvailable_ = textureAvailable;
if (textureAvailable) {
compositePreview_->SetTexture(compositeTexture);
} else {
compositePreview_->SetTexture(nullptr);
compositePreview_->SetSize(0.0f, 0.0f);
compositePreview_->SetVisible(false);
if (compositeOriginMarker_) {
compositeOriginMarker_->SetVisible(false);
}
}
}
if (!textureAvailable) {
compositeText_->SetText("Composite: unavailable");
compositeText_->SetVisible(true);
return;
}
Vec2 textureSize = trackedCharacter_->GetCompositeTextureSize();
Vec2 previewSize = computeScreenMatchedPreviewSize(textureSize);
Vec2 origin = trackedCharacter_->GetCompositeOriginInTexture();
Vec2 groundAnchor = trackedCharacter_->GetCompositeGroundAnchorInTexture();
compositePreview_->SetSize(previewSize);
compositePreview_->SetVisible(true);
updateCompositePreviewMarker(textureSize, previewSize, origin);
char compositeTextBuffer[256];
SDL_snprintf(
compositeTextBuffer, sizeof(compositeTextBuffer),
"Composite: tex %.0fx%.0f preview %.0fx%.0f origin(%.0f, %.0f) ground(%.0f, %.0f) ver %llu",
textureSize.x, textureSize.y, previewSize.x, previewSize.y, origin.x,
origin.y, groundAnchor.x, groundAnchor.y,
static_cast<unsigned long long>(trackedCharacter_->GetCompositeTextureVersion()));
compositeText_->SetText(compositeTextBuffer);
compositeText_->SetVisible(true);
}
Vec2 GameDebugActor::computeScreenMatchedPreviewSize(const Vec2& textureSize) const {
if (!trackedCharacter_ || textureSize.x <= 0.0f || textureSize.y <= 0.0f) {
return Vec2::Zero();
}
Renderer& renderer = Renderer::get();
Camera* worldCamera = renderer.getCamera();
float zoom = worldCamera ? worldCamera->getZoom() : 1.0f;
Vec2 worldScale = math::extractScale(trackedCharacter_->GetWorldTransform().matrix);
return Vec2(std::max(textureSize.x * std::abs(worldScale.x) * zoom, 1.0f),
std::max(textureSize.y * std::abs(worldScale.y) * zoom, 1.0f));
}
void GameDebugActor::updateCompositePreviewMarker(const Vec2& textureSize,
const Vec2& previewSize,
const Vec2& originInTexture) {
if (!compositeOriginMarker_ || !compositePreview_ || textureSize.x <= 0.0f ||
textureSize.y <= 0.0f || previewSize.x <= 0.0f || previewSize.y <= 0.0f) {
if (compositeOriginMarker_) {
compositeOriginMarker_->SetVisible(false);
}
return;
}
float scaleX = previewSize.x / textureSize.x;
float scaleY = previewSize.y / textureSize.y;
Vec2 previewTopLeft = compositePreview_->GetPosition();
compositeOriginMarker_->SetPosition(previewTopLeft.x + originInTexture.x * scaleX,
previewTopLeft.y + originInTexture.y * scaleY);
compositeOriginMarker_->SetVisible(compositePreview_->IsVisible());
}
void GameDebugActor::setOverlayVisible(bool visible) {
@@ -166,6 +405,20 @@ void GameDebugActor::setOverlayVisible(bool visible) {
if (coordText_) {
coordText_->SetVisible(visible);
}
if (actionText_) {
actionText_->SetVisible(visible);
}
if (compositeText_) {
compositeText_->SetVisible(visible);
}
if (compositePreview_) {
compositePreview_->SetVisible(visible && previewTextureAvailable_);
}
if (compositeOriginMarker_) {
compositeOriginMarker_->SetVisible(visible && previewTextureAvailable_ &&
compositePreview_ &&
compositePreview_->IsVisible());
}
}
} // namespace frostbite2D