feat(场景管理): 添加UIScene支持并重构场景管理器

refactor(渲染器): 优化相机切换时的渲染批处理

feat(调试工具): 新增游戏调试UI场景和九宫格面板组件

fix(动画系统): 跳过缺失的动画资源加载并记录日志

perf(资源加载): 使用BinaryFileStreamReader优化NPK文件解析

feat(地图系统): 支持多边形可行走区域调试显示

style(代码格式): 清理多余空格和统一文件编码

docs(注释): 补充关键类和方法的文档说明

test(启动跟踪): 添加启动过程性能跟踪工具

chore(依赖): 添加SDL2_ttf库支持
This commit is contained in:
2026-04-06 01:18:21 +08:00
parent 6cd1b42fef
commit bcc285eed6
36 changed files with 2675 additions and 513 deletions

View File

@@ -8,6 +8,7 @@
#include <functional>
#include <map>
#include <string>
#include <unordered_set>
#include <vector>
namespace frostbite2D {
@@ -44,6 +45,7 @@ private:
Animation* GetPrimaryAnimation(const std::string& actionName) const;
Animation* GetCurrentPrimaryAnimation() const;
void RefreshRuntimeCallbacks();
bool shouldSkipMissingAnimation(const std::string& aniPath);
void CreateAnimationBySlot(const std::string& actionName,
const std::string& slotName,
@@ -57,6 +59,8 @@ private:
int direction_ = 1;
ActionFrameFlagCallback actionFrameFlagCallback_;
ActionEndCallback actionEndCallback_;
std::unordered_set<std::string> missingAnimationPaths_;
size_t skippedMissingAnimationCount_ = 0;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,47 @@
#pragma once
#include <frostbite2D/2d/actor.h>
namespace frostbite2D {
class CharacterObject;
class GameMap;
class NineSliceActor;
class TextSprite;
/**
* @brief Reusable singleton debug actor for UI scenes.
*/
class GameDebugActor : public Actor {
public:
static GameDebugActor& get();
static Ptr<GameDebugActor> getPtr();
GameDebugActor(const GameDebugActor&) = delete;
GameDebugActor& operator=(const GameDebugActor&) = delete;
bool AttachToScene(Scene* scene);
bool AttachToParent(Actor* parent);
void DetachFromParent();
void SetDebugMap(GameMap* map);
void SetTrackedCharacter(CharacterObject* character);
void ClearDebugContext();
void OnUpdate(float deltaTime) override;
private:
void initOverlay();
void updateOverlay();
void setOverlayVisible(bool visible);
GameDebugActor();
~GameDebugActor() override = default;
GameMap* debugMap_ = nullptr;
CharacterObject* trackedCharacter_ = nullptr;
RefPtr<NineSliceActor> background_;
RefPtr<TextSprite> coordText_;
};
} // namespace frostbite2D

View File

@@ -63,7 +63,7 @@ struct MapConfig {
std::vector<BackgroundAnimationConfig> backgroundAnimations;
std::vector<MapAnimationConfig> mapAnimations;
std::vector<std::string> soundIds;
std::vector<Rect> virtualMovableAreas;
std::vector<Vec2> virtualMovablePolygon;
std::vector<Rect> townMovableAreas;
std::vector<MoveAreaTarget> townMovableAreaTargets;
};

View File

@@ -49,6 +49,7 @@ public:
Rect GetMovablePositionArea(size_t index) const;
int GetBackgroundRepeatWidth() const { return backgroundRepeatWidth_; }
bool IsDebugModeEnabled() const { return debugMode_; }
private:
/// 初始化固定图层。图层名字与 DNF 地图层概念保持一致。
@@ -75,14 +76,14 @@ private:
/// bottom 层里的专用地板容器,固定放在该层最底部,避免地图动画被地板压住。
RefPtr<Actor> tileRoot_;
/// 角色可行走矩形区域。
std::vector<Rect> movableArea_;
std::vector<Vec2> movablePolygon_;
/// 进入后会触发切图/传送的矩形区域。
std::vector<Rect> moveArea_;
/// 地板铺设后推导出的横向覆盖宽度,用于背景动画横向平铺。
int backgroundRepeatWidth_ = 0;
/// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。
int mapOffsetY_ = 0;
bool debugMode_ = false;
bool debugMode_ = true;
/// 当前地图正在播放的背景音乐。
Ptr<Music> currentMusic_;
};

View File

@@ -9,11 +9,14 @@ class GameMapLayer : public Actor {
public:
void Render() override;
void AddDebugFeasibleAreaInfo(const Rect& rect, int type);
void SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon);
void AddDebugMoveAreaInfo(const Rect& rect);
void ClearDebugAreaInfo();
void AddObject(RefPtr<Actor> obj);
private:
std::vector<Rect> feasibleAreaInfoList_;
std::vector<Vec2> feasibleAreaPolygon_;
std::vector<Rect> feasibleAreaFillRects_;
std::vector<Rect> moveAreaInfoList_;
};

View File

@@ -0,0 +1,22 @@
#pragma once
#include <frostbite2D/scene/ui_scene.h>
namespace frostbite2D {
class CharacterObject;
class GameMap;
class GameDebugUIScene : public UIScene {
public:
GameDebugUIScene() = default;
~GameDebugUIScene() override = default;
void onEnter() override;
void onExit() override;
void SetDebugContext(GameMap* map, CharacterObject* character);
void ClearDebugContext();
};
} // namespace frostbite2D

View File

@@ -1,8 +1,9 @@
#pragma once
#pragma once
#include "camera/GameCameraController.h"
#include "character/CharacterObject.h"
#include "map/GameMap.h"
#include "scene/GameDebugUIScene.h"
#include <frostbite2D/scene/scene.h>
namespace frostbite2D {
@@ -19,6 +20,7 @@ public:
private:
GameCameraController cameraController_;
RefPtr<CharacterObject> character_;
RefPtr<GameDebugUIScene> debugScene_;
bool initialized_ = false;
RefPtr<GameMap> map_;
};

View File

@@ -0,0 +1,54 @@
#pragma once
#include <array>
#include <string>
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/2d/sprite.h>
namespace frostbite2D {
/**
* @brief Composes 9 consecutive NPK frames into a resizable nine-slice panel.
*/
class NineSliceActor : public Actor {
public:
NineSliceActor();
~NineSliceActor() override = default;
static Ptr<NineSliceActor> createFromNpk(const std::string& imgPath,
size_t startIndex = 0);
bool SetSlices(const std::string& imgPath, size_t startIndex);
void SetSize(const Vec2& size);
void SetSize(float width, float height);
const std::string& GetImgPath() const { return imgPath_; }
size_t GetStartIndex() const { return startIndex_; }
private:
enum SliceIndex : size_t {
TopLeft = 0,
Top = 1,
TopRight = 2,
Left = 3,
Center = 4,
Right = 5,
BottomLeft = 6,
Bottom = 7,
BottomRight = 8,
SliceCount = 9
};
void updateLayout();
void applySliceLayout(Ptr<Sprite> sprite, const Vec2& position,
const Vec2& size);
Vec2 getNaturalSize() const;
std::string imgPath_;
size_t startIndex_ = 0;
std::array<Ptr<Sprite>, SliceCount> sliceSprites_;
std::array<Vec2, SliceCount> sliceNaturalSizes_ = {};
};
} // namespace frostbite2D

View File

@@ -19,6 +19,7 @@
#include <frostbite2D/resource/sound_pack_archive.h>
#include <frostbite2D/scene/scene.h>
#include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/utils/startup_trace.h>
#include "scene/GameMapTestScene.h"
#include "world/GameWorld.h"
@@ -28,6 +29,9 @@ int main(int argc, char **argv) {
(void)argc;
(void)argv;
StartupTrace::reset("main entry");
StartupTrace::mark("main entry");
AppConfig config = AppConfig::createDefault();
config.appName = "Frostbite2D Test App";
config.appVersion = "1.0.0";
@@ -36,79 +40,117 @@ int main(int argc, char **argv) {
config.windowConfig.title = "Frostbite2D - Async Init Demo";
Application &app = Application::get();
if (!app.init(config)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize application!");
return -1;
{
ScopedStartupTrace startupTrace("Application::init");
if (!app.init(config)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize application!");
return -1;
}
}
StartupTrace::mark("Application::init complete");
app.run([]() {
auto &fontManager = FontManager::get();
fontManager.init();
fontManager.registerFont("default", "assets/Fonts/VonwaonBitmap-12px.ttf",
12);
ScopedStartupTrace startupTrace("Application::run startup callback");
auto &audioSystem = AudioSystem::get();
audioSystem.init();
audioSystem.setMasterVolume(1.0f);
audioSystem.setSoundVolume(0.8f);
audioSystem.setMusicVolume(0.6f);
{
ScopedStartupTrace stageTrace("FontManager startup");
auto &fontManager = FontManager::get();
fontManager.init();
fontManager.registerFont("default", "assets/Fonts/VonwaonBitmap-12px.ttf",
12);
}
{
ScopedStartupTrace stageTrace("AudioSystem startup");
auto &audioSystem = AudioSystem::get();
audioSystem.init();
audioSystem.setMasterVolume(1.0f);
audioSystem.setSoundVolume(0.8f);
audioSystem.setMusicVolume(0.6f);
}
SDL_Log("游戏启动");
auto LoadingScene = MakePtr<Scene>();
SceneManager::get().PushScene(LoadingScene);
{
ScopedStartupTrace stageTrace("Loading scene bootstrap");
auto LoadingScene = MakePtr<Scene>();
SceneManager::get().PushScene(LoadingScene);
auto Background = Sprite::createFromFile("assets/ImagePacks2/Loading0.jpg");
Background->SetSize(1280, 720);
LoadingScene->AddChild(Background);
auto Background = Sprite::createFromFile("assets/ImagePacks2/Loading0.jpg");
Background->SetSize(1280, 720);
LoadingScene->AddChild(Background);
auto BackgroundBar =
Sprite::createFromFile("assets/ImagePacks2/Loading1.png");
BackgroundBar->SetPosition(0, 686);
LoadingScene->AddChild(BackgroundBar);
auto BackgroundBar =
Sprite::createFromFile("assets/ImagePacks2/Loading1.png");
BackgroundBar->SetPosition(0, 686);
LoadingScene->AddChild(BackgroundBar);
auto LoadCircleSp =
Sprite::createFromFile("assets/ImagePacks2/Loading2.png");
LoadCircleSp->SetAnchor(Vec2(0.5f, 0.5f));
LoadCircleSp->SetPosition(1280 / 2.0f, 686 - 60);
LoadCircleSp->SetBlendMode(BlendMode::Additive);
LoadCircleSp->AddUpdateListener([](Actor &self, float dt) {
auto rotation = self.GetRotation();
self.SetRotation(rotation + 180.0f * dt);
});
LoadingScene->AddChild(LoadCircleSp);
auto LoadCircleSp =
Sprite::createFromFile("assets/ImagePacks2/Loading2.png");
LoadCircleSp->SetAnchor(Vec2(0.5f, 0.5f));
LoadCircleSp->SetPosition(1280 / 2.0f, 686 - 60);
LoadCircleSp->SetBlendMode(BlendMode::Additive);
LoadCircleSp->AddUpdateListener([](Actor &self, float dt) {
auto rotation = self.GetRotation();
self.SetRotation(rotation + 180.0f * dt);
});
LoadingScene->AddChild(LoadCircleSp);
}
StartupTrace::mark("loading scene ready");
TaskSystem::get().submitThen(
[]() -> std::string {
ScopedStartupTrace startupTrace("Async resource bootstrap");
SDL_Log("Async init task on main thread: %s",
TaskSystem::get().isMainThread() ? "true" : "false");
auto &pvf = PvfArchive::get();
if (!pvf.open("assets/Script.pvf")) {
throw std::runtime_error("Failed to open assets/Script.pvf");
{
ScopedStartupTrace stageTrace("PvfArchive open+init");
auto &pvf = PvfArchive::get();
if (!pvf.open("assets/Script.pvf")) {
throw std::runtime_error("Failed to open assets/Script.pvf");
}
pvf.init();
}
pvf.init();
auto &npk = NpkArchive::get();
npk.setImagePackDirectory("assets/ImagePacks2");
npk.setDefaultImg("sprite/interface/base.img", 0);
npk.init();
{
ScopedStartupTrace stageTrace("NpkArchive init");
auto &npk = NpkArchive::get();
npk.setImagePackDirectory("assets/ImagePacks2");
npk.setDefaultImg("sprite/interface/base.img", 0);
npk.init();
}
auto &archive = SoundPackArchive::get();
archive.setSoundPackDirectory("assets/SoundPacks");
archive.init();
{
ScopedStartupTrace stageTrace("SoundPackArchive init");
auto &archive = SoundPackArchive::get();
archive.setSoundPackDirectory("assets/SoundPacks");
archive.init();
}
auto &audioDatabase = AudioDatabase::get();
audioDatabase.loadFromFile("assets/audio.xml");
{
ScopedStartupTrace stageTrace("AudioDatabase load");
auto &audioDatabase = AudioDatabase::get();
audioDatabase.loadFromFile("assets/audio.xml");
}
return "后台资源加载成功";
},
[](std::string message) mutable {
SDL_Log("后台资源加载成功");
ScopedStartupTrace startupTrace("Async completion main-thread scene switch");
SDL_Log("%s", message.c_str());
StartupTrace::mark("before SceneManager::ReplaceScene");
auto testMapScene = MakePtr<GameMapTestScene>();
SceneManager::get().ReplaceScene(testMapScene);
{
ScopedStartupTrace stageTrace(
"SceneManager::ReplaceScene(GameMapTestScene)");
auto testMapScene = MakePtr<GameMapTestScene>();
SceneManager::get().ReplaceScene(testMapScene);
}
StartupTrace::mark("after SceneManager::ReplaceScene");
},
[](std::exception_ptr error) {
try {

View File

@@ -1,5 +1,6 @@
#include "character/CharacterAnimation.h"
#include "character/CharacterObject.h"
#include <frostbite2D/resource/pvf_archive.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <array>
@@ -38,6 +39,8 @@ bool CharacterAnimation::Init(CharacterObject* parent,
const CharacterEquipmentManager& equipmentManager) {
parent_ = parent;
actionAnimations_.clear();
missingAnimationPaths_.clear();
skippedMissingAnimationCount_ = 0;
currentActionTag_.clear();
direction_ = 1;
actionFrameFlagCallback_ = nullptr;
@@ -56,6 +59,12 @@ bool CharacterAnimation::Init(CharacterObject* parent,
return false;
}
if (skippedMissingAnimationCount_ > 0) {
SDL_Log("CharacterAnimation: skipped %zu missing animation loads across %zu unique paths for job %d",
skippedMissingAnimationCount_, missingAnimationPaths_.size(),
config.jobId);
}
return true;
}
@@ -77,6 +86,10 @@ void CharacterAnimation::CreateAnimationBySlot(
const character::CharacterConfig& config,
const CharacterEquipmentManager& equipmentManager) {
if (slotName == std::string("skin_avatar")) {
if (shouldSkipMissingAnimation(actionPath)) {
return;
}
Animation::ReplaceData replaceData(0, 0);
if (const auto* equip = equipmentManager.GetEquip(slotName)) {
auto it = equip->jobAnimations.find(config.jobId);
@@ -114,6 +127,10 @@ void CharacterAnimation::CreateAnimationBySlot(
}
std::string aniPath = equipDir + "/" + variation.animationGroup + actionPathTail;
if (shouldSkipMissingAnimation(aniPath)) {
continue;
}
auto animation = MakePtr<Animation>(
aniPath, FormatImgPath,
Animation::ReplaceData(variation.imgFormat[0], variation.imgFormat[1]));
@@ -128,6 +145,23 @@ void CharacterAnimation::CreateAnimationBySlot(
}
}
bool CharacterAnimation::shouldSkipMissingAnimation(const std::string& aniPath) {
PvfArchive& pvf = PvfArchive::get();
std::string normalizedPath = pvf.normalizePath(aniPath);
if (missingAnimationPaths_.find(normalizedPath) != missingAnimationPaths_.end()) {
++skippedMissingAnimationCount_;
return true;
}
if (pvf.hasFile(normalizedPath)) {
return false;
}
missingAnimationPaths_.insert(std::move(normalizedPath));
++skippedMissingAnimationCount_;
return true;
}
std::string CharacterAnimation::DescribeAvailableActions() const {
if (actionAnimations_.empty()) {
return "<none>";

View File

@@ -4,6 +4,7 @@
#include <SDL2/SDL.h>
#include <cmath>
#include <frostbite2D/core/application.h>
#include <frostbite2D/utils/startup_trace.h>
#include <sstream>
#include <utility>
@@ -49,6 +50,7 @@ int32 RoundWorldCoordinate(float value) {
} // namespace
bool CharacterObject::Construction(int jobId) {
ScopedStartupTrace startupTrace("CharacterObject::Construction");
// Reset all runtime state before rebuilding the character from config.
EnableEventReceive();
RemoveAllChildren();
@@ -67,7 +69,10 @@ bool CharacterObject::Construction(int jobId) {
lastDeltaTime_ = 0.0f;
inputEnabled_ = true;
auto config = character::loadCharacterConfig(jobId);
auto config = [&]() {
ScopedStartupTrace stageTrace("character::loadCharacterConfig");
return character::loadCharacterConfig(jobId);
}();
if (!config) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CharacterObject: failed to load job %d config", jobId);
@@ -79,24 +84,36 @@ bool CharacterObject::Construction(int jobId) {
direction_ = 1;
config_ = *config;
stateMachine_.Configure(*config_);
equipmentManager_.Init(config_->baseJobConfig);
if (auto actionLibrary = loadCharacterActionLibrary(*config_)) {
{
ScopedStartupTrace stageTrace("CharacterEquipmentManager::Init");
equipmentManager_.Init(config_->baseJobConfig);
}
if (auto actionLibrary = [&]() {
ScopedStartupTrace stageTrace("loadCharacterActionLibrary");
return loadCharacterActionLibrary(*config_);
}()) {
actionLibrary_ = *actionLibrary;
}
animationManager_ = MakePtr<CharacterAnimation>();
if (!animationManager_->Init(this, *config_, equipmentManager_)) {
ReportFatalCharacterError("CharacterObject::Construction",
"no usable animation tags were loaded");
return false;
{
ScopedStartupTrace stageTrace("CharacterAnimation::Init");
if (!animationManager_->Init(this, *config_, equipmentManager_)) {
ReportFatalCharacterError("CharacterObject::Construction",
"no usable animation tags were loaded");
return false;
}
}
AddChild(animationManager_);
if (!RequireAction("idle", "CharacterObject::Construction")) {
return false;
}
if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) {
return false;
{
ScopedStartupTrace stageTrace("CharacterObject initial action setup");
if (!RequireAction("idle", "CharacterObject::Construction")) {
return false;
}
if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) {
return false;
}
}
SetWorldPosition({});

View File

@@ -0,0 +1,147 @@
#include "common/GameDebugActor.h"
#include "character/CharacterObject.h"
#include "map/GameMap.h"
#include "ui/NineSliceActor.h"
#include <SDL2/SDL.h>
#include <frostbite2D/2d/text_sprite.h>
#include <frostbite2D/scene/scene.h>
#include <limits>
namespace frostbite2D {
namespace {
constexpr float kDebugHudMarginX = 12.0f;
constexpr float kDebugHudMarginY = 12.0f;
constexpr float kDebugHudPaddingX = 8.0f;
constexpr float kDebugHudPaddingY = 6.0f;
constexpr char kDebugHudPopupImg[] = "sprite/interface/newstyle/windows/popup/popup.img";
} // namespace
GameDebugActor::GameDebugActor() {
SetName("GameDebugActor");
SetZOrder(std::numeric_limits<int>::max());
initOverlay();
}
GameDebugActor& GameDebugActor::get() {
return *getPtr();
}
Ptr<GameDebugActor> GameDebugActor::getPtr() {
static Ptr<GameDebugActor> instance(new GameDebugActor());
return instance;
}
bool GameDebugActor::AttachToScene(Scene* scene) {
return AttachToParent(scene);
}
bool GameDebugActor::AttachToParent(Actor* parent) {
if (!parent || parent == this) {
return false;
}
Ptr<GameDebugActor> self = getPtr();
if (GetParent() == parent) {
return true;
}
if (GetParent()) {
GetParent()->RemoveChild(self);
}
parent->AddChild(self);
return GetParent() == parent;
}
void GameDebugActor::DetachFromParent() {
Ptr<GameDebugActor> self = getPtr();
if (GetParent()) {
GetParent()->RemoveChild(self);
}
}
void GameDebugActor::SetDebugMap(GameMap* map) {
debugMap_ = map;
updateOverlay();
}
void GameDebugActor::SetTrackedCharacter(CharacterObject* character) {
trackedCharacter_ = character;
updateOverlay();
}
void GameDebugActor::ClearDebugContext() {
debugMap_ = nullptr;
trackedCharacter_ = nullptr;
setOverlayVisible(false);
}
void GameDebugActor::OnUpdate(float deltaTime) {
(void)deltaTime;
updateOverlay();
}
void GameDebugActor::initOverlay() {
if (background_ || coordText_) {
return;
}
background_ = NineSliceActor::createFromNpk(kDebugHudPopupImg, 0);
if (background_) {
background_->SetName("debugHudBackground");
AddChild(background_);
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameDebugActor: failed to load HUD background from %s",
kDebugHudPopupImg);
}
coordText_ = TextSprite::create();
coordText_->SetName("debugCoordText");
coordText_->SetFont("default");
coordText_->SetTextColor(1.0f, 1.0f, 1.0f, 1.0f);
coordText_->SetZOrder(1);
AddChild(coordText_);
setOverlayVisible(false);
}
void GameDebugActor::updateOverlay() {
if (!coordText_ || !debugMap_ || !trackedCharacter_ ||
!debugMap_->IsDebugModeEnabled()) {
setOverlayVisible(false);
return;
}
const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition();
char textBuffer[64];
SDL_snprintf(textBuffer, sizeof(textBuffer), "角色坐标: (%d, %d, %d)",
worldPosition.x, worldPosition.y, worldPosition.z);
coordText_->SetText(textBuffer);
Vec2 textSize = coordText_->GetTextSize();
Vec2 panelSize(textSize.x + kDebugHudPaddingX * 2.0f,
textSize.y + kDebugHudPaddingY * 2.0f);
if (background_) {
background_->SetSize(panelSize);
}
SetPosition(kDebugHudMarginX, kDebugHudMarginY);
SetScale(1.0f);
coordText_->SetPosition(kDebugHudPaddingX, kDebugHudPaddingY);
setOverlayVisible(true);
}
void GameDebugActor::setOverlayVisible(bool visible) {
if (background_) {
background_->SetVisible(visible);
}
if (coordText_) {
coordText_->SetVisible(visible);
}
}
} // namespace frostbite2D

View File

@@ -268,16 +268,39 @@ bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig) {
outConfig.townMovableAreaTargets.push_back(target);
}
} else if (segment == "[virtual movable area]") {
std::vector<int> polygonCoords;
while (!stream.isEnd()) {
std::string token = stream.get();
if (token == "[/virtual movable area]") {
break;
}
Rect rect(static_cast<float>(toInt(token)),
static_cast<float>(toInt(stream.get())),
static_cast<float>(toInt(stream.get())),
static_cast<float>(toInt(stream.get())));
outConfig.virtualMovableAreas.push_back(rect);
polygonCoords.push_back(toInt(token));
}
if (polygonCoords.empty()) {
continue;
}
if (polygonCoords.size() % 2 != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameDataLoader: map %s has odd [virtual movable area] coordinate count (%zu)",
outConfig.mapPath.c_str(), polygonCoords.size());
continue;
}
if (polygonCoords.size() < 6) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameDataLoader: map %s [virtual movable area] needs at least 3 vertices, got %zu",
outConfig.mapPath.c_str(), polygonCoords.size() / 2);
continue;
}
outConfig.virtualMovablePolygon.clear();
outConfig.virtualMovablePolygon.reserve(polygonCoords.size() / 2);
for (size_t i = 0; i + 1 < polygonCoords.size(); i += 2) {
outConfig.virtualMovablePolygon.emplace_back(
static_cast<float>(polygonCoords[i]),
static_cast<float>(polygonCoords[i + 1]));
}
}
}

View File

@@ -6,6 +6,7 @@
#include <frostbite2D/core/application.h>
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/resource/audio_database.h>
#include <frostbite2D/utils/startup_trace.h>
#include <algorithm>
#include <cmath>
@@ -23,6 +24,7 @@ static const int kLayerOrders[] = {
constexpr int kTileRootZOrder = -1000000;
constexpr float kExtendedTileStepY = 120.0f;
constexpr double kPolygonEpsilon = 0.001;
int RoundWorldCoordinate(float value) {
return static_cast<int>(std::lround(value));
@@ -37,6 +39,57 @@ Vec3 MakeIntegerWorldPosition(int x, int y, int z) {
static_cast<float>(z));
}
bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end) {
Vec2 segment = end - start;
Vec2 toPoint = point - start;
double cross = static_cast<double>(segment.cross(toPoint));
if (std::abs(cross) > kPolygonEpsilon) {
return false;
}
double minX = std::min(start.x, end.x) - kPolygonEpsilon;
double maxX = std::max(start.x, end.x) + kPolygonEpsilon;
double minY = std::min(start.y, end.y) - kPolygonEpsilon;
double maxY = std::max(start.y, end.y) + kPolygonEpsilon;
return point.x >= minX && point.x <= maxX && point.y >= minY &&
point.y <= maxY;
}
bool IsPointInPolygon(const std::vector<Vec2>& polygon, const Vec2& point) {
if (polygon.size() < 3) {
return false;
}
bool inside = false;
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
const Vec2& start = polygon[j];
const Vec2& end = polygon[i];
if (IsPointOnSegment(point, start, end)) {
return true;
}
bool crossesScanline = (start.y > point.y) != (end.y > point.y);
if (!crossesScanline) {
continue;
}
double intersectX =
static_cast<double>(start.x) +
(static_cast<double>(point.y) - static_cast<double>(start.y)) *
(static_cast<double>(end.x) - static_cast<double>(start.x)) /
(static_cast<double>(end.y) - static_cast<double>(start.y));
if (intersectX >= static_cast<double>(point.x) - kPolygonEpsilon) {
inside = !inside;
}
}
return inside;
}
bool IsPointMovable(const std::vector<Vec2>& polygon, int x, int y) {
return IsPointInPolygon(polygon, MakeIntegerWorldPoint(x, y));
}
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
// 占位,避免整张地图中断。
Ptr<Sprite> createMapSprite(const std::string& path, int index) {
@@ -70,33 +123,49 @@ void GameMap::clearLayerChildren() {
for (auto& [_, layer] : layerMap_) {
if (layer) {
layer->RemoveAllChildren();
layer->ClearDebugAreaInfo();
}
}
tileRoot_.Reset();
movableArea_.clear();
movablePolygon_.clear();
moveArea_.clear();
currentMusic_.Reset();
backgroundRepeatWidth_ = 0;
}
bool GameMap::LoadMap(const std::string &mapName) {
// 清空所有图层子节点。
ScopedStartupTrace startupTrace("GameMap::LoadMap");
clearLayerChildren();
// 读取PVF地图配置。
if (!game::loadMapConfig(mapName, mapConfig_)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to load map %s",
mapName.c_str());
return false;
{
ScopedStartupTrace stageTrace("game::loadMapConfig");
if (!game::loadMapConfig(mapName, mapConfig_)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GameMap: failed to load map %s", mapName.c_str());
return false;
}
}
// backgroundPos 会影响地图整体的视觉基线,所以地板和各显示层都会参考它。
mapOffsetY_ = mapConfig_.backgroundPos;
// 加载顺序基本就是地图的组装顺序:先地板,再背景,再对象,再可行走数据。
InitTile();
InitBackgroundAnimation();
InitMapAnimation();
InitVirtualMovableArea();
InitMoveArea();
{
ScopedStartupTrace stageTrace("GameMap::InitTile");
InitTile();
}
{
ScopedStartupTrace stageTrace("GameMap::InitBackgroundAnimation");
InitBackgroundAnimation();
}
{
ScopedStartupTrace stageTrace("GameMap::InitMapAnimation");
InitMapAnimation();
}
{
ScopedStartupTrace stageTrace("GameMap::InitVirtualMovableArea");
InitVirtualMovableArea();
}
{
ScopedStartupTrace stageTrace("GameMap::InitMoveArea");
InitMoveArea();
}
return true;
}
@@ -248,26 +317,48 @@ void GameMap::InitMapAnimation() {
}
void GameMap::InitVirtualMovableArea() {
movableArea_ = mapConfig_.virtualMovableAreas;
// debugMode 打开时,把可行走区域可视化到最高层,方便校验地图配置。
movablePolygon_ = mapConfig_.virtualMovablePolygon;
if (movablePolygon_.empty()) {
return;
}
float minX = movablePolygon_.front().x;
float minY = movablePolygon_.front().y;
float maxX = movablePolygon_.front().x;
float maxY = movablePolygon_.front().y;
for (const auto& point : movablePolygon_) {
minX = std::min(minX, point.x);
minY = std::min(minY, point.y);
maxX = std::max(maxX, point.x);
maxY = std::max(maxY, point.y);
}
SDL_Log("GameMap: movable polygon vertices=%zu bounds=(%.0f, %.0f)-(%.0f, %.0f)",
movablePolygon_.size(), minX, minY, maxX, maxY);
if (!debugMode_) {
return;
}
for (const auto& rect : movableArea_) {
layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 0);
auto layerIt = layerMap_.find("max");
if (layerIt == layerMap_.end() || !layerIt->second) {
return;
}
layerIt->second->SetDebugFeasibleAreaPolygon(movablePolygon_);
}
void GameMap::InitMoveArea() {
moveArea_ = mapConfig_.townMovableAreas;
// move area 和普通 movable area 分开展示,方便区分“能走”与“会触发切图”。
if (!debugMode_) {
return;
}
auto layerIt = layerMap_.find("max");
if (layerIt == layerMap_.end() || !layerIt->second) {
return;
}
for (const auto& rect : moveArea_) {
layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 1);
layerIt->second->AddDebugMoveAreaInfo(rect);
}
}
@@ -357,7 +448,7 @@ void GameMap::AddObject(RefPtr<Actor> object) {
if (!object) {
return;
}
// 动态对象默认进 normal 层,并沿用 2D 地图里常见的“按 y 值排序”规则。
// Keep dynamic objects on the normal layer and sort them by y.
object->SetZOrder(static_cast<int>(object->GetPosition().y));
layerMap_["normal"]->AddObject(object);
}
@@ -365,42 +456,41 @@ void GameMap::AddObject(RefPtr<Actor> object) {
Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const {
int currentX = RoundWorldCoordinate(curPos.x);
int currentY = RoundWorldCoordinate(curPos.y);
int currentZ = RoundWorldCoordinate(curPos.z);
Vec3 result = MakeIntegerWorldPosition(currentX, currentY, currentZ);
if (movableArea_.empty()) {
return MakeIntegerWorldPosition(RoundWorldCoordinate(curPos.x + posOffset.x),
RoundWorldCoordinate(curPos.y + posOffset.y),
RoundWorldCoordinate(curPos.z + posOffset.z));
}
int targetX = RoundWorldCoordinate(curPos.x + posOffset.x);
int targetY = RoundWorldCoordinate(curPos.y + posOffset.y);
// X/Y
// Check X and Y separately so edge sliding does not lock both axes.
bool isXValid = false;
for (const auto& area : movableArea_) {
if (area.containsPoint(MakeIntegerWorldPoint(targetX, currentY))) {
isXValid = true;
break;
}
int targetZ = RoundWorldCoordinate(curPos.z + posOffset.z);
Vec3 result = MakeIntegerWorldPosition(currentX, currentY, targetZ);
if (movablePolygon_.size() < 3) {
return MakeIntegerWorldPosition(targetX, targetY, targetZ);
}
// Prefer the full destination first; only fall back to single-axis sliding
// when the combined move would leave the movable polygon.
if (IsPointMovable(movablePolygon_, targetX, targetY)) {
result.x = static_cast<float>(targetX);
result.y = static_cast<float>(targetY);
return result;
}
bool isYValid = false;
for (const auto& area : movableArea_) {
if (area.containsPoint(MakeIntegerWorldPoint(currentX, targetY))) {
isYValid = true;
break;
bool isXValid = IsPointMovable(movablePolygon_, targetX, currentY);
bool isYValid = IsPointMovable(movablePolygon_, currentX, targetY);
if (isXValid && isYValid) {
int moveX = std::abs(targetX - currentX);
int moveY = std::abs(targetY - currentY);
if (moveX >= moveY) {
result.x = static_cast<float>(targetX);
} else {
result.y = static_cast<float>(targetY);
}
return result;
}
if (isXValid) {
result.x = static_cast<float>(targetX);
}
if (isYValid) {
} else if (isYValid) {
result.y = static_cast<float>(targetY);
}
result.z = static_cast<float>(RoundWorldCoordinate(curPos.z + posOffset.z));
return result;
}

View File

@@ -1,32 +1,155 @@
#include "map/GameMapLayer.h"
#include <algorithm>
#include <cmath>
#include <frostbite2D/graphics/renderer.h>
namespace frostbite2D {
namespace {
constexpr float kDebugAreaAlpha = 0.45f;
constexpr float kDebugOutlineAlpha = 0.85f;
constexpr float kDebugEdgePointSize = 5.0f;
constexpr float kDebugVertexSize = 9.0f;
constexpr float kDebugEdgeStep = 4.0f;
std::vector<Rect> BuildPolygonFillRects(const std::vector<Vec2>& polygon) {
std::vector<Rect> fillRects;
if (polygon.size() < 3) {
return fillRects;
}
float minY = polygon.front().y;
float maxY = polygon.front().y;
for (const auto& point : polygon) {
minY = std::min(minY, point.y);
maxY = std::max(maxY, point.y);
}
int scanlineMinY = static_cast<int>(std::floor(minY));
int scanlineMaxY = static_cast<int>(std::ceil(maxY)) - 1;
if (scanlineMaxY < scanlineMinY) {
return fillRects;
}
for (int y = scanlineMinY; y <= scanlineMaxY; ++y) {
float scanY = static_cast<float>(y) + 0.5f;
std::vector<float> intersections;
intersections.reserve(polygon.size());
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
const Vec2& start = polygon[j];
const Vec2& end = polygon[i];
if (start.y == end.y) {
continue;
}
float edgeMinY = std::min(start.y, end.y);
float edgeMaxY = std::max(start.y, end.y);
if (scanY < edgeMinY || scanY >= edgeMaxY) {
continue;
}
float t = (scanY - start.y) / (end.y - start.y);
intersections.push_back(start.x + (end.x - start.x) * t);
}
if (intersections.size() < 2) {
continue;
}
std::sort(intersections.begin(), intersections.end());
for (size_t i = 0; i + 1 < intersections.size(); i += 2) {
float left = std::floor(intersections[i]);
float right = std::ceil(intersections[i + 1]);
float width = right - left;
if (width > 0.0f) {
fillRects.emplace_back(left, static_cast<float>(y), width, 1.0f);
}
}
}
return fillRects;
}
void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& worldOrigin,
const Color& color) {
if (polygon.size() < 2) {
return;
}
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 delta = end - start;
float length = delta.length();
int steps = std::max(1, static_cast<int>(std::ceil(length / kDebugEdgeStep)));
for (int step = 0; step <= steps; ++step) {
float t = static_cast<float>(step) / static_cast<float>(steps);
Vec2 point = start + delta * t;
renderer.drawQuad(
Rect(point.x - kDebugEdgePointSize * 0.5f,
point.y - kDebugEdgePointSize * 0.5f, kDebugEdgePointSize,
kDebugEdgePointSize),
color);
}
}
}
void DrawPolygonVertices(const std::vector<Vec2>& polygon,
const Vec2& worldOrigin, const Color& color) {
auto& renderer = Renderer::get();
for (const auto& vertex : polygon) {
Vec2 point = worldOrigin + vertex;
renderer.drawQuad(
Rect(point.x - kDebugVertexSize * 0.5f,
point.y - kDebugVertexSize * 0.5f, kDebugVertexSize,
kDebugVertexSize),
color);
}
}
} // namespace
void GameMapLayer::Render() {
Actor::Render();
Vec2 worldOrigin = GetWorldTransform().transformPoint(Vec2::Zero());
for (const auto& rect : feasibleAreaInfoList_) {
for (const auto& rect : feasibleAreaFillRects_) {
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
rect.width(), rect.height());
Renderer::get().drawQuad(drawRect, Color(1.0f, 0.0f, 0.0f, 0.35f));
Renderer::get().drawQuad(drawRect, Color(0.0f, 1.0f, 0.0f, kDebugAreaAlpha));
}
for (const auto& rect : moveAreaInfoList_) {
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
rect.width(), rect.height());
Renderer::get().drawQuad(drawRect, Color(0.0f, 0.0f, 1.0f, 0.35f));
Renderer::get().drawQuad(drawRect, Color(0.0f, 0.0f, 1.0f, kDebugAreaAlpha));
}
if (!feasibleAreaPolygon_.empty()) {
Color outlineColor(0.0f, 1.0f, 0.0f, kDebugOutlineAlpha);
DrawPolygonOutline(feasibleAreaPolygon_, worldOrigin, outlineColor);
DrawPolygonVertices(feasibleAreaPolygon_, worldOrigin, outlineColor);
}
}
void GameMapLayer::AddDebugFeasibleAreaInfo(const Rect& rect, int type) {
if (type == 0) {
feasibleAreaInfoList_.push_back(rect);
} else if (type == 1) {
moveAreaInfoList_.push_back(rect);
}
void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon) {
feasibleAreaPolygon_ = polygon;
feasibleAreaFillRects_ = BuildPolygonFillRects(feasibleAreaPolygon_);
}
void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect) {
moveAreaInfoList_.push_back(rect);
}
void GameMapLayer::ClearDebugAreaInfo() {
feasibleAreaPolygon_.clear();
feasibleAreaFillRects_.clear();
moveAreaInfoList_.clear();
}
void GameMapLayer::AddObject(RefPtr<Actor> obj) {

View File

@@ -0,0 +1,28 @@
#include "scene/GameDebugUIScene.h"
#include "character/CharacterObject.h"
#include "common/GameDebugActor.h"
#include "map/GameMap.h"
namespace frostbite2D {
void GameDebugUIScene::onEnter() {
UIScene::onEnter();
GameDebugActor::get().AttachToScene(this);
}
void GameDebugUIScene::onExit() {
GameDebugActor::get().ClearDebugContext();
GameDebugActor::get().DetachFromParent();
UIScene::onExit();
}
void GameDebugUIScene::SetDebugContext(GameMap* map, CharacterObject* character) {
GameDebugActor::get().SetDebugMap(map);
GameDebugActor::get().SetTrackedCharacter(character);
}
void GameDebugUIScene::ClearDebugContext() {
GameDebugActor::get().ClearDebugContext();
}
} // namespace frostbite2D

View File

@@ -1,5 +1,7 @@
#include "scene/GameMapTestScene.h"
#include <SDL2/SDL.h>
#include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/utils/startup_trace.h>
namespace frostbite2D {
@@ -12,31 +14,48 @@ constexpr char kTestMapPath[] = "map/elvengard/elvengard.map";
GameMapTestScene::GameMapTestScene() = default;
void GameMapTestScene::onEnter() {
ScopedStartupTrace startupTrace("GameMapTestScene::onEnter");
Scene::onEnter();
if (!debugScene_) {
debugScene_ = MakePtr<GameDebugUIScene>();
}
SceneManager::get().RemoveUIScene(debugScene_.Get());
SceneManager::get().PushUIScene(debugScene_);
if (initialized_) {
debugScene_->SetDebugContext(map_.Get(), character_.Get());
return;
}
map_ = MakePtr<GameMap>();
if (!map_->LoadMap(kTestMapPath)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GameMapTestScene: failed to load map %s", kTestMapPath);
map_.Reset();
return;
{
ScopedStartupTrace stageTrace("GameMapTestScene map load");
map_ = MakePtr<GameMap>();
if (!map_->LoadMap(kTestMapPath)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GameMapTestScene: failed to load map %s", kTestMapPath);
map_.Reset();
debugScene_->ClearDebugContext();
return;
}
}
AddChild(map_);
character_ = MakePtr<CharacterObject>();
if (!character_->Construction(0)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameMapTestScene: failed to construct default character");
character_.Reset();
} else {
Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f);
character_->SetCharacterPosition(spawnPos);
character_->EnableEventReceive();
character_->SetEventPriority(-100);
map_->AddObject(character_);
{
ScopedStartupTrace stageTrace("GameMapTestScene character construction");
character_ = MakePtr<CharacterObject>();
if (!character_->Construction(0)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameMapTestScene: failed to construct default character");
character_.Reset();
} else {
Vec2 spawnPos =
map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f);
character_->SetCharacterPosition(spawnPos);
character_->EnableEventReceive();
character_->SetEventPriority(-100);
map_->AddObject(character_);
}
}
cameraController_.SetMap(map_.Get());
@@ -49,11 +68,20 @@ void GameMapTestScene::onEnter() {
cameraController_.ClearTarget();
cameraController_.SnapToDefaultFocus();
}
map_->Enter();
{
ScopedStartupTrace stageTrace("GameMap::Enter");
map_->Enter();
}
debugScene_->SetDebugContext(map_.Get(), character_.Get());
initialized_ = true;
}
void GameMapTestScene::onExit() {
if (debugScene_) {
debugScene_->ClearDebugContext();
SceneManager::get().RemoveUIScene(debugScene_.Get());
}
Scene::onExit();
}

View File

@@ -0,0 +1,203 @@
#include "ui/NineSliceActor.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <array>
#include <frostbite2D/resource/npk_archive.h>
namespace frostbite2D {
namespace {
constexpr std::array<const char*, 9> kSliceNames = {
"topLeft", "top", "topRight", "left", "center",
"right", "bottomLeft", "bottom", "bottomRight"};
struct AxisLayout {
float start = 0.0f;
float center = 0.0f;
float end = 0.0f;
};
AxisLayout resolveAxis(float totalSize, float startSize, float endSize) {
totalSize = std::max(totalSize, 0.0f);
startSize = std::max(startSize, 0.0f);
endSize = std::max(endSize, 0.0f);
AxisLayout layout;
float fixedSize = startSize + endSize;
if (totalSize >= fixedSize) {
layout.start = startSize;
layout.center = totalSize - fixedSize;
layout.end = endSize;
return layout;
}
if (fixedSize <= 0.0f) {
layout.center = totalSize;
return layout;
}
float scale = totalSize / fixedSize;
layout.start = startSize * scale;
layout.end = endSize * scale;
return layout;
}
} // namespace
NineSliceActor::NineSliceActor() {
SetName("NineSliceActor");
}
Ptr<NineSliceActor> NineSliceActor::createFromNpk(const std::string& imgPath,
size_t startIndex) {
auto actor = MakePtr<NineSliceActor>();
if (!actor->SetSlices(imgPath, startIndex)) {
return nullptr;
}
return actor;
}
bool NineSliceActor::SetSlices(const std::string& imgPath, size_t startIndex) {
NpkArchive& npk = NpkArchive::get();
auto imgOpt = npk.getImg(imgPath);
if (!imgOpt) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"NineSliceActor: img not found: %s", imgPath.c_str());
return false;
}
std::array<Ptr<Sprite>, SliceCount> newSlices;
std::array<Vec2, SliceCount> newNaturalSizes = {};
for (size_t i = 0; i < SliceCount; ++i) {
size_t frameIndex = startIndex + i;
auto frameOpt = npk.getImageFrame(*imgOpt, frameIndex);
if (!frameOpt || frameOpt->data.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"NineSliceActor: frame %u missing for img %s",
static_cast<unsigned>(frameIndex), imgPath.c_str());
return false;
}
auto sprite = Sprite::createFromNpk(imgPath, frameIndex);
if (!sprite || !sprite->GetTexture()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"NineSliceActor: failed to create sprite for frame %u of %s",
static_cast<unsigned>(frameIndex), imgPath.c_str());
return false;
}
sprite->SetName(kSliceNames[i]);
sprite->SetOffset(0.0f, 0.0f);
sprite->SetPosition(0.0f, 0.0f);
newNaturalSizes[i] = sprite->GetSize();
newSlices[i] = sprite;
}
for (auto& sprite : sliceSprites_) {
if (sprite && sprite->GetParent() == this) {
RemoveChild(sprite);
}
}
sliceSprites_ = newSlices;
sliceNaturalSizes_ = newNaturalSizes;
imgPath_ = imgPath;
startIndex_ = startIndex;
Vec2 targetSize = GetSize();
if (targetSize.x <= 0.0f && targetSize.y <= 0.0f) {
targetSize = getNaturalSize();
} else {
targetSize.x = std::max(targetSize.x, 0.0f);
targetSize.y = std::max(targetSize.y, 0.0f);
}
for (auto& sprite : sliceSprites_) {
AddChild(sprite);
}
Actor::SetSize(targetSize);
updateLayout();
return true;
}
void NineSliceActor::SetSize(const Vec2& size) {
SetSize(size.x, size.y);
}
void NineSliceActor::SetSize(float width, float height) {
Actor::SetSize(std::max(width, 0.0f), std::max(height, 0.0f));
updateLayout();
}
Vec2 NineSliceActor::getNaturalSize() const {
return Vec2(sliceNaturalSizes_[TopLeft].x + sliceNaturalSizes_[Center].x +
sliceNaturalSizes_[TopRight].x,
sliceNaturalSizes_[TopLeft].y + sliceNaturalSizes_[Center].y +
sliceNaturalSizes_[BottomLeft].y);
}
void NineSliceActor::updateLayout() {
if (imgPath_.empty()) {
return;
}
AxisLayout horizontal = resolveAxis(GetSize().x, sliceNaturalSizes_[TopLeft].x,
sliceNaturalSizes_[TopRight].x);
AxisLayout vertical = resolveAxis(GetSize().y, sliceNaturalSizes_[TopLeft].y,
sliceNaturalSizes_[BottomLeft].y);
float leftX = 0.0f;
float centerX = horizontal.start;
float rightX = horizontal.start + horizontal.center;
float topY = 0.0f;
float middleY = vertical.start;
float bottomY = vertical.start + vertical.center;
applySliceLayout(sliceSprites_[TopLeft], Vec2(leftX, topY),
Vec2(horizontal.start, vertical.start));
applySliceLayout(sliceSprites_[Top], Vec2(centerX, topY),
Vec2(horizontal.center, vertical.start));
applySliceLayout(sliceSprites_[TopRight], Vec2(rightX, topY),
Vec2(horizontal.end, vertical.start));
applySliceLayout(sliceSprites_[Left], Vec2(leftX, middleY),
Vec2(horizontal.start, vertical.center));
applySliceLayout(sliceSprites_[Center], Vec2(centerX, middleY),
Vec2(horizontal.center, vertical.center));
applySliceLayout(sliceSprites_[Right], Vec2(rightX, middleY),
Vec2(horizontal.end, vertical.center));
applySliceLayout(sliceSprites_[BottomLeft], Vec2(leftX, bottomY),
Vec2(horizontal.start, vertical.end));
applySliceLayout(sliceSprites_[Bottom], Vec2(centerX, bottomY),
Vec2(horizontal.center, vertical.end));
applySliceLayout(sliceSprites_[BottomRight], Vec2(rightX, bottomY),
Vec2(horizontal.end, vertical.end));
}
void NineSliceActor::applySliceLayout(Ptr<Sprite> sprite, const Vec2& position,
const Vec2& size) {
if (!sprite) {
return;
}
sprite->SetPosition(position);
Vec2 clampedSize(std::max(size.x, 0.0f), std::max(size.y, 0.0f));
if (clampedSize.x <= 0.0f || clampedSize.y <= 0.0f) {
sprite->SetSize(0.0f, 0.0f);
sprite->SetVisible(false);
return;
}
sprite->SetVisible(true);
sprite->SetSize(clampedSize);
}
} // namespace frostbite2D