From 5af657c5c926de07904c59ac88da769ec14ca024 Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Tue, 7 Apr 2026 23:17:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(npc):=20=E6=B7=BB=E5=8A=A0NPC=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E9=AB=98=E4=BA=AE=E6=95=88=E6=9E=9C=E5=8F=8A=E5=A4=8D?= =?UTF-8?q?=E5=90=88=E7=BA=B9=E7=90=86=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现NPC交互时的绿色高亮效果,通过InteractionHighlightSprite类实现 重构NpcAnimation支持复合纹理渲染,优化高亮效果的性能 添加ShaderManager获取所有加载Shader的方法,优化渲染器uniform更新逻辑 --- Frostbite2D/include/frostbite2D/2d/sprite.h | 5 +- .../frostbite2D/graphics/shader_manager.h | 5 +- Frostbite2D/src/frostbite2D/2d/sprite.cpp | 43 +--- .../src/frostbite2D/graphics/renderer.cpp | 31 +-- .../common/InteractionHighlightSprite.h | 20 ++ Game/include/npc/NpcAnimation.h | 31 +++ Game/include/npc/NpcObject.h | 10 +- .../src/common/InteractionHighlightSprite.cpp | 35 +++ Game/src/npc/NpcAnimation.cpp | 217 ++++++++++++++++++ Game/src/npc/NpcObject.cpp | 64 ++++++ Game/src/scene/GameMapTestScene.cpp | 13 ++ 11 files changed, 418 insertions(+), 56 deletions(-) create mode 100644 Game/include/common/InteractionHighlightSprite.h create mode 100644 Game/src/common/InteractionHighlightSprite.cpp diff --git a/Frostbite2D/include/frostbite2D/2d/sprite.h b/Frostbite2D/include/frostbite2D/2d/sprite.h index d12a8e2..a47d4ad 100644 --- a/Frostbite2D/include/frostbite2D/2d/sprite.h +++ b/Frostbite2D/include/frostbite2D/2d/sprite.h @@ -49,11 +49,13 @@ public: void SetOffset(const Vec2& offset); void SetOffset(float x, float y); -private: +protected: + virtual void ConfigureShader(Shader* shader) const; void updateTransform(); Shader* getActiveShader() const; Quad createQuad() const; +private: Ptr texture_; std::string shaderName_; Color color_ = Color(1.0f, 1.0f, 1.0f, 1.0f); @@ -68,4 +70,3 @@ private: }; } - diff --git a/Frostbite2D/include/frostbite2D/graphics/shader_manager.h b/Frostbite2D/include/frostbite2D/graphics/shader_manager.h index 352cd05..7abdf95 100644 --- a/Frostbite2D/include/frostbite2D/graphics/shader_manager.h +++ b/Frostbite2D/include/frostbite2D/graphics/shader_manager.h @@ -18,6 +18,9 @@ public: Shader* getShader(const std::string& name); bool hasShader(const std::string& name) const; + const std::unordered_map>& getLoadedShaders() const { + return shaders_; + } ~ShaderManager() = default; @@ -36,4 +39,4 @@ private: ShaderManager& operator=(const ShaderManager&) = delete; }; -} \ No newline at end of file +} diff --git a/Frostbite2D/src/frostbite2D/2d/sprite.cpp b/Frostbite2D/src/frostbite2D/2d/sprite.cpp index a319087..7626d97 100644 --- a/Frostbite2D/src/frostbite2D/2d/sprite.cpp +++ b/Frostbite2D/src/frostbite2D/2d/sprite.cpp @@ -58,46 +58,19 @@ void Sprite::Render() { } Renderer& renderer = Renderer::get(); - - Vec2 size = GetSize(); - if (size.x == 0 || size.y == 0) { - size = Vec2((float)texture_->getWidth(), (float)texture_->getHeight()); - } - - // 使用完整的变换矩阵进行渲染 - Rect destRect(0, 0, size.x, size.y); - Rect srcRect = srcRect_; - if (srcRect.empty()) { - srcRect = Rect(0, 0, (float)texture_->getWidth(), (float)texture_->getHeight()); - } - - float worldOpacity = GetWorldOpacity(); - Quad quad = Quad::createTextured( - destRect, srcRect, - Vec2((float)texture_->getWidth(), (float)texture_->getHeight()), - color_.r, color_.g, color_.b, color_.a * worldOpacity, - renderer.shouldShrinkSubTextureUVs() - ); - - if (flippedX_) { - std::swap(quad.vertices[0].texCoord, quad.vertices[1].texCoord); - std::swap(quad.vertices[2].texCoord, quad.vertices[3].texCoord); - } - if (flippedY_) { - std::swap(quad.vertices[0].texCoord, quad.vertices[2].texCoord); - std::swap(quad.vertices[1].texCoord, quad.vertices[3].texCoord); - } - - auto* shader = renderer.getShaderManager().getShader("sprite"); + Quad quad = createQuad(); + + auto* shader = getActiveShader(); if (shader) { shader->use(); + ConfigureShader(shader); } - + Transform2D worldTransform = GetWorldTransform(); if (offset_ != Vec2::Zero()) { worldTransform = Transform2D::translation(offset_.x, offset_.y) * worldTransform; } - + renderer.getBatch().submitQuad(quad, worldTransform, texture_, shader, blendMode_); RenderChildren(); @@ -154,6 +127,10 @@ void Sprite::SetOffset(float x, float y) { offset_.y = y; } +void Sprite::ConfigureShader(Shader* shader) const { + (void)shader; +} + void Sprite::updateTransform() { Vec2 pos = GetPosition(); float rotation = GetRotation(); diff --git a/Frostbite2D/src/frostbite2D/graphics/renderer.cpp b/Frostbite2D/src/frostbite2D/graphics/renderer.cpp index 9b6e114..11cfa73 100644 --- a/Frostbite2D/src/frostbite2D/graphics/renderer.cpp +++ b/Frostbite2D/src/frostbite2D/graphics/renderer.cpp @@ -503,27 +503,20 @@ void Renderer::setupBlendMode(BlendMode mode) { } void Renderer::updateUniforms() { - if (camera_) { - auto* spriteShader = shaderManager_.getShader("sprite"); - if (spriteShader) { - spriteShader->use(); - spriteShader->setMat4("u_view", camera_->getViewMatrix()); - spriteShader->setMat4("u_projection", camera_->getProjectionMatrix()); + if (!camera_) { + return; + } + + for (const auto& [name, shaderPtr] : shaderManager_.getLoadedShaders()) { + (void)name; + Shader* shader = shaderPtr.Get(); + if (!shader) { + continue; } - auto* coloredShader = shaderManager_.getShader("colored_quad"); - if (coloredShader) { - coloredShader->use(); - coloredShader->setMat4("u_view", camera_->getViewMatrix()); - coloredShader->setMat4("u_projection", camera_->getProjectionMatrix()); - } - - auto* textShader = shaderManager_.getShader("text"); - if (textShader) { - textShader->use(); - textShader->setMat4("u_view", camera_->getViewMatrix()); - textShader->setMat4("u_projection", camera_->getProjectionMatrix()); - } + shader->use(); + shader->setMat4("u_view", camera_->getViewMatrix()); + shader->setMat4("u_projection", camera_->getProjectionMatrix()); } } diff --git a/Game/include/common/InteractionHighlightSprite.h b/Game/include/common/InteractionHighlightSprite.h new file mode 100644 index 0000000..d5a6f48 --- /dev/null +++ b/Game/include/common/InteractionHighlightSprite.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace frostbite2D { + +class InteractionHighlightSprite : public Sprite { +public: + InteractionHighlightSprite(); + + void SetGroundAnchorInTexture(const Vec2& groundAnchorInTexture); + +protected: + void ConfigureShader(Shader* shader) const override; + +private: + Vec2 texelSize_ = Vec2::One(); +}; + +} // namespace frostbite2D diff --git a/Game/include/npc/NpcAnimation.h b/Game/include/npc/NpcAnimation.h index 319ee00..98da389 100644 --- a/Game/include/npc/NpcAnimation.h +++ b/Game/include/npc/NpcAnimation.h @@ -2,6 +2,7 @@ #include "npc/NpcDataLoader.h" #include +#include #include #include @@ -9,17 +10,47 @@ namespace frostbite2D { class NpcAnimation : public Actor { public: + struct CompositeFrameInfo { + bool valid = false; + Rect localBounds; + Vec2 originInTexture = Vec2::Zero(); + Vec2 groundAnchorInTexture = Vec2::Zero(); + int width = 1; + int height = 1; + }; + bool Init(const npc::NpcConfig& config); + void Update(float deltaTime) override; + void Render() override; void SetDirection(int direction); bool IsReady() const { return displayAnimation_ != nullptr; } bool IsAnimationFinished() const; const std::string& GetAnimationPath() const { return animationPath_; } bool GetDisplayLocalBounds(Rect& outBounds) const; + bool EnsureCompositeTextureReady(); + bool HasCompositeTexture() const; + Ptr GetCompositeTexture() const; + Vec2 GetCompositeTextureSize() const; + Vec2 GetCompositeGroundAnchorInTexture() const; + uint64 GetCompositeVersion() const { return compositeVersion_; } private: + CompositeFrameInfo ComputeCompositeFrameInfo(int direction) const; + CompositeFrameInfo GetCurrentCompositeFrameInfo() const { return compositeFrameInfo_; } + uint64 CaptureCurrentRenderSignature() const; + void MarkCompositeDirty(); + void UpdateCompositeCamera(); + void RefreshCompositeTextureIfNeeded(); + void RenderCurrentAnimationToCompositeCanvas(); + RefPtr displayAnimation_ = nullptr; + RefPtr compositeCanvas_ = nullptr; + CompositeFrameInfo compositeFrameInfo_; std::string animationPath_; int direction_ = 1; + bool compositeDirty_ = true; + uint64 compositeVersion_ = 0; + uint64 lastCompositeSignature_ = 0; }; } // namespace frostbite2D diff --git a/Game/include/npc/NpcObject.h b/Game/include/npc/NpcObject.h index 72b9f9a..8d40665 100644 --- a/Game/include/npc/NpcObject.h +++ b/Game/include/npc/NpcObject.h @@ -1,9 +1,10 @@ #pragma once #include "character/CharacterActionTypes.h" +#include "common/InteractionHighlightSprite.h" #include "npc/NpcAnimation.h" -#include #include +#include #include #include #include @@ -21,11 +22,13 @@ public: void SetNpcPosition(const Vec2& pos); void SetWorldPosition(const CharacterWorldPosition& pos); void SetInteractable(bool interactable); + void SetInteractionHighlighted(bool highlighted); void BeginInteract(); void EndInteract(); bool CanInteract() const; bool IsInteracting() const { return interacting_; } + bool IsInteractionHighlighted() const { return interactionHighlighted_; } int GetNpcId() const { return npcId_; } int GetDirection() const { return direction_; } const std::string& GetName() const; @@ -41,8 +44,10 @@ public: void Update(float deltaTime) override; private: + void EnsureInteractionHighlight(); void EnsureNameLabel(); void RefreshNameLabel(); + void SyncInteractionHighlight(); void SyncActorPositionFromWorld(); int npcId_ = -1; @@ -50,10 +55,13 @@ private: CharacterWorldPosition worldPosition_; std::optional config_; RefPtr animation_ = nullptr; + RefPtr interactionHighlight_ = nullptr; std::array, 8> nameOutlineLabels_; RefPtr nameLabel_ = nullptr; bool interactable_ = true; bool interacting_ = false; + bool interactionHighlighted_ = false; + uint64 syncedHighlightCompositeVersion_ = 0; }; } // namespace frostbite2D diff --git a/Game/src/common/InteractionHighlightSprite.cpp b/Game/src/common/InteractionHighlightSprite.cpp new file mode 100644 index 0000000..e9e04c9 --- /dev/null +++ b/Game/src/common/InteractionHighlightSprite.cpp @@ -0,0 +1,35 @@ +#include "common/InteractionHighlightSprite.h" + +namespace frostbite2D { + +InteractionHighlightSprite::InteractionHighlightSprite() { + SetName("interactionHighlight"); + SetShader("outline_sprite"); + SetColor(Colors::Green); + SetVisible(false); +} + +void InteractionHighlightSprite::SetGroundAnchorInTexture( + const Vec2& groundAnchorInTexture) { + Vec2 textureSize = GetSize(); + if (textureSize.x <= 0.0f || textureSize.y <= 0.0f) { + SetAnchor(0.5f, 1.0f); + texelSize_ = Vec2::One(); + return; + } + + SetAnchor(groundAnchorInTexture.x / textureSize.x, + groundAnchorInTexture.y / textureSize.y); + texelSize_ = Vec2(1.0f / textureSize.x, 1.0f / textureSize.y); +} + +void InteractionHighlightSprite::ConfigureShader(Shader* shader) const { + Sprite::ConfigureShader(shader); + if (!shader) { + return; + } + + shader->setVec2("u_texelSize", texelSize_); +} + +} // namespace frostbite2D diff --git a/Game/src/npc/NpcAnimation.cpp b/Game/src/npc/NpcAnimation.cpp index 5f39be9..2d3af23 100644 --- a/Game/src/npc/NpcAnimation.cpp +++ b/Game/src/npc/NpcAnimation.cpp @@ -1,13 +1,74 @@ #include "npc/NpcAnimation.h" +#include #include +#include +#include namespace frostbite2D { +namespace { + +constexpr float kCompositePadding = 1.0f; + +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); +} + +NpcAnimation::CompositeFrameInfo combineCompositeFrameInfo( + const NpcAnimation::CompositeFrameInfo& lhs, + const NpcAnimation::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())); + + NpcAnimation::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 NpcAnimation::Init(const npc::NpcConfig& config) { RemoveAllChildren(); displayAnimation_ = nullptr; + compositeCanvas_ = nullptr; + compositeFrameInfo_ = CompositeFrameInfo(); animationPath_.clear(); direction_ = 1; + compositeDirty_ = true; + compositeVersion_ = 0; + lastCompositeSignature_ = 0; if (config.fieldAnimationPath.empty()) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, @@ -15,6 +76,18 @@ bool NpcAnimation::Init(const npc::NpcConfig& config) { return false; } + compositeCanvas_ = MakePtr(); + if (!compositeCanvas_ || !compositeCanvas_->Init(1, 1)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "NpcAnimation: failed to initialize composite canvas for npc %d", + config.id); + compositeCanvas_.Reset(); + } else { + compositeCanvas_->SetCustomDrawCallback([this]() { + RenderCurrentAnimationToCompositeCanvas(); + }); + } + auto animation = MakePtr(config.fieldAnimationPath); if (!animation || !animation->IsUsable()) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, @@ -33,14 +106,37 @@ bool NpcAnimation::Init(const npc::NpcConfig& config) { SetDirection(1); displayAnimation_->Reset(); SetDirection(direction_); + + CompositeFrameInfo rightInfo = ComputeCompositeFrameInfo(1); + CompositeFrameInfo leftInfo = ComputeCompositeFrameInfo(-1); + compositeFrameInfo_ = combineCompositeFrameInfo(rightInfo, leftInfo); + lastCompositeSignature_ = CaptureCurrentRenderSignature(); return true; } +void NpcAnimation::Update(float deltaTime) { + Actor::Update(deltaTime); + + uint64 renderSignature = CaptureCurrentRenderSignature(); + if (renderSignature != lastCompositeSignature_) { + compositeDirty_ = true; + } +} + +void NpcAnimation::Render() { + if (compositeDirty_ && compositeCanvas_ && Renderer::get().isFrameActive()) { + RefreshCompositeTextureIfNeeded(); + } + + Actor::Render(); +} + void NpcAnimation::SetDirection(int direction) { direction_ = direction >= 0 ? 1 : -1; if (displayAnimation_) { displayAnimation_->SetDirection(direction_); } + MarkCompositeDirty(); } bool NpcAnimation::IsAnimationFinished() const { @@ -55,4 +151,125 @@ bool NpcAnimation::GetDisplayLocalBounds(Rect& outBounds) const { return displayAnimation_->GetStaticLocalBounds(outBounds); } +bool NpcAnimation::EnsureCompositeTextureReady() { + RefreshCompositeTextureIfNeeded(); + return HasCompositeTexture(); +} + +bool NpcAnimation::HasCompositeTexture() const { + return compositeCanvas_ && compositeCanvas_->IsCanvasReady() && + GetCurrentCompositeFrameInfo().valid; +} + +Ptr NpcAnimation::GetCompositeTexture() const { + return compositeCanvas_ ? compositeCanvas_->GetOutputTexture() : nullptr; +} + +Vec2 NpcAnimation::GetCompositeTextureSize() const { + CompositeFrameInfo info = GetCurrentCompositeFrameInfo(); + return Vec2(static_cast(info.width), static_cast(info.height)); +} + +Vec2 NpcAnimation::GetCompositeGroundAnchorInTexture() const { + return GetCurrentCompositeFrameInfo().groundAnchorInTexture; +} + +NpcAnimation::CompositeFrameInfo NpcAnimation::ComputeCompositeFrameInfo( + int direction) const { + CompositeFrameInfo info; + if (!displayAnimation_) { + return info; + } + + Rect animationBounds; + if (!displayAnimation_->GetStaticLocalBounds(direction, animationBounds)) { + return info; + } + + Rect bounds = + transformRectBounds(animationBounds, displayAnimation_->GetLocalTransform()); + if (bounds.empty()) { + return info; + } + + float left = std::floor(bounds.left() - kCompositePadding); + float top = std::floor(bounds.top() - kCompositePadding); + float right = std::ceil(bounds.right() + kCompositePadding); + float bottom = std::ceil(bounds.bottom() + kCompositePadding); + + 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; +} + +uint64 NpcAnimation::CaptureCurrentRenderSignature() const { + uint64 signature = 1469598103934665603ULL; + combineHash(signature, static_cast(direction_ >= 0 ? 1 : 0)); + if (displayAnimation_) { + combineHash(signature, displayAnimation_->GetRenderSignature()); + } + return signature; +} + +void NpcAnimation::MarkCompositeDirty() { + compositeDirty_ = true; + if (compositeCanvas_) { + compositeCanvas_->SetDirty(); + } +} + +void NpcAnimation::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 NpcAnimation::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 NpcAnimation::RenderCurrentAnimationToCompositeCanvas() { + if (!displayAnimation_) { + return; + } + + RenderChildren(); +} + } // namespace frostbite2D diff --git a/Game/src/npc/NpcObject.cpp b/Game/src/npc/NpcObject.cpp index 93a000d..f3ebdc4 100644 --- a/Game/src/npc/NpcObject.cpp +++ b/Game/src/npc/NpcObject.cpp @@ -42,10 +42,13 @@ bool NpcObject::Construction(int npcId) { worldPosition_ = CharacterWorldPosition(); config_.reset(); animation_ = nullptr; + interactionHighlight_ = nullptr; nameOutlineLabels_.fill(nullptr); nameLabel_ = nullptr; interactable_ = true; interacting_ = false; + interactionHighlighted_ = false; + syncedHighlightCompositeVersion_ = 0; auto config = npc::loadNpcConfig(npcId); if (!config) { @@ -68,11 +71,13 @@ bool NpcObject::Construction(int npcId) { config_ = *config; npcId_ = npcId; animation_ = animation; + EnsureInteractionHighlight(); AddChild(animation_); EnsureNameLabel(); SetWorldPosition({}); SetDirection(1); + SetInteractionHighlighted(false); RefreshNameLabel(); SDL_Log("NpcObject: npc %d construction complete", npcId_); return true; @@ -84,6 +89,7 @@ void NpcObject::SetDirection(int direction) { animation_->SetDirection(direction_); } RefreshNameLabel(); + SyncInteractionHighlight(); } void NpcObject::SetNpcPosition(const Vec2& pos) { @@ -108,6 +114,19 @@ void NpcObject::SetInteractable(bool interactable) { } } +void NpcObject::SetInteractionHighlighted(bool highlighted) { + if (interactionHighlighted_ == highlighted) { + SyncInteractionHighlight(); + return; + } + + interactionHighlighted_ = highlighted; + if (!interactionHighlighted_) { + syncedHighlightCompositeVersion_ = 0; + } + SyncInteractionHighlight(); +} + void NpcObject::BeginInteract() { if (CanInteract()) { interacting_ = true; @@ -144,6 +163,19 @@ bool NpcObject::IsAnimationFinished() const { void NpcObject::Update(float deltaTime) { Actor::Update(deltaTime); + SyncInteractionHighlight(); +} + +void NpcObject::EnsureInteractionHighlight() { + if (interactionHighlight_) { + return; + } + + interactionHighlight_ = MakePtr(); + interactionHighlight_->SetZOrder(-1); + interactionHighlight_->SetPosition(0.0f, 0.0f); + interactionHighlight_->SetVisible(false); + AddChild(interactionHighlight_); } void NpcObject::EnsureNameLabel() { @@ -206,6 +238,38 @@ void NpcObject::RefreshNameLabel() { } } +void NpcObject::SyncInteractionHighlight() { + if (!interactionHighlight_) { + return; + } + + if (!interactionHighlighted_ || !animation_ || + !animation_->EnsureCompositeTextureReady() || + !animation_->HasCompositeTexture()) { + interactionHighlight_->SetVisible(false); + return; + } + + uint64 compositeVersion = animation_->GetCompositeVersion(); + if (syncedHighlightCompositeVersion_ != compositeVersion) { + Ptr compositeTexture = animation_->GetCompositeTexture(); + Vec2 compositeSize = animation_->GetCompositeTextureSize(); + if (!compositeTexture || compositeSize.x <= 0.0f || compositeSize.y <= 0.0f) { + interactionHighlight_->SetVisible(false); + return; + } + + interactionHighlight_->SetTexture(compositeTexture); + interactionHighlight_->SetSize(compositeSize); + interactionHighlight_->SetGroundAnchorInTexture( + animation_->GetCompositeGroundAnchorInTexture()); + interactionHighlight_->SetPosition(0.0f, 0.0f); + syncedHighlightCompositeVersion_ = compositeVersion; + } + + interactionHighlight_->SetVisible(true); +} + void NpcObject::SyncActorPositionFromWorld() { SetPosition(worldPosition_.ToScreenPosition()); SetZOrder(worldPosition_.y); diff --git a/Game/src/scene/GameMapTestScene.cpp b/Game/src/scene/GameMapTestScene.cpp index 7e6442e..c1efec8 100644 --- a/Game/src/scene/GameMapTestScene.cpp +++ b/Game/src/scene/GameMapTestScene.cpp @@ -11,6 +11,7 @@ constexpr char kTestMapPath[] = "map/elvengard/d_elvengard.map"; constexpr int kTestNpcId = 2; constexpr float kTestNpcOffsetX = 180.0f; constexpr float kTestNpcOffsetY = -24.0f; +constexpr float kNpcInteractionHighlightRadius = 40.0f; } // namespace @@ -111,6 +112,18 @@ void GameMapTestScene::onExit() { void GameMapTestScene::Update(float deltaTime) { Scene::Update(deltaTime); + + if (npc_) { + bool shouldHighlight = false; + if (character_) { + shouldHighlight = + character_->GetWorldPosition().ToGroundPosition().distance( + npc_->GetWorldPosition().ToGroundPosition()) <= + kNpcInteractionHighlightRadius; + } + npc_->SetInteractionHighlighted(shouldHighlight); + } + cameraController_.Update(deltaTime); }