feat(渲染): 实现NPC和地图层的渲染优化

重构NPC渲染逻辑,将交互高亮同步移至Render方法
为NpcAnimation添加帧激活检查以避免无效纹理刷新
为GameMapLayer添加调试覆盖层画布,优化可行区域和移动区域渲染
更新测试场景地图路径和相机控制器设置
This commit is contained in:
2026-04-08 00:17:03 +08:00
parent 5af657c5c9
commit db0fd3a17e
7 changed files with 268 additions and 24 deletions

View File

@@ -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<Music> currentMusic_;
};

View File

@@ -1,5 +1,6 @@
#pragma once
#include <frostbite2D/2d/canvas_actor.h>
#include <frostbite2D/2d/actor.h>
#include <vector>
@@ -18,15 +19,29 @@ public:
void AddObject(RefPtr<Actor> 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<CanvasActor> feasibleAreaOverlayCanvas_ = nullptr;
RefPtr<CanvasActor> moveAreaOverlayCanvas_ = nullptr;
std::vector<Vec2> feasibleAreaPolygon_;
std::vector<Rect> feasibleAreaFillRects_;
std::vector<DebugMoveAreaInfo> moveAreaInfoList_;
size_t highlightedMoveAreaIndex_ = kInvalidMoveAreaIndex;
Rect feasibleAreaOverlayBounds_;
Rect moveAreaOverlayBounds_;
bool feasibleAreaOverlayDirty_ = true;
bool moveAreaOverlayDirty_ = true;
};
} // namespace frostbite2D

View File

@@ -42,6 +42,7 @@ public:
}
void Update(float deltaTime) override;
void Render() override;
private:
void EnsureInteractionHighlight();

View File

@@ -1,5 +1,6 @@
#include "map/GameMapLayer.h"
#include "common/math/GameMath.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
#include <frostbite2D/graphics/renderer.h>
@@ -14,7 +15,54 @@ constexpr float kDebugEdgePointSize = 5.0f;
constexpr float kDebugVertexSize = 9.0f;
constexpr float kDebugEdgeStep = 4.0f;
void DrawPolygonOutline(const std::vector<Vec2>& 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<Vec2>& polygon, const Vec2& drawOrigin,
const Color& color) {
if (polygon.size() < 2) {
return;
@@ -22,8 +70,8 @@ void DrawPolygonOutline(const std::vector<Vec2>& 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<int>(std::ceil(length / kDebugEdgeStep)));
@@ -41,10 +89,10 @@ void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& worldOrigi
}
void DrawPolygonVertices(const std::vector<Vec2>& 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<Vec2>& 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<CanvasActor>& canvas, const char* label,
std::function<void()> callback) {
if (canvas) {
return;
}
canvas = MakePtr<CanvasActor>();
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<int>(std::lround(feasibleAreaOverlayBounds_.width())), 1);
int height =
std::max(static_cast<int>(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<int>(std::lround(moveAreaOverlayBounds_.width())), 1);
int height =
std::max(static_cast<int>(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<Vec2> 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<Vec2>& 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<Actor> obj) {

View File

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

View File

@@ -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() {

View File

@@ -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<CharacterObject>();
@@ -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);