feat: 添加游戏数学工具类并重构相关代码
refactor: 将数学工具函数移至GameMath类 feat(音频): 实现地图音频控制器 feat(调试): 添加游戏调试UI组件 feat(地图): 增加移动区域边界获取方法 fix(角色): 修复角色移动区域抑制逻辑 refactor(世界): 重构游戏世界场景初始化 docs(音频): 完善音频数据库注释
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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_; ///< 随机组映射
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
48
Game/include/audio/MapAudioController.h
Normal file
48
Game/include/audio/MapAudioController.h
Normal 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
|
||||
@@ -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));
|
||||
|
||||
19
Game/include/common/math/GameMath.h
Normal file
19
Game/include/common/math/GameMath.h
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
172
Game/src/audio/MapAudioController.cpp
Normal file
172
Game/src/audio/MapAudioController.cpp
Normal 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
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
148
Game/src/common/math/GameMath.cpp
Normal file
148
Game/src/common/math/GameMath.cpp
Normal 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
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user