feat: 实现游戏地图和城镇系统基础架构

新增GameMap和GameTown类实现游戏地图和城镇的核心功能
添加GameWorld作为游戏世界管理器处理场景切换
完善音频系统支持从路径加载音乐和音效
重构PVF资源系统增加路径规范化功能
添加.gitignore排除游戏资源目录
This commit is contained in:
2026-04-01 09:53:06 +08:00
parent 42e5579cc3
commit d55808d80f
20 changed files with 1288 additions and 25 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ build/
参考代码/
*.pvf
nul
Game/assets/

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 要分割的字符串

View File

@@ -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()) {

View File

@@ -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()) {

View File

@@ -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;
}

View File

@@ -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.

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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
View 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

View 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
View 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

View 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

View File

@@ -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 {