feat: 实现游戏地图和城镇系统基础架构
新增GameMap和GameTown类实现游戏地图和城镇的核心功能 添加GameWorld作为游戏世界管理器处理场景切换 完善音频系统支持从路径加载音乐和音效 重构PVF资源系统增加路径规范化功能 添加.gitignore排除游戏资源目录
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,3 +15,4 @@ build/
|
||||
参考代码/
|
||||
*.pvf
|
||||
nul
|
||||
Game/assets/
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace frostbite2D {
|
||||
*/
|
||||
class Music : public RefObject {
|
||||
public:
|
||||
static Ptr<Music> loadFromPath(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> loadFromNpk(const std::string& audioPath);
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace frostbite2D {
|
||||
*/
|
||||
class Sound : public RefObject {
|
||||
public:
|
||||
static Ptr<Sound> loadFromPath(const std::string& path);
|
||||
static Ptr<Sound> loadFromFile(const std::string& path);
|
||||
static Ptr<Sound> loadFromMemory(const uint8* data, size_t size);
|
||||
static Ptr<Sound> loadFromNpk(const std::string& audioPath);
|
||||
|
||||
@@ -186,17 +186,13 @@ public:
|
||||
*/
|
||||
bool hasLoadString(const std::string& type, const std::string& key) const;
|
||||
|
||||
std::string normalizePath(const std::string& path) const;
|
||||
std::string resolvePath(const std::string& baseDir, const std::string& path) const;
|
||||
|
||||
private:
|
||||
PvfArchive() = default;
|
||||
~PvfArchive() = default;
|
||||
|
||||
/**
|
||||
* @brief 规范化路径(转为小写)
|
||||
* @param path 原始路径
|
||||
* @return 规范化后的路径
|
||||
*/
|
||||
std::string normalizePath(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* @brief 分割字符串
|
||||
* @param str 要分割的字符串
|
||||
|
||||
@@ -8,6 +8,38 @@
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string normalizeAudioLogicalPath(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;
|
||||
}
|
||||
|
||||
bool startsWithPrefix(const std::string& value, const char* prefix) {
|
||||
const size_t prefixLength = std::char_traits<char>::length(prefix);
|
||||
return value.size() >= prefixLength &&
|
||||
value.compare(0, prefixLength, prefix) == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Ptr<Music> Music::loadFromPath(const std::string& path) {
|
||||
std::string logicalPath = normalizeAudioLogicalPath(path);
|
||||
if (startsWithPrefix(logicalPath, "music/")) {
|
||||
return loadFromFile("assets/" + logicalPath);
|
||||
}
|
||||
if (startsWithPrefix(logicalPath, "sounds/")) {
|
||||
return loadFromNpk(logicalPath);
|
||||
}
|
||||
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unsupported audio path prefix for music: %s", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Ptr<Music> Music::loadFromFile(const std::string& path) {
|
||||
auto& asset = Asset::get();
|
||||
std::string fullPath = asset.resolvePath(path);
|
||||
@@ -41,13 +73,14 @@ Ptr<Music> Music::loadFromMemory(const uint8* data, size_t size) {
|
||||
|
||||
Ptr<Music> Music::loadFromNpk(const std::string& audioPath) {
|
||||
SoundPackArchive& archive = SoundPackArchive::get();
|
||||
std::string normalizedPath = normalizeAudioLogicalPath(audioPath);
|
||||
|
||||
if (!archive.isOpen()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SoundPackArchive not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto audioOpt = archive.getAudio(audioPath);
|
||||
auto audioOpt = archive.getAudio(normalizedPath);
|
||||
if (!audioOpt) {
|
||||
std::string defaultPath = archive.getDefaultAudioPath();
|
||||
if (!defaultPath.empty()) {
|
||||
|
||||
@@ -8,6 +8,38 @@
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string normalizeAudioLogicalPath(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;
|
||||
}
|
||||
|
||||
bool startsWithPrefix(const std::string& value, const char* prefix) {
|
||||
const size_t prefixLength = std::char_traits<char>::length(prefix);
|
||||
return value.size() >= prefixLength &&
|
||||
value.compare(0, prefixLength, prefix) == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Ptr<Sound> Sound::loadFromPath(const std::string& path) {
|
||||
std::string logicalPath = normalizeAudioLogicalPath(path);
|
||||
if (startsWithPrefix(logicalPath, "music/")) {
|
||||
return loadFromFile("assets/" + logicalPath);
|
||||
}
|
||||
if (startsWithPrefix(logicalPath, "sounds/")) {
|
||||
return loadFromNpk(logicalPath);
|
||||
}
|
||||
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unsupported audio path prefix for sound: %s", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Ptr<Sound> Sound::loadFromFile(const std::string& path) {
|
||||
auto& asset = Asset::get();
|
||||
std::string fullPath = asset.resolvePath(path);
|
||||
@@ -39,13 +71,14 @@ Ptr<Sound> Sound::loadFromMemory(const uint8* data, size_t size) {
|
||||
|
||||
Ptr<Sound> Sound::loadFromNpk(const std::string& audioPath) {
|
||||
SoundPackArchive& archive = SoundPackArchive::get();
|
||||
std::string normalizedPath = normalizeAudioLogicalPath(audioPath);
|
||||
|
||||
if (!archive.isOpen()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SoundPackArchive not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto audioOpt = archive.getAudio(audioPath);
|
||||
auto audioOpt = archive.getAudio(normalizedPath);
|
||||
if (!audioOpt) {
|
||||
std::string defaultPath = archive.getDefaultAudioPath();
|
||||
if (!defaultPath.empty()) {
|
||||
|
||||
@@ -5,6 +5,35 @@
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* getZlibErrorName(int code) {
|
||||
switch (code) {
|
||||
case Z_OK:
|
||||
return "Z_OK";
|
||||
case Z_STREAM_END:
|
||||
return "Z_STREAM_END";
|
||||
case Z_NEED_DICT:
|
||||
return "Z_NEED_DICT";
|
||||
case Z_ERRNO:
|
||||
return "Z_ERRNO";
|
||||
case Z_STREAM_ERROR:
|
||||
return "Z_STREAM_ERROR";
|
||||
case Z_DATA_ERROR:
|
||||
return "Z_DATA_ERROR";
|
||||
case Z_MEM_ERROR:
|
||||
return "Z_MEM_ERROR";
|
||||
case Z_BUF_ERROR:
|
||||
return "Z_BUF_ERROR";
|
||||
case Z_VERSION_ERROR:
|
||||
return "Z_VERSION_ERROR";
|
||||
default:
|
||||
return "UNKNOWN_ZLIB_ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const uint8 NpkArchive::NPK_KEY[256] = {
|
||||
112, 117, 99, 104, 105, 107, 111, 110, 64, 110, 101, 111, 112, 108, 101, 32,
|
||||
100, 117, 110, 103, 101, 111, 110, 32, 97, 110, 100, 32, 102, 105, 103, 104,
|
||||
@@ -257,7 +286,23 @@ bool NpkArchive::loadImgData(ImgRef& img) {
|
||||
);
|
||||
|
||||
if (uncompressResult != Z_OK) {
|
||||
SDL_Log("Failed to uncompress image data: %d", uncompressResult);
|
||||
if (frame.type == 16 && frame.compressionType == 5 &&
|
||||
frame.size == static_cast<uint32>(deSize)) {
|
||||
frame.data = std::move(compressedData);
|
||||
cachedData.memoryUsage += frame.data.size();
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, "
|
||||
"type=%d, compression=%d, size=%dx%d",
|
||||
img.path.c_str(), i, frame.type, frame.compressionType,
|
||||
frame.width, frame.height);
|
||||
continue;
|
||||
}
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to uncompress image data: %d (%s), img=%s, npk=%s, frame=%d, "
|
||||
"type=%d, compression=%d, size=%dx%d, offset=%u, packed=%u, expected=%d",
|
||||
uncompressResult, getZlibErrorName(uncompressResult), img.path.c_str(),
|
||||
img.npkFile.c_str(), i, frame.type, frame.compressionType, frame.width,
|
||||
frame.height, frame.offset, frame.size, deSize);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <frostbite2D/resource/pvf_archive.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <SDL.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
@@ -296,12 +297,70 @@ bool PvfArchive::hasLoadString(const std::string& type, const std::string& key)
|
||||
}
|
||||
|
||||
std::string PvfArchive::normalizePath(const std::string& path) const {
|
||||
std::string result = path;
|
||||
std::transform(result.begin(), result.end(), result.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
if (path.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
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)); });
|
||||
|
||||
std::vector<std::string> parts;
|
||||
std::stringstream stream(normalized);
|
||||
std::string part;
|
||||
while (std::getline(stream, part, '/')) {
|
||||
if (part.empty() || part == ".") {
|
||||
continue;
|
||||
}
|
||||
if (part == "..") {
|
||||
if (!parts.empty() && parts.back() != "..") {
|
||||
parts.pop_back();
|
||||
} else {
|
||||
parts.push_back(part);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
parts.push_back(part);
|
||||
}
|
||||
|
||||
std::string result;
|
||||
for (size_t i = 0; i < parts.size(); ++i) {
|
||||
if (i > 0) {
|
||||
result += '/';
|
||||
}
|
||||
result += parts[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string PvfArchive::resolvePath(const std::string& baseDir, const std::string& path) const {
|
||||
if (path.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string rawPath = path;
|
||||
std::replace(rawPath.begin(), rawPath.end(), '\\', '/');
|
||||
std::transform(rawPath.begin(), rawPath.end(), rawPath.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
size_t slashPos = rawPath.find('/');
|
||||
std::string root = slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos);
|
||||
static const char* kLogicalRoots[] = {"map", "sprite", "town", "sound", "audio",
|
||||
"monster", "character", "ui"};
|
||||
for (const char* logicalRoot : kLogicalRoots) {
|
||||
if (root == logicalRoot) {
|
||||
return normalizePath(rawPath);
|
||||
}
|
||||
}
|
||||
|
||||
std::string normalizedBaseDir = normalizePath(baseDir);
|
||||
if (!normalizedBaseDir.empty() && normalizedBaseDir.back() != '/') {
|
||||
normalizedBaseDir.push_back('/');
|
||||
}
|
||||
return normalizePath(normalizedBaseDir + rawPath);
|
||||
}
|
||||
|
||||
std::vector<std::string> PvfArchive::splitString(const std::string& str, const std::string& delimiter) const {
|
||||
std::vector<std::string> tokens;
|
||||
size_t pos = 0;
|
||||
|
||||
Binary file not shown.
76
Game/include/Actor/GameDataLoader.h
Normal file
76
Game/include/Actor/GameDataLoader.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/types/type_math.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D::game {
|
||||
|
||||
struct AreaConfig {
|
||||
std::string mapPath;
|
||||
std::string mapType;
|
||||
Vec2 generatePos = Vec2(-1.0f, -1.0f);
|
||||
};
|
||||
|
||||
struct TownConfig {
|
||||
std::string townName;
|
||||
std::string enteringTitle;
|
||||
std::string enteringCutscene;
|
||||
int needQuestId = -1;
|
||||
int needLevel = -1;
|
||||
std::map<int, AreaConfig> areas;
|
||||
};
|
||||
|
||||
struct BackgroundAnimationConfig {
|
||||
std::string filename;
|
||||
std::string layer;
|
||||
std::string order;
|
||||
};
|
||||
|
||||
struct MapAnimationConfig {
|
||||
std::string filename;
|
||||
std::string layer;
|
||||
int xPos = 0;
|
||||
int yPos = 0;
|
||||
int zPos = 0;
|
||||
};
|
||||
|
||||
struct MoveAreaTarget {
|
||||
int town = -2;
|
||||
int area = -2;
|
||||
};
|
||||
|
||||
struct TileInfo {
|
||||
std::string spritePath;
|
||||
int spriteIndex = 0;
|
||||
int imgPos = 0;
|
||||
};
|
||||
|
||||
struct MapConfig {
|
||||
std::string name;
|
||||
std::string mapPath;
|
||||
std::string mapDir;
|
||||
int backgroundPos = 0;
|
||||
int farSightScroll = 100;
|
||||
bool hasCameraLimit = false;
|
||||
int cameraLimitMinX = -1;
|
||||
int cameraLimitMaxX = -1;
|
||||
int cameraLimitMinY = -1;
|
||||
int cameraLimitMaxY = -1;
|
||||
std::vector<std::string> tilePaths;
|
||||
std::vector<std::string> extendedTilePaths;
|
||||
std::vector<BackgroundAnimationConfig> backgroundAnimations;
|
||||
std::vector<MapAnimationConfig> mapAnimations;
|
||||
std::vector<std::string> soundIds;
|
||||
std::vector<Rect> virtualMovableAreas;
|
||||
std::vector<Rect> townMovableAreas;
|
||||
std::vector<MoveAreaTarget> townMovableAreaTargets;
|
||||
};
|
||||
|
||||
std::map<int, std::string> loadTownList();
|
||||
bool loadTownConfig(const std::string& townPath, TownConfig& outConfig);
|
||||
bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig);
|
||||
bool loadTileInfo(const std::string& path, TileInfo& outInfo);
|
||||
|
||||
} // namespace frostbite2D::game
|
||||
55
Game/include/Actor/GameMap.h
Normal file
55
Game/include/Actor/GameMap.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "GameDataLoader.h"
|
||||
#include "GameMapLayer.h"
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <frostbite2D/audio/music.h>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class GameMap : public Actor {
|
||||
public:
|
||||
using MapMoveArea = game::MoveAreaTarget;
|
||||
|
||||
GameMap();
|
||||
~GameMap() override = default;
|
||||
|
||||
bool LoadMap(const std::string& mapName);
|
||||
void Enter();
|
||||
void Update(float deltaTime) override;
|
||||
|
||||
void AddObject(RefPtr<Actor> object);
|
||||
|
||||
Vec3 CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const;
|
||||
MapMoveArea CheckIsItMoveArea(const Vec3& curPos) const;
|
||||
const std::vector<MapMoveArea>& GetMoveAreaInfo() const;
|
||||
Rect GetMovablePositionArea(size_t index) const;
|
||||
|
||||
int GetMapLength() const { return mapLength_; }
|
||||
int GetMapHeight() const { return mapHeight_; }
|
||||
|
||||
private:
|
||||
void initLayers();
|
||||
void clearLayerChildren();
|
||||
void InitTile();
|
||||
void InitBackgroundAnimation();
|
||||
void InitMapAnimation();
|
||||
void InitVirtualMovableArea();
|
||||
void InitMoveArea();
|
||||
void updateCamera();
|
||||
void updateLayerPositions(const Vec2& cameraPos);
|
||||
|
||||
game::MapConfig mapConfig_;
|
||||
std::unordered_map<std::string, RefPtr<GameMapLayer>> layerMap_;
|
||||
std::vector<Rect> movableArea_;
|
||||
std::vector<Rect> moveArea_;
|
||||
int mapLength_ = 0;
|
||||
int mapHeight_ = 0;
|
||||
int mapOffsetY_ = 0;
|
||||
bool debugMode_ = false;
|
||||
Vec2 cameraFocus_ = Vec2::Zero();
|
||||
Ptr<Music> currentMusic_;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
20
Game/include/Actor/GameMapLayer.h
Normal file
20
Game/include/Actor/GameMapLayer.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class GameMapLayer : public Actor {
|
||||
public:
|
||||
void Render() override;
|
||||
|
||||
void AddDebugFeasibleAreaInfo(const Rect& rect, int type);
|
||||
void AddObject(RefPtr<Actor> obj);
|
||||
|
||||
private:
|
||||
std::vector<Rect> feasibleAreaInfoList_;
|
||||
std::vector<Rect> moveAreaInfoList_;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -1,12 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "GameDataLoader.h"
|
||||
#include "GameMap.h"
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D {
|
||||
class GameTown : public Actor {
|
||||
|
||||
|
||||
class GameTown : public Actor {
|
||||
public:
|
||||
struct MapInfo {
|
||||
int areaId = -1;
|
||||
RefPtr<GameMap> map;
|
||||
std::string type;
|
||||
Vec2 generatePos = Vec2(-1.0f, -1.0f);
|
||||
};
|
||||
|
||||
GameTown();
|
||||
~GameTown();
|
||||
~GameTown() override = default;
|
||||
|
||||
bool Init(int index, const std::string& townPath);
|
||||
void AddCharacter(RefPtr<Actor> actor, int areaIndex = -2);
|
||||
|
||||
int GetCurAreaIndex() const { return curMapIndex_; }
|
||||
RefPtr<GameMap> GetArea(int index) const;
|
||||
const std::vector<MapInfo>& GetAreas() const { return mapList_; }
|
||||
|
||||
void Update(float deltaTime) override;
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::string enteringTitle_;
|
||||
std::string enteringCutscene_;
|
||||
int needQuestId_ = -1;
|
||||
int needLevel_ = -1;
|
||||
int sariaRoomId_ = -1;
|
||||
Vec2 sariaRoomPos_ = Vec2(-1.0f, -1.0f);
|
||||
std::vector<MapInfo> mapList_;
|
||||
int curMapIndex_ = -1;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "GameTown.h"
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
#include <map>
|
||||
#include "GameTown.h"
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class GameWorld : public Scene {
|
||||
private:
|
||||
/**城镇Map */
|
||||
std::map<int, RefPtr<GameTown>> m_TownMap;
|
||||
|
||||
|
||||
public:
|
||||
GameWorld();
|
||||
~GameWorld();
|
||||
~GameWorld() override = default;
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
|
||||
void AddCharacter(RefPtr<Actor> actor, int townId);
|
||||
void MoveCharacter(RefPtr<Actor> actor, int townId, int area);
|
||||
|
||||
static GameWorld* GetWorld();
|
||||
|
||||
private:
|
||||
bool InitWorld();
|
||||
|
||||
std::map<int, std::string> townPathMap_;
|
||||
std::map<int, RefPtr<GameTown>> townMap_;
|
||||
RefPtr<Actor> mainActor_;
|
||||
int curTown_ = -1;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
301
Game/src/Actor/GameDataLoader.cpp
Normal file
301
Game/src/Actor/GameDataLoader.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
#include "Actor/GameDataLoader.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <frostbite2D/resource/pvf_archive.h>
|
||||
#include <frostbite2D/resource/script_parser.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <optional>
|
||||
|
||||
namespace frostbite2D::game {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string toLowerCase(const std::string& str) {
|
||||
std::string result = str;
|
||||
std::transform(result.begin(), result.end(), result.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string trimLayer(const std::string& str) {
|
||||
if (str.size() >= 2 && str.front() == '[' && str.back() == ']') {
|
||||
return toLowerCase(str.substr(1, str.size() - 2));
|
||||
}
|
||||
return toLowerCase(str);
|
||||
}
|
||||
|
||||
std::string normalizeWithPrefix(const std::string& prefix, const std::string& path) {
|
||||
auto& pvf = PvfArchive::get();
|
||||
std::string normalizedPath = pvf.normalizePath(path);
|
||||
if (normalizedPath.empty()) {
|
||||
return normalizedPath;
|
||||
}
|
||||
if (normalizedPath.rfind(prefix + "/", 0) == 0) {
|
||||
return normalizedPath;
|
||||
}
|
||||
return pvf.normalizePath(prefix + "/" + normalizedPath);
|
||||
}
|
||||
|
||||
std::string resolveMapPath(const std::string& baseDir, const std::string& leaf) {
|
||||
return PvfArchive::get().resolvePath(baseDir, leaf);
|
||||
}
|
||||
|
||||
std::string resolveSpritePath(const std::string& baseDir, const std::string& leaf) {
|
||||
return normalizeWithPrefix("sprite", PvfArchive::get().resolvePath(baseDir, leaf));
|
||||
}
|
||||
|
||||
class ScriptTokenStream {
|
||||
public:
|
||||
explicit ScriptTokenStream(const std::string& path)
|
||||
: path_(toLowerCase(path)) {
|
||||
auto& pvf = PvfArchive::get();
|
||||
auto rawData = pvf.getFileRawData(path_);
|
||||
if (!rawData) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameDataLoader: missing script %s",
|
||||
path_.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ScriptParser parser(*rawData, path_);
|
||||
if (!parser.isValid()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"GameDataLoader: invalid script payload %s", path_.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& value : parser.parseAll()) {
|
||||
tokens_.push_back(value.toString());
|
||||
}
|
||||
valid_ = true;
|
||||
}
|
||||
|
||||
bool isValid() const { return valid_; }
|
||||
bool isEnd() const { return index_ >= tokens_.size(); }
|
||||
const std::string& path() const { return path_; }
|
||||
|
||||
std::string get() {
|
||||
if (isEnd()) {
|
||||
return {};
|
||||
}
|
||||
return tokens_[index_++];
|
||||
}
|
||||
|
||||
private:
|
||||
std::string path_;
|
||||
std::vector<std::string> tokens_;
|
||||
size_t index_ = 0;
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
int toInt(const std::string& value, int fallback = 0) {
|
||||
try {
|
||||
return std::stoi(value);
|
||||
} catch (...) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
bool readScript(const std::string& path, ScriptTokenStream& outStream) {
|
||||
(void)path;
|
||||
return outStream.isValid();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::map<int, std::string> loadTownList() {
|
||||
std::map<int, std::string> townList;
|
||||
ScriptTokenStream stream("town/town.lst");
|
||||
if (!stream.isValid()) {
|
||||
return townList;
|
||||
}
|
||||
|
||||
while (!stream.isEnd()) {
|
||||
std::string index = stream.get();
|
||||
if (index.empty()) {
|
||||
break;
|
||||
}
|
||||
std::string townPath = stream.get();
|
||||
if (townPath.empty()) {
|
||||
break;
|
||||
}
|
||||
townList[toInt(index)] = normalizeWithPrefix("town", townPath);
|
||||
}
|
||||
|
||||
return townList;
|
||||
}
|
||||
|
||||
bool loadTownConfig(const std::string& townPath, TownConfig& outConfig) {
|
||||
ScriptTokenStream stream(townPath);
|
||||
if (!stream.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outConfig = TownConfig();
|
||||
while (!stream.isEnd()) {
|
||||
std::string segment = stream.get();
|
||||
if (segment == "[entering title]") {
|
||||
outConfig.enteringTitle = "sprite/" + toLowerCase(stream.get());
|
||||
} else if (segment == "[cutscene image]") {
|
||||
outConfig.enteringCutscene = "sprite/" + toLowerCase(stream.get());
|
||||
} else if (segment == "[area]") {
|
||||
int index = toInt(stream.get(), -1);
|
||||
std::string mapPath = normalizeWithPrefix("map", stream.get());
|
||||
std::string type = trimLayer(stream.get());
|
||||
AreaConfig areaConfig;
|
||||
areaConfig.mapPath = mapPath;
|
||||
areaConfig.mapType = type;
|
||||
if (type == "gate") {
|
||||
areaConfig.generatePos.x = static_cast<float>(toInt(stream.get(), -1));
|
||||
areaConfig.generatePos.y = static_cast<float>(toInt(stream.get(), -1));
|
||||
}
|
||||
outConfig.areas[index] = areaConfig;
|
||||
} else if (segment == "[name]") {
|
||||
outConfig.townName = stream.get();
|
||||
} else if (segment == "[need level]") {
|
||||
outConfig.needLevel = toInt(stream.get(), -1);
|
||||
} else if (segment == "[need quest id]") {
|
||||
outConfig.needQuestId = toInt(stream.get(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
return !outConfig.areas.empty();
|
||||
}
|
||||
|
||||
bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig) {
|
||||
ScriptTokenStream stream(mapPath);
|
||||
if (!stream.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outConfig = MapConfig();
|
||||
outConfig.mapPath = toLowerCase(mapPath);
|
||||
size_t slashPos = outConfig.mapPath.find_last_of('/');
|
||||
outConfig.mapDir = slashPos == std::string::npos ? "" : outConfig.mapPath.substr(0, slashPos + 1);
|
||||
|
||||
while (!stream.isEnd()) {
|
||||
std::string segment = stream.get();
|
||||
if (segment == "[background pos]") {
|
||||
outConfig.backgroundPos = toInt(stream.get());
|
||||
} else if (segment == "[map name]") {
|
||||
outConfig.name = stream.get();
|
||||
} else if (segment == "[limit map camera move]") {
|
||||
outConfig.hasCameraLimit = true;
|
||||
outConfig.cameraLimitMinX = toInt(stream.get(), -1);
|
||||
outConfig.cameraLimitMaxX = toInt(stream.get(), -1);
|
||||
outConfig.cameraLimitMinY = toInt(stream.get(), -1);
|
||||
outConfig.cameraLimitMaxY = toInt(stream.get(), -1);
|
||||
} else if (segment == "[far sight scroll]") {
|
||||
outConfig.farSightScroll = toInt(stream.get(), 100);
|
||||
} else if (segment == "[tile]") {
|
||||
while (!stream.isEnd()) {
|
||||
std::string token = stream.get();
|
||||
if (token == "[/tile]") {
|
||||
break;
|
||||
}
|
||||
outConfig.tilePaths.push_back(resolveMapPath(outConfig.mapDir, token));
|
||||
}
|
||||
} else if (segment == "[extended tile]") {
|
||||
while (!stream.isEnd()) {
|
||||
std::string token = stream.get();
|
||||
if (token == "[/extended tile]") {
|
||||
break;
|
||||
}
|
||||
outConfig.extendedTilePaths.push_back(resolveMapPath(outConfig.mapDir, token));
|
||||
}
|
||||
} else if (segment == "[background animation]") {
|
||||
while (!stream.isEnd()) {
|
||||
std::string token = stream.get();
|
||||
if (token == "[/background animation]") {
|
||||
break;
|
||||
}
|
||||
if (token != "[ani info]") {
|
||||
continue;
|
||||
}
|
||||
BackgroundAnimationConfig ani;
|
||||
stream.get();
|
||||
ani.filename = resolveMapPath(outConfig.mapDir, stream.get());
|
||||
stream.get();
|
||||
ani.layer = trimLayer(stream.get());
|
||||
stream.get();
|
||||
ani.order = stream.get();
|
||||
outConfig.backgroundAnimations.push_back(ani);
|
||||
}
|
||||
} else if (segment == "[sound]") {
|
||||
while (!stream.isEnd()) {
|
||||
std::string token = stream.get();
|
||||
if (token == "[/sound]") {
|
||||
break;
|
||||
}
|
||||
outConfig.soundIds.push_back(token);
|
||||
}
|
||||
} else if (segment == "[animation]") {
|
||||
while (!stream.isEnd()) {
|
||||
std::string token = stream.get();
|
||||
if (token == "[/animation]") {
|
||||
break;
|
||||
}
|
||||
MapAnimationConfig ani;
|
||||
ani.filename = resolveMapPath(outConfig.mapDir, token);
|
||||
ani.layer = trimLayer(stream.get());
|
||||
ani.xPos = toInt(stream.get());
|
||||
ani.yPos = toInt(stream.get());
|
||||
ani.zPos = toInt(stream.get());
|
||||
outConfig.mapAnimations.push_back(ani);
|
||||
}
|
||||
} else if (segment == "[town movable area]") {
|
||||
while (!stream.isEnd()) {
|
||||
std::string token = stream.get();
|
||||
if (token == "[/town movable area]") {
|
||||
break;
|
||||
}
|
||||
Rect rect(static_cast<float>(toInt(token)),
|
||||
static_cast<float>(toInt(stream.get())),
|
||||
static_cast<float>(toInt(stream.get())),
|
||||
static_cast<float>(toInt(stream.get())));
|
||||
MoveAreaTarget target;
|
||||
target.town = toInt(stream.get(), -2);
|
||||
target.area = toInt(stream.get(), -2);
|
||||
outConfig.townMovableAreas.push_back(rect);
|
||||
outConfig.townMovableAreaTargets.push_back(target);
|
||||
}
|
||||
} else if (segment == "[virtual movable area]") {
|
||||
while (!stream.isEnd()) {
|
||||
std::string token = stream.get();
|
||||
if (token == "[/virtual movable area]") {
|
||||
break;
|
||||
}
|
||||
Rect rect(static_cast<float>(toInt(token)),
|
||||
static_cast<float>(toInt(stream.get())),
|
||||
static_cast<float>(toInt(stream.get())),
|
||||
static_cast<float>(toInt(stream.get())));
|
||||
outConfig.virtualMovableAreas.push_back(rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadTileInfo(const std::string& path, TileInfo& outInfo) {
|
||||
ScriptTokenStream stream(path);
|
||||
if (!stream.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outInfo = TileInfo();
|
||||
size_t slashPos = path.find_last_of('/');
|
||||
std::string baseDir = slashPos == std::string::npos ? "" : path.substr(0, slashPos + 1);
|
||||
while (!stream.isEnd()) {
|
||||
std::string segment = stream.get();
|
||||
if (segment == "[image]") {
|
||||
outInfo.spritePath = resolveSpritePath(baseDir, stream.get());
|
||||
outInfo.spriteIndex = toInt(stream.get());
|
||||
} else if (segment == "[img pos]") {
|
||||
outInfo.imgPos = toInt(stream.get());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace frostbite2D::game
|
||||
364
Game/src/Actor/GameMap.cpp
Normal file
364
Game/src/Actor/GameMap.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
#include "Actor/GameMap.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <frostbite2D/2d/sprite.h>
|
||||
#include <frostbite2D/animation/animation.h>
|
||||
#include <frostbite2D/audio/music.h>
|
||||
#include <frostbite2D/core/application.h>
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
#include <frostbite2D/resource/audio_database.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
static const char* kLayerNames[] = {
|
||||
"contact", "distantback", "middleback", "bottom", "closeback",
|
||||
"normal", "close", "cover", "max"};
|
||||
|
||||
static const int kLayerOrders[] = {
|
||||
10000, 50000, 100000, 150000, 200000, 250000, 300000, 350000, 400000};
|
||||
|
||||
constexpr float kScreenWidth = 1280.0f;
|
||||
constexpr float kScreenHeight = 720.0f;
|
||||
constexpr float kTileSpacing = 224.0f;
|
||||
constexpr float kExtendedTileStepY = 120.0f;
|
||||
|
||||
Ptr<Sprite> createMapSprite(const std::string& path, int index) {
|
||||
auto sprite = Sprite::createFromNpk(path, static_cast<size_t>(index));
|
||||
if (!sprite) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to create sprite %s[%d]",
|
||||
path.c_str(), index);
|
||||
sprite = MakePtr<Sprite>();
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GameMap::GameMap() {
|
||||
initLayers();
|
||||
}
|
||||
|
||||
void GameMap::initLayers() {
|
||||
for (size_t i = 0; i < std::size(kLayerNames); ++i) {
|
||||
auto layer = MakePtr<GameMapLayer>();
|
||||
layer->SetName(kLayerNames[i]);
|
||||
layer->SetZOrder(kLayerOrders[i]);
|
||||
layerMap_[kLayerNames[i]] = layer;
|
||||
AddChild(layer);
|
||||
}
|
||||
}
|
||||
|
||||
void GameMap::clearLayerChildren() {
|
||||
for (auto& [_, layer] : layerMap_) {
|
||||
if (layer) {
|
||||
layer->RemoveAllChildren();
|
||||
}
|
||||
}
|
||||
movableArea_.clear();
|
||||
moveArea_.clear();
|
||||
currentMusic_.Reset();
|
||||
mapLength_ = 0;
|
||||
mapHeight_ = 0;
|
||||
}
|
||||
|
||||
bool GameMap::LoadMap(const std::string& mapName) {
|
||||
clearLayerChildren();
|
||||
|
||||
if (!game::loadMapConfig(mapName, mapConfig_)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to load map %s",
|
||||
mapName.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
mapOffsetY_ = mapConfig_.backgroundPos;
|
||||
InitTile();
|
||||
InitBackgroundAnimation();
|
||||
InitMapAnimation();
|
||||
InitVirtualMovableArea();
|
||||
InitMoveArea();
|
||||
|
||||
if (cameraFocus_ == Vec2::Zero()) {
|
||||
if (!moveArea_.empty()) {
|
||||
cameraFocus_ = moveArea_.front().center();
|
||||
} else if (mapLength_ > 0 || mapHeight_ > 0) {
|
||||
cameraFocus_ = Vec2(mapLength_ * 0.5f, mapHeight_ * 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameMap::Enter() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCamera();
|
||||
}
|
||||
|
||||
void GameMap::Update(float deltaTime) {
|
||||
Actor::Update(deltaTime);
|
||||
(void)deltaTime;
|
||||
updateCamera();
|
||||
}
|
||||
|
||||
void GameMap::InitTile() {
|
||||
if (mapConfig_.tilePaths.empty() && mapConfig_.extendedTilePaths.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float maxBaseHeight = 0.0f;
|
||||
float maxTotalBottom = 0.0f;
|
||||
float maxOffset = 0.0f;
|
||||
int normalTileCount = static_cast<int>(mapConfig_.tilePaths.size());
|
||||
|
||||
for (size_t i = 0; i < mapConfig_.tilePaths.size(); ++i) {
|
||||
game::TileInfo info;
|
||||
if (!game::loadTileInfo(mapConfig_.tilePaths[i], info)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto sprite = createMapSprite(info.spritePath.empty() ? "sprite/character/common/circlecooltime.img"
|
||||
: info.spritePath,
|
||||
info.spriteIndex);
|
||||
sprite->SetPosition(static_cast<float>(i) * kTileSpacing, static_cast<float>(info.imgPos));
|
||||
layerMap_["bottom"]->AddChild(sprite);
|
||||
|
||||
Vec2 size = sprite->GetSize();
|
||||
maxOffset = std::max(maxOffset, static_cast<float>(info.imgPos));
|
||||
maxBaseHeight = std::max(maxBaseHeight, size.y);
|
||||
mapLength_ = std::max(mapLength_, static_cast<int>(i * kTileSpacing + size.x));
|
||||
maxTotalBottom = std::max(maxTotalBottom, info.imgPos + size.y);
|
||||
}
|
||||
|
||||
if (normalTileCount > 0 && mapLength_ == 0) {
|
||||
mapLength_ = static_cast<int>(normalTileCount * kTileSpacing);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < mapConfig_.extendedTilePaths.size(); ++i) {
|
||||
game::TileInfo info;
|
||||
if (!game::loadTileInfo(mapConfig_.extendedTilePaths[i], info)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto sprite = createMapSprite(info.spritePath.empty() ? "sprite/character/common/circlecooltime.img"
|
||||
: info.spritePath,
|
||||
info.spriteIndex);
|
||||
int row = normalTileCount > 0 ? static_cast<int>(i) / normalTileCount : 0;
|
||||
float posY = maxOffset + maxBaseHeight + row * kExtendedTileStepY;
|
||||
sprite->SetPosition(static_cast<float>(i) * kTileSpacing, posY);
|
||||
layerMap_["bottom"]->AddChild(sprite);
|
||||
|
||||
Vec2 size = sprite->GetSize();
|
||||
mapLength_ = std::max(mapLength_, static_cast<int>(i * kTileSpacing + size.x));
|
||||
maxTotalBottom = std::max(maxTotalBottom, posY + size.y);
|
||||
}
|
||||
|
||||
mapHeight_ = std::max(mapHeight_, static_cast<int>(maxTotalBottom));
|
||||
}
|
||||
|
||||
void GameMap::InitBackgroundAnimation() {
|
||||
for (const auto& ani : mapConfig_.backgroundAnimations) {
|
||||
auto animation = MakePtr<Animation>(ani.filename);
|
||||
if (!animation || !animation->IsUsable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int repeatCount = 1;
|
||||
Vec2 animationSize = animation->GetSize();
|
||||
float rate = std::max(1.0f, mapConfig_.farSightScroll / 100.0f);
|
||||
if (animationSize.x > 0.0f && mapLength_ > 0) {
|
||||
repeatCount = std::max(1, static_cast<int>((mapLength_ * rate) / animationSize.x) + 1);
|
||||
}
|
||||
|
||||
auto layerIt = layerMap_.find(ani.layer);
|
||||
if (layerIt == layerMap_.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < repeatCount; ++i) {
|
||||
auto backgroundAni = MakePtr<Animation>(ani.filename);
|
||||
if (!backgroundAni || !backgroundAni->IsUsable()) {
|
||||
continue;
|
||||
}
|
||||
backgroundAni->SetPosition(i * animationSize.x, -120.0f);
|
||||
backgroundAni->SetZOrder(-1000000);
|
||||
layerIt->second->AddChild(backgroundAni);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameMap::InitMapAnimation() {
|
||||
for (const auto& ani : mapConfig_.mapAnimations) {
|
||||
auto animation = MakePtr<Animation>(ani.filename);
|
||||
if (!animation || !animation->IsUsable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
animation->SetPosition(static_cast<float>(ani.xPos),
|
||||
static_cast<float>(ani.yPos - ani.zPos));
|
||||
animation->SetZOrder(ani.yPos);
|
||||
|
||||
auto layerIt = layerMap_.find(ani.layer);
|
||||
if (layerIt != layerMap_.end()) {
|
||||
layerIt->second->AddChild(animation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameMap::InitVirtualMovableArea() {
|
||||
movableArea_ = mapConfig_.virtualMovableAreas;
|
||||
if (!debugMode_) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& rect : movableArea_) {
|
||||
layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void GameMap::InitMoveArea() {
|
||||
moveArea_ = mapConfig_.townMovableAreas;
|
||||
if (!debugMode_) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& rect : moveArea_) {
|
||||
layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void GameMap::updateCamera() {
|
||||
Camera* camera = Renderer::get().getCamera();
|
||||
if (!camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
float halfWidth = kScreenWidth * 0.5f;
|
||||
float halfHeight = kScreenHeight * 0.5f;
|
||||
float targetX = cameraFocus_.x;
|
||||
float targetY = cameraFocus_.y;
|
||||
|
||||
if (mapLength_ > 0) {
|
||||
targetX = std::clamp(targetX, halfWidth, std::max(halfWidth, static_cast<float>(mapLength_) - halfWidth));
|
||||
}
|
||||
if (mapHeight_ > 0) {
|
||||
targetY = std::clamp(targetY, halfHeight, std::max(halfHeight, static_cast<float>(mapHeight_) - halfHeight));
|
||||
}
|
||||
|
||||
if (mapConfig_.hasCameraLimit) {
|
||||
if (mapConfig_.cameraLimitMinX != -1) {
|
||||
targetX = std::max(targetX, static_cast<float>(mapConfig_.cameraLimitMinX));
|
||||
}
|
||||
if (mapConfig_.cameraLimitMaxX != -1) {
|
||||
targetX = std::min(targetX, static_cast<float>(mapConfig_.cameraLimitMaxX));
|
||||
}
|
||||
if (mapConfig_.cameraLimitMinY != -1) {
|
||||
targetY = std::max(targetY, static_cast<float>(mapConfig_.cameraLimitMinY));
|
||||
}
|
||||
if (mapConfig_.cameraLimitMaxY != -1) {
|
||||
targetY = std::min(targetY, static_cast<float>(mapConfig_.cameraLimitMaxY));
|
||||
}
|
||||
}
|
||||
|
||||
Vec2 cameraPos(targetX - halfWidth, targetY - halfHeight);
|
||||
camera->setPosition(cameraPos);
|
||||
updateLayerPositions(Vec2(targetX, targetY));
|
||||
}
|
||||
|
||||
void GameMap::updateLayerPositions(const Vec2& cameraTarget) {
|
||||
float halfWidth = kScreenWidth * 0.5f;
|
||||
float halfHeight = kScreenHeight * 0.5f;
|
||||
|
||||
for (auto& entry : layerMap_) {
|
||||
const std::string& name = entry.first;
|
||||
RefPtr<GameMapLayer>& layer = entry.second;
|
||||
float posX = -cameraTarget.x + halfWidth;
|
||||
float posY = -cameraTarget.y + halfHeight + static_cast<float>(mapOffsetY_);
|
||||
if (name == "distantback") {
|
||||
posX *= static_cast<float>(mapConfig_.farSightScroll) / 100.0f;
|
||||
}
|
||||
layer->SetPosition(posX, posY);
|
||||
}
|
||||
}
|
||||
|
||||
void GameMap::AddObject(RefPtr<Actor> object) {
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
object->SetZOrder(static_cast<int>(object->GetPosition().y));
|
||||
layerMap_["normal"]->AddObject(object);
|
||||
}
|
||||
|
||||
Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const {
|
||||
Vec3 result = curPos;
|
||||
if (movableArea_.empty()) {
|
||||
result += posOffset;
|
||||
return result;
|
||||
}
|
||||
|
||||
float targetX = curPos.x + posOffset.x;
|
||||
float targetY = curPos.y + posOffset.y;
|
||||
|
||||
bool isXValid = false;
|
||||
for (const auto& area : movableArea_) {
|
||||
if (area.containsPoint(Vec2(targetX, curPos.y))) {
|
||||
isXValid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool isYValid = false;
|
||||
for (const auto& area : movableArea_) {
|
||||
if (area.containsPoint(Vec2(curPos.x, targetY))) {
|
||||
isYValid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isXValid) {
|
||||
result.x = targetX;
|
||||
}
|
||||
if (isYValid) {
|
||||
result.y = targetY;
|
||||
}
|
||||
result.z += posOffset.z;
|
||||
return result;
|
||||
}
|
||||
|
||||
GameMap::MapMoveArea GameMap::CheckIsItMoveArea(const Vec3& curPos) const {
|
||||
for (size_t i = 0; i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size(); ++i) {
|
||||
if (moveArea_[i].containsPoint(Vec2(curPos.x, curPos.y))) {
|
||||
return mapConfig_.townMovableAreaTargets[i];
|
||||
}
|
||||
}
|
||||
return MapMoveArea();
|
||||
}
|
||||
|
||||
const std::vector<GameMap::MapMoveArea>& GameMap::GetMoveAreaInfo() const {
|
||||
return mapConfig_.townMovableAreaTargets;
|
||||
}
|
||||
|
||||
Rect GameMap::GetMovablePositionArea(size_t index) const {
|
||||
if (index >= mapConfig_.townMovableAreas.size()) {
|
||||
return Rect::Zero();
|
||||
}
|
||||
return mapConfig_.townMovableAreas[index];
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
36
Game/src/Actor/GameMapLayer.cpp
Normal file
36
Game/src/Actor/GameMapLayer.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "Actor/GameMapLayer.h"
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
void GameMapLayer::Render() {
|
||||
Actor::Render();
|
||||
|
||||
Vec2 worldOrigin = GetWorldTransform().transformPoint(Vec2::Zero());
|
||||
|
||||
for (const auto& rect : feasibleAreaInfoList_) {
|
||||
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
|
||||
rect.width(), rect.height());
|
||||
Renderer::get().drawQuad(drawRect, Color(1.0f, 0.0f, 0.0f, 0.35f));
|
||||
}
|
||||
|
||||
for (const auto& rect : moveAreaInfoList_) {
|
||||
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
|
||||
rect.width(), rect.height());
|
||||
Renderer::get().drawQuad(drawRect, Color(0.0f, 0.0f, 1.0f, 0.35f));
|
||||
}
|
||||
}
|
||||
|
||||
void GameMapLayer::AddDebugFeasibleAreaInfo(const Rect& rect, int type) {
|
||||
if (type == 0) {
|
||||
feasibleAreaInfoList_.push_back(rect);
|
||||
} else if (type == 1) {
|
||||
moveAreaInfoList_.push_back(rect);
|
||||
}
|
||||
}
|
||||
|
||||
void GameMapLayer::AddObject(RefPtr<Actor> obj) {
|
||||
AddChild(obj);
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
107
Game/src/Actor/GameTown.cpp
Normal file
107
Game/src/Actor/GameTown.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "Actor/GameTown.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
GameTown::GameTown() = default;
|
||||
|
||||
bool GameTown::Init(int index, const std::string& townPath) {
|
||||
game::TownConfig config;
|
||||
if (!game::loadTownConfig(townPath, config)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameTown: failed to init town %d (%s)",
|
||||
index, townPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
name_ = config.townName;
|
||||
enteringTitle_ = config.enteringTitle;
|
||||
enteringCutscene_ = config.enteringCutscene;
|
||||
needQuestId_ = config.needQuestId;
|
||||
needLevel_ = config.needLevel;
|
||||
mapList_.clear();
|
||||
curMapIndex_ = -1;
|
||||
sariaRoomId_ = -1;
|
||||
sariaRoomPos_ = Vec2(-1.0f, -1.0f);
|
||||
|
||||
for (const auto& [areaId, areaConfig] : config.areas) {
|
||||
auto map = MakePtr<GameMap>();
|
||||
if (!map->LoadMap(areaConfig.mapPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MapInfo info;
|
||||
info.areaId = areaId;
|
||||
info.map = map;
|
||||
info.type = areaConfig.mapType;
|
||||
info.generatePos = areaConfig.generatePos;
|
||||
mapList_.push_back(info);
|
||||
|
||||
if (info.type == "gate") {
|
||||
sariaRoomId_ = areaId;
|
||||
sariaRoomPos_ = areaConfig.generatePos;
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(mapList_.begin(), mapList_.end(),
|
||||
[](const MapInfo& a, const MapInfo& b) { return a.areaId < b.areaId; });
|
||||
return !mapList_.empty();
|
||||
}
|
||||
|
||||
RefPtr<GameMap> GameTown::GetArea(int index) const {
|
||||
auto it = std::find_if(mapList_.begin(), mapList_.end(),
|
||||
[index](const MapInfo& info) { return info.areaId == index; });
|
||||
if (it == mapList_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return it->map;
|
||||
}
|
||||
|
||||
void GameTown::AddCharacter(RefPtr<Actor> actor, int areaIndex) {
|
||||
int targetMapIndex = areaIndex;
|
||||
if (areaIndex == -2) {
|
||||
targetMapIndex = sariaRoomId_ != -1 ? sariaRoomId_ : (mapList_.empty() ? -1 : mapList_.front().areaId);
|
||||
}
|
||||
|
||||
auto mapIt = std::find_if(mapList_.begin(), mapList_.end(),
|
||||
[targetMapIndex](const MapInfo& info) {
|
||||
return info.areaId == targetMapIndex;
|
||||
});
|
||||
if (mapIt == mapList_.end()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"GameTown: target area %d not found, fallback to first map", targetMapIndex);
|
||||
if (mapList_.empty()) {
|
||||
return;
|
||||
}
|
||||
mapIt = mapList_.begin();
|
||||
}
|
||||
|
||||
if (curMapIndex_ != -1) {
|
||||
auto oldMap = GetArea(curMapIndex_);
|
||||
if (oldMap) {
|
||||
RemoveChild(oldMap);
|
||||
}
|
||||
}
|
||||
|
||||
if (actor && actor->GetParent()) {
|
||||
actor->GetParent()->RemoveChild(actor);
|
||||
}
|
||||
|
||||
AddChild(mapIt->map);
|
||||
mapIt->map->Enter();
|
||||
|
||||
if (actor) {
|
||||
if (areaIndex == -2 && sariaRoomPos_.x >= 0.0f && sariaRoomPos_.y >= 0.0f) {
|
||||
actor->SetPosition(sariaRoomPos_);
|
||||
}
|
||||
mapIt->map->AddObject(actor);
|
||||
}
|
||||
|
||||
curMapIndex_ = mapIt->areaId;
|
||||
}
|
||||
|
||||
void GameTown::Update(float deltaTime) {
|
||||
Actor::Update(deltaTime);
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
85
Game/src/Actor/GameWorld.cpp
Normal file
85
Game/src/Actor/GameWorld.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#include "Actor/GameWorld.h"
|
||||
#include "Actor/GameDataLoader.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
GameWorld::GameWorld() = default;
|
||||
|
||||
void GameWorld::onEnter() {
|
||||
Scene::onEnter();
|
||||
if (initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetScale(1.2f);
|
||||
initialized_ = InitWorld();
|
||||
}
|
||||
|
||||
void GameWorld::onExit() {
|
||||
Scene::onExit();
|
||||
}
|
||||
|
||||
bool GameWorld::InitWorld() {
|
||||
townPathMap_ = game::loadTownList();
|
||||
if (townPathMap_.empty()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: no town entries found");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& [townId, townPath] : townPathMap_) {
|
||||
auto town = MakePtr<GameTown>();
|
||||
if (!town->Init(townId, townPath)) {
|
||||
continue;
|
||||
}
|
||||
townMap_[townId] = town;
|
||||
}
|
||||
|
||||
if (townMap_.empty()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: failed to create any town");
|
||||
return false;
|
||||
}
|
||||
|
||||
AddCharacter(nullptr, townMap_.begin()->first);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameWorld::AddCharacter(RefPtr<Actor> actor, int townId) {
|
||||
auto it = townMap_.find(townId);
|
||||
if (it == townMap_.end()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: town %d not found", townId);
|
||||
return;
|
||||
}
|
||||
|
||||
mainActor_ = actor;
|
||||
curTown_ = townId;
|
||||
it->second->AddCharacter(actor);
|
||||
AddChild(it->second);
|
||||
}
|
||||
|
||||
void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
|
||||
auto townIt = townMap_.find(townId);
|
||||
if (townIt == townMap_.end()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: target town %d missing", townId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (curTown_ != -1) {
|
||||
auto currentTown = townMap_.find(curTown_);
|
||||
if (currentTown != townMap_.end()) {
|
||||
RemoveChild(currentTown->second);
|
||||
}
|
||||
}
|
||||
|
||||
curTown_ = townId;
|
||||
mainActor_ = actor;
|
||||
townIt->second->AddCharacter(actor, area);
|
||||
AddChild(townIt->second);
|
||||
}
|
||||
|
||||
GameWorld* GameWorld::GetWorld() {
|
||||
return dynamic_cast<GameWorld*>(SceneManager::get().GetCurrentScene());
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -13,11 +13,13 @@
|
||||
#include <frostbite2D/core/task_system.h>
|
||||
#include <frostbite2D/graphics/font_manager.h>
|
||||
#include <frostbite2D/resource/asset.h>
|
||||
#include <frostbite2D/resource/audio_database.h>
|
||||
#include <frostbite2D/resource/npk_archive.h>
|
||||
#include <frostbite2D/resource/pvf_archive.h>
|
||||
#include <frostbite2D/resource/sound_pack_archive.h>
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
#include "Actor/GameWorld.h"
|
||||
|
||||
using namespace frostbite2D;
|
||||
|
||||
@@ -96,13 +98,16 @@ int main(int argc, char **argv) {
|
||||
archive.setSoundPackDirectory("assets/SoundPacks");
|
||||
archive.init();
|
||||
|
||||
auto &audioDatabase = AudioDatabase::get();
|
||||
audioDatabase.loadFromFile("assets/audio.xml");
|
||||
|
||||
return "后台资源加载成功";
|
||||
},
|
||||
[](std::string message) mutable {
|
||||
SDL_Log("后台资源加载成功");
|
||||
|
||||
auto TestScene = MakePtr<Scene>();
|
||||
SceneManager::get().PushScene(TestScene);
|
||||
auto gameWorld = MakePtr<GameWorld>();
|
||||
SceneManager::get().ReplaceScene(gameWorld);
|
||||
},
|
||||
[](std::exception_ptr error) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user