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> loadFromFile(const std::string& path);
|
||||||
static Ptr<Music> loadFromMemory(const uint8* data, size_t size);
|
static Ptr<Music> loadFromMemory(const uint8* data, size_t size);
|
||||||
static Ptr<Music> loadFromNpk(const std::string& audioPath);
|
static Ptr<Music> loadFromNpk(const std::string& audioPath);
|
||||||
|
static bool isPathPlaying(const std::string& path);
|
||||||
|
static std::string getCurrentPlayingPath();
|
||||||
|
|
||||||
~Music();
|
~Music();
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ namespace frostbite2D {
|
|||||||
* @brief 音频项类型枚举
|
* @brief 音频项类型枚举
|
||||||
*/
|
*/
|
||||||
enum class AudioEntryType {
|
enum class AudioEntryType {
|
||||||
None, ///< 无效
|
None, ///< invalid
|
||||||
Effect, ///< 音效
|
Effect, ///< one-shot sound effect
|
||||||
Music, ///< 背景音乐
|
Ambient, ///< looping ambient track
|
||||||
Random ///< 随机音效组
|
Music, ///< background music
|
||||||
|
Random ///< random sound group
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +40,16 @@ struct AudioMusic {
|
|||||||
int32 loopDelay = -1; ///< 循环延迟(-1 表示未设置)
|
int32 loopDelay = -1; ///< 循环延迟(-1 表示未设置)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief ?????????
|
||||||
|
*/
|
||||||
|
struct AudioAmbient {
|
||||||
|
std::string id; ///< ?????ID
|
||||||
|
std::string file; ///< ??????
|
||||||
|
int32 loopDelay = -1; ///< ????????1 ?????????
|
||||||
|
int32 loopDelayRange = -1;///< ???????????1 ?????????
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 随机音效项
|
* @brief 随机音效项
|
||||||
*/
|
*/
|
||||||
@@ -62,6 +73,7 @@ struct AudioEntry {
|
|||||||
AudioEntryType type = AudioEntryType::None; ///< 条目类型
|
AudioEntryType type = AudioEntryType::None; ///< 条目类型
|
||||||
|
|
||||||
const AudioEffect* effect = nullptr; ///< 音效指针(type == Effect 时有效)
|
const AudioEffect* effect = nullptr; ///< 音效指针(type == Effect 时有效)
|
||||||
|
const AudioAmbient* ambient = nullptr;///< ???????????ype == Ambient ??????
|
||||||
const AudioMusic* music = nullptr; ///< 音乐指针(type == Music 时有效)
|
const AudioMusic* music = nullptr; ///< 音乐指针(type == Music 时有效)
|
||||||
const AudioRandom* random = nullptr; ///< 随机组指针(type == Random 时有效)
|
const AudioRandom* random = nullptr; ///< 随机组指针(type == Random 时有效)
|
||||||
|
|
||||||
@@ -71,6 +83,9 @@ struct AudioEntry {
|
|||||||
/// @brief 判断是否为音效
|
/// @brief 判断是否为音效
|
||||||
bool isEffect() const { return type == AudioEntryType::Effect; }
|
bool isEffect() const { return type == AudioEntryType::Effect; }
|
||||||
|
|
||||||
|
/// @brief ????????????
|
||||||
|
bool isAmbient() const { return type == AudioEntryType::Ambient; }
|
||||||
|
|
||||||
/// @brief 判断是否为音乐
|
/// @brief 判断是否为音乐
|
||||||
bool isMusic() const { return type == AudioEntryType::Music; }
|
bool isMusic() const { return type == AudioEntryType::Music; }
|
||||||
|
|
||||||
@@ -81,6 +96,8 @@ struct AudioEntry {
|
|||||||
std::string getFilePath() const {
|
std::string getFilePath() const {
|
||||||
if (type == AudioEntryType::Effect && effect)
|
if (type == AudioEntryType::Effect && effect)
|
||||||
return effect->file;
|
return effect->file;
|
||||||
|
if (type == AudioEntryType::Ambient && ambient)
|
||||||
|
return ambient->file;
|
||||||
if (type == AudioEntryType::Music && music)
|
if (type == AudioEntryType::Music && music)
|
||||||
return music->file;
|
return music->file;
|
||||||
return "";
|
return "";
|
||||||
@@ -203,6 +220,13 @@ public:
|
|||||||
*/
|
*/
|
||||||
std::optional<const AudioEffect*> getEffect(const std::string& id);
|
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 获取音乐配置
|
* @brief 获取音乐配置
|
||||||
* @param id 音乐 ID
|
* @param id 音乐 ID
|
||||||
@@ -226,6 +250,11 @@ public:
|
|||||||
*/
|
*/
|
||||||
size_t effectCount() const;
|
size_t effectCount() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief ???????????
|
||||||
|
*/
|
||||||
|
size_t ambientCount() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获取音乐数量
|
* @brief 获取音乐数量
|
||||||
*/
|
*/
|
||||||
@@ -312,6 +341,7 @@ private:
|
|||||||
std::string selectFromRandom(const AudioRandom& randomGroup);
|
std::string selectFromRandom(const AudioRandom& randomGroup);
|
||||||
|
|
||||||
std::unordered_map<std::string, AudioEffect> effectMap_; ///< 音效映射
|
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, AudioMusic> musicMap_; ///< 音乐映射
|
||||||
std::unordered_map<std::string, AudioRandom> randomMap_; ///< 随机组映射
|
std::unordered_map<std::string, AudioRandom> randomMap_; ///< 随机组映射
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ namespace frostbite2D {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
Mix_Music* gCurrentMusicHandle = nullptr;
|
||||||
|
std::string gCurrentMusicPath;
|
||||||
|
|
||||||
std::string normalizeAudioLogicalPath(const std::string& path) {
|
std::string normalizeAudioLogicalPath(const std::string& path) {
|
||||||
std::string normalized = path;
|
std::string normalized = path;
|
||||||
std::replace(normalized.begin(), normalized.end(), '\\', '/');
|
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;
|
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
|
} // namespace
|
||||||
|
|
||||||
Ptr<Music> Music::loadFromPath(const std::string& path) {
|
Ptr<Music> Music::loadFromPath(const std::string& path) {
|
||||||
@@ -98,18 +125,37 @@ Ptr<Music> Music::loadFromNpk(const std::string& audioPath) {
|
|||||||
return nullptr;
|
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(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(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() {
|
Music::~Music() {
|
||||||
|
if (gCurrentMusicHandle == music_) {
|
||||||
|
clearCurrentMusicState();
|
||||||
|
}
|
||||||
if (music_) {
|
if (music_) {
|
||||||
Mix_FreeMusic(music_);
|
Mix_FreeMusic(music_);
|
||||||
music_ = nullptr;
|
music_ = nullptr;
|
||||||
@@ -131,8 +177,11 @@ bool Music::play(int loops) {
|
|||||||
Mix_VolumeMusic(vol);
|
Mix_VolumeMusic(vol);
|
||||||
|
|
||||||
if (Mix_PlayMusic(music_, loops) == 0) {
|
if (Mix_PlayMusic(music_, loops) == 0) {
|
||||||
|
gCurrentMusicHandle = music_;
|
||||||
|
gCurrentMusicPath = path_;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
clearCurrentMusicStateIfStopped();
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to play music: %s", Mix_GetError());
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to play music: %s", Mix_GetError());
|
||||||
return false;
|
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);
|
int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
|
||||||
Mix_VolumeMusic(vol);
|
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() {
|
void Music::pause() {
|
||||||
@@ -173,13 +229,20 @@ void Music::stop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Mix_HaltMusic();
|
Mix_HaltMusic();
|
||||||
|
if (gCurrentMusicHandle == music_) {
|
||||||
|
clearCurrentMusicState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Music::fadeOut(int ms) {
|
bool Music::fadeOut(int ms) {
|
||||||
if (!music_) {
|
if (!music_) {
|
||||||
return false;
|
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) {
|
void Music::setVolume(float volume) {
|
||||||
|
|||||||
@@ -95,6 +95,42 @@ bool AudioDatabase::loadFromString(const std::string& xmlContent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
effectMap_[effect.id] = std::move(effect);
|
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) {
|
} else if (strcmp(name, "MUSIC") == 0) {
|
||||||
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
|
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
|
||||||
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
|
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
|
||||||
@@ -163,14 +199,16 @@ bool AudioDatabase::loadFromString(const std::string& xmlContent) {
|
|||||||
|
|
||||||
loaded_ = true;
|
loaded_ = true;
|
||||||
|
|
||||||
SDL_Log("AudioDatabase: 加载完成 - %zu 个音效, %zu 个音乐, %zu 个随机组",
|
SDL_Log("AudioDatabase: loaded - %zu effects, %zu ambients, %zu musics, %zu random groups",
|
||||||
effectMap_.size(), musicMap_.size(), randomMap_.size());
|
effectMap_.size(), ambientMap_.size(), musicMap_.size(),
|
||||||
|
randomMap_.size());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDatabase::clear() {
|
void AudioDatabase::clear() {
|
||||||
effectMap_.clear();
|
effectMap_.clear();
|
||||||
|
ambientMap_.clear();
|
||||||
musicMap_.clear();
|
musicMap_.clear();
|
||||||
randomMap_.clear();
|
randomMap_.clear();
|
||||||
loaded_ = false;
|
loaded_ = false;
|
||||||
@@ -194,6 +232,13 @@ std::optional<AudioEntry> AudioDatabase::get(const std::string& id) {
|
|||||||
return entry;
|
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);
|
auto musicIt = musicMap_.find(id);
|
||||||
if (musicIt != musicMap_.end()) {
|
if (musicIt != musicMap_.end()) {
|
||||||
entry.type = AudioEntryType::Music;
|
entry.type = AudioEntryType::Music;
|
||||||
@@ -235,6 +280,10 @@ std::optional<std::string> AudioDatabase::getFilePath(const std::string& id) {
|
|||||||
|
|
||||||
auto soundIdOpt = getSound(id);
|
auto soundIdOpt = getSound(id);
|
||||||
if (!soundIdOpt) {
|
if (!soundIdOpt) {
|
||||||
|
auto ambientIt = ambientMap_.find(id);
|
||||||
|
if (ambientIt != ambientMap_.end()) {
|
||||||
|
return ambientIt->second.file;
|
||||||
|
}
|
||||||
auto musicIt = musicMap_.find(id);
|
auto musicIt = musicMap_.find(id);
|
||||||
if (musicIt != musicMap_.end()) {
|
if (musicIt != musicMap_.end()) {
|
||||||
return musicIt->second.file;
|
return musicIt->second.file;
|
||||||
@@ -263,6 +312,18 @@ std::optional<const AudioEffect*> AudioDatabase::getEffect(const std::string& id
|
|||||||
return std::nullopt;
|
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) {
|
std::optional<const AudioMusic*> AudioDatabase::getMusic(const std::string& id) {
|
||||||
if (!loaded_) {
|
if (!loaded_) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -291,6 +352,10 @@ size_t AudioDatabase::effectCount() const {
|
|||||||
return effectMap_.size();
|
return effectMap_.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t AudioDatabase::ambientCount() const {
|
||||||
|
return ambientMap_.size();
|
||||||
|
}
|
||||||
|
|
||||||
size_t AudioDatabase::musicCount() const {
|
size_t AudioDatabase::musicCount() const {
|
||||||
return musicMap_.size();
|
return musicMap_.size();
|
||||||
}
|
}
|
||||||
@@ -364,7 +429,8 @@ bool AudioDatabase::has(const std::string& id) {
|
|||||||
if (!loaded_) {
|
if (!loaded_) {
|
||||||
return false;
|
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) {
|
std::string AudioDatabase::filePath(const std::string& id) {
|
||||||
@@ -395,6 +461,9 @@ AudioEntryType AudioDatabase::typeOf(const std::string& id) {
|
|||||||
if (effectMap_.count(id) > 0) {
|
if (effectMap_.count(id) > 0) {
|
||||||
return AudioEntryType::Effect;
|
return AudioEntryType::Effect;
|
||||||
}
|
}
|
||||||
|
if (ambientMap_.count(id) > 0) {
|
||||||
|
return AudioEntryType::Ambient;
|
||||||
|
}
|
||||||
if (musicMap_.count(id) > 0) {
|
if (musicMap_.count(id) > 0) {
|
||||||
return AudioEntryType::Music;
|
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
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/math/GameMath.h"
|
||||||
#include <frostbite2D/types/type_math.h>
|
#include <frostbite2D/types/type_math.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -81,8 +82,8 @@ struct CharacterMotor {
|
|||||||
float gravity = 1600.0f;
|
float gravity = 1600.0f;
|
||||||
|
|
||||||
void SetGroundPosition(const Vec2& groundPosition) {
|
void SetGroundPosition(const Vec2& groundPosition) {
|
||||||
position.x = RoundWorldCoordinate(groundPosition.x);
|
position.x = gameMath::RoundWorldCoordinate(groundPosition.x);
|
||||||
position.y = RoundWorldCoordinate(groundPosition.y);
|
position.y = gameMath::RoundWorldCoordinate(groundPosition.y);
|
||||||
positionRemainder_ = Vec2::Zero();
|
positionRemainder_ = Vec2::Zero();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,10 +168,6 @@ struct CharacterMotor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static int32 RoundWorldCoordinate(float value) {
|
|
||||||
return static_cast<int32>(std::lround(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32 ConsumeWholeUnits(float& remainder) {
|
static int32 ConsumeWholeUnits(float& remainder) {
|
||||||
if (remainder >= 1.0f) {
|
if (remainder >= 1.0f) {
|
||||||
int32 wholeUnits = static_cast<int32>(std::floor(remainder));
|
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;
|
size_t FindMoveAreaIndex(const Vec3& curPos) const;
|
||||||
const std::vector<MapMoveArea>& GetMoveAreaInfo() const;
|
const std::vector<MapMoveArea>& GetMoveAreaInfo() const;
|
||||||
size_t GetMoveAreaCount() const { return moveArea_.size(); }
|
size_t GetMoveAreaCount() const { return moveArea_.size(); }
|
||||||
|
game::MoveAreaBounds GetMovablePositionBounds(size_t index) const;
|
||||||
Rect GetMovablePositionArea(size_t index) const;
|
Rect GetMovablePositionArea(size_t index) const;
|
||||||
const std::string& GetMapPath() const { return mapConfig_.mapPath; }
|
const std::string& GetMapPath() const { return mapConfig_.mapPath; }
|
||||||
void SetDebugHighlightedMoveAreaIndex(size_t index);
|
void SetDebugHighlightedMoveAreaIndex(size_t index);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public:
|
|||||||
|
|
||||||
int GetCurAreaIndex() const { return curMapIndex_; }
|
int GetCurAreaIndex() const { return curMapIndex_; }
|
||||||
RefPtr<GameMap> GetArea(int index) const;
|
RefPtr<GameMap> GetArea(int index) const;
|
||||||
|
RefPtr<GameMap> GetCurrentArea() const;
|
||||||
const std::vector<MapInfo>& GetAreas() const { return mapList_; }
|
const std::vector<MapInfo>& GetAreas() const { return mapList_; }
|
||||||
|
|
||||||
void Update(float deltaTime) override;
|
void Update(float deltaTime) override;
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
class Actor;
|
||||||
|
class CharacterObject;
|
||||||
|
class GameDebugUIScene;
|
||||||
|
class GameMap;
|
||||||
|
|
||||||
class GameWorld : public Scene {
|
class GameWorld : public Scene {
|
||||||
public:
|
public:
|
||||||
GameWorld();
|
GameWorld();
|
||||||
@@ -19,6 +24,13 @@ public:
|
|||||||
void AddCharacter(RefPtr<Actor> actor, int townId);
|
void AddCharacter(RefPtr<Actor> actor, int townId);
|
||||||
void MoveCharacter(RefPtr<Actor> actor, int townId, int area);
|
void MoveCharacter(RefPtr<Actor> actor, int townId, int area);
|
||||||
void RequestMoveCharacter(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();
|
static GameWorld* GetWorld();
|
||||||
|
|
||||||
@@ -29,13 +41,25 @@ private:
|
|||||||
int area = -1;
|
int area = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct SuppressedMoveArea {
|
||||||
|
GameMap* map = nullptr;
|
||||||
|
size_t moveAreaIndex = GameMap::kInvalidMoveAreaIndex;
|
||||||
|
};
|
||||||
|
|
||||||
bool InitWorld();
|
bool InitWorld();
|
||||||
|
bool InitMainCharacter(int townId);
|
||||||
void ProcessPendingCharacterMove();
|
void ProcessPendingCharacterMove();
|
||||||
|
void EnsureDebugScene();
|
||||||
|
void RefreshDebugContext();
|
||||||
|
void SetSuppressedMoveArea(GameMap* map, size_t moveAreaIndex);
|
||||||
|
void ClearSuppressedMoveArea();
|
||||||
|
|
||||||
std::map<int, std::string> townPathMap_;
|
std::map<int, std::string> townPathMap_;
|
||||||
std::map<int, RefPtr<GameTown>> townMap_;
|
std::map<int, RefPtr<GameTown>> townMap_;
|
||||||
RefPtr<Actor> mainActor_;
|
RefPtr<Actor> mainActor_;
|
||||||
|
RefPtr<GameDebugUIScene> debugScene_;
|
||||||
std::optional<PendingCharacterMove> pendingCharacterMove_;
|
std::optional<PendingCharacterMove> pendingCharacterMove_;
|
||||||
|
std::optional<SuppressedMoveArea> suppressedMoveArea_;
|
||||||
int curTown_ = -1;
|
int curTown_ = -1;
|
||||||
bool initialized_ = false;
|
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.h>
|
||||||
#include <frostbite2D/scene/scene_manager.h>
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
#include <frostbite2D/utils/startup_trace.h>
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
#include "scene/GameMapTestScene.h"
|
|
||||||
#include "world/GameWorld.h"
|
#include "world/GameWorld.h"
|
||||||
|
|
||||||
using namespace frostbite2D;
|
using namespace frostbite2D;
|
||||||
@@ -144,10 +143,9 @@ int main(int argc, char **argv) {
|
|||||||
StartupTrace::mark("before SceneManager::ReplaceScene");
|
StartupTrace::mark("before SceneManager::ReplaceScene");
|
||||||
|
|
||||||
{
|
{
|
||||||
ScopedStartupTrace stageTrace(
|
ScopedStartupTrace stageTrace("SceneManager::ReplaceScene(GameWorld)");
|
||||||
"SceneManager::ReplaceScene(GameMapTestScene)");
|
auto gameWorld = MakePtr<GameWorld>();
|
||||||
auto testMapScene = MakePtr<GameMapTestScene>();
|
SceneManager::get().ReplaceScene(gameWorld);
|
||||||
SceneManager::get().ReplaceScene(testMapScene);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StartupTrace::mark("after SceneManager::ReplaceScene");
|
StartupTrace::mark("after SceneManager::ReplaceScene");
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "character/CharacterObject.h"
|
#include "character/CharacterObject.h"
|
||||||
|
#include "common/math/GameMath.h"
|
||||||
#include "map/GameMap.h"
|
#include "map/GameMap.h"
|
||||||
#include "world/GameWorld.h"
|
#include "world/GameWorld.h"
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <cmath>
|
|
||||||
#include <frostbite2D/core/application.h>
|
#include <frostbite2D/core/application.h>
|
||||||
#include <frostbite2D/utils/startup_trace.h>
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -43,10 +43,6 @@ Vec3 ToWorldVector(const CharacterWorldPosition& position) {
|
|||||||
static_cast<float>(position.z));
|
static_cast<float>(position.z));
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 RoundWorldCoordinate(float value) {
|
|
||||||
return static_cast<int32>(std::lround(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool CharacterObject::Construction(int jobId) {
|
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.y - previousPosition.y),
|
||||||
static_cast<float>(motor_.position.z - previousPosition.z));
|
static_cast<float>(motor_.position.z - previousPosition.z));
|
||||||
Vec3 resolvedWorldPos = map->CheckIsItMovable(previousWorldPos, worldOffset);
|
Vec3 resolvedWorldPos = map->CheckIsItMovable(previousWorldPos, worldOffset);
|
||||||
motor_.position.x = RoundWorldCoordinate(resolvedWorldPos.x);
|
motor_.position.x = gameMath::RoundWorldCoordinate(resolvedWorldPos.x);
|
||||||
motor_.position.y = RoundWorldCoordinate(resolvedWorldPos.y);
|
motor_.position.y = gameMath::RoundWorldCoordinate(resolvedWorldPos.y);
|
||||||
motor_.position.z = RoundWorldCoordinate(resolvedWorldPos.z);
|
motor_.position.z = gameMath::RoundWorldCoordinate(resolvedWorldPos.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterObject::QueueMapTransitionIfNeeded() {
|
void CharacterObject::QueueMapTransitionIfNeeded() {
|
||||||
@@ -210,16 +206,24 @@ void CharacterObject::QueueMapTransitionIfNeeded() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameMap::MapMoveArea target;
|
|
||||||
if (!map->TryGetMoveAreaTarget(ToWorldVector(motor_.position), target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameWorld* world = GameWorld::GetWorld();
|
GameWorld* world = GameWorld::GetWorld();
|
||||||
if (!world) {
|
if (!world) {
|
||||||
return;
|
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);
|
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 "character/CharacterObject.h"
|
||||||
#include "map/GameMap.h"
|
#include "map/GameMap.h"
|
||||||
#include "ui/NineSliceActor.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 "map/GameMap.h"
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <frostbite2D/2d/sprite.h>
|
#include <frostbite2D/2d/sprite.h>
|
||||||
@@ -24,71 +26,6 @@ static const int kLayerOrders[] = {
|
|||||||
|
|
||||||
constexpr int kTileRootZOrder = -1000000;
|
constexpr int kTileRootZOrder = -1000000;
|
||||||
constexpr float kExtendedTileStepY = 120.0f;
|
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
|
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
|
||||||
// 占位,避免整张地图中断。
|
// 占位,避免整张地图中断。
|
||||||
@@ -102,6 +39,49 @@ Ptr<Sprite> createMapSprite(const std::string& path, int index) {
|
|||||||
return sprite;
|
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
|
} // namespace
|
||||||
|
|
||||||
GameMap::GameMap() {
|
GameMap::GameMap() {
|
||||||
@@ -172,23 +152,7 @@ bool GameMap::LoadMap(const std::string &mapName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameMap::Enter() {
|
void GameMap::Enter() {
|
||||||
// 地图进入时尝试找到第一个可播放的 BGM 并循环播放。
|
MapAudioController::get().ApplyMapAudio(BuildMapAudioState(mapConfig_));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameMap::Update(float deltaTime) { Actor::Update(deltaTime); }
|
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 {
|
Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const {
|
||||||
int currentX = RoundWorldCoordinate(curPos.x);
|
int currentX = gameMath::RoundWorldCoordinate(curPos.x);
|
||||||
int currentY = RoundWorldCoordinate(curPos.y);
|
int currentY = gameMath::RoundWorldCoordinate(curPos.y);
|
||||||
int targetX = RoundWorldCoordinate(curPos.x + posOffset.x);
|
int targetX = gameMath::RoundWorldCoordinate(curPos.x + posOffset.x);
|
||||||
int targetY = RoundWorldCoordinate(curPos.y + posOffset.y);
|
int targetY = gameMath::RoundWorldCoordinate(curPos.y + posOffset.y);
|
||||||
int targetZ = RoundWorldCoordinate(curPos.z + posOffset.z);
|
int targetZ = gameMath::RoundWorldCoordinate(curPos.z + posOffset.z);
|
||||||
Vec3 result = MakeIntegerWorldPosition(currentX, currentY, targetZ);
|
Vec3 result = gameMath::MakeIntegerWorldPosition(currentX, currentY, targetZ);
|
||||||
if (!movableAreaCheckEnabled_) {
|
if (!movableAreaCheckEnabled_) {
|
||||||
return MakeIntegerWorldPosition(targetX, targetY, targetZ);
|
return gameMath::MakeIntegerWorldPosition(targetX, targetY, targetZ);
|
||||||
}
|
}
|
||||||
if (movablePolygon_.size() < 3) {
|
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
|
// Prefer the full destination first; only fall back to single-axis sliding
|
||||||
// when the combined move would leave the movable polygon.
|
// 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.x = static_cast<float>(targetX);
|
||||||
result.y = static_cast<float>(targetY);
|
result.y = static_cast<float>(targetY);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isXValid = IsPointMovable(movablePolygon_, targetX, currentY);
|
bool isXValid = gameMath::IsPointMovable(movablePolygon_, targetX, currentY);
|
||||||
bool isYValid = IsPointMovable(movablePolygon_, currentX, targetY);
|
bool isYValid = gameMath::IsPointMovable(movablePolygon_, currentX, targetY);
|
||||||
|
|
||||||
if (isXValid && isYValid) {
|
if (isXValid && isYValid) {
|
||||||
int moveX = std::abs(targetX - currentX);
|
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 {
|
size_t GameMap::FindMoveAreaIndex(const Vec3& curPos) const {
|
||||||
int currentX = RoundWorldCoordinate(curPos.x);
|
int currentX = gameMath::RoundWorldCoordinate(curPos.x);
|
||||||
int currentY = RoundWorldCoordinate(curPos.y);
|
int currentY = gameMath::RoundWorldCoordinate(curPos.y);
|
||||||
for (size_t i = 0;
|
for (size_t i = 0;
|
||||||
i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size();
|
i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size();
|
||||||
++i) {
|
++i) {
|
||||||
if (moveArea_[i].containsPoint(MakeIntegerWorldPoint(currentX, currentY))) {
|
if (moveArea_[i].containsPoint(gameMath::MakeIntegerWorldPoint(currentX, currentY))) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,6 +498,13 @@ const std::vector<GameMap::MapMoveArea>& GameMap::GetMoveAreaInfo() const {
|
|||||||
return mapConfig_.townMovableAreaTargets;
|
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 {
|
Rect GameMap::GetMovablePositionArea(size_t index) const {
|
||||||
if (index >= mapConfig_.townMovableAreas.size()) {
|
if (index >= mapConfig_.townMovableAreas.size()) {
|
||||||
return Rect::Zero();
|
return Rect::Zero();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "map/GameMapLayer.h"
|
#include "map/GameMapLayer.h"
|
||||||
|
#include "common/math/GameMath.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <frostbite2D/graphics/renderer.h>
|
#include <frostbite2D/graphics/renderer.h>
|
||||||
@@ -13,78 +14,6 @@ constexpr float kDebugEdgePointSize = 5.0f;
|
|||||||
constexpr float kDebugVertexSize = 9.0f;
|
constexpr float kDebugVertexSize = 9.0f;
|
||||||
constexpr float kDebugEdgeStep = 4.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,
|
void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& worldOrigin,
|
||||||
const Color& color) {
|
const Color& color) {
|
||||||
if (polygon.size() < 2) {
|
if (polygon.size() < 2) {
|
||||||
@@ -155,7 +84,7 @@ void GameMapLayer::Render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto& moveArea : moveAreaInfoList_) {
|
for (auto& moveArea : moveAreaInfoList_) {
|
||||||
std::vector<Vec2> polygon = BuildRectPolygon(moveArea.rect);
|
std::vector<Vec2> polygon = gameMath::BuildRectPolygon(moveArea.rect);
|
||||||
if (polygon.empty()) {
|
if (polygon.empty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -170,7 +99,7 @@ void GameMapLayer::Render() {
|
|||||||
|
|
||||||
void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon) {
|
void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon) {
|
||||||
feasibleAreaPolygon_ = polygon;
|
feasibleAreaPolygon_ = polygon;
|
||||||
feasibleAreaFillRects_ = BuildPolygonFillRects(feasibleAreaPolygon_);
|
feasibleAreaFillRects_ = gameMath::BuildPolygonFillRects(feasibleAreaPolygon_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect, size_t index) {
|
void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect, size_t index) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "scene/GameDebugUIScene.h"
|
#include "scene/GameDebugUIScene.h"
|
||||||
#include "character/CharacterObject.h"
|
#include "character/CharacterObject.h"
|
||||||
#include "common/GameDebugActor.h"
|
#include "common/debug/GameDebugActor.h"
|
||||||
#include "map/GameMap.h"
|
#include "map/GameMap.h"
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ Vec2 RoundWorldPoint(const Vec2& pos) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
GameTown::GameTown() = default;
|
GameTown::GameTown() {
|
||||||
|
EnableEventReceive();
|
||||||
|
}
|
||||||
|
|
||||||
bool GameTown::Init(int index, const std::string& townPath) {
|
bool GameTown::Init(int index, const std::string& townPath) {
|
||||||
game::TownConfig config;
|
game::TownConfig config;
|
||||||
@@ -68,10 +70,25 @@ RefPtr<GameMap> GameTown::GetArea(int index) const {
|
|||||||
return it->map;
|
return it->map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefPtr<GameMap> GameTown::GetCurrentArea() const {
|
||||||
|
if (curMapIndex_ == -1) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetArea(curMapIndex_);
|
||||||
|
}
|
||||||
|
|
||||||
void GameTown::AddCharacter(RefPtr<Actor> actor, int areaIndex) {
|
void GameTown::AddCharacter(RefPtr<Actor> actor, int areaIndex) {
|
||||||
int targetMapIndex = areaIndex;
|
int targetMapIndex = areaIndex;
|
||||||
if (areaIndex == -2) {
|
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(),
|
auto mapIt = std::find_if(mapList_.begin(), mapList_.end(),
|
||||||
|
|||||||
@@ -1,22 +1,57 @@
|
|||||||
#include "world/GameWorld.h"
|
#include "world/GameWorld.h"
|
||||||
|
#include "character/CharacterObject.h"
|
||||||
#include "map/GameDataLoader.h"
|
#include "map/GameDataLoader.h"
|
||||||
|
#include "scene/GameDebugUIScene.h"
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <frostbite2D/scene/scene_manager.h>
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
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;
|
GameWorld::GameWorld() = default;
|
||||||
|
|
||||||
void GameWorld::onEnter() {
|
void GameWorld::onEnter() {
|
||||||
Scene::onEnter();
|
Scene::onEnter();
|
||||||
if (initialized_) {
|
EnsureDebugScene();
|
||||||
return;
|
|
||||||
|
if (!initialized_) {
|
||||||
|
initialized_ = InitWorld();
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized_ = InitWorld();
|
RefreshDebugContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorld::onExit() {
|
void GameWorld::onExit() {
|
||||||
|
pendingCharacterMove_.reset();
|
||||||
|
ClearSuppressedMoveArea();
|
||||||
|
if (debugScene_) {
|
||||||
|
debugScene_->ClearDebugContext();
|
||||||
|
SceneManager::get().RemoveUIScene(debugScene_.Get());
|
||||||
|
}
|
||||||
|
|
||||||
Scene::onExit();
|
Scene::onExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +61,14 @@ void GameWorld::Update(float deltaTime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool GameWorld::InitWorld() {
|
bool GameWorld::InitWorld() {
|
||||||
|
RemoveAllChildren();
|
||||||
|
townPathMap_.clear();
|
||||||
|
townMap_.clear();
|
||||||
|
mainActor_.Reset();
|
||||||
|
pendingCharacterMove_.reset();
|
||||||
|
ClearSuppressedMoveArea();
|
||||||
|
curTown_ = -1;
|
||||||
|
|
||||||
townPathMap_ = game::loadTownList();
|
townPathMap_ = game::loadTownList();
|
||||||
if (townPathMap_.empty()) {
|
if (townPathMap_.empty()) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: no town entries found");
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: no town entries found");
|
||||||
@@ -45,7 +88,36 @@ bool GameWorld::InitWorld() {
|
|||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +132,7 @@ void GameWorld::AddCharacter(RefPtr<Actor> actor, int townId) {
|
|||||||
curTown_ = townId;
|
curTown_ = townId;
|
||||||
it->second->AddCharacter(actor);
|
it->second->AddCharacter(actor);
|
||||||
AddChild(it->second);
|
AddChild(it->second);
|
||||||
|
RefreshDebugContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
|
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;
|
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) {
|
if (curTown_ != -1) {
|
||||||
auto currentTown = townMap_.find(curTown_);
|
auto currentTown = townMap_.find(curTown_);
|
||||||
if (currentTown != townMap_.end()) {
|
if (currentTown != townMap_.end()) {
|
||||||
@@ -80,6 +191,7 @@ void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
|
|||||||
mainActor_ = actor;
|
mainActor_ = actor;
|
||||||
townIt->second->AddCharacter(actor, area);
|
townIt->second->AddCharacter(actor, area);
|
||||||
AddChild(townIt->second);
|
AddChild(townIt->second);
|
||||||
|
RefreshDebugContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorld::RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area) {
|
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};
|
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() {
|
void GameWorld::ProcessPendingCharacterMove() {
|
||||||
if (!pendingCharacterMove_) {
|
if (!pendingCharacterMove_) {
|
||||||
return;
|
return;
|
||||||
@@ -102,6 +230,63 @@ void GameWorld::ProcessPendingCharacterMove() {
|
|||||||
MoveCharacter(move.actor, move.townId, move.area);
|
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() {
|
GameWorld* GameWorld::GetWorld() {
|
||||||
return dynamic_cast<GameWorld*>(SceneManager::get().GetCurrentScene());
|
return dynamic_cast<GameWorld*>(SceneManager::get().GetCurrentScene());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user