feat: 实现游戏摄像机控制器并优化地图系统

重构地图系统,增加摄像机控制器管理相机行为。主要变更包括:
- 新增 GameCameraController 类,支持跟随目标和调试模式
- 重构 GameMap 类,分离相机逻辑到控制器
- 优化地图资源加载和同步逻辑
- 改进动画系统的事件处理
- 添加地图测试场景用于快速验证
This commit is contained in:
2026-04-02 20:07:42 +08:00
parent d55808d80f
commit ec16aeffa6
27 changed files with 16891 additions and 15694 deletions

View File

@@ -33,6 +33,8 @@ public:
virtual void Update(float deltaTime); virtual void Update(float deltaTime);
virtual void Render(); virtual void Render();
virtual void OnUpdate(float deltaTime) {}
virtual void OnAdded(Actor* parent) {}
void AddChild(RefPtr<Actor> child); void AddChild(RefPtr<Actor> child);
void RemoveChild(RefPtr<Actor> child); void RemoveChild(RefPtr<Actor> child);

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <frostbite2D/2d/actor.h> #include <frostbite2D/2d/actor.h>
#include <frostbite2D/2d/sprite.h>
#include <frostbite2D/animation/animation_data.h> #include <frostbite2D/animation/animation_data.h>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -30,8 +31,8 @@ public:
void Init(const std::string& aniPath); void Init(const std::string& aniPath);
void Update(float deltaTime) override; void OnUpdate(float deltaTime) override;
void OnAdded(Actor* parent); void OnAdded(Actor* parent) override;
void SetVisible(bool visible); void SetVisible(bool visible);
public: public:

View File

@@ -15,6 +15,8 @@ public:
const Vec2& getPosition() const { return position_; } const Vec2& getPosition() const { return position_; }
float getZoom() const { return zoom_; } float getZoom() const { return zoom_; }
int getViewportWidth() const { return viewportWidth_; }
int getViewportHeight() const { return viewportHeight_; }
void lookAt(const Vec2& target); void lookAt(const Vec2& target);
void move(const Vec2& delta); void move(const Vec2& delta);

View File

@@ -142,6 +142,7 @@ Actor::~Actor() {
void Actor::Update(float deltaTime) { void Actor::Update(float deltaTime) {
executeUpdateListeners(UpdatePhase::BeforeChildren, deltaTime); executeUpdateListeners(UpdatePhase::BeforeChildren, deltaTime);
OnUpdate(deltaTime);
UpdateChildren(deltaTime); UpdateChildren(deltaTime);
executeUpdateListeners(UpdatePhase::AfterChildren, deltaTime); executeUpdateListeners(UpdatePhase::AfterChildren, deltaTime);
} }
@@ -188,6 +189,7 @@ void Actor::AddChild(Ptr<Actor> child) {
child->SetParent(this); child->SetParent(this);
child->SetScene(scene_); child->SetScene(scene_);
insertChildByZOrder(child); insertChildByZOrder(child);
child->OnAdded(this);
} }
void Actor::SetZOrder(int zOrder) { void Actor::SetZOrder(int zOrder) {

View File

@@ -91,7 +91,14 @@ void Animation::Init(const std::string& aniPath) {
std::string dir = (lastSlash != std::string::npos) ? aniPath.substr(0, lastSlash + 1) : ""; std::string dir = (lastSlash != std::string::npos) ? aniPath.substr(0, lastSlash + 1) : "";
for (auto& ani : alsInfo->aniList) { for (auto& ani : alsInfo->aniList) {
auto subAni = MakePtr<Animation>(dir + ani.second.path); std::string subAniPath = dir + ani.second.path;
auto subAni = MakePtr<Animation>(subAniPath);
if (!subAni || !subAni->IsUsable()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Animation: failed to load ALS child animation %s",
subAniPath.c_str());
continue;
}
if (ani.second.layer.size() >= 2) { if (ani.second.layer.size() >= 2) {
subAni->SetZOrder(ani.second.layer[1]); subAni->SetZOrder(ani.second.layer[1]);
} }
@@ -103,8 +110,21 @@ void Animation::Init(const std::string& aniPath) {
FlushFrame(0); FlushFrame(0);
} }
void Animation::Update(float deltaTime) { void Animation::OnUpdate(float deltaTime) {
if (usable_ && IsVisible()) { if (!IsVisible()) {
for (auto& sp : spriteFrames_) {
sp->SetVisible(false);
}
return;
}
if (!usable_) {
if (currentFrame_) {
currentFrame_->SetVisible(true);
}
return;
}
float dtMs = deltaTime * 1000.0f; float dtMs = deltaTime * 1000.0f;
currentFrameTime_ += dtMs; currentFrameTime_ += dtMs;
@@ -118,6 +138,10 @@ void Animation::Update(float deltaTime) {
FlushFrame(0); FlushFrame(0);
} else { } else {
usable_ = false; usable_ = false;
currentFrameTime_ = 0.0f;
if (currentFrame_) {
currentFrame_->SetVisible(true);
}
if (endCallback_) { if (endCallback_) {
endCallback_(); endCallback_();
} }
@@ -125,11 +149,6 @@ void Animation::Update(float deltaTime) {
} }
} }
} }
} else {
for (auto& sp : spriteFrames_) {
sp->SetVisible(false);
}
}
} }
void Animation::OnAdded(Actor* parent) { void Animation::OnAdded(Actor* parent) {

View File

@@ -1,6 +1,7 @@
#include <frostbite2D/animation/animation_data.h> #include <frostbite2D/animation/animation_data.h>
#include <frostbite2D/resource/binary_reader.h> #include <frostbite2D/resource/binary_reader.h>
#include <frostbite2D/resource/pvf_archive.h> #include <frostbite2D/resource/pvf_archive.h>
#include <frostbite2D/resource/script_parser.h>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <sstream> #include <sstream>
@@ -368,12 +369,46 @@ std::optional<AlsInfo> parseAlsFromPvf(const std::string& path) {
return std::nullopt; return std::nullopt;
} }
auto content = pvf.getFileContent(path); auto rawData = pvf.getFileRawData(path);
if (!content) { if (!rawData) {
return std::nullopt; return std::nullopt;
} }
return parseAlsInfo(*content); ScriptParser parser(*rawData, path);
if (!parser.isValid()) {
return std::nullopt;
}
AlsInfo info;
while (!parser.isEnd()) {
auto segmentValue = parser.next();
if (!segmentValue || segmentValue->type != ScriptValueType::String) {
continue;
}
std::string segment = toLowerCase(segmentValue->toString());
if (segment == "[use animation]") {
auto aniPath = parser.next();
auto aniKey = parser.next();
if (!aniPath || !aniKey) {
break;
}
info.aniList[aniKey->toString()].path = toLowerCase(aniPath->toString());
} else if (segment == "[none effect add]" || segment == "[add]") {
auto layer1 = parser.next();
auto layer2 = parser.next();
auto aniKey = parser.next();
if (!layer1 || !layer2 || !aniKey) {
break;
}
info.aniList[aniKey->toString()].layer = {
std::stoi(layer1->toString()),
std::stoi(layer2->toString())};
}
}
return info;
} }
} // namespace animation } // namespace animation

View File

@@ -6,6 +6,25 @@
namespace frostbite2D { namespace frostbite2D {
namespace {
std::string resolveScriptString(PvfArchive& archive, const std::string& fileType, int32 key) {
auto binStr = archive.getBinString(key);
if (!binStr || binStr->empty()) {
return "";
}
if (auto loadStr = archive.getLoadString(fileType, *binStr)) {
if (!loadStr->empty()) {
return *loadStr;
}
}
return *binStr;
}
} // namespace
std::string ScriptValue::toString() const { std::string ScriptValue::toString() const {
switch (type) { switch (type) {
case ScriptValueType::Integer: case ScriptValueType::Integer:
@@ -174,9 +193,7 @@ std::optional<ScriptValue> ScriptParser::parseValueAt(size_t offset) const {
case ScriptOpcode::StringRef7: case ScriptOpcode::StringRef7:
case ScriptOpcode::StringRef8: { case ScriptOpcode::StringRef8: {
result.type = ScriptValueType::String; result.type = ScriptValueType::String;
if (auto str = archive.getBinString(data)) { result.stringValue = resolveScriptString(archive, fileType_, data);
result.stringValue = *str;
}
break; break;
} }
@@ -186,27 +203,14 @@ std::optional<ScriptValue> ScriptParser::parseValueAt(size_t offset) const {
uint8 newOpcode = readByte(offset + 5); uint8 newOpcode = readByte(offset + 5);
(void)newOpcode; (void)newOpcode;
int32 newData = readInt32(offset + 6); int32 newData = readInt32(offset + 6);
result.stringValue = resolveScriptString(archive, fileType_, newData);
if (auto binStr = archive.getBinString(newData)) {
if (!binStr->empty()) {
if (auto loadStr = archive.getLoadString(fileType_, *binStr)) {
result.stringValue = *loadStr;
}
}
}
} }
break; break;
} }
case ScriptOpcode::ExtendedString10: { case ScriptOpcode::ExtendedString10: {
result.type = ScriptValueType::String; result.type = ScriptValueType::String;
if (auto binStr = archive.getBinString(data)) { result.stringValue = resolveScriptString(archive, fileType_, data);
if (!binStr->empty()) {
if (auto loadStr = archive.getLoadString(fileType_, *binStr)) {
result.stringValue = *loadStr;
}
}
}
break; break;
} }

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,6 @@ struct MoveAreaTarget {
struct TileInfo { struct TileInfo {
std::string spritePath; std::string spritePath;
int spriteIndex = 0; int spriteIndex = 0;
int imgPos = 0;
}; };
struct MapConfig { struct MapConfig {
@@ -52,6 +51,7 @@ struct MapConfig {
std::string mapPath; std::string mapPath;
std::string mapDir; std::string mapDir;
int backgroundPos = 0; int backgroundPos = 0;
Vec2 tilePos = Vec2::Zero();
int farSightScroll = 100; int farSightScroll = 100;
bool hasCameraLimit = false; bool hasCameraLimit = false;
int cameraLimitMinX = -1; int cameraLimitMinX = -1;

View File

@@ -8,6 +8,15 @@
namespace frostbite2D { namespace frostbite2D {
/**
* @brief 地图运行时容器,负责把 map 配置组织成可显示/可交互的场景结构
*
* 这里不直接解析底层资源格式,而是消费 `GameDataLoader` 已经整理好的
* `MapConfig/TileInfo`。GameMap 主要负责三件事:
* 1. 按固定图层顺序挂载地板、背景动画、场景动画和动态对象
* 2. 根据 camera limit 和可移动区域维护摄像机关注点
* 3. 提供移动判定和切图区域查询给上层角色/场景逻辑使用
*/
class GameMap : public Actor { class GameMap : public Actor {
public: public:
using MapMoveArea = game::MoveAreaTarget; using MapMoveArea = game::MoveAreaTarget;
@@ -15,40 +24,65 @@ public:
GameMap(); GameMap();
~GameMap() override = default; ~GameMap() override = default;
/// 读取地图配置并重建当前地图内容。重复调用时会先清空旧地图运行态。
bool LoadMap(const std::string& mapName); bool LoadMap(const std::string& mapName);
/// 地图进入场景后的初始化入口,目前主要负责音乐播放。
void Enter(); void Enter();
void Update(float deltaTime) override; void Update(float deltaTime) override;
/// 将运行时对象挂到 normal 层,并按 y 值设置基础排序。
void AddObject(RefPtr<Actor> object); void AddObject(RefPtr<Actor> object);
/// 返回地图推荐的默认相机关注点。
Vec2 GetDefaultCameraFocus() const;
/// 根据 camera limit 和缩放约束修正相机关注点。
Vec2 ClampCameraFocus(const Vec2& focus, float zoom) const;
/// 将各层转换到屏幕空间;远景层在这里做视差滚动。
void ApplyCameraFocus(const Vec2& focus);
/// 根据虚拟可行走区域限制位移,返回修正后的目标坐标。
Vec3 CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const; Vec3 CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const;
/// 检查当前位置是否进入 town move area用于切图/传送判定。
MapMoveArea CheckIsItMoveArea(const Vec3& curPos) const; MapMoveArea CheckIsItMoveArea(const Vec3& curPos) const;
const std::vector<MapMoveArea>& GetMoveAreaInfo() const; const std::vector<MapMoveArea>& GetMoveAreaInfo() const;
Rect GetMovablePositionArea(size_t index) const; Rect GetMovablePositionArea(size_t index) const;
int GetMapLength() const { return mapLength_; } int GetBackgroundRepeatWidth() const { return backgroundRepeatWidth_; }
int GetMapHeight() const { return mapHeight_; }
private: private:
/// 初始化固定图层。图层名字与 DNF 地图层概念保持一致。
void initLayers(); void initLayers();
/// 清理当前地图的运行态节点和缓存数据,但保留 GameMap 自身及图层骨架。
void clearLayerChildren(); void clearLayerChildren();
/// 创建普通 tile 和扩展 tile并推导背景平铺需要的横向覆盖宽度。
void InitTile(); void InitTile();
/// 初始化背景动画层,必要时按横向覆盖宽度做横向平铺。
void InitBackgroundAnimation(); void InitBackgroundAnimation();
/// 初始化地图对象动画(门、特效、场景摆件等)。
void InitMapAnimation(); void InitMapAnimation();
/// 初始化角色移动用的可行走区域。
void InitVirtualMovableArea(); void InitVirtualMovableArea();
/// 初始化切图/传送区域。
void InitMoveArea(); void InitMoveArea();
void updateCamera(); /// 将各层转换到屏幕空间;远景层在这里做视差滚动。
void updateLayerPositions(const Vec2& cameraPos); void updateLayerPositions(const Vec2& cameraFocus);
/// 原始地图配置,作为运行时装配地图内容的输入。
game::MapConfig mapConfig_; game::MapConfig mapConfig_;
/// 地图的固定图层集合key 为地图层名。
std::unordered_map<std::string, RefPtr<GameMapLayer>> layerMap_; std::unordered_map<std::string, RefPtr<GameMapLayer>> layerMap_;
/// bottom 层里的专用地板容器,固定放在该层最底部,避免地图动画被地板压住。
RefPtr<Actor> tileRoot_;
/// 角色可行走矩形区域。
std::vector<Rect> movableArea_; std::vector<Rect> movableArea_;
/// 进入后会触发切图/传送的矩形区域。
std::vector<Rect> moveArea_; std::vector<Rect> moveArea_;
int mapLength_ = 0; /// 地板铺设后推导出的横向覆盖宽度,用于背景动画横向平铺。
int mapHeight_ = 0; int backgroundRepeatWidth_ = 0;
/// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。
int mapOffsetY_ = 0; int mapOffsetY_ = 0;
bool debugMode_ = false; bool debugMode_ = false;
Vec2 cameraFocus_ = Vec2::Zero(); /// 当前地图正在播放的背景音乐。
Ptr<Music> currentMusic_; Ptr<Music> currentMusic_;
}; };

View File

@@ -0,0 +1,24 @@
#pragma once
#include "Camera/GameCameraController.h"
#include "GameMap.h"
#include <frostbite2D/scene/scene.h>
namespace frostbite2D {
class GameMapTestScene : public Scene {
public:
GameMapTestScene();
~GameMapTestScene() override = default;
void onEnter() override;
void onExit() override;
void Update(float deltaTime) override;
private:
GameCameraController cameraController_;
bool initialized_ = false;
RefPtr<GameMap> map_;
};
} // namespace frostbite2D

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "Camera/GameCameraController.h"
#include "GameDataLoader.h" #include "GameDataLoader.h"
#include "GameMap.h" #include "GameMap.h"
#include <frostbite2D/2d/actor.h> #include <frostbite2D/2d/actor.h>
@@ -38,6 +39,7 @@ private:
Vec2 sariaRoomPos_ = Vec2(-1.0f, -1.0f); Vec2 sariaRoomPos_ = Vec2(-1.0f, -1.0f);
std::vector<MapInfo> mapList_; std::vector<MapInfo> mapList_;
int curMapIndex_ = -1; int curMapIndex_ = -1;
GameCameraController cameraController_;
}; };
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -0,0 +1,53 @@
#pragma once
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/types/type_math.h>
struct _SDL_GameController;
typedef struct _SDL_GameController SDL_GameController;
namespace frostbite2D {
class GameMap;
class GameCameraController {
public:
GameCameraController() = default;
~GameCameraController();
void SetMap(GameMap* map);
void SetTarget(Actor* target);
void ClearTarget();
void SetDebugEnabled(bool enabled);
bool IsDebugEnabled() const { return debugEnabled_; }
void SetZoom(float zoom);
float GetZoom() const { return zoom_; }
void SetFocus(const Vec2& focus);
const Vec2& GetFocus() const { return focus_; }
void SnapToDefaultFocus();
void Update(float deltaTime);
private:
void openDebugController();
void closeDebugController();
void updateDebugInput(float deltaTime);
void applyFocus() const;
GameMap* map_ = nullptr;
Actor* target_ = nullptr;
SDL_GameController* debugController_ = nullptr;
Vec2 focus_ = Vec2::Zero();
bool debugEnabled_ = false;
bool initialized_ = false;
float zoom_ = 1.2f;
float followLerpSpeed_ = 8.0f;
float debugMoveSpeed_ = 800.0f;
};
} // namespace frostbite2D

View File

@@ -80,6 +80,12 @@ public:
return tokens_[index_++]; return tokens_[index_++];
} }
void back() {
if (index_ > 0) {
--index_;
}
}
private: private:
std::string path_; std::string path_;
std::vector<std::string> tokens_; std::vector<std::string> tokens_;
@@ -176,6 +182,9 @@ bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig) {
std::string segment = stream.get(); std::string segment = stream.get();
if (segment == "[background pos]") { if (segment == "[background pos]") {
outConfig.backgroundPos = toInt(stream.get()); outConfig.backgroundPos = toInt(stream.get());
} else if (segment == "[tile pos]") {
outConfig.tilePos.x = static_cast<float>(toInt(stream.get(), 0));
outConfig.tilePos.y = static_cast<float>(toInt(stream.get(), 0));
} else if (segment == "[map name]") { } else if (segment == "[map name]") {
outConfig.name = stream.get(); outConfig.name = stream.get();
} else if (segment == "[limit map camera move]") { } else if (segment == "[limit map camera move]") {
@@ -273,6 +282,17 @@ bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig) {
} }
} }
if (!outConfig.hasCameraLimit) {
outConfig.hasCameraLimit = true;
outConfig.cameraLimitMinX = 0;
outConfig.cameraLimitMaxX = 0;
outConfig.cameraLimitMinY = 0;
outConfig.cameraLimitMaxY = 0;
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameDataLoader: map %s missing [limit map camera move], fallback to 0 0 0 0",
outConfig.mapPath.c_str());
}
return true; return true;
} }
@@ -283,15 +303,17 @@ bool loadTileInfo(const std::string& path, TileInfo& outInfo) {
} }
outInfo = TileInfo(); outInfo = TileInfo();
size_t slashPos = path.find_last_of('/');
std::string baseDir = slashPos == std::string::npos ? "" : path.substr(0, slashPos + 1);
while (!stream.isEnd()) { while (!stream.isEnd()) {
std::string segment = stream.get(); std::string segment = toLowerCase(stream.get());
if (segment == "[image]") { if (segment == "[image]") {
outInfo.spritePath = resolveSpritePath(baseDir, stream.get()); std::string rawImagePath = stream.get();
outInfo.spritePath = normalizeWithPrefix("sprite", rawImagePath);
outInfo.spriteIndex = toInt(stream.get()); outInfo.spriteIndex = toInt(stream.get());
} else if (segment == "[img pos]") { if (outInfo.spritePath.empty()) {
outInfo.imgPos = toInt(stream.get()); SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameDataLoader: tile image path resolved empty for %s, raw=%s",
path.c_str(), rawImagePath.c_str());
}
} }
} }

View File

@@ -12,6 +12,7 @@ namespace frostbite2D {
namespace { namespace {
// 图层名和层级顺序与原始地图资源的层语义保持一致,方便直接按 layer 字段挂载。
static const char* kLayerNames[] = { static const char* kLayerNames[] = {
"contact", "distantback", "middleback", "bottom", "closeback", "contact", "distantback", "middleback", "bottom", "closeback",
"normal", "close", "cover", "max"}; "normal", "close", "cover", "max"};
@@ -19,11 +20,11 @@ static const char* kLayerNames[] = {
static const int kLayerOrders[] = { static const int kLayerOrders[] = {
10000, 50000, 100000, 150000, 200000, 250000, 300000, 350000, 400000}; 10000, 50000, 100000, 150000, 200000, 250000, 300000, 350000, 400000};
constexpr float kScreenWidth = 1280.0f; constexpr int kTileRootZOrder = -1000000;
constexpr float kScreenHeight = 720.0f;
constexpr float kTileSpacing = 224.0f;
constexpr float kExtendedTileStepY = 120.0f; constexpr float kExtendedTileStepY = 120.0f;
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
// 占位,避免整张地图中断。
Ptr<Sprite> createMapSprite(const std::string& path, int index) { Ptr<Sprite> createMapSprite(const std::string& path, int index) {
auto sprite = Sprite::createFromNpk(path, static_cast<size_t>(index)); auto sprite = Sprite::createFromNpk(path, static_cast<size_t>(index));
if (!sprite) { if (!sprite) {
@@ -56,41 +57,37 @@ void GameMap::clearLayerChildren() {
layer->RemoveAllChildren(); layer->RemoveAllChildren();
} }
} }
tileRoot_.Reset();
movableArea_.clear(); movableArea_.clear();
moveArea_.clear(); moveArea_.clear();
currentMusic_.Reset(); currentMusic_.Reset();
mapLength_ = 0; backgroundRepeatWidth_ = 0;
mapHeight_ = 0;
} }
bool GameMap::LoadMap(const std::string& mapName) { bool GameMap::LoadMap(const std::string &mapName) {
// 清空所有图层子节点。
clearLayerChildren(); clearLayerChildren();
// 读取PVF地图配置。
if (!game::loadMapConfig(mapName, mapConfig_)) { if (!game::loadMapConfig(mapName, mapConfig_)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to load map %s", SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to load map %s",
mapName.c_str()); mapName.c_str());
return false; return false;
} }
// backgroundPos 会影响地图整体的视觉基线,所以地板和各显示层都会参考它。
mapOffsetY_ = mapConfig_.backgroundPos; mapOffsetY_ = mapConfig_.backgroundPos;
// 加载顺序基本就是地图的组装顺序:先地板,再背景,再对象,再可行走数据。
InitTile(); InitTile();
InitBackgroundAnimation(); InitBackgroundAnimation();
InitMapAnimation(); InitMapAnimation();
InitVirtualMovableArea(); // InitVirtualMovableArea();
InitMoveArea(); // 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; return true;
} }
void GameMap::Enter() { void GameMap::Enter() {
// 地图进入时尝试找到第一个可播放的 BGM 并循环播放。
if (!mapConfig_.soundIds.empty()) { if (!mapConfig_.soundIds.empty()) {
auto& audioDatabase = AudioDatabase::get(); auto& audioDatabase = AudioDatabase::get();
for (const auto& soundId : mapConfig_.soundIds) { for (const auto& soundId : mapConfig_.soundIds) {
@@ -107,25 +104,30 @@ void GameMap::Enter() {
} }
} }
} }
updateCamera();
} }
void GameMap::Update(float deltaTime) { void GameMap::Update(float deltaTime) { Actor::Update(deltaTime); }
Actor::Update(deltaTime);
(void)deltaTime;
updateCamera();
}
void GameMap::InitTile() { void GameMap::InitTile() {
if (mapConfig_.tilePaths.empty() && mapConfig_.extendedTilePaths.empty()) { if (mapConfig_.tilePaths.empty() && mapConfig_.extendedTilePaths.empty()) {
return; return;
} }
auto bottomIt = layerMap_.find("bottom");
if (bottomIt == layerMap_.end() || !bottomIt->second) {
return;
}
tileRoot_ = MakePtr<Actor>();
tileRoot_->SetName("tileRoot");
tileRoot_->SetZOrder(kTileRootZOrder);
bottomIt->second->AddChild(tileRoot_);
float maxBaseHeight = 0.0f; float maxBaseHeight = 0.0f;
float maxTotalBottom = 0.0f; float maxOffset = mapConfig_.tilePos.y;
float maxOffset = 0.0f;
int normalTileCount = static_cast<int>(mapConfig_.tilePaths.size()); int normalTileCount = static_cast<int>(mapConfig_.tilePaths.size());
float nextNormalX = mapConfig_.tilePos.x;
for (size_t i = 0; i < mapConfig_.tilePaths.size(); ++i) { for (size_t i = 0; i < mapConfig_.tilePaths.size(); ++i) {
game::TileInfo info; game::TileInfo info;
@@ -133,43 +135,45 @@ void GameMap::InitTile() {
continue; continue;
} }
auto sprite = createMapSprite(info.spritePath.empty() ? "sprite/character/common/circlecooltime.img" std::string spritePath = info.spritePath.empty()
: info.spritePath, ? "sprite/character/common/circlecooltime.img"
info.spriteIndex); : info.spritePath;
sprite->SetPosition(static_cast<float>(i) * kTileSpacing, static_cast<float>(info.imgPos)); auto sprite = createMapSprite(spritePath, info.spriteIndex);
layerMap_["bottom"]->AddChild(sprite); float posX = nextNormalX;
float posY = mapConfig_.tilePos.y;
sprite->SetPosition(posX, posY);
tileRoot_->AddChild(sprite);
Vec2 size = sprite->GetSize(); Vec2 size = sprite->GetSize();
maxOffset = std::max(maxOffset, static_cast<float>(info.imgPos)); maxOffset = std::max(maxOffset, posY);
maxBaseHeight = std::max(maxBaseHeight, size.y); maxBaseHeight = std::max(maxBaseHeight, size.y);
mapLength_ = std::max(mapLength_, static_cast<int>(i * kTileSpacing + size.x)); backgroundRepeatWidth_ =
maxTotalBottom = std::max(maxTotalBottom, info.imgPos + size.y); std::max(backgroundRepeatWidth_, static_cast<int>(posX + size.x));
} nextNormalX += size.x;
if (normalTileCount > 0 && mapLength_ == 0) {
mapLength_ = static_cast<int>(normalTileCount * kTileSpacing);
} }
float nextExtendedX = mapConfig_.tilePos.x;
for (size_t i = 0; i < mapConfig_.extendedTilePaths.size(); ++i) { for (size_t i = 0; i < mapConfig_.extendedTilePaths.size(); ++i) {
game::TileInfo info; game::TileInfo info;
if (!game::loadTileInfo(mapConfig_.extendedTilePaths[i], info)) { if (!game::loadTileInfo(mapConfig_.extendedTilePaths[i], info)) {
continue; continue;
} }
auto sprite = createMapSprite(info.spritePath.empty() ? "sprite/character/common/circlecooltime.img" std::string spritePath = info.spritePath.empty()
: info.spritePath, ? "sprite/character/common/circlecooltime.img"
info.spriteIndex); : info.spritePath;
auto sprite = createMapSprite(spritePath, info.spriteIndex);
int row = normalTileCount > 0 ? static_cast<int>(i) / normalTileCount : 0; int row = normalTileCount > 0 ? static_cast<int>(i) / normalTileCount : 0;
float posX = nextExtendedX;
float posY = maxOffset + maxBaseHeight + row * kExtendedTileStepY; float posY = maxOffset + maxBaseHeight + row * kExtendedTileStepY;
sprite->SetPosition(static_cast<float>(i) * kTileSpacing, posY); sprite->SetPosition(posX, posY);
layerMap_["bottom"]->AddChild(sprite); tileRoot_->AddChild(sprite);
Vec2 size = sprite->GetSize(); Vec2 size = sprite->GetSize();
mapLength_ = std::max(mapLength_, static_cast<int>(i * kTileSpacing + size.x)); backgroundRepeatWidth_ =
maxTotalBottom = std::max(maxTotalBottom, posY + size.y); std::max(backgroundRepeatWidth_, static_cast<int>(posX + size.x));
nextExtendedX += size.x;
} }
mapHeight_ = std::max(mapHeight_, static_cast<int>(maxTotalBottom));
} }
void GameMap::InitBackgroundAnimation() { void GameMap::InitBackgroundAnimation() {
@@ -179,11 +183,15 @@ void GameMap::InitBackgroundAnimation() {
continue; continue;
} }
// 远景背景通常需要横向重复铺满地图;滚动倍率越大,需要铺的份数也越多。
int repeatCount = 1; int repeatCount = 1;
Vec2 animationSize = animation->GetSize(); Vec2 animationSize = animation->GetSize();
float rate = std::max(1.0f, mapConfig_.farSightScroll / 100.0f); float rate = std::max(1.0f, mapConfig_.farSightScroll / 100.0f);
if (animationSize.x > 0.0f && mapLength_ > 0) { if (animationSize.x > 0.0f && backgroundRepeatWidth_ > 0) {
repeatCount = std::max(1, static_cast<int>((mapLength_ * rate) / animationSize.x) + 1); repeatCount =
std::max(1, static_cast<int>((backgroundRepeatWidth_ * rate) /
animationSize.x) +
1);
} }
auto layerIt = layerMap_.find(ani.layer); auto layerIt = layerMap_.find(ani.layer);
@@ -196,6 +204,7 @@ void GameMap::InitBackgroundAnimation() {
if (!backgroundAni || !backgroundAni->IsUsable()) { if (!backgroundAni || !backgroundAni->IsUsable()) {
continue; continue;
} }
// 背景动画在各自层内从左到右平铺zOrder 放到极低,避免压住该层其他内容。
backgroundAni->SetPosition(i * animationSize.x, -120.0f); backgroundAni->SetPosition(i * animationSize.x, -120.0f);
backgroundAni->SetZOrder(-1000000); backgroundAni->SetZOrder(-1000000);
layerIt->second->AddChild(backgroundAni); layerIt->second->AddChild(backgroundAni);
@@ -210,8 +219,10 @@ void GameMap::InitMapAnimation() {
continue; continue;
} }
// 地图对象的显示 y 会扣掉 zPos等价于把“离地高度”折算回屏幕坐标。
animation->SetPosition(static_cast<float>(ani.xPos), animation->SetPosition(static_cast<float>(ani.xPos),
static_cast<float>(ani.yPos - ani.zPos)); static_cast<float>(ani.yPos - ani.zPos));
// 同层内继续按 y 排序,保证越靠下的物件越靠前。
animation->SetZOrder(ani.yPos); animation->SetZOrder(ani.yPos);
auto layerIt = layerMap_.find(ani.layer); auto layerIt = layerMap_.find(ani.layer);
@@ -223,6 +234,7 @@ void GameMap::InitMapAnimation() {
void GameMap::InitVirtualMovableArea() { void GameMap::InitVirtualMovableArea() {
movableArea_ = mapConfig_.virtualMovableAreas; movableArea_ = mapConfig_.virtualMovableAreas;
// debugMode 打开时,把可行走区域可视化到最高层,方便校验地图配置。
if (!debugMode_) { if (!debugMode_) {
return; return;
} }
@@ -234,6 +246,7 @@ void GameMap::InitVirtualMovableArea() {
void GameMap::InitMoveArea() { void GameMap::InitMoveArea() {
moveArea_ = mapConfig_.townMovableAreas; moveArea_ = mapConfig_.townMovableAreas;
// move area 和普通 movable area 分开展示,方便区分“能走”与“会触发切图”。
if (!debugMode_) { if (!debugMode_) {
return; return;
} }
@@ -243,55 +256,83 @@ void GameMap::InitMoveArea() {
} }
} }
void GameMap::updateCamera() { Vec2 GameMap::GetDefaultCameraFocus() const {
float centerX = static_cast<float>(mapConfig_.cameraLimitMinX +
mapConfig_.cameraLimitMaxX) *
0.5f;
float centerY = static_cast<float>(mapConfig_.cameraLimitMinY +
mapConfig_.cameraLimitMaxY) *
0.5f;
return Vec2(centerX, centerY);
}
Vec2 GameMap::ClampCameraFocus(const Vec2 &focus, float zoom) const {
Camera* camera = Renderer::get().getCamera(); Camera* camera = Renderer::get().getCamera();
float targetX = focus.x;
float targetY = focus.y;
float halfWidth = 0.0f;
float halfHeight = 0.0f;
if (camera) {
float safeZoom = std::max(zoom, 0.01f);
halfWidth =
static_cast<float>(camera->getViewportWidth()) * 0.5f / safeZoom;
halfHeight =
static_cast<float>(camera->getViewportHeight()) * 0.5f / safeZoom;
}
float minFocusX = static_cast<float>(mapConfig_.cameraLimitMinX) + halfWidth;
float maxFocusX = static_cast<float>(mapConfig_.cameraLimitMaxX) - halfWidth;
float minFocusY = static_cast<float>(mapConfig_.cameraLimitMinY) + halfHeight;
float maxFocusY = static_cast<float>(mapConfig_.cameraLimitMaxY) - halfHeight;
if (minFocusX > maxFocusX) {
targetX = (static_cast<float>(mapConfig_.cameraLimitMinX) +
static_cast<float>(mapConfig_.cameraLimitMaxX)) *
0.5f;
} else {
targetX = std::clamp(targetX, minFocusX, maxFocusX);
}
if (minFocusY > maxFocusY) {
targetY = (static_cast<float>(mapConfig_.cameraLimitMinY) +
static_cast<float>(mapConfig_.cameraLimitMaxY)) *
0.5f;
} else {
targetY = std::clamp(targetY, minFocusY, maxFocusY);
}
return Vec2(targetX, targetY);
}
void GameMap::ApplyCameraFocus(const Vec2 &focus) {
updateLayerPositions(focus);
}
void GameMap::updateLayerPositions(const Vec2 &cameraFocus) {
(void)cameraFocus;
Camera *camera = Renderer::get().getCamera();
if (!camera) { if (!camera) {
return; return;
} }
float halfWidth = kScreenWidth * 0.5f; Vec2 cameraPos = camera->getPosition();
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;
// 主视图平移完全交给底层
// Camera这里只保留地图层自己的静态校准和少量视差偏移。
for (auto& entry : layerMap_) { for (auto& entry : layerMap_) {
const std::string& name = entry.first; const std::string& name = entry.first;
RefPtr<GameMapLayer>& layer = entry.second; RefPtr<GameMapLayer>& layer = entry.second;
float posX = -cameraTarget.x + halfWidth; float posX = 0.0f;
float posY = -cameraTarget.y + halfHeight + static_cast<float>(mapOffsetY_); float posY = static_cast<float>(mapOffsetY_);
// 远景层只做部分水平跟随;主 Camera
// 已经负责完整位移,这里补上“少动一些”的差值。
if (name == "distantback") { if (name == "distantback") {
posX *= static_cast<float>(mapConfig_.farSightScroll) / 100.0f; float scrollFactor =
static_cast<float>(mapConfig_.farSightScroll) / 100.0f;
posX = cameraPos.x * (1.0f - scrollFactor);
} }
layer->SetPosition(posX, posY); layer->SetPosition(posX, posY);
} }
@@ -301,6 +342,7 @@ void GameMap::AddObject(RefPtr<Actor> object) {
if (!object) { if (!object) {
return; return;
} }
// 动态对象默认进 normal 层,并沿用 2D 地图里常见的“按 y 值排序”规则。
object->SetZOrder(static_cast<int>(object->GetPosition().y)); object->SetZOrder(static_cast<int>(object->GetPosition().y));
layerMap_["normal"]->AddObject(object); layerMap_["normal"]->AddObject(object);
} }
@@ -315,6 +357,8 @@ Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const
float targetX = curPos.x + posOffset.x; float targetX = curPos.x + posOffset.x;
float targetY = curPos.y + posOffset.y; float targetY = curPos.y + posOffset.y;
// X/Y
// 分开判断,这样贴着边移动时不会因为一个方向越界而把另一个方向也一起锁死。
bool isXValid = false; bool isXValid = false;
for (const auto& area : movableArea_) { for (const auto& area : movableArea_) {
if (area.containsPoint(Vec2(targetX, curPos.y))) { if (area.containsPoint(Vec2(targetX, curPos.y))) {
@@ -342,6 +386,7 @@ Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const
} }
GameMap::MapMoveArea GameMap::CheckIsItMoveArea(const Vec3& curPos) const { GameMap::MapMoveArea GameMap::CheckIsItMoveArea(const Vec3& curPos) const {
// moveArea_ 和配置里的 target 是一一对应的,命中后直接返回同索引的目标信息。
for (size_t i = 0; i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size(); ++i) { for (size_t i = 0; i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size(); ++i) {
if (moveArea_[i].containsPoint(Vec2(curPos.x, curPos.y))) { if (moveArea_[i].containsPoint(Vec2(curPos.x, curPos.y))) {
return mapConfig_.townMovableAreaTargets[i]; return mapConfig_.townMovableAreaTargets[i];

View File

@@ -0,0 +1,48 @@
#include "Actor/GameMapTestScene.h"
#include <SDL2/SDL.h>
#include <frostbite2D/animation/animation.h>
namespace frostbite2D {
namespace {
constexpr char kTestMapPath[] = "map/elvengard/elvengard.map";
} // namespace
GameMapTestScene::GameMapTestScene() = default;
void GameMapTestScene::onEnter() {
Scene::onEnter();
if (initialized_) {
return;
}
map_ = MakePtr<GameMap>();
if (!map_->LoadMap(kTestMapPath)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GameMapTestScene: failed to load map %s", kTestMapPath);
map_.Reset();
return;
}
AddChild(map_);
cameraController_.SetMap(map_.Get());
cameraController_.SetZoom(1.2f);
cameraController_.ClearTarget();
cameraController_.SetDebugEnabled(true);
cameraController_.SnapToDefaultFocus();
map_->Enter();
initialized_ = true;
}
void GameMapTestScene::onExit() {
Scene::onExit();
}
void GameMapTestScene::Update(float deltaTime) {
Scene::Update(deltaTime);
cameraController_.Update(deltaTime);
}
} // namespace frostbite2D

View File

@@ -88,20 +88,30 @@ void GameTown::AddCharacter(RefPtr<Actor> actor, int areaIndex) {
} }
AddChild(mapIt->map); AddChild(mapIt->map);
mapIt->map->Enter(); cameraController_.SetMap(mapIt->map.Get());
cameraController_.SetZoom(1.2f);
cameraController_.SetTarget(actor.Get());
cameraController_.SnapToDefaultFocus();
if (actor) { if (actor) {
cameraController_.SetDebugEnabled(false);
if (areaIndex == -2 && sariaRoomPos_.x >= 0.0f && sariaRoomPos_.y >= 0.0f) { if (areaIndex == -2 && sariaRoomPos_.x >= 0.0f && sariaRoomPos_.y >= 0.0f) {
actor->SetPosition(sariaRoomPos_); actor->SetPosition(sariaRoomPos_);
} }
mapIt->map->AddObject(actor); mapIt->map->AddObject(actor);
cameraController_.SetFocus(actor->GetPosition());
} else {
cameraController_.SetDebugEnabled(true);
} }
mapIt->map->Enter();
curMapIndex_ = mapIt->areaId; curMapIndex_ = mapIt->areaId;
} }
void GameTown::Update(float deltaTime) { void GameTown::Update(float deltaTime) {
Actor::Update(deltaTime); Actor::Update(deltaTime);
cameraController_.Update(deltaTime);
} }
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -13,7 +13,6 @@ void GameWorld::onEnter() {
return; return;
} }
SetScale(1.2f);
initialized_ = InitWorld(); initialized_ = InitWorld();
} }

View File

@@ -0,0 +1,193 @@
#include "Camera/GameCameraController.h"
#include "Actor/GameMap.h"
#include <SDL2/SDL.h>
#include <cmath>
#include <frostbite2D/graphics/camera.h>
#include <frostbite2D/graphics/renderer.h>
#include <algorithm>
namespace frostbite2D {
namespace {
constexpr float kScreenWidth = 1280.0f;
constexpr float kScreenHeight = 720.0f;
constexpr int kDebugStickDeadzone = 8000;
float normalizeControllerAxis(int16 value) {
float magnitude = static_cast<float>(value);
if (std::abs(magnitude) <= static_cast<float>(kDebugStickDeadzone)) {
return 0.0f;
}
float sign = magnitude < 0.0f ? -1.0f : 1.0f;
float normalized =
(std::abs(magnitude) - static_cast<float>(kDebugStickDeadzone)) /
(32767.0f - static_cast<float>(kDebugStickDeadzone));
return std::clamp(normalized, 0.0f, 1.0f) * sign;
}
}
GameCameraController::~GameCameraController() {
closeDebugController();
}
void GameCameraController::SetMap(GameMap* map) {
map_ = map;
initialized_ = false;
}
void GameCameraController::SetTarget(Actor* target) {
target_ = target;
}
void GameCameraController::ClearTarget() {
target_ = nullptr;
}
void GameCameraController::SetDebugEnabled(bool enabled) {
debugEnabled_ = enabled;
if (debugEnabled_) {
openDebugController();
} else {
closeDebugController();
}
}
void GameCameraController::SetZoom(float zoom) {
zoom_ = std::max(zoom, 0.01f);
if (map_) {
focus_ = map_->ClampCameraFocus(focus_, zoom_);
}
applyFocus();
}
void GameCameraController::SetFocus(const Vec2& focus) {
focus_ = map_ ? map_->ClampCameraFocus(focus, zoom_) : focus;
initialized_ = true;
applyFocus();
}
void GameCameraController::SnapToDefaultFocus() {
if (!map_) {
return;
}
focus_ = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), zoom_);
initialized_ = true;
applyFocus();
}
void GameCameraController::Update(float deltaTime) {
if (!map_) {
return;
}
if (!initialized_) {
SnapToDefaultFocus();
}
if (target_) {
Vec2 targetFocus = target_->GetPosition();
float t = std::clamp(deltaTime * followLerpSpeed_, 0.0f, 1.0f);
focus_ = focus_ + (targetFocus - focus_) * t;
} else if (debugEnabled_) {
updateDebugInput(deltaTime);
}
focus_ = map_->ClampCameraFocus(focus_, zoom_);
applyFocus();
}
void GameCameraController::openDebugController() {
if (debugController_ && SDL_GameControllerGetAttached(debugController_)) {
return;
}
closeDebugController();
int joystickCount = SDL_NumJoysticks();
for (int i = 0; i < joystickCount; ++i) {
if (!SDL_IsGameController(i)) {
continue;
}
debugController_ = SDL_GameControllerOpen(i);
if (debugController_) {
return;
}
}
}
void GameCameraController::closeDebugController() {
if (!debugController_) {
return;
}
SDL_GameControllerClose(debugController_);
debugController_ = nullptr;
}
void GameCameraController::updateDebugInput(float deltaTime) {
const uint8* keyboardState = SDL_GetKeyboardState(nullptr);
float moveX = 0.0f;
float moveY = 0.0f;
if (keyboardState) {
if (keyboardState[SDL_SCANCODE_A]) {
moveX -= 1.0f;
}
if (keyboardState[SDL_SCANCODE_D]) {
moveX += 1.0f;
}
if (keyboardState[SDL_SCANCODE_W]) {
moveY -= 1.0f;
}
if (keyboardState[SDL_SCANCODE_S]) {
moveY += 1.0f;
}
}
if (debugEnabled_ && !debugController_) {
openDebugController();
}
if (debugController_ && !SDL_GameControllerGetAttached(debugController_)) {
closeDebugController();
openDebugController();
}
if (debugController_ && SDL_GameControllerGetAttached(debugController_)) {
moveX += normalizeControllerAxis(
SDL_GameControllerGetAxis(debugController_, SDL_CONTROLLER_AXIS_LEFTX));
moveY += normalizeControllerAxis(
SDL_GameControllerGetAxis(debugController_, SDL_CONTROLLER_AXIS_LEFTY));
}
moveX = std::clamp(moveX, -1.0f, 1.0f);
moveY = std::clamp(moveY, -1.0f, 1.0f);
if (moveX == 0.0f && moveY == 0.0f) {
return;
}
focus_.x += moveX * debugMoveSpeed_ * deltaTime;
focus_.y += moveY * debugMoveSpeed_ * deltaTime;
}
void GameCameraController::applyFocus() const {
Camera* camera = Renderer::get().getCamera();
if (!camera || !map_) {
return;
}
float halfWidth = kScreenWidth * 0.5f / zoom_;
float halfHeight = kScreenHeight * 0.5f / zoom_;
Vec2 cameraPos(focus_.x - halfWidth, focus_.y - halfHeight);
camera->setZoom(zoom_);
camera->setPosition(cameraPos);
map_->ApplyCameraFocus(focus_);
}
} // namespace frostbite2D

View File

@@ -19,6 +19,7 @@
#include <frostbite2D/resource/sound_pack_archive.h> #include <frostbite2D/resource/sound_pack_archive.h>
#include <frostbite2D/scene/scene.h> #include <frostbite2D/scene/scene.h>
#include <frostbite2D/scene/scene_manager.h> #include <frostbite2D/scene/scene_manager.h>
#include "Actor/GameMapTestScene.h"
#include "Actor/GameWorld.h" #include "Actor/GameWorld.h"
using namespace frostbite2D; using namespace frostbite2D;
@@ -106,8 +107,8 @@ int main(int argc, char **argv) {
[](std::string message) mutable { [](std::string message) mutable {
SDL_Log("后台资源加载成功"); SDL_Log("后台资源加载成功");
auto gameWorld = MakePtr<GameWorld>(); auto testMapScene = MakePtr<GameMapTestScene>();
SceneManager::get().ReplaceScene(gameWorld); SceneManager::get().ReplaceScene(testMapScene);
}, },
[](std::exception_ptr error) { [](std::exception_ptr error) {
try { try {

View File

@@ -0,0 +1,49 @@
function syncAssets(source_dir, target_dir)
if not os.isdir(source_dir) then
return
end
local copied = 0
local skipped = 0
local removed = 0
local source_files = {}
for _, source_file in ipairs(os.files(path.join(source_dir, "**"))) do
local relative_path = path.relative(source_file, source_dir)
local target_file = path.join(target_dir, relative_path)
local target_subdir = path.directory(target_file)
local should_copy = not os.isfile(target_file)
source_files[relative_path] = true
if not should_copy then
local source_mtime = os.mtime(source_file)
local target_mtime = os.mtime(target_file)
local source_size = os.filesize(source_file)
local target_size = os.filesize(target_file)
should_copy = source_mtime > target_mtime or source_size ~= target_size
end
if should_copy then
if target_subdir ~= "." and not os.isdir(target_subdir) then
os.mkdir(target_subdir)
end
os.cp(source_file, target_file)
copied = copied + 1
else
skipped = skipped + 1
end
end
if os.isdir(target_dir) then
for _, target_file in ipairs(os.files(path.join(target_dir, "**"))) do
local relative_path = path.relative(target_file, target_dir)
if not source_files[relative_path] then
os.rm(target_file)
removed = removed + 1
end
end
end
print(string.format("Sync assets: %d copied, %d skipped, %d removed", copied, skipped, removed))
end

View File

@@ -24,15 +24,13 @@ target("Frostbite2D")
-- 复制着色器文件到输出目录 -- 复制着色器文件到输出目录
after_build(function (target) after_build(function (target)
local assetHelper = import("build_helpers.assets")
-- 复制 assets 目录 -- 复制 assets 目录
local assets_dir = path.join(os.projectdir(), "Game/assets") local assets_dir = path.join(os.projectdir(), "Game/assets")
local output_dir = target:targetdir() local output_dir = target:targetdir()
local target_assets_dir = path.join(output_dir, "assets") local target_assets_dir = path.join(output_dir, "assets")
if os.isdir(assets_dir) then assetHelper.syncAssets(assets_dir, target_assets_dir)
os.rm(target_assets_dir)
os.cp(assets_dir, output_dir)
print("Copy assets directory: " .. assets_dir .. " -> " .. target_assets_dir)
end
end) end)
target_end() target_end()

View File

@@ -28,16 +28,14 @@ target("Frostbite2D")
-- 复制 assets 目录到输出目录 -- 复制 assets 目录到输出目录
after_build(function (target) after_build(function (target)
local assetHelper = import("build_helpers.assets")
-- 复制 assets 目录 -- 复制 assets 目录
local assets_dir = path.join(os.projectdir(), "Game/assets") local assets_dir = path.join(os.projectdir(), "Game/assets")
local output_dir = target:targetdir() local output_dir = target:targetdir()
local target_assets_dir = path.join(output_dir, "assets") local target_assets_dir = path.join(output_dir, "assets")
if os.isdir(assets_dir) then assetHelper.syncAssets(assets_dir, target_assets_dir)
os.rm(target_assets_dir)
os.cp(assets_dir, output_dir)
print("Copy assets directory: " .. assets_dir .. " -> " .. target_assets_dir)
end
-- 复制所有依赖的 DLL (Windows 平台) -- 复制所有依赖的 DLL (Windows 平台)
if is_plat("mingw") or is_plat("windows") then if is_plat("mingw") or is_plat("windows") then

View File

@@ -47,6 +47,8 @@ target("Frostbite2D")
-- 构建后生成 NRO 文件 -- 构建后生成 NRO 文件
after_build(function (target) after_build(function (target)
local assetHelper = import("build_helpers.assets")
local elf_file = target:targetfile() local elf_file = target:targetfile()
local output_dir = path.directory(elf_file) local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "hello_world.nacp") local nacp_file = path.join(output_dir, "hello_world.nacp")
@@ -72,11 +74,7 @@ target("Frostbite2D")
local output_dir = target:targetdir() local output_dir = target:targetdir()
local target_assets_dir = path.join(output_dir, "assets") local target_assets_dir = path.join(output_dir, "assets")
if os.isdir(assets_dir) then assetHelper.syncAssets(assets_dir, target_assets_dir)
os.rm(target_assets_dir)
os.cp(assets_dir, output_dir)
print("Copy assets directory: " .. assets_dir .. " -> " .. target_assets_dir)
end
end) end)
target_end() target_end()

View File

@@ -7,6 +7,7 @@ set_languages("c++17")
set_encodings("utf-8") set_encodings("utf-8")
add_rules("mode.debug", "mode.release") add_rules("mode.debug", "mode.release")
add_moduledirs("modules")
local host_plat = "mingw" local host_plat = "mingw"
local target_plat = get_config("plat") or host_plat local target_plat = get_config("plat") or host_plat