diff --git a/Frostbite2D/include/frostbite2D/2d/canvas_actor.h b/Frostbite2D/include/frostbite2D/2d/canvas_actor.h new file mode 100644 index 0000000..f78eba6 --- /dev/null +++ b/Frostbite2D/include/frostbite2D/2d/canvas_actor.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include + +namespace frostbite2D { + +/// @brief 离屏画布节点。 +/// +/// 子节点通过 `AddCanvasChild()` 挂到内部画布树,只会在重绘到离屏纹理时参与渲染, +/// 不会像普通场景子节点那样直接显示到屏幕。 +class CanvasActor : public Sprite { +public: + CanvasActor(); + ~CanvasActor() override = default; + + bool Init(int width, int height); + bool SetCanvasSize(int width, int height); + Vec2 GetCanvasSize() const { + return Vec2(static_cast(canvasWidth_), static_cast(canvasHeight_)); + } + + void SetClearColor(const Color& color); + const Color& GetClearColor() const { return clearColor_; } + + void SetDirty(); + bool IsDirty() const { return dirty_; } + bool Redraw(); + void SetCustomDrawCallback(std::function callback); + void ClearCustomDrawCallback(); + + void AddCanvasChild(RefPtr child); + void RemoveCanvasChild(RefPtr child); + void RemoveAllCanvasChildren(); + Actor* GetCanvasRoot() const { return canvasRoot_.Get(); } + ActorList& GetCanvasChildren() { return canvasRoot_->GetChildren(); } + const ActorList& GetCanvasChildren() const { return canvasRoot_->GetChildren(); } + + Ptr GetOutputTexture() const; + Ptr GetRenderTexture() const { return renderTexture_; } + bool IsCanvasReady() const { return renderTexture_ && renderTexture_->IsValid(); } + Camera& GetCanvasCamera() { return canvasCamera_; } + const Camera& GetCanvasCamera() const { return canvasCamera_; } + + void OnUpdate(float deltaTime) override; + void Render() override; + +private: + bool redrawInternal(); + void syncCanvasResources(); + + RefPtr canvasRoot_; + Ptr renderTexture_ = nullptr; + Camera canvasCamera_; + Color clearColor_ = Colors::Transparent; + int canvasWidth_ = 0; + int canvasHeight_ = 0; + bool dirty_ = true; + std::function customDrawCallback_; +}; + +} // namespace frostbite2D diff --git a/Frostbite2D/include/frostbite2D/animation/animation.h b/Frostbite2D/include/frostbite2D/animation/animation.h index 3916e3d..2befd4e 100644 --- a/Frostbite2D/include/frostbite2D/animation/animation.h +++ b/Frostbite2D/include/frostbite2D/animation/animation.h @@ -44,6 +44,9 @@ public: void InterpolationLogic(); Vec2 GetMaxSize() const; + bool GetStaticLocalBounds(Rect& outBounds) const; + bool GetStaticLocalBounds(int direction, Rect& outBounds) const; + uint64 GetRenderSignature() const; bool IsUsable() const { return usable_; } void SetUsable(bool usable) { usable_ = usable; } diff --git a/Frostbite2D/include/frostbite2D/graphics/render_texture.h b/Frostbite2D/include/frostbite2D/graphics/render_texture.h new file mode 100644 index 0000000..5e85b3f --- /dev/null +++ b/Frostbite2D/include/frostbite2D/graphics/render_texture.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace frostbite2D { + +class Renderer; + +/// @brief 2D 离屏渲染目标,封装颜色纹理和对应的 framebuffer。 +class RenderTexture : public RefObject { +public: + RenderTexture() = default; + ~RenderTexture() override; + + bool Init(int width, int height); + bool Resize(int width, int height); + void Reset(); + + bool IsValid() const { return framebufferID_ != 0 && texture_ != nullptr; } + int GetWidth() const { return width_; } + int GetHeight() const { return height_; } + Ptr GetTexture() const { return texture_; } + +private: + Ptr texture_ = nullptr; + uint32 framebufferID_ = 0; + int width_ = 0; + int height_ = 0; + + friend class Renderer; +}; + +} // namespace frostbite2D diff --git a/Frostbite2D/include/frostbite2D/graphics/renderer.h b/Frostbite2D/include/frostbite2D/graphics/renderer.h index a50f21f..736c5eb 100644 --- a/Frostbite2D/include/frostbite2D/graphics/renderer.h +++ b/Frostbite2D/include/frostbite2D/graphics/renderer.h @@ -9,9 +9,12 @@ #include #include #include +#include namespace frostbite2D { +class RenderTexture; + class Renderer { public: static Renderer& get(); @@ -22,6 +25,10 @@ public: void beginFrame(); void endFrame(); void flush(); + bool beginRenderToTexture(RenderTexture& target, Camera* camera, + const Color& clearColor = Color(0.0f, 0.0f, 0.0f, 0.0f), + bool clear = true); + bool endRenderToTexture(); void setViewport(int x, int y, int width, int height); void setWindowSize(int width, int height, float contentScaleX = 1.0f, @@ -61,6 +68,9 @@ public: const RenderResolutionState& getResolutionState() const { return resolutionState_; } + bool isInitialized() const { return initialized_; } + bool isFrameActive() const { return frameActive_; } + bool isRenderingToTexture() const { return !renderTargetStack_.empty(); } Vec2 screenToVirtual(const Vec2& screenPos) const; Vec2 virtualToScreen(const Vec2& virtualPos) const; @@ -107,6 +117,17 @@ private: void recalculateResolutionState(); void syncCameraViewport(); + struct RenderTargetState { + uint32 framebuffer = 0; + int viewportX = 0; + int viewportY = 0; + int viewportWidth = 1; + int viewportHeight = 1; + float clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + Camera* camera = nullptr; + bool startedStandaloneBatch = false; + }; + ShaderManager shaderManager_; Batch batch_; Camera* camera_ = nullptr; @@ -123,6 +144,8 @@ private: RenderResolutionState resolutionState_; bool initialized_ = false; + bool frameActive_ = false; + std::vector renderTargetStack_; Renderer(const Renderer&) = delete; Renderer& operator=(const Renderer&) = delete; diff --git a/Frostbite2D/src/frostbite2D/2d/canvas_actor.cpp b/Frostbite2D/src/frostbite2D/2d/canvas_actor.cpp new file mode 100644 index 0000000..591bbe4 --- /dev/null +++ b/Frostbite2D/src/frostbite2D/2d/canvas_actor.cpp @@ -0,0 +1,141 @@ +#include +#include +#include + +namespace frostbite2D { + +CanvasActor::CanvasActor() { + canvasRoot_ = MakePtr(); + canvasRoot_->SetName("canvasRoot"); +} + +bool CanvasActor::Init(int width, int height) { + return SetCanvasSize(width, height); +} + +bool CanvasActor::SetCanvasSize(int width, int height) { + width = std::max(width, 1); + height = std::max(height, 1); + + if (!renderTexture_) { + renderTexture_ = MakePtr(); + } + + if (!renderTexture_->Resize(width, height)) { + return false; + } + + canvasWidth_ = width; + canvasHeight_ = height; + canvasCamera_.setPosition(Vec2::Zero()); + canvasCamera_.setZoom(1.0f); + canvasCamera_.setViewport(canvasWidth_, canvasHeight_); + + syncCanvasResources(); + dirty_ = true; + return true; +} + +void CanvasActor::SetClearColor(const Color& color) { + clearColor_ = color; + dirty_ = true; +} + +void CanvasActor::SetDirty() { + dirty_ = true; +} + +bool CanvasActor::Redraw() { + dirty_ = true; + return redrawInternal(); +} + +void CanvasActor::SetCustomDrawCallback(std::function callback) { + customDrawCallback_ = std::move(callback); + dirty_ = true; +} + +void CanvasActor::ClearCustomDrawCallback() { + if (!customDrawCallback_) { + return; + } + + customDrawCallback_ = nullptr; + dirty_ = true; +} + +void CanvasActor::AddCanvasChild(RefPtr child) { + if (!child || child.Get() == this) { + return; + } + + canvasRoot_->AddChild(child); + dirty_ = true; +} + +void CanvasActor::RemoveCanvasChild(RefPtr child) { + if (!child) { + return; + } + + canvasRoot_->RemoveChild(child); + dirty_ = true; +} + +void CanvasActor::RemoveAllCanvasChildren() { + canvasRoot_->RemoveAllChildren(); + dirty_ = true; +} + +Ptr CanvasActor::GetOutputTexture() const { + return renderTexture_ ? renderTexture_->GetTexture() : nullptr; +} + +void CanvasActor::OnUpdate(float deltaTime) { + if (canvasRoot_) { + canvasRoot_->Update(deltaTime); + } +} + +void CanvasActor::Render() { + if (dirty_) { + redrawInternal(); + } + + Sprite::Render(); +} + +bool CanvasActor::redrawInternal() { + if (!renderTexture_ || !renderTexture_->IsValid()) { + return false; + } + + Renderer& renderer = Renderer::get(); + if (!renderer.beginRenderToTexture(*renderTexture_, &canvasCamera_, clearColor_)) { + dirty_ = true; + return false; + } + + if (canvasRoot_) { + canvasRoot_->Render(); + } + if (customDrawCallback_) { + customDrawCallback_(); + } + + renderer.endRenderToTexture(); + dirty_ = false; + return true; +} + +void CanvasActor::syncCanvasResources() { + Ptr outputTexture = renderTexture_ ? renderTexture_->GetTexture() : nullptr; + if (outputTexture) { + outputTexture->setSampling(TextureSampling::PixelArt); + } + + SetTexture(outputTexture); + SetSize(static_cast(canvasWidth_), static_cast(canvasHeight_)); +} + +} // namespace frostbite2D diff --git a/Frostbite2D/src/frostbite2D/animation/animation.cpp b/Frostbite2D/src/frostbite2D/animation/animation.cpp index c82e309..90e2967 100644 --- a/Frostbite2D/src/frostbite2D/animation/animation.cpp +++ b/Frostbite2D/src/frostbite2D/animation/animation.cpp @@ -12,6 +12,46 @@ using namespace frostbite2D::animation; namespace frostbite2D { namespace { +struct BoundsAccumulator { + bool valid = false; + float minX = 0.0f; + float minY = 0.0f; + float maxX = 0.0f; + float maxY = 0.0f; + + void includePoint(const Vec2& point) { + if (!valid) { + minX = maxX = point.x; + minY = maxY = point.y; + valid = true; + return; + } + + minX = std::min(minX, point.x); + minY = std::min(minY, point.y); + maxX = std::max(maxX, point.x); + maxY = std::max(maxY, point.y); + } + + void includeRect(const Rect& rect) { + if (rect.empty()) { + return; + } + + includePoint(Vec2(rect.left(), rect.top())); + includePoint(Vec2(rect.right(), rect.top())); + includePoint(Vec2(rect.left(), rect.bottom())); + includePoint(Vec2(rect.right(), rect.bottom())); + } + + Rect toRect() const { + if (!valid) { + return Rect(); + } + return Rect(minX, minY, maxX - minX, maxY - minY); + } +}; + Vec2 resolveFramePosition(const AniFrame& frameInfo, const Vec2& imageRate) { return Vec2(frameInfo.imgPos.x * imageRate.x, frameInfo.imgPos.y * imageRate.y); } @@ -37,6 +77,64 @@ BlendMode resolveBlendMode(const AniFrame& frameInfo) { return BlendMode::Normal; } +Rect transformRectBounds(const Rect& rect, const Transform2D& transform) { + BoundsAccumulator bounds; + bounds.includePoint(transform.transformPoint(Vec2(rect.left(), rect.top()))); + bounds.includePoint(transform.transformPoint(Vec2(rect.right(), rect.top()))); + bounds.includePoint(transform.transformPoint(Vec2(rect.left(), rect.bottom()))); + bounds.includePoint(transform.transformPoint(Vec2(rect.right(), rect.bottom()))); + return bounds.toRect(); +} + +bool computeAnimationFrameBounds(const Animation& animation, + int frameIndex, + int direction, + Rect& outBounds) { + if (frameIndex < 0 || + frameIndex >= static_cast(animation.frames_.size()) || + frameIndex >= static_cast(animation.spriteFrames_.size()) || + frameIndex >= static_cast(animation.spriteFrameOffsets_.size())) { + return false; + } + + const AniFrame& frameInfo = animation.frames_[frameIndex]; + const auto& sprite = animation.spriteFrames_[frameIndex]; + if (!sprite) { + return false; + } + + Vec2 spriteSize = sprite->GetSize(); + if (spriteSize.x <= 0.0f || spriteSize.y <= 0.0f) { + return false; + } + + Vec2 imageRate = resolveImageRate(frameInfo); + float rotation = resolveFrameRotation(frameInfo); + Vec2 framePos = resolveFramePosition(frameInfo, imageRate); + Vec2 offset = animation.spriteFrameOffsets_[frameIndex]; + float mirroredRotation = rotation; + if (direction < 0) { + float visualWidth = spriteSize.x * std::abs(imageRate.x); + framePos.x = -framePos.x; + offset.x = -offset.x - visualWidth; + mirroredRotation = -rotation; + } + + Transform2D transform = + Transform2D::translation(offset) * + Transform2D::translation(framePos) * + Transform2D::rotation(mirroredRotation) * + Transform2D::scaling(imageRate.x, imageRate.y); + outBounds = transformRectBounds(Rect(0.0f, 0.0f, spriteSize.x, spriteSize.y), + transform); + return !outBounds.empty(); +} + +void combineHash(uint64& seed, uint64 value) { + constexpr uint64 kOffset = 0x9e3779b97f4a7c15ULL; + seed ^= value + kOffset + (seed << 6) + (seed >> 2); +} + } // namespace Animation::Animation() { @@ -346,6 +444,66 @@ Vec2 Animation::GetMaxSize() const { return maxSize_; } +bool Animation::GetStaticLocalBounds(Rect& outBounds) const { + BoundsAccumulator bounds; + Rect directionalBounds; + if (GetStaticLocalBounds(1, directionalBounds)) { + bounds.includeRect(directionalBounds); + } + if (GetStaticLocalBounds(-1, directionalBounds)) { + bounds.includeRect(directionalBounds); + } + + outBounds = bounds.toRect(); + return bounds.valid; +} + +bool Animation::GetStaticLocalBounds(int direction, Rect& outBounds) const { + BoundsAccumulator bounds; + + for (int i = 0; i < static_cast(frames_.size()); ++i) { + Rect frameBounds; + if (computeAnimationFrameBounds(*this, i, direction, frameBounds)) { + bounds.includeRect(frameBounds); + } + } + + for (const auto& child : GetChildren()) { + auto* subAnimation = dynamic_cast(child.Get()); + if (!subAnimation) { + continue; + } + + Rect childBounds; + if (!subAnimation->GetStaticLocalBounds(direction, childBounds)) { + continue; + } + + bounds.includeRect(transformRectBounds(childBounds, subAnimation->GetLocalTransform())); + } + + outBounds = bounds.toRect(); + return bounds.valid; +} + +uint64 Animation::GetRenderSignature() const { + uint64 signature = 1469598103934665603ULL; + combineHash(signature, static_cast(usable_)); + combineHash(signature, static_cast(direction_ >= 0 ? 1 : 0)); + combineHash(signature, static_cast(std::max(currentFrameIndex_, 0))); + combineHash(signature, static_cast(std::max(totalFrameCount_, 0))); + + for (const auto& child : GetChildren()) { + auto* subAnimation = dynamic_cast(child.Get()); + if (!subAnimation) { + continue; + } + combineHash(signature, subAnimation->GetRenderSignature()); + } + + return signature; +} + void Animation::ApplyFramePresentation(const Vec2& framePos, const Vec2& imageRate, float rotation, diff --git a/Frostbite2D/src/frostbite2D/graphics/render_texture.cpp b/Frostbite2D/src/frostbite2D/graphics/render_texture.cpp new file mode 100644 index 0000000..53e3487 --- /dev/null +++ b/Frostbite2D/src/frostbite2D/graphics/render_texture.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +namespace frostbite2D { + +RenderTexture::~RenderTexture() { + Reset(); +} + +bool RenderTexture::Init(int width, int height) { + return Resize(width, height); +} + +bool RenderTexture::Resize(int width, int height) { + width = std::max(width, 1); + height = std::max(height, 1); + + if (framebufferID_ != 0 && texture_ && width_ == width && height_ == height) { + return true; + } + + Reset(); + + texture_ = Texture::createEmpty(width, height); + if (!texture_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "RenderTexture: failed to create color texture %dx%d", + width, height); + return false; + } + + glGenFramebuffers(1, &framebufferID_); + glBindFramebuffer(GL_FRAMEBUFFER, framebufferID_); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + texture_->getID(), 0); + + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + if (status != GL_FRAMEBUFFER_COMPLETE) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "RenderTexture: framebuffer incomplete (status=0x%x)", + static_cast(status)); + Reset(); + return false; + } + + width_ = width; + height_ = height; + return true; +} + +void RenderTexture::Reset() { + texture_.Reset(); + + if (framebufferID_ != 0) { + glDeleteFramebuffers(1, &framebufferID_); + framebufferID_ = 0; + } + + width_ = 0; + height_ = 0; +} + +} // namespace frostbite2D diff --git a/Frostbite2D/src/frostbite2D/graphics/renderer.cpp b/Frostbite2D/src/frostbite2D/graphics/renderer.cpp index 238e189..9b6e114 100644 --- a/Frostbite2D/src/frostbite2D/graphics/renderer.cpp +++ b/Frostbite2D/src/frostbite2D/graphics/renderer.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -60,9 +61,11 @@ bool Renderer::init() { } void Renderer::shutdown() { + renderTargetStack_.clear(); batch_.shutdown(); shaderManager_.shutdown(); initialized_ = false; + frameActive_ = false; } void Renderer::beginFrame() { @@ -74,16 +77,109 @@ void Renderer::beginFrame() { updateUniforms(); batch_.begin(); + frameActive_ = true; } void Renderer::endFrame() { + while (!renderTargetStack_.empty()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Renderer: endFrame() auto-closing unfinished render target scope"); + endRenderToTexture(); + } batch_.end(); + frameActive_ = false; } void Renderer::flush() { batch_.flush(); } +bool Renderer::beginRenderToTexture(RenderTexture& target, Camera* camera, + const Color& clearColor, bool clear) { + if (!initialized_) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Renderer: beginRenderToTexture called before renderer init"); + return false; + } + if (!target.IsValid()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Renderer: beginRenderToTexture called with invalid target"); + return false; + } + + Camera* activeCamera = camera ? camera : camera_; + if (!activeCamera) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Renderer: beginRenderToTexture requires an active camera"); + return false; + } + + RenderTargetState state; + GLint framebufferBinding = 0; + GLint viewport[4] = {}; + GLfloat clearColorValue[4] = {}; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &framebufferBinding); + glGetIntegerv(GL_VIEWPORT, viewport); + glGetFloatv(GL_COLOR_CLEAR_VALUE, clearColorValue); + state.framebuffer = static_cast(framebufferBinding); + state.viewportX = viewport[0]; + state.viewportY = viewport[1]; + state.viewportWidth = std::max(viewport[2], 1); + state.viewportHeight = std::max(viewport[3], 1); + state.clearColor[0] = clearColorValue[0]; + state.clearColor[1] = clearColorValue[1]; + state.clearColor[2] = clearColorValue[2]; + state.clearColor[3] = clearColorValue[3]; + state.camera = camera_; + + if (frameActive_ || !renderTargetStack_.empty()) { + batch_.flush(); + } else { + batch_.begin(); + state.startedStandaloneBatch = true; + } + + renderTargetStack_.push_back(state); + + glBindFramebuffer(GL_FRAMEBUFFER, target.framebufferID_); + glViewport(0, 0, std::max(target.width_, 1), std::max(target.height_, 1)); + if (clear) { + glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a); + glClear(GL_COLOR_BUFFER_BIT); + } + + camera_ = activeCamera; + updateUniforms(); + return true; +} + +bool Renderer::endRenderToTexture() { + if (renderTargetStack_.empty()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Renderer: endRenderToTexture called without matching begin"); + return false; + } + + RenderTargetState state = renderTargetStack_.back(); + renderTargetStack_.pop_back(); + + if (state.startedStandaloneBatch) { + batch_.end(); + } else { + batch_.flush(); + } + + glBindFramebuffer(GL_FRAMEBUFFER, state.framebuffer); + glViewport(state.viewportX, state.viewportY, state.viewportWidth, + state.viewportHeight); + glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2], + state.clearColor[3]); + + camera_ = state.camera; + updateUniforms(); + return true; +} + void Renderer::setViewport(int x, int y, int width, int height) { resolutionState_.viewportX = x; resolutionState_.viewportY = y; diff --git a/Game/include/character/CharacterAnimation.h b/Game/include/character/CharacterAnimation.h index dc682dd..0611f6c 100644 --- a/Game/include/character/CharacterAnimation.h +++ b/Game/include/character/CharacterAnimation.h @@ -3,6 +3,7 @@ #include "character/CharacterDataLoader.h" #include "character/CharacterEquipmentManager.h" #include +#include #include #include #include @@ -21,9 +22,20 @@ public: using ActionFrameFlagCallback = std::function; using ActionEndCallback = std::function; + 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 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>& 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 missingAnimationPaths_; size_t skippedMissingAnimationCount_ = 0; + RefPtr compositeCanvas_ = nullptr; + std::map compositeFrameInfoByAction_; + bool compositeDirty_ = true; + uint64 compositeVersion_ = 0; + uint64 lastCompositeSignature_ = 0; }; } // namespace frostbite2D diff --git a/Game/include/character/CharacterObject.h b/Game/include/character/CharacterObject.h index 514e4cf..bd80e93 100644 --- a/Game/include/character/CharacterObject.h +++ b/Game/include/character/CharacterObject.h @@ -141,6 +141,12 @@ public: int GetCurrentAnimationFrameCount() const; float GetCurrentAnimationProgressNormalized() const; animation::AniFrame GetCurrentAnimationFrameInfo() const; + bool HasCompositeTexture() const; + Ptr GetCompositeTexture() const; + Vec2 GetCompositeTextureSize() const; + Vec2 GetCompositeOriginInTexture() const; + Vec2 GetCompositeGroundAnchorInTexture() const; + uint64 GetCompositeTextureVersion() const; /// @brief 绑定当前动作主动画的运行时回调,供后续脚本系统接入。 void SetAnimationFrameFlagCallback(CharacterAnimation::ActionFrameFlagCallback callback); diff --git a/Game/include/common/debug/GameDebugActor.h b/Game/include/common/debug/GameDebugActor.h index 8d65f5e..1fae139 100644 --- a/Game/include/common/debug/GameDebugActor.h +++ b/Game/include/common/debug/GameDebugActor.h @@ -1,12 +1,14 @@ #pragma once #include +#include 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 background_; RefPtr coordText_; + RefPtr actionText_; + RefPtr compositeText_; + RefPtr compositePreview_; + RefPtr compositeOriginMarker_; + CharacterObject* previewCharacter_ = nullptr; + uint64 previewCompositeVersion_ = 0; + bool previewTextureAvailable_ = false; }; } // namespace frostbite2D diff --git a/Game/src/character/CharacterAnimation.cpp b/Game/src/character/CharacterAnimation.cpp index 79ecd50..74b7f9b 100644 --- a/Game/src/character/CharacterAnimation.cpp +++ b/Game/src/character/CharacterAnimation.cpp @@ -1,9 +1,11 @@ #include "character/CharacterAnimation.h" #include "character/CharacterObject.h" +#include #include #include #include #include +#include #include #include #include @@ -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(std::lround(result.localBounds.width())), 1); + result.height = + std::max(static_cast(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(); + 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 CharacterAnimation::GetCompositeTexture() const { + return compositeCanvas_ ? compositeCanvas_->GetOutputTexture() : nullptr; +} + +Vec2 CharacterAnimation::GetCompositeTextureSize() const { + CompositeFrameInfo info = GetCurrentCompositeFrameInfo(); + return Vec2(static_cast(info.width), static_cast(info.height)); +} + +Vec2 CharacterAnimation::GetCompositeOriginInTexture() const { + return GetCurrentCompositeFrameInfo().originInTexture; +} + +Vec2 CharacterAnimation::GetCompositeGroundAnchorInTexture() const { + return GetCurrentCompositeFrameInfo().groundAnchorInTexture; +} + +CharacterAnimation::CompositeFrameInfo CharacterAnimation::ComputeCompositeFrameInfo( + const std::vector>& 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(std::lround(info.localBounds.width())), 1); + info.height = std::max(static_cast(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(direction_ >= 0 ? 1 : 0)); + combineHash(signature, static_cast(std::hash{}(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 diff --git a/Game/src/character/CharacterObject.cpp b/Game/src/character/CharacterObject.cpp index 0d6875c..5ed1f7b 100644 --- a/Game/src/character/CharacterObject.cpp +++ b/Game/src/character/CharacterObject.cpp @@ -404,6 +404,32 @@ animation::AniFrame CharacterObject::GetCurrentAnimationFrameInfo() const { return animationManager_ ? animationManager_->GetCurrentFrameInfo() : animation::AniFrame(); } +bool CharacterObject::HasCompositeTexture() const { + return animationManager_ && animationManager_->HasCompositeTexture(); +} + +Ptr 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_) { diff --git a/Game/src/common/debug/GameDebugActor.cpp b/Game/src/common/debug/GameDebugActor.cpp index 740cc61..b710254 100644 --- a/Game/src/common/debug/GameDebugActor.cpp +++ b/Game/src/common/debug/GameDebugActor.cpp @@ -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 +#include +#include +#include #include +#include #include #include @@ -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, 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(); + compositePreview_->SetName("debugCompositePreview"); + compositePreview_->SetZOrder(1); + AddChild(compositePreview_); + + compositeOriginMarker_ = MakePtr(); + 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() + ? "" + : 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(worldPosition.x), static_cast(worldPosition.y), static_cast(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 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(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