feat(渲染): 实现NPC和地图层的渲染优化
重构NPC渲染逻辑,将交互高亮同步移至Render方法 为NpcAnimation添加帧激活检查以避免无效纹理刷新 为GameMapLayer添加调试覆盖层画布,优化可行区域和移动区域渲染 更新测试场景地图路径和相机控制器设置
This commit is contained in:
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,6 +42,7 @@ public:
|
||||
}
|
||||
|
||||
void Update(float deltaTime) override;
|
||||
void Render() override;
|
||||
|
||||
private:
|
||||
void EnsureInteractionHighlight();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user