feat(场景管理): 添加UIScene支持并重构场景管理器
refactor(渲染器): 优化相机切换时的渲染批处理 feat(调试工具): 新增游戏调试UI场景和九宫格面板组件 fix(动画系统): 跳过缺失的动画资源加载并记录日志 perf(资源加载): 使用BinaryFileStreamReader优化NPK文件解析 feat(地图系统): 支持多边形可行走区域调试显示 style(代码格式): 清理多余空格和统一文件编码 docs(注释): 补充关键类和方法的文档说明 test(启动跟踪): 添加启动过程性能跟踪工具 chore(依赖): 添加SDL2_ttf库支持
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>";
|
||||
|
||||
@@ -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({});
|
||||
|
||||
147
Game/src/common/GameDebugActor.cpp
Normal file
147
Game/src/common/GameDebugActor.cpp
Normal 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
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
28
Game/src/scene/GameDebugUIScene.cpp
Normal file
28
Game/src/scene/GameDebugUIScene.cpp
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
203
Game/src/ui/NineSliceActor.cpp
Normal file
203
Game/src/ui/NineSliceActor.cpp
Normal 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
|
||||
Reference in New Issue
Block a user