feat: 添加游戏数学工具类并重构相关代码

refactor: 将数学工具函数移至GameMath类
feat(音频): 实现地图音频控制器
feat(调试): 添加游戏调试UI组件
feat(地图): 增加移动区域边界获取方法
fix(角色): 修复角色移动区域抑制逻辑
refactor(世界): 重构游戏世界场景初始化
docs(音频): 完善音频数据库注释
This commit is contained in:
2026-04-06 22:22:40 +08:00
parent f86ce35b68
commit 35c80247b3
21 changed files with 893 additions and 215 deletions

View File

@@ -19,6 +19,8 @@ public:
static Ptr<Music> loadFromFile(const std::string& path);
static Ptr<Music> loadFromMemory(const uint8* data, size_t size);
static Ptr<Music> loadFromNpk(const std::string& audioPath);
static bool isPathPlaying(const std::string& path);
static std::string getCurrentPlayingPath();
~Music();

View File

@@ -13,10 +13,11 @@ namespace frostbite2D {
* @brief 音频项类型枚举
*/
enum class AudioEntryType {
None, ///< 无效
Effect, ///< 音效
Music, ///< 背景音乐
Random ///< 随机音效组
None, ///< invalid
Effect, ///< one-shot sound effect
Ambient, ///< looping ambient track
Music, ///< background music
Random ///< random sound group
};
/**
@@ -39,6 +40,16 @@ struct AudioMusic {
int32 loopDelay = -1; ///< 循环延迟(-1 表示未设置)
};
/**
* @brief ?????????
*/
struct AudioAmbient {
std::string id; ///< ?????ID
std::string file; ///< ??????
int32 loopDelay = -1; ///< ????????1 ?????????
int32 loopDelayRange = -1;///< ???????????1 ?????????
};
/**
* @brief 随机音效项
*/
@@ -62,6 +73,7 @@ struct AudioEntry {
AudioEntryType type = AudioEntryType::None; ///< 条目类型
const AudioEffect* effect = nullptr; ///< 音效指针type == Effect 时有效)
const AudioAmbient* ambient = nullptr;///< ???????????ype == Ambient ??????
const AudioMusic* music = nullptr; ///< 音乐指针type == Music 时有效)
const AudioRandom* random = nullptr; ///< 随机组指针type == Random 时有效)
@@ -71,6 +83,9 @@ struct AudioEntry {
/// @brief 判断是否为音效
bool isEffect() const { return type == AudioEntryType::Effect; }
/// @brief ????????????
bool isAmbient() const { return type == AudioEntryType::Ambient; }
/// @brief 判断是否为音乐
bool isMusic() const { return type == AudioEntryType::Music; }
@@ -81,6 +96,8 @@ struct AudioEntry {
std::string getFilePath() const {
if (type == AudioEntryType::Effect && effect)
return effect->file;
if (type == AudioEntryType::Ambient && ambient)
return ambient->file;
if (type == AudioEntryType::Music && music)
return music->file;
return "";
@@ -203,6 +220,13 @@ public:
*/
std::optional<const AudioEffect*> getEffect(const std::string& id);
/**
* @brief ????????????
* @param id ?????ID
* @return ????????????????????? std::nullopt
*/
std::optional<const AudioAmbient*> getAmbient(const std::string& id);
/**
* @brief 获取音乐配置
* @param id 音乐 ID
@@ -226,6 +250,11 @@ public:
*/
size_t effectCount() const;
/**
* @brief ???????????
*/
size_t ambientCount() const;
/**
* @brief 获取音乐数量
*/
@@ -312,6 +341,7 @@ private:
std::string selectFromRandom(const AudioRandom& randomGroup);
std::unordered_map<std::string, AudioEffect> effectMap_; ///< 音效映射
std::unordered_map<std::string, AudioAmbient> ambientMap_; ///< ????????
std::unordered_map<std::string, AudioMusic> musicMap_; ///< 音乐映射
std::unordered_map<std::string, AudioRandom> randomMap_; ///< 随机组映射

View File

@@ -10,6 +10,9 @@ namespace frostbite2D {
namespace {
Mix_Music* gCurrentMusicHandle = nullptr;
std::string gCurrentMusicPath;
std::string normalizeAudioLogicalPath(const std::string& path) {
std::string normalized = path;
std::replace(normalized.begin(), normalized.end(), '\\', '/');
@@ -24,6 +27,30 @@ bool startsWithPrefix(const std::string& value, const char* prefix) {
value.compare(0, prefixLength, prefix) == 0;
}
std::string canonicalizeAudioPlaybackPath(const std::string& path) {
std::string normalized = normalizeAudioLogicalPath(path);
if (startsWithPrefix(normalized, "assets/music/") ||
startsWithPrefix(normalized, "assets/sounds/")) {
return normalized.substr(7);
}
return normalized;
}
bool hasActiveMusicPlayback() {
return Mix_PlayingMusic() == 1 || Mix_PausedMusic() == 1;
}
void clearCurrentMusicState() {
gCurrentMusicHandle = nullptr;
gCurrentMusicPath.clear();
}
void clearCurrentMusicStateIfStopped() {
if (!hasActiveMusicPlayback()) {
clearCurrentMusicState();
}
}
} // namespace
Ptr<Music> Music::loadFromPath(const std::string& path) {
@@ -98,18 +125,37 @@ Ptr<Music> Music::loadFromNpk(const std::string& audioPath) {
return nullptr;
}
return loadFromMemory(dataOpt->data(), dataOpt->size());
Ptr<Music> music = loadFromMemory(dataOpt->data(), dataOpt->size());
if (music) {
music->path_ = canonicalizeAudioPlaybackPath(normalizedPath);
}
return music;
}
bool Music::isPathPlaying(const std::string& path) {
clearCurrentMusicStateIfStopped();
return hasActiveMusicPlayback() &&
gCurrentMusicPath == canonicalizeAudioPlaybackPath(path);
}
std::string Music::getCurrentPlayingPath() {
clearCurrentMusicStateIfStopped();
return gCurrentMusicPath;
}
Music::Music(Mix_Music* music, const std::string& path)
: music_(music), path_(path) {
: music_(music), path_(canonicalizeAudioPlaybackPath(path)) {
}
Music::Music(Mix_Music* music, std::vector<uint8> data, const std::string& path)
: music_(music), path_(path), data_(std::move(data)) {
: music_(music), path_(canonicalizeAudioPlaybackPath(path)),
data_(std::move(data)) {
}
Music::~Music() {
if (gCurrentMusicHandle == music_) {
clearCurrentMusicState();
}
if (music_) {
Mix_FreeMusic(music_);
music_ = nullptr;
@@ -131,8 +177,11 @@ bool Music::play(int loops) {
Mix_VolumeMusic(vol);
if (Mix_PlayMusic(music_, loops) == 0) {
gCurrentMusicHandle = music_;
gCurrentMusicPath = path_;
return true;
} else {
clearCurrentMusicStateIfStopped();
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to play music: %s", Mix_GetError());
return false;
}
@@ -151,7 +200,14 @@ bool Music::fadeIn(int ms, int loops) {
int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
Mix_VolumeMusic(vol);
return Mix_FadeInMusic(music_, loops, ms) == 0;
if (Mix_FadeInMusic(music_, loops, ms) == 0) {
gCurrentMusicHandle = music_;
gCurrentMusicPath = path_;
return true;
}
clearCurrentMusicStateIfStopped();
return false;
}
void Music::pause() {
@@ -173,13 +229,20 @@ void Music::stop() {
return;
}
Mix_HaltMusic();
if (gCurrentMusicHandle == music_) {
clearCurrentMusicState();
}
}
bool Music::fadeOut(int ms) {
if (!music_) {
return false;
}
return Mix_FadeOutMusic(ms) == 0;
bool result = Mix_FadeOutMusic(ms) == 0;
if (result && gCurrentMusicHandle == music_) {
clearCurrentMusicState();
}
return result;
}
void Music::setVolume(float volume) {

View File

@@ -95,6 +95,42 @@ bool AudioDatabase::loadFromString(const std::string& xmlContent) {
}
effectMap_[effect.id] = std::move(effect);
} else if (strcmp(name, "AMBIENT") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
if (!idAttr || !fileAttr) {
continue;
}
if (strlen(idAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"AudioDatabase: skip invalid AMBIENT entry (empty ID)");
continue;
}
if (strlen(fileAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"AudioDatabase: skip AMBIENT entry with empty FILE: %s",
idAttr->value());
continue;
}
AudioAmbient ambient;
ambient.id = idAttr->value();
ambient.file = fileAttr->value();
rapidxml::xml_attribute<>* loopDelayAttr = node->first_attribute("LOOP_DELAY");
if (loopDelayAttr) {
ambient.loopDelay = std::stoi(loopDelayAttr->value());
}
rapidxml::xml_attribute<>* loopDelayRangeAttr = node->first_attribute("LOOP_DELAY_RANGE");
if (loopDelayRangeAttr) {
ambient.loopDelayRange = std::stoi(loopDelayRangeAttr->value());
}
ambientMap_[ambient.id] = std::move(ambient);
} else if (strcmp(name, "MUSIC") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
@@ -162,15 +198,17 @@ bool AudioDatabase::loadFromString(const std::string& xmlContent) {
}
loaded_ = true;
SDL_Log("AudioDatabase: 加载完成 - %zu 个音效, %zu 个音乐, %zu 个随机组",
effectMap_.size(), musicMap_.size(), randomMap_.size());
SDL_Log("AudioDatabase: loaded - %zu effects, %zu ambients, %zu musics, %zu random groups",
effectMap_.size(), ambientMap_.size(), musicMap_.size(),
randomMap_.size());
return true;
}
void AudioDatabase::clear() {
effectMap_.clear();
ambientMap_.clear();
musicMap_.clear();
randomMap_.clear();
loaded_ = false;
@@ -194,6 +232,13 @@ std::optional<AudioEntry> AudioDatabase::get(const std::string& id) {
return entry;
}
auto ambientIt = ambientMap_.find(id);
if (ambientIt != ambientMap_.end()) {
entry.type = AudioEntryType::Ambient;
entry.ambient = &ambientIt->second;
return entry;
}
auto musicIt = musicMap_.find(id);
if (musicIt != musicMap_.end()) {
entry.type = AudioEntryType::Music;
@@ -235,6 +280,10 @@ std::optional<std::string> AudioDatabase::getFilePath(const std::string& id) {
auto soundIdOpt = getSound(id);
if (!soundIdOpt) {
auto ambientIt = ambientMap_.find(id);
if (ambientIt != ambientMap_.end()) {
return ambientIt->second.file;
}
auto musicIt = musicMap_.find(id);
if (musicIt != musicMap_.end()) {
return musicIt->second.file;
@@ -263,6 +312,18 @@ std::optional<const AudioEffect*> AudioDatabase::getEffect(const std::string& id
return std::nullopt;
}
std::optional<const AudioAmbient*> AudioDatabase::getAmbient(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto it = ambientMap_.find(id);
if (it != ambientMap_.end()) {
return &it->second;
}
return std::nullopt;
}
std::optional<const AudioMusic*> AudioDatabase::getMusic(const std::string& id) {
if (!loaded_) {
return std::nullopt;
@@ -291,6 +352,10 @@ size_t AudioDatabase::effectCount() const {
return effectMap_.size();
}
size_t AudioDatabase::ambientCount() const {
return ambientMap_.size();
}
size_t AudioDatabase::musicCount() const {
return musicMap_.size();
}
@@ -364,7 +429,8 @@ bool AudioDatabase::has(const std::string& id) {
if (!loaded_) {
return false;
}
return effectMap_.count(id) > 0 || musicMap_.count(id) > 0 || randomMap_.count(id) > 0;
return effectMap_.count(id) > 0 || ambientMap_.count(id) > 0 ||
musicMap_.count(id) > 0 || randomMap_.count(id) > 0;
}
std::string AudioDatabase::filePath(const std::string& id) {
@@ -395,6 +461,9 @@ AudioEntryType AudioDatabase::typeOf(const std::string& id) {
if (effectMap_.count(id) > 0) {
return AudioEntryType::Effect;
}
if (ambientMap_.count(id) > 0) {
return AudioEntryType::Ambient;
}
if (musicMap_.count(id) > 0) {
return AudioEntryType::Music;
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include <frostbite2D/audio/music.h>
#include <frostbite2D/audio/sound.h>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace frostbite2D {
struct MapAudioTrack {
std::string id;
std::string path;
};
struct MapAudioState {
std::optional<MapAudioTrack> bgmTrack;
std::vector<MapAudioTrack> ambientLoops;
};
class MapAudioController {
public:
static MapAudioController& get();
void ApplyMapAudio(const MapAudioState& state);
void ClearMapAudio();
private:
struct ActiveAmbientLoop {
MapAudioTrack track;
RefPtr<Sound> sound;
int channel = -1;
};
MapAudioController() = default;
void ApplyBgmTrack(const std::optional<MapAudioTrack>& bgmTrack);
void ApplyAmbientLoops(const std::vector<MapAudioTrack>& ambientLoops);
void StopAmbientLoop(ActiveAmbientLoop& loop);
static std::string NormalizeTrackKey(const MapAudioTrack& track);
std::optional<MapAudioTrack> currentBgmTrack_;
RefPtr<Music> currentBgmMusic_;
std::unordered_map<std::string, ActiveAmbientLoop> activeAmbientLoops_;
};
} // namespace frostbite2D

View File

@@ -1,5 +1,6 @@
#pragma once
#include "common/math/GameMath.h"
#include <frostbite2D/types/type_math.h>
#include <string>
#include <unordered_map>
@@ -81,8 +82,8 @@ struct CharacterMotor {
float gravity = 1600.0f;
void SetGroundPosition(const Vec2& groundPosition) {
position.x = RoundWorldCoordinate(groundPosition.x);
position.y = RoundWorldCoordinate(groundPosition.y);
position.x = gameMath::RoundWorldCoordinate(groundPosition.x);
position.y = gameMath::RoundWorldCoordinate(groundPosition.y);
positionRemainder_ = Vec2::Zero();
}
@@ -167,10 +168,6 @@ struct CharacterMotor {
}
private:
static int32 RoundWorldCoordinate(float value) {
return static_cast<int32>(std::lround(value));
}
static int32 ConsumeWholeUnits(float& remainder) {
if (remainder >= 1.0f) {
int32 wholeUnits = static_cast<int32>(std::floor(remainder));

View File

@@ -0,0 +1,19 @@
#pragma once
#include <frostbite2D/types/type_math.h>
#include <vector>
namespace frostbite2D::gameMath {
int32 RoundWorldCoordinate(float value);
Vec2 MakeIntegerWorldPoint(int x, int y);
Vec3 MakeIntegerWorldPosition(int x, int y, int z);
bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end);
bool IsPointInPolygon(const std::vector<Vec2>& polygon, const Vec2& point);
bool IsPointMovable(const std::vector<Vec2>& polygon, int x, int y);
std::vector<Vec2> BuildRectPolygon(const Rect& rect);
std::vector<Rect> BuildPolygonFillRects(const std::vector<Vec2>& polygon);
} // namespace frostbite2D::gameMath

View File

@@ -49,6 +49,7 @@ public:
size_t FindMoveAreaIndex(const Vec3& curPos) const;
const std::vector<MapMoveArea>& GetMoveAreaInfo() const;
size_t GetMoveAreaCount() const { return moveArea_.size(); }
game::MoveAreaBounds GetMovablePositionBounds(size_t index) const;
Rect GetMovablePositionArea(size_t index) const;
const std::string& GetMapPath() const { return mapConfig_.mapPath; }
void SetDebugHighlightedMoveAreaIndex(size_t index);

View File

@@ -25,6 +25,7 @@ public:
int GetCurAreaIndex() const { return curMapIndex_; }
RefPtr<GameMap> GetArea(int index) const;
RefPtr<GameMap> GetCurrentArea() const;
const std::vector<MapInfo>& GetAreas() const { return mapList_; }
void Update(float deltaTime) override;

View File

@@ -7,6 +7,11 @@
namespace frostbite2D {
class Actor;
class CharacterObject;
class GameDebugUIScene;
class GameMap;
class GameWorld : public Scene {
public:
GameWorld();
@@ -19,6 +24,13 @@ public:
void AddCharacter(RefPtr<Actor> actor, int townId);
void MoveCharacter(RefPtr<Actor> actor, int townId, int area);
void RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area);
void UpdateMoveAreaSuppression(GameMap* map, size_t moveAreaIndex);
bool ShouldSuppressMoveArea(GameMap* map, size_t moveAreaIndex) const;
Actor* GetMainActor() const;
CharacterObject* GetMainCharacter() const;
GameTown* GetCurrentTown() const;
GameMap* GetCurrentMap() const;
static GameWorld* GetWorld();
@@ -29,13 +41,25 @@ private:
int area = -1;
};
struct SuppressedMoveArea {
GameMap* map = nullptr;
size_t moveAreaIndex = GameMap::kInvalidMoveAreaIndex;
};
bool InitWorld();
bool InitMainCharacter(int townId);
void ProcessPendingCharacterMove();
void EnsureDebugScene();
void RefreshDebugContext();
void SetSuppressedMoveArea(GameMap* map, size_t moveAreaIndex);
void ClearSuppressedMoveArea();
std::map<int, std::string> townPathMap_;
std::map<int, RefPtr<GameTown>> townMap_;
RefPtr<Actor> mainActor_;
RefPtr<GameDebugUIScene> debugScene_;
std::optional<PendingCharacterMove> pendingCharacterMove_;
std::optional<SuppressedMoveArea> suppressedMoveArea_;
int curTown_ = -1;
bool initialized_ = false;
};

View File

@@ -0,0 +1,172 @@
#include "audio/MapAudioController.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <unordered_map>
namespace frostbite2D {
namespace {
std::string NormalizeAudioPath(const std::string& path) {
std::string normalized = path;
std::replace(normalized.begin(), normalized.end(), '\\', '/');
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return normalized;
}
} // namespace
MapAudioController& MapAudioController::get() {
static MapAudioController instance;
return instance;
}
void MapAudioController::ApplyMapAudio(const MapAudioState& state) {
ApplyBgmTrack(state.bgmTrack);
ApplyAmbientLoops(state.ambientLoops);
}
void MapAudioController::ClearMapAudio() {
if (currentBgmMusic_) {
currentBgmMusic_->stop();
currentBgmMusic_.Reset();
}
currentBgmTrack_.reset();
for (auto& [_, loop] : activeAmbientLoops_) {
StopAmbientLoop(loop);
}
activeAmbientLoops_.clear();
}
void MapAudioController::ApplyBgmTrack(
const std::optional<MapAudioTrack>& bgmTrack) {
if (!bgmTrack || bgmTrack->path.empty()) {
if (currentBgmMusic_) {
currentBgmMusic_->stop();
currentBgmMusic_.Reset();
}
currentBgmTrack_.reset();
return;
}
const std::string desiredKey = NormalizeTrackKey(*bgmTrack);
const bool sameTrack =
currentBgmTrack_ && NormalizeTrackKey(*currentBgmTrack_) == desiredKey;
if (sameTrack && Music::isPathPlaying(bgmTrack->path)) {
return;
}
RefPtr<Music> music = Music::loadFromPath(bgmTrack->path);
if (!music) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to load bgm %s",
bgmTrack->path.c_str());
return;
}
if (!music->play(-1)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to play bgm %s",
bgmTrack->path.c_str());
return;
}
currentBgmMusic_ = music;
currentBgmTrack_ = *bgmTrack;
}
void MapAudioController::ApplyAmbientLoops(
const std::vector<MapAudioTrack>& ambientLoops) {
std::unordered_map<std::string, MapAudioTrack> desiredLoops;
desiredLoops.reserve(ambientLoops.size());
for (const auto& loop : ambientLoops) {
if (loop.path.empty()) {
continue;
}
desiredLoops.emplace(NormalizeTrackKey(loop), loop);
}
for (auto it = activeAmbientLoops_.begin(); it != activeAmbientLoops_.end();) {
if (desiredLoops.find(it->first) != desiredLoops.end()) {
++it;
continue;
}
StopAmbientLoop(it->second);
it = activeAmbientLoops_.erase(it);
}
for (const auto& [key, track] : desiredLoops) {
auto activeIt = activeAmbientLoops_.find(key);
if (activeIt != activeAmbientLoops_.end()) {
ActiveAmbientLoop& loop = activeIt->second;
if (NormalizeAudioPath(loop.track.path) == NormalizeAudioPath(track.path) &&
Sound::isPlaying(loop.channel)) {
continue;
}
StopAmbientLoop(loop);
loop.track = track;
if (!loop.sound ||
NormalizeAudioPath(loop.sound->getPath()) !=
NormalizeAudioPath(track.path)) {
loop.sound = Sound::loadFromPath(track.path);
}
if (!loop.sound) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to load ambient %s",
track.path.c_str());
continue;
}
int channel = loop.sound->playLoop();
if (channel == -1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to play ambient %s",
track.path.c_str());
continue;
}
loop.channel = channel;
continue;
}
ActiveAmbientLoop loop;
loop.track = track;
loop.sound = Sound::loadFromPath(track.path);
if (!loop.sound) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to load ambient %s",
track.path.c_str());
continue;
}
loop.channel = loop.sound->playLoop();
if (loop.channel == -1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to play ambient %s",
track.path.c_str());
continue;
}
activeAmbientLoops_.emplace(key, std::move(loop));
}
}
void MapAudioController::StopAmbientLoop(ActiveAmbientLoop& loop) {
if (loop.channel >= 0) {
Sound::stop(loop.channel);
loop.channel = -1;
}
}
std::string MapAudioController::NormalizeTrackKey(const MapAudioTrack& track) {
if (!track.id.empty()) {
return NormalizeAudioPath(track.id);
}
return NormalizeAudioPath(track.path);
}
} // namespace frostbite2D

View File

@@ -20,7 +20,6 @@
#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"
using namespace frostbite2D;
@@ -144,10 +143,9 @@ int main(int argc, char **argv) {
StartupTrace::mark("before SceneManager::ReplaceScene");
{
ScopedStartupTrace stageTrace(
"SceneManager::ReplaceScene(GameMapTestScene)");
auto testMapScene = MakePtr<GameMapTestScene>();
SceneManager::get().ReplaceScene(testMapScene);
ScopedStartupTrace stageTrace("SceneManager::ReplaceScene(GameWorld)");
auto gameWorld = MakePtr<GameWorld>();
SceneManager::get().ReplaceScene(gameWorld);
}
StartupTrace::mark("after SceneManager::ReplaceScene");

View File

@@ -1,8 +1,8 @@
#include "character/CharacterObject.h"
#include "common/math/GameMath.h"
#include "map/GameMap.h"
#include "world/GameWorld.h"
#include <SDL2/SDL.h>
#include <cmath>
#include <frostbite2D/core/application.h>
#include <frostbite2D/utils/startup_trace.h>
#include <sstream>
@@ -43,10 +43,6 @@ Vec3 ToWorldVector(const CharacterWorldPosition& position) {
static_cast<float>(position.z));
}
int32 RoundWorldCoordinate(float value) {
return static_cast<int32>(std::lround(value));
}
} // namespace
bool CharacterObject::Construction(int jobId) {
@@ -199,9 +195,9 @@ void CharacterObject::ApplyMapMovementConstraints(
static_cast<float>(motor_.position.y - previousPosition.y),
static_cast<float>(motor_.position.z - previousPosition.z));
Vec3 resolvedWorldPos = map->CheckIsItMovable(previousWorldPos, worldOffset);
motor_.position.x = RoundWorldCoordinate(resolvedWorldPos.x);
motor_.position.y = RoundWorldCoordinate(resolvedWorldPos.y);
motor_.position.z = RoundWorldCoordinate(resolvedWorldPos.z);
motor_.position.x = gameMath::RoundWorldCoordinate(resolvedWorldPos.x);
motor_.position.y = gameMath::RoundWorldCoordinate(resolvedWorldPos.y);
motor_.position.z = gameMath::RoundWorldCoordinate(resolvedWorldPos.z);
}
void CharacterObject::QueueMapTransitionIfNeeded() {
@@ -210,16 +206,24 @@ void CharacterObject::QueueMapTransitionIfNeeded() {
return;
}
GameMap::MapMoveArea target;
if (!map->TryGetMoveAreaTarget(ToWorldVector(motor_.position), target)) {
return;
}
GameWorld* world = GameWorld::GetWorld();
if (!world) {
return;
}
size_t moveAreaIndex = map->FindMoveAreaIndex(ToWorldVector(motor_.position));
world->UpdateMoveAreaSuppression(map, moveAreaIndex);
if (moveAreaIndex == GameMap::kInvalidMoveAreaIndex ||
world->ShouldSuppressMoveArea(map, moveAreaIndex)) {
return;
}
const auto& moveAreaInfo = map->GetMoveAreaInfo();
if (moveAreaIndex >= moveAreaInfo.size()) {
return;
}
const GameMap::MapMoveArea& target = moveAreaInfo[moveAreaIndex];
world->RequestMoveCharacter(RefPtr<Actor>(this), target.town, target.area);
}

View File

@@ -1,4 +1,4 @@
#include "common/GameDebugActor.h"
#include "common/debug/GameDebugActor.h"
#include "character/CharacterObject.h"
#include "map/GameMap.h"
#include "ui/NineSliceActor.h"

View File

@@ -0,0 +1,148 @@
#include "common/math/GameMath.h"
#include <algorithm>
#include <cmath>
namespace frostbite2D::gameMath {
namespace {
constexpr double kPolygonEpsilon = 0.001;
} // namespace
int32 RoundWorldCoordinate(float value) {
return static_cast<int32>(std::lround(value));
}
Vec2 MakeIntegerWorldPoint(int x, int y) {
return Vec2(static_cast<float>(x), static_cast<float>(y));
}
Vec3 MakeIntegerWorldPosition(int x, int y, int z) {
return Vec3(static_cast<float>(x), static_cast<float>(y),
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));
}
std::vector<Vec2> BuildRectPolygon(const Rect& rect) {
if (rect.empty()) {
return {};
}
return {
Vec2(rect.left(), rect.top()),
Vec2(rect.right(), rect.top()),
Vec2(rect.right(), rect.bottom()),
Vec2(rect.left(), rect.bottom()),
};
}
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;
}
} // namespace frostbite2D::gameMath

View File

@@ -1,3 +1,5 @@
#include "audio/MapAudioController.h"
#include "common/math/GameMath.h"
#include "map/GameMap.h"
#include <SDL2/SDL.h>
#include <frostbite2D/2d/sprite.h>
@@ -24,71 +26,6 @@ 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));
}
Vec2 MakeIntegerWorldPoint(int x, int y) {
return Vec2(static_cast<float>(x), static_cast<float>(y));
}
Vec3 MakeIntegerWorldPosition(int x, int y, int z) {
return Vec3(static_cast<float>(x), static_cast<float>(y),
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
// 占位,避免整张地图中断。
@@ -102,6 +39,49 @@ Ptr<Sprite> createMapSprite(const std::string& path, int index) {
return sprite;
}
MapAudioState BuildMapAudioState(const game::MapConfig& mapConfig) {
MapAudioState state;
if (mapConfig.soundIds.empty()) {
return state;
}
auto& audioDatabase = AudioDatabase::get();
for (const auto& soundId : mapConfig.soundIds) {
AudioEntryType type = audioDatabase.typeOf(soundId);
std::string filePath = audioDatabase.filePath(soundId);
if (filePath.empty()) {
continue;
}
if (type == AudioEntryType::Music) {
if (!state.bgmTrack) {
state.bgmTrack = MapAudioTrack{soundId, filePath};
}
continue;
}
if (type == AudioEntryType::Ambient) {
state.ambientLoops.push_back(MapAudioTrack{soundId, filePath});
continue;
}
if (type == AudioEntryType::Effect) {
SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION,
"GameMap: ignore one-shot map sound %s in [%s]",
soundId.c_str(), mapConfig.mapPath.c_str());
continue;
}
if (type == AudioEntryType::Random) {
SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION,
"GameMap: ignore random map audio %s in [%s]",
soundId.c_str(), mapConfig.mapPath.c_str());
}
}
return state;
}
} // namespace
GameMap::GameMap() {
@@ -172,23 +152,7 @@ bool GameMap::LoadMap(const std::string &mapName) {
}
void GameMap::Enter() {
// 地图进入时尝试找到第一个可播放的 BGM 并循环播放。
if (!mapConfig_.soundIds.empty()) {
auto& audioDatabase = AudioDatabase::get();
for (const auto& soundId : mapConfig_.soundIds) {
if (audioDatabase.typeOf(soundId) != AudioEntryType::Music) {
continue;
}
std::string filePath = audioDatabase.filePath(soundId);
if (filePath.empty()) {
continue;
}
currentMusic_ = Music::loadFromPath(filePath);
if (currentMusic_ && currentMusic_->play(-1)) {
break;
}
}
}
MapAudioController::get().ApplyMapAudio(BuildMapAudioState(mapConfig_));
}
void GameMap::Update(float deltaTime) { Actor::Update(deltaTime); }
@@ -448,28 +412,28 @@ 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 targetX = RoundWorldCoordinate(curPos.x + posOffset.x);
int targetY = RoundWorldCoordinate(curPos.y + posOffset.y);
int targetZ = RoundWorldCoordinate(curPos.z + posOffset.z);
Vec3 result = MakeIntegerWorldPosition(currentX, currentY, targetZ);
int currentX = gameMath::RoundWorldCoordinate(curPos.x);
int currentY = gameMath::RoundWorldCoordinate(curPos.y);
int targetX = gameMath::RoundWorldCoordinate(curPos.x + posOffset.x);
int targetY = gameMath::RoundWorldCoordinate(curPos.y + posOffset.y);
int targetZ = gameMath::RoundWorldCoordinate(curPos.z + posOffset.z);
Vec3 result = gameMath::MakeIntegerWorldPosition(currentX, currentY, targetZ);
if (!movableAreaCheckEnabled_) {
return MakeIntegerWorldPosition(targetX, targetY, targetZ);
return gameMath::MakeIntegerWorldPosition(targetX, targetY, targetZ);
}
if (movablePolygon_.size() < 3) {
return MakeIntegerWorldPosition(targetX, targetY, targetZ);
return gameMath::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)) {
if (gameMath::IsPointMovable(movablePolygon_, targetX, targetY)) {
result.x = static_cast<float>(targetX);
result.y = static_cast<float>(targetY);
return result;
}
bool isXValid = IsPointMovable(movablePolygon_, targetX, currentY);
bool isYValid = IsPointMovable(movablePolygon_, currentX, targetY);
bool isXValid = gameMath::IsPointMovable(movablePolygon_, targetX, currentY);
bool isYValid = gameMath::IsPointMovable(movablePolygon_, currentX, targetY);
if (isXValid && isYValid) {
int moveX = std::abs(targetX - currentX);
@@ -500,12 +464,12 @@ GameMap::MapMoveArea GameMap::CheckIsItMoveArea(const Vec3& curPos) const {
}
size_t GameMap::FindMoveAreaIndex(const Vec3& curPos) const {
int currentX = RoundWorldCoordinate(curPos.x);
int currentY = RoundWorldCoordinate(curPos.y);
int currentX = gameMath::RoundWorldCoordinate(curPos.x);
int currentY = gameMath::RoundWorldCoordinate(curPos.y);
for (size_t i = 0;
i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size();
++i) {
if (moveArea_[i].containsPoint(MakeIntegerWorldPoint(currentX, currentY))) {
if (moveArea_[i].containsPoint(gameMath::MakeIntegerWorldPoint(currentX, currentY))) {
return i;
}
}
@@ -534,6 +498,13 @@ const std::vector<GameMap::MapMoveArea>& GameMap::GetMoveAreaInfo() const {
return mapConfig_.townMovableAreaTargets;
}
game::MoveAreaBounds GameMap::GetMovablePositionBounds(size_t index) const {
if (index >= mapConfig_.townMovableAreaBounds.size()) {
return game::MoveAreaBounds();
}
return mapConfig_.townMovableAreaBounds[index];
}
Rect GameMap::GetMovablePositionArea(size_t index) const {
if (index >= mapConfig_.townMovableAreas.size()) {
return Rect::Zero();

View File

@@ -1,4 +1,5 @@
#include "map/GameMapLayer.h"
#include "common/math/GameMath.h"
#include <algorithm>
#include <cmath>
#include <frostbite2D/graphics/renderer.h>
@@ -13,78 +14,6 @@ constexpr float kDebugEdgePointSize = 5.0f;
constexpr float kDebugVertexSize = 9.0f;
constexpr float kDebugEdgeStep = 4.0f;
std::vector<Vec2> BuildRectPolygon(const Rect& rect) {
if (rect.empty()) {
return {};
}
return {
Vec2(rect.left(), rect.top()),
Vec2(rect.right(), rect.top()),
Vec2(rect.right(), rect.bottom()),
Vec2(rect.left(), rect.bottom()),
};
}
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) {
@@ -155,7 +84,7 @@ void GameMapLayer::Render() {
}
for (auto& moveArea : moveAreaInfoList_) {
std::vector<Vec2> polygon = BuildRectPolygon(moveArea.rect);
std::vector<Vec2> polygon = gameMath::BuildRectPolygon(moveArea.rect);
if (polygon.empty()) {
continue;
}
@@ -170,7 +99,7 @@ void GameMapLayer::Render() {
void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon) {
feasibleAreaPolygon_ = polygon;
feasibleAreaFillRects_ = BuildPolygonFillRects(feasibleAreaPolygon_);
feasibleAreaFillRects_ = gameMath::BuildPolygonFillRects(feasibleAreaPolygon_);
}
void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect, size_t index) {

View File

@@ -1,6 +1,6 @@
#include "scene/GameDebugUIScene.h"
#include "character/CharacterObject.h"
#include "common/GameDebugActor.h"
#include "common/debug/GameDebugActor.h"
#include "map/GameMap.h"
namespace frostbite2D {

View File

@@ -15,7 +15,9 @@ Vec2 RoundWorldPoint(const Vec2& pos) {
} // namespace
GameTown::GameTown() = default;
GameTown::GameTown() {
EnableEventReceive();
}
bool GameTown::Init(int index, const std::string& townPath) {
game::TownConfig config;
@@ -68,10 +70,25 @@ RefPtr<GameMap> GameTown::GetArea(int index) const {
return it->map;
}
RefPtr<GameMap> GameTown::GetCurrentArea() const {
if (curMapIndex_ == -1) {
return nullptr;
}
return GetArea(curMapIndex_);
}
void GameTown::AddCharacter(RefPtr<Actor> actor, int areaIndex) {
int targetMapIndex = areaIndex;
if (areaIndex == -2) {
targetMapIndex = sariaRoomId_ != -1 ? sariaRoomId_ : (mapList_.empty() ? -1 : mapList_.front().areaId);
if (sariaRoomId_ != -1) {
targetMapIndex = sariaRoomId_;
} else {
targetMapIndex = mapList_.empty() ? -1 : mapList_.front().areaId;
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameTown: no gate area configured, fallback to first area %d",
targetMapIndex);
}
}
auto mapIt = std::find_if(mapList_.begin(), mapList_.end(),

View File

@@ -1,22 +1,57 @@
#include "world/GameWorld.h"
#include "character/CharacterObject.h"
#include "map/GameDataLoader.h"
#include "scene/GameDebugUIScene.h"
#include <SDL2/SDL.h>
#include <frostbite2D/scene/scene_manager.h>
namespace frostbite2D {
namespace {
constexpr int kDefaultTownId = 1;
Vec2 ComputeMoveAreaCenter(const game::MoveAreaBounds& bounds) {
return Vec2((bounds.leftTop.x + bounds.rightBottom.x) * 0.5f,
(bounds.leftTop.y + bounds.rightBottom.y) * 0.5f);
}
void SetActorGroundPosition(RefPtr<Actor> actor, const Vec2& position) {
if (!actor) {
return;
}
if (auto* character = dynamic_cast<CharacterObject*>(actor.Get())) {
character->SetCharacterPosition(position);
return;
}
actor->SetPosition(position);
}
} // namespace
GameWorld::GameWorld() = default;
void GameWorld::onEnter() {
Scene::onEnter();
if (initialized_) {
return;
EnsureDebugScene();
if (!initialized_) {
initialized_ = InitWorld();
}
initialized_ = InitWorld();
RefreshDebugContext();
}
void GameWorld::onExit() {
pendingCharacterMove_.reset();
ClearSuppressedMoveArea();
if (debugScene_) {
debugScene_->ClearDebugContext();
SceneManager::get().RemoveUIScene(debugScene_.Get());
}
Scene::onExit();
}
@@ -26,6 +61,14 @@ void GameWorld::Update(float deltaTime) {
}
bool GameWorld::InitWorld() {
RemoveAllChildren();
townPathMap_.clear();
townMap_.clear();
mainActor_.Reset();
pendingCharacterMove_.reset();
ClearSuppressedMoveArea();
curTown_ = -1;
townPathMap_ = game::loadTownList();
if (townPathMap_.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: no town entries found");
@@ -45,7 +88,36 @@ bool GameWorld::InitWorld() {
return false;
}
AddCharacter(nullptr, townMap_.begin()->first);
int defaultTownId = kDefaultTownId;
if (townMap_.find(defaultTownId) == townMap_.end()) {
defaultTownId = townMap_.begin()->first;
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: default town %d missing, fallback to town %d",
kDefaultTownId, defaultTownId);
}
if (!InitMainCharacter(defaultTownId)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: falling back to empty town %d without main character",
defaultTownId);
AddCharacter(nullptr, defaultTownId);
}
return true;
}
bool GameWorld::InitMainCharacter(int townId) {
auto mainCharacter = MakePtr<CharacterObject>();
if (!mainCharacter->Construction(0)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: failed to construct default main character");
return false;
}
mainCharacter->EnableEventReceive();
mainCharacter->SetEventPriority(-100);
mainCharacter->SetInputEnabled(true);
AddCharacter(mainCharacter, townId);
return true;
}
@@ -60,6 +132,7 @@ void GameWorld::AddCharacter(RefPtr<Actor> actor, int townId) {
curTown_ = townId;
it->second->AddCharacter(actor);
AddChild(it->second);
RefreshDebugContext();
}
void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
@@ -69,6 +142,44 @@ void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
return;
}
ClearSuppressedMoveArea();
int sourceTownId = curTown_;
int sourceAreaId = -1;
if (curTown_ != -1) {
auto currentTown = townMap_.find(curTown_);
if (currentTown != townMap_.end()) {
sourceAreaId = currentTown->second->GetCurAreaIndex();
}
}
RefPtr<GameMap> targetMap = townIt->second->GetArea(area);
if (actor && targetMap && sourceTownId != -1 && sourceAreaId != -1) {
bool foundTransitionEntry = false;
const auto& moveAreaInfo = targetMap->GetMoveAreaInfo();
for (size_t i = 0; i < moveAreaInfo.size(); ++i) {
if (moveAreaInfo[i].town != sourceTownId || moveAreaInfo[i].area != sourceAreaId) {
continue;
}
game::MoveAreaBounds bounds = targetMap->GetMovablePositionBounds(i);
SetActorGroundPosition(actor, ComputeMoveAreaCenter(bounds));
SetSuppressedMoveArea(targetMap.Get(), i);
foundTransitionEntry = true;
break;
}
if (!foundTransitionEntry) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: no matching move area in town %d area %d for source town %d area %d",
townId, area, sourceTownId, sourceAreaId);
}
} else if (actor && !targetMap) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: target area %d missing in town %d, use town fallback spawn",
area, townId);
}
if (curTown_ != -1) {
auto currentTown = townMap_.find(curTown_);
if (currentTown != townMap_.end()) {
@@ -80,6 +191,7 @@ void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
mainActor_ = actor;
townIt->second->AddCharacter(actor, area);
AddChild(townIt->second);
RefreshDebugContext();
}
void GameWorld::RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area) {
@@ -92,6 +204,22 @@ void GameWorld::RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area)
pendingCharacterMove_ = PendingCharacterMove{actor, townId, area};
}
void GameWorld::UpdateMoveAreaSuppression(GameMap* map, size_t moveAreaIndex) {
if (!suppressedMoveArea_) {
return;
}
if (suppressedMoveArea_->map != map ||
suppressedMoveArea_->moveAreaIndex != moveAreaIndex) {
ClearSuppressedMoveArea();
}
}
bool GameWorld::ShouldSuppressMoveArea(GameMap* map, size_t moveAreaIndex) const {
return suppressedMoveArea_ && suppressedMoveArea_->map == map &&
suppressedMoveArea_->moveAreaIndex == moveAreaIndex;
}
void GameWorld::ProcessPendingCharacterMove() {
if (!pendingCharacterMove_) {
return;
@@ -102,6 +230,63 @@ void GameWorld::ProcessPendingCharacterMove() {
MoveCharacter(move.actor, move.townId, move.area);
}
void GameWorld::EnsureDebugScene() {
if (!debugScene_) {
debugScene_ = MakePtr<GameDebugUIScene>();
}
SceneManager::get().RemoveUIScene(debugScene_.Get());
SceneManager::get().PushUIScene(debugScene_);
}
void GameWorld::RefreshDebugContext() {
if (!debugScene_) {
return;
}
debugScene_->SetDebugContext(GetCurrentMap(), GetMainCharacter());
}
void GameWorld::SetSuppressedMoveArea(GameMap* map, size_t moveAreaIndex) {
if (!map || moveAreaIndex == GameMap::kInvalidMoveAreaIndex) {
ClearSuppressedMoveArea();
return;
}
suppressedMoveArea_ = SuppressedMoveArea{map, moveAreaIndex};
}
void GameWorld::ClearSuppressedMoveArea() {
suppressedMoveArea_.reset();
}
Actor* GameWorld::GetMainActor() const {
return mainActor_.Get();
}
CharacterObject* GameWorld::GetMainCharacter() const {
return dynamic_cast<CharacterObject*>(mainActor_.Get());
}
GameTown* GameWorld::GetCurrentTown() const {
auto it = townMap_.find(curTown_);
if (it == townMap_.end()) {
return nullptr;
}
return it->second.Get();
}
GameMap* GameWorld::GetCurrentMap() const {
GameTown* town = GetCurrentTown();
if (!town) {
return nullptr;
}
RefPtr<GameMap> area = town->GetCurrentArea();
return area.Get();
}
GameWorld* GameWorld::GetWorld() {
return dynamic_cast<GameWorld*>(SceneManager::get().GetCurrentScene());
}