diff --git a/Game/include/map/GameMap.h b/Game/include/map/GameMap.h index cdb914d..f92becd 100644 --- a/Game/include/map/GameMap.h +++ b/Game/include/map/GameMap.h @@ -93,9 +93,9 @@ private: int backgroundRepeatWidth_ = 0; /// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。 int mapOffsetY_ = 0; - bool debugMode_ = true; + bool debugMode_ = false; /// 硬编码调试开关:关闭后忽略可行走区域检测,允许角色自由移动。 - bool movableAreaCheckEnabled_ = false; + bool movableAreaCheckEnabled_ = true; /// 当前地图正在播放的背景音乐。 Ptr currentMusic_; }; diff --git a/Game/include/map/GameMapLayer.h b/Game/include/map/GameMapLayer.h index 53e9004..58d5158 100644 --- a/Game/include/map/GameMapLayer.h +++ b/Game/include/map/GameMapLayer.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -18,15 +19,29 @@ public: void AddObject(RefPtr obj); private: + void EnsureDebugOverlayCanvases(); + void RefreshFeasibleAreaOverlay(); + void RefreshMoveAreaOverlay(); + Rect ComputeFeasibleAreaOverlayBounds() const; + Rect ComputeMoveAreaOverlayBounds() const; + void DrawFeasibleAreaOverlay() const; + void DrawMoveAreaOverlay() const; + struct DebugMoveAreaInfo { Rect rect; size_t index = kInvalidMoveAreaIndex; }; + RefPtr feasibleAreaOverlayCanvas_ = nullptr; + RefPtr moveAreaOverlayCanvas_ = nullptr; std::vector feasibleAreaPolygon_; std::vector feasibleAreaFillRects_; std::vector moveAreaInfoList_; size_t highlightedMoveAreaIndex_ = kInvalidMoveAreaIndex; + Rect feasibleAreaOverlayBounds_; + Rect moveAreaOverlayBounds_; + bool feasibleAreaOverlayDirty_ = true; + bool moveAreaOverlayDirty_ = true; }; } // namespace frostbite2D diff --git a/Game/include/npc/NpcObject.h b/Game/include/npc/NpcObject.h index 8d40665..d186a5b 100644 --- a/Game/include/npc/NpcObject.h +++ b/Game/include/npc/NpcObject.h @@ -42,6 +42,7 @@ public: } void Update(float deltaTime) override; + void Render() override; private: void EnsureInteractionHighlight(); diff --git a/Game/src/map/GameMapLayer.cpp b/Game/src/map/GameMapLayer.cpp index e005825..4a1b376 100644 --- a/Game/src/map/GameMapLayer.cpp +++ b/Game/src/map/GameMapLayer.cpp @@ -1,5 +1,6 @@ #include "map/GameMapLayer.h" #include "common/math/GameMath.h" +#include #include #include #include @@ -14,7 +15,54 @@ constexpr float kDebugEdgePointSize = 5.0f; constexpr float kDebugVertexSize = 9.0f; constexpr float kDebugEdgeStep = 4.0f; -void DrawPolygonOutline(const std::vector& polygon, const Vec2& worldOrigin, +struct BoundsAccumulator { + bool valid = false; + float left = 0.0f; + float top = 0.0f; + float right = 0.0f; + float bottom = 0.0f; + + void IncludePoint(const Vec2& point) { + if (!valid) { + valid = true; + left = point.x; + top = point.y; + right = point.x; + bottom = point.y; + return; + } + + left = std::min(left, point.x); + top = std::min(top, point.y); + right = std::max(right, point.x); + bottom = std::max(bottom, point.y); + } + + void IncludeRect(const Rect& rect) { + if (rect.empty()) { + return; + } + + IncludePoint(Vec2(rect.left(), rect.top())); + IncludePoint(Vec2(rect.right(), rect.bottom())); + } + + Rect Build(float padding) const { + if (!valid) { + return Rect::Zero(); + } + + float paddedLeft = std::floor(left - padding); + float paddedTop = std::floor(top - padding); + float paddedRight = std::ceil(right + padding); + float paddedBottom = std::ceil(bottom + padding); + return Rect(paddedLeft, paddedTop, + std::max(paddedRight - paddedLeft, 1.0f), + std::max(paddedBottom - paddedTop, 1.0f)); + } +}; + +void DrawPolygonOutline(const std::vector& polygon, const Vec2& drawOrigin, const Color& color) { if (polygon.size() < 2) { return; @@ -22,8 +70,8 @@ void DrawPolygonOutline(const std::vector& polygon, const Vec2& worldOrigi auto& renderer = Renderer::get(); for (size_t i = 0; i < polygon.size(); ++i) { - Vec2 start = worldOrigin + polygon[i]; - Vec2 end = worldOrigin + polygon[(i + 1) % polygon.size()]; + Vec2 start = drawOrigin + polygon[i]; + Vec2 end = drawOrigin + polygon[(i + 1) % polygon.size()]; Vec2 delta = end - start; float length = delta.length(); int steps = std::max(1, static_cast(std::ceil(length / kDebugEdgeStep))); @@ -41,10 +89,10 @@ void DrawPolygonOutline(const std::vector& polygon, const Vec2& worldOrigi } void DrawPolygonVertices(const std::vector& polygon, - const Vec2& worldOrigin, const Color& color) { + const Vec2& drawOrigin, const Color& color) { auto& renderer = Renderer::get(); for (const auto& vertex : polygon) { - Vec2 point = worldOrigin + vertex; + Vec2 point = drawOrigin + vertex; renderer.drawQuad( Rect(point.x - kDebugVertexSize * 0.5f, point.y - kDebugVertexSize * 0.5f, kDebugVertexSize, @@ -57,49 +105,197 @@ void DrawPolygonVertices(const std::vector& polygon, void GameMapLayer::Render() { Actor::Render(); + EnsureDebugOverlayCanvases(); - Vec2 worldOrigin = GetWorldTransform().transformPoint(Vec2::Zero()); + if (feasibleAreaOverlayDirty_) { + RefreshFeasibleAreaOverlay(); + } + if (moveAreaOverlayDirty_) { + RefreshMoveAreaOverlay(); + } + + if (feasibleAreaOverlayCanvas_ && feasibleAreaOverlayCanvas_->IsVisible()) { + feasibleAreaOverlayCanvas_->SetPosition( + GetWorldTransform().transformPoint(feasibleAreaOverlayBounds_.origin)); + feasibleAreaOverlayCanvas_->SetOpacity(GetWorldOpacity()); + feasibleAreaOverlayCanvas_->Render(); + } + + if (moveAreaOverlayCanvas_ && moveAreaOverlayCanvas_->IsVisible()) { + moveAreaOverlayCanvas_->SetPosition( + GetWorldTransform().transformPoint(moveAreaOverlayBounds_.origin)); + moveAreaOverlayCanvas_->SetOpacity(GetWorldOpacity()); + moveAreaOverlayCanvas_->Render(); + } +} + +void GameMapLayer::EnsureDebugOverlayCanvases() { + auto initCanvas = [this](RefPtr& canvas, const char* label, + std::function callback) { + if (canvas) { + return; + } + + canvas = MakePtr(); + if (!canvas || !canvas->Init(1, 1)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameMapLayer: failed to initialize %s canvas", label); + canvas.Reset(); + return; + } + + canvas->SetVisible(false); + canvas->SetClearColor(Colors::Transparent); + canvas->SetCustomDrawCallback(callback); + }; + + initCanvas(feasibleAreaOverlayCanvas_, "feasible area overlay", + [this]() { DrawFeasibleAreaOverlay(); }); + initCanvas(moveAreaOverlayCanvas_, "move area overlay", + [this]() { DrawMoveAreaOverlay(); }); +} + +void GameMapLayer::RefreshFeasibleAreaOverlay() { + feasibleAreaOverlayDirty_ = false; + if (!feasibleAreaOverlayCanvas_) { + return; + } + + feasibleAreaOverlayBounds_ = ComputeFeasibleAreaOverlayBounds(); + if (feasibleAreaOverlayBounds_.empty()) { + feasibleAreaOverlayCanvas_->SetVisible(false); + return; + } + + int width = + std::max(static_cast(std::lround(feasibleAreaOverlayBounds_.width())), 1); + int height = + std::max(static_cast(std::lround(feasibleAreaOverlayBounds_.height())), 1); + if (!feasibleAreaOverlayCanvas_->SetCanvasSize(width, height)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameMapLayer: failed to resize feasible area overlay canvas to %dx%d", + width, height); + feasibleAreaOverlayCanvas_->SetVisible(false); + feasibleAreaOverlayDirty_ = true; + return; + } + + feasibleAreaOverlayCanvas_->SetVisible(true); + feasibleAreaOverlayCanvas_->SetDirty(); +} + +void GameMapLayer::RefreshMoveAreaOverlay() { + moveAreaOverlayDirty_ = false; + if (!moveAreaOverlayCanvas_) { + return; + } + + moveAreaOverlayBounds_ = ComputeMoveAreaOverlayBounds(); + if (moveAreaOverlayBounds_.empty()) { + moveAreaOverlayCanvas_->SetVisible(false); + return; + } + + int width = + std::max(static_cast(std::lround(moveAreaOverlayBounds_.width())), 1); + int height = + std::max(static_cast(std::lround(moveAreaOverlayBounds_.height())), 1); + if (!moveAreaOverlayCanvas_->SetCanvasSize(width, height)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameMapLayer: failed to resize move area overlay canvas to %dx%d", + width, height); + moveAreaOverlayCanvas_->SetVisible(false); + moveAreaOverlayDirty_ = true; + return; + } + + moveAreaOverlayCanvas_->SetVisible(true); + moveAreaOverlayCanvas_->SetDirty(); +} + +Rect GameMapLayer::ComputeFeasibleAreaOverlayBounds() const { + BoundsAccumulator bounds; + for (const auto& rect : feasibleAreaFillRects_) { + bounds.IncludeRect(rect); + } + for (const auto& point : feasibleAreaPolygon_) { + bounds.IncludePoint(point); + } + + float padding = std::max(kDebugEdgePointSize, kDebugVertexSize) * 0.5f; + return bounds.Build(padding); +} + +Rect GameMapLayer::ComputeMoveAreaOverlayBounds() const { + BoundsAccumulator bounds; + for (const auto& moveArea : moveAreaInfoList_) { + bounds.IncludeRect(moveArea.rect); + } + + float padding = std::max(kDebugEdgePointSize, kDebugVertexSize) * 0.5f; + return bounds.Build(padding); +} + +void GameMapLayer::DrawFeasibleAreaOverlay() const { + if (feasibleAreaOverlayBounds_.empty()) { + return; + } + + Vec2 drawOrigin(-feasibleAreaOverlayBounds_.origin.x, + -feasibleAreaOverlayBounds_.origin.y); for (const auto& rect : feasibleAreaFillRects_) { - Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y, + Rect drawRect(drawOrigin.x + rect.origin.x, drawOrigin.y + rect.origin.y, rect.width(), rect.height()); Renderer::get().drawQuad(drawRect, Color(0.0f, 1.0f, 0.0f, kDebugAreaAlpha)); } + if (feasibleAreaPolygon_.empty()) { + return; + } + + Color outlineColor(0.0f, 1.0f, 0.0f, kDebugOutlineAlpha); + DrawPolygonOutline(feasibleAreaPolygon_, drawOrigin, outlineColor); + DrawPolygonVertices(feasibleAreaPolygon_, drawOrigin, outlineColor); +} + +void GameMapLayer::DrawMoveAreaOverlay() const { + if (moveAreaOverlayBounds_.empty()) { + return; + } + + Vec2 drawOrigin(-moveAreaOverlayBounds_.origin.x, -moveAreaOverlayBounds_.origin.y); + for (const auto& moveArea : moveAreaInfoList_) { bool isHighlighted = moveArea.index == highlightedMoveAreaIndex_; Color fillColor = isHighlighted ? Color(0.0f, 1.0f, 1.0f, 0.60f) : Color(0.0f, 0.0f, 1.0f, kDebugAreaAlpha); - Rect drawRect(worldOrigin.x + moveArea.rect.origin.x, - worldOrigin.y + moveArea.rect.origin.y, + Rect drawRect(drawOrigin.x + moveArea.rect.origin.x, + drawOrigin.y + moveArea.rect.origin.y, moveArea.rect.width(), moveArea.rect.height()); Renderer::get().drawQuad(drawRect, fillColor); } - if (!feasibleAreaPolygon_.empty()) { - Color outlineColor(0.0f, 1.0f, 0.0f, kDebugOutlineAlpha); - DrawPolygonOutline(feasibleAreaPolygon_, worldOrigin, outlineColor); - DrawPolygonVertices(feasibleAreaPolygon_, worldOrigin, outlineColor); - } - - for (auto& moveArea : moveAreaInfoList_) { + for (const auto& moveArea : moveAreaInfoList_) { std::vector polygon = gameMath::BuildRectPolygon(moveArea.rect); if (polygon.empty()) { continue; } + bool isHighlighted = moveArea.index == highlightedMoveAreaIndex_; Color moveAreaOutlineColor = isHighlighted ? Color(0.0f, 1.0f, 1.0f, 1.0f) : Color(0.0f, 0.0f, 1.0f, kDebugOutlineAlpha); - DrawPolygonOutline(polygon, worldOrigin, moveAreaOutlineColor); - DrawPolygonVertices(polygon, worldOrigin, moveAreaOutlineColor); + DrawPolygonOutline(polygon, drawOrigin, moveAreaOutlineColor); + DrawPolygonVertices(polygon, drawOrigin, moveAreaOutlineColor); } } void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector& polygon) { feasibleAreaPolygon_ = polygon; feasibleAreaFillRects_ = gameMath::BuildPolygonFillRects(feasibleAreaPolygon_); + feasibleAreaOverlayDirty_ = true; } void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect, size_t index) { @@ -108,10 +304,16 @@ void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect, size_t index) { debugArea.index = index; moveAreaInfoList_.push_back(std::move(debugArea)); + moveAreaOverlayDirty_ = true; } void GameMapLayer::SetDebugHighlightedMoveAreaIndex(size_t index) { + if (highlightedMoveAreaIndex_ == index) { + return; + } + highlightedMoveAreaIndex_ = index; + moveAreaOverlayDirty_ = true; } void GameMapLayer::ClearDebugAreaInfo() { @@ -119,6 +321,17 @@ void GameMapLayer::ClearDebugAreaInfo() { feasibleAreaFillRects_.clear(); moveAreaInfoList_.clear(); highlightedMoveAreaIndex_ = kInvalidMoveAreaIndex; + feasibleAreaOverlayBounds_ = Rect::Zero(); + moveAreaOverlayBounds_ = Rect::Zero(); + feasibleAreaOverlayDirty_ = true; + moveAreaOverlayDirty_ = true; + + if (feasibleAreaOverlayCanvas_) { + feasibleAreaOverlayCanvas_->SetVisible(false); + } + if (moveAreaOverlayCanvas_) { + moveAreaOverlayCanvas_->SetVisible(false); + } } void GameMapLayer::AddObject(RefPtr obj) { diff --git a/Game/src/npc/NpcAnimation.cpp b/Game/src/npc/NpcAnimation.cpp index 2d3af23..2825846 100644 --- a/Game/src/npc/NpcAnimation.cpp +++ b/Game/src/npc/NpcAnimation.cpp @@ -152,6 +152,10 @@ bool NpcAnimation::GetDisplayLocalBounds(Rect& outBounds) const { } bool NpcAnimation::EnsureCompositeTextureReady() { + if (!Renderer::get().isFrameActive()) { + return HasCompositeTexture(); + } + RefreshCompositeTextureIfNeeded(); return HasCompositeTexture(); } @@ -242,6 +246,10 @@ void NpcAnimation::RefreshCompositeTextureIfNeeded() { return; } + if (!Renderer::get().isFrameActive()) { + return; + } + CompositeFrameInfo info = GetCurrentCompositeFrameInfo(); if (!info.valid) { compositeDirty_ = false; diff --git a/Game/src/npc/NpcObject.cpp b/Game/src/npc/NpcObject.cpp index f3ebdc4..2d7fef6 100644 --- a/Game/src/npc/NpcObject.cpp +++ b/Game/src/npc/NpcObject.cpp @@ -89,7 +89,6 @@ void NpcObject::SetDirection(int direction) { animation_->SetDirection(direction_); } RefreshNameLabel(); - SyncInteractionHighlight(); } void NpcObject::SetNpcPosition(const Vec2& pos) { @@ -163,7 +162,11 @@ bool NpcObject::IsAnimationFinished() const { void NpcObject::Update(float deltaTime) { Actor::Update(deltaTime); +} + +void NpcObject::Render() { SyncInteractionHighlight(); + Actor::Render(); } void NpcObject::EnsureInteractionHighlight() { diff --git a/Game/src/scene/GameMapTestScene.cpp b/Game/src/scene/GameMapTestScene.cpp index c1efec8..003cdd0 100644 --- a/Game/src/scene/GameMapTestScene.cpp +++ b/Game/src/scene/GameMapTestScene.cpp @@ -7,7 +7,7 @@ namespace frostbite2D { namespace { -constexpr char kTestMapPath[] = "map/elvengard/d_elvengard.map"; +constexpr char kTestMapPath[] = "map/cataclysm/town/seria_room/elvengard.map"; constexpr int kTestNpcId = 2; constexpr float kTestNpcOffsetX = 180.0f; constexpr float kTestNpcOffsetY = -24.0f; @@ -28,6 +28,9 @@ void GameMapTestScene::onEnter() { SceneManager::get().PushUIScene(debugScene_); if (initialized_) { + cameraController_.SetMap(map_.Get()); + cameraController_.SetTarget(character_.Get()); + cameraController_.SetDebugEnabled(false); debugScene_->SetDebugContext(map_.Get(), character_.Get()); return; } @@ -45,6 +48,8 @@ void GameMapTestScene::onEnter() { } AddChild(map_); + character_.Reset(); + npc_.Reset(); { ScopedStartupTrace stageTrace("GameMapTestScene character construction"); character_ = MakePtr(); @@ -53,8 +58,7 @@ void GameMapTestScene::onEnter() { "GameMapTestScene: failed to construct default character"); character_.Reset(); } else { - Vec2 spawnPos = - map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.0f); + Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.0f); character_->SetCharacterPosition(spawnPos); character_->EnableEventReceive(); character_->SetEventPriority(-100);