feat: 实现游戏摄像机控制器并优化地图系统
重构地图系统,增加摄像机控制器管理相机行为。主要变更包括: - 新增 GameCameraController 类,支持跟随目标和调试模式 - 重构 GameMap 类,分离相机逻辑到控制器 - 优化地图资源加载和同步逻辑 - 改进动画系统的事件处理 - 添加地图测试场景用于快速验证
This commit is contained in:
Binary file not shown.
Binary file not shown.
31707
Game/assets/audio.xml
31707
Game/assets/audio.xml
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,6 @@ struct MoveAreaTarget {
|
||||
struct TileInfo {
|
||||
std::string spritePath;
|
||||
int spriteIndex = 0;
|
||||
int imgPos = 0;
|
||||
};
|
||||
|
||||
struct MapConfig {
|
||||
@@ -52,6 +51,7 @@ struct MapConfig {
|
||||
std::string mapPath;
|
||||
std::string mapDir;
|
||||
int backgroundPos = 0;
|
||||
Vec2 tilePos = Vec2::Zero();
|
||||
int farSightScroll = 100;
|
||||
bool hasCameraLimit = false;
|
||||
int cameraLimitMinX = -1;
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
/**
|
||||
* @brief 地图运行时容器,负责把 map 配置组织成可显示/可交互的场景结构
|
||||
*
|
||||
* 这里不直接解析底层资源格式,而是消费 `GameDataLoader` 已经整理好的
|
||||
* `MapConfig/TileInfo`。GameMap 主要负责三件事:
|
||||
* 1. 按固定图层顺序挂载地板、背景动画、场景动画和动态对象
|
||||
* 2. 根据 camera limit 和可移动区域维护摄像机关注点
|
||||
* 3. 提供移动判定和切图区域查询给上层角色/场景逻辑使用
|
||||
*/
|
||||
class GameMap : public Actor {
|
||||
public:
|
||||
using MapMoveArea = game::MoveAreaTarget;
|
||||
@@ -15,40 +24,65 @@ public:
|
||||
GameMap();
|
||||
~GameMap() override = default;
|
||||
|
||||
/// 读取地图配置并重建当前地图内容。重复调用时会先清空旧地图运行态。
|
||||
bool LoadMap(const std::string& mapName);
|
||||
/// 地图进入场景后的初始化入口,目前主要负责音乐播放。
|
||||
void Enter();
|
||||
void Update(float deltaTime) override;
|
||||
|
||||
/// 将运行时对象挂到 normal 层,并按 y 值设置基础排序。
|
||||
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;
|
||||
/// 检查当前位置是否进入 town move area,用于切图/传送判定。
|
||||
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_; }
|
||||
int GetBackgroundRepeatWidth() const { return backgroundRepeatWidth_; }
|
||||
|
||||
private:
|
||||
/// 初始化固定图层。图层名字与 DNF 地图层概念保持一致。
|
||||
void initLayers();
|
||||
/// 清理当前地图的运行态节点和缓存数据,但保留 GameMap 自身及图层骨架。
|
||||
void clearLayerChildren();
|
||||
/// 创建普通 tile 和扩展 tile,并推导背景平铺需要的横向覆盖宽度。
|
||||
void InitTile();
|
||||
/// 初始化背景动画层,必要时按横向覆盖宽度做横向平铺。
|
||||
void InitBackgroundAnimation();
|
||||
/// 初始化地图对象动画(门、特效、场景摆件等)。
|
||||
void InitMapAnimation();
|
||||
/// 初始化角色移动用的可行走区域。
|
||||
void InitVirtualMovableArea();
|
||||
/// 初始化切图/传送区域。
|
||||
void InitMoveArea();
|
||||
void updateCamera();
|
||||
void updateLayerPositions(const Vec2& cameraPos);
|
||||
/// 将各层转换到屏幕空间;远景层在这里做视差滚动。
|
||||
void updateLayerPositions(const Vec2& cameraFocus);
|
||||
|
||||
/// 原始地图配置,作为运行时装配地图内容的输入。
|
||||
game::MapConfig mapConfig_;
|
||||
/// 地图的固定图层集合,key 为地图层名。
|
||||
std::unordered_map<std::string, RefPtr<GameMapLayer>> layerMap_;
|
||||
/// bottom 层里的专用地板容器,固定放在该层最底部,避免地图动画被地板压住。
|
||||
RefPtr<Actor> tileRoot_;
|
||||
/// 角色可行走矩形区域。
|
||||
std::vector<Rect> movableArea_;
|
||||
/// 进入后会触发切图/传送的矩形区域。
|
||||
std::vector<Rect> moveArea_;
|
||||
int mapLength_ = 0;
|
||||
int mapHeight_ = 0;
|
||||
/// 地板铺设后推导出的横向覆盖宽度,用于背景动画横向平铺。
|
||||
int backgroundRepeatWidth_ = 0;
|
||||
/// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。
|
||||
int mapOffsetY_ = 0;
|
||||
bool debugMode_ = false;
|
||||
Vec2 cameraFocus_ = Vec2::Zero();
|
||||
/// 当前地图正在播放的背景音乐。
|
||||
Ptr<Music> currentMusic_;
|
||||
};
|
||||
|
||||
|
||||
24
Game/include/Actor/GameMapTestScene.h
Normal file
24
Game/include/Actor/GameMapTestScene.h
Normal 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
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "Camera/GameCameraController.h"
|
||||
#include "GameDataLoader.h"
|
||||
#include "GameMap.h"
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
@@ -38,6 +39,7 @@ private:
|
||||
Vec2 sariaRoomPos_ = Vec2(-1.0f, -1.0f);
|
||||
std::vector<MapInfo> mapList_;
|
||||
int curMapIndex_ = -1;
|
||||
GameCameraController cameraController_;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
53
Game/include/Camera/GameCameraController.h
Normal file
53
Game/include/Camera/GameCameraController.h
Normal 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
|
||||
@@ -80,6 +80,12 @@ public:
|
||||
return tokens_[index_++];
|
||||
}
|
||||
|
||||
void back() {
|
||||
if (index_ > 0) {
|
||||
--index_;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::string path_;
|
||||
std::vector<std::string> tokens_;
|
||||
@@ -176,6 +182,9 @@ bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig) {
|
||||
std::string segment = stream.get();
|
||||
if (segment == "[background pos]") {
|
||||
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]") {
|
||||
outConfig.name = stream.get();
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -283,15 +303,17 @@ bool loadTileInfo(const std::string& path, TileInfo& outInfo) {
|
||||
}
|
||||
|
||||
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();
|
||||
std::string segment = toLowerCase(stream.get());
|
||||
if (segment == "[image]") {
|
||||
outInfo.spritePath = resolveSpritePath(baseDir, stream.get());
|
||||
std::string rawImagePath = stream.get();
|
||||
outInfo.spritePath = normalizeWithPrefix("sprite", rawImagePath);
|
||||
outInfo.spriteIndex = toInt(stream.get());
|
||||
} else if (segment == "[img pos]") {
|
||||
outInfo.imgPos = toInt(stream.get());
|
||||
if (outInfo.spritePath.empty()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"GameDataLoader: tile image path resolved empty for %s, raw=%s",
|
||||
path.c_str(), rawImagePath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
// 图层名和层级顺序与原始地图资源的层语义保持一致,方便直接按 layer 字段挂载。
|
||||
static const char* kLayerNames[] = {
|
||||
"contact", "distantback", "middleback", "bottom", "closeback",
|
||||
"normal", "close", "cover", "max"};
|
||||
@@ -19,11 +20,11 @@ static const char* kLayerNames[] = {
|
||||
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 int kTileRootZOrder = -1000000;
|
||||
constexpr float kExtendedTileStepY = 120.0f;
|
||||
|
||||
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
|
||||
// 占位,避免整张地图中断。
|
||||
Ptr<Sprite> createMapSprite(const std::string& path, int index) {
|
||||
auto sprite = Sprite::createFromNpk(path, static_cast<size_t>(index));
|
||||
if (!sprite) {
|
||||
@@ -56,41 +57,37 @@ void GameMap::clearLayerChildren() {
|
||||
layer->RemoveAllChildren();
|
||||
}
|
||||
}
|
||||
tileRoot_.Reset();
|
||||
movableArea_.clear();
|
||||
moveArea_.clear();
|
||||
currentMusic_.Reset();
|
||||
mapLength_ = 0;
|
||||
mapHeight_ = 0;
|
||||
backgroundRepeatWidth_ = 0;
|
||||
}
|
||||
|
||||
bool GameMap::LoadMap(const std::string& mapName) {
|
||||
bool GameMap::LoadMap(const std::string &mapName) {
|
||||
// 清空所有图层子节点。
|
||||
clearLayerChildren();
|
||||
|
||||
// 读取PVF地图配置。
|
||||
if (!game::loadMapConfig(mapName, mapConfig_)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to load map %s",
|
||||
mapName.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// backgroundPos 会影响地图整体的视觉基线,所以地板和各显示层都会参考它。
|
||||
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);
|
||||
}
|
||||
}
|
||||
// InitVirtualMovableArea();
|
||||
// InitMoveArea();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameMap::Enter() {
|
||||
// 地图进入时尝试找到第一个可播放的 BGM 并循环播放。
|
||||
if (!mapConfig_.soundIds.empty()) {
|
||||
auto& audioDatabase = AudioDatabase::get();
|
||||
for (const auto& soundId : mapConfig_.soundIds) {
|
||||
@@ -107,25 +104,30 @@ void GameMap::Enter() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCamera();
|
||||
}
|
||||
|
||||
void GameMap::Update(float deltaTime) {
|
||||
Actor::Update(deltaTime);
|
||||
(void)deltaTime;
|
||||
updateCamera();
|
||||
}
|
||||
void GameMap::Update(float deltaTime) { Actor::Update(deltaTime); }
|
||||
|
||||
void GameMap::InitTile() {
|
||||
if (mapConfig_.tilePaths.empty() && mapConfig_.extendedTilePaths.empty()) {
|
||||
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 maxTotalBottom = 0.0f;
|
||||
float maxOffset = 0.0f;
|
||||
float maxOffset = mapConfig_.tilePos.y;
|
||||
|
||||
int normalTileCount = static_cast<int>(mapConfig_.tilePaths.size());
|
||||
float nextNormalX = mapConfig_.tilePos.x;
|
||||
|
||||
for (size_t i = 0; i < mapConfig_.tilePaths.size(); ++i) {
|
||||
game::TileInfo info;
|
||||
@@ -133,43 +135,45 @@ void GameMap::InitTile() {
|
||||
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);
|
||||
std::string spritePath = info.spritePath.empty()
|
||||
? "sprite/character/common/circlecooltime.img"
|
||||
: info.spritePath;
|
||||
auto sprite = createMapSprite(spritePath, info.spriteIndex);
|
||||
float posX = nextNormalX;
|
||||
float posY = mapConfig_.tilePos.y;
|
||||
sprite->SetPosition(posX, posY);
|
||||
tileRoot_->AddChild(sprite);
|
||||
|
||||
Vec2 size = sprite->GetSize();
|
||||
maxOffset = std::max(maxOffset, static_cast<float>(info.imgPos));
|
||||
maxOffset = std::max(maxOffset, posY);
|
||||
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);
|
||||
backgroundRepeatWidth_ =
|
||||
std::max(backgroundRepeatWidth_, static_cast<int>(posX + size.x));
|
||||
nextNormalX += size.x;
|
||||
}
|
||||
|
||||
float nextExtendedX = mapConfig_.tilePos.x;
|
||||
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);
|
||||
std::string spritePath = info.spritePath.empty()
|
||||
? "sprite/character/common/circlecooltime.img"
|
||||
: info.spritePath;
|
||||
auto sprite = createMapSprite(spritePath, info.spriteIndex);
|
||||
int row = normalTileCount > 0 ? static_cast<int>(i) / normalTileCount : 0;
|
||||
float posX = nextExtendedX;
|
||||
float posY = maxOffset + maxBaseHeight + row * kExtendedTileStepY;
|
||||
sprite->SetPosition(static_cast<float>(i) * kTileSpacing, posY);
|
||||
layerMap_["bottom"]->AddChild(sprite);
|
||||
sprite->SetPosition(posX, posY);
|
||||
tileRoot_->AddChild(sprite);
|
||||
|
||||
Vec2 size = sprite->GetSize();
|
||||
mapLength_ = std::max(mapLength_, static_cast<int>(i * kTileSpacing + size.x));
|
||||
maxTotalBottom = std::max(maxTotalBottom, posY + size.y);
|
||||
backgroundRepeatWidth_ =
|
||||
std::max(backgroundRepeatWidth_, static_cast<int>(posX + size.x));
|
||||
nextExtendedX += size.x;
|
||||
}
|
||||
|
||||
mapHeight_ = std::max(mapHeight_, static_cast<int>(maxTotalBottom));
|
||||
}
|
||||
|
||||
void GameMap::InitBackgroundAnimation() {
|
||||
@@ -179,11 +183,15 @@ void GameMap::InitBackgroundAnimation() {
|
||||
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);
|
||||
if (animationSize.x > 0.0f && backgroundRepeatWidth_ > 0) {
|
||||
repeatCount =
|
||||
std::max(1, static_cast<int>((backgroundRepeatWidth_ * rate) /
|
||||
animationSize.x) +
|
||||
1);
|
||||
}
|
||||
|
||||
auto layerIt = layerMap_.find(ani.layer);
|
||||
@@ -196,6 +204,7 @@ void GameMap::InitBackgroundAnimation() {
|
||||
if (!backgroundAni || !backgroundAni->IsUsable()) {
|
||||
continue;
|
||||
}
|
||||
// 背景动画在各自层内从左到右平铺,zOrder 放到极低,避免压住该层其他内容。
|
||||
backgroundAni->SetPosition(i * animationSize.x, -120.0f);
|
||||
backgroundAni->SetZOrder(-1000000);
|
||||
layerIt->second->AddChild(backgroundAni);
|
||||
@@ -210,8 +219,10 @@ void GameMap::InitMapAnimation() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 地图对象的显示 y 会扣掉 zPos,等价于把“离地高度”折算回屏幕坐标。
|
||||
animation->SetPosition(static_cast<float>(ani.xPos),
|
||||
static_cast<float>(ani.yPos - ani.zPos));
|
||||
// 同层内继续按 y 排序,保证越靠下的物件越靠前。
|
||||
animation->SetZOrder(ani.yPos);
|
||||
|
||||
auto layerIt = layerMap_.find(ani.layer);
|
||||
@@ -223,6 +234,7 @@ void GameMap::InitMapAnimation() {
|
||||
|
||||
void GameMap::InitVirtualMovableArea() {
|
||||
movableArea_ = mapConfig_.virtualMovableAreas;
|
||||
// debugMode 打开时,把可行走区域可视化到最高层,方便校验地图配置。
|
||||
if (!debugMode_) {
|
||||
return;
|
||||
}
|
||||
@@ -234,6 +246,7 @@ void GameMap::InitVirtualMovableArea() {
|
||||
|
||||
void GameMap::InitMoveArea() {
|
||||
moveArea_ = mapConfig_.townMovableAreas;
|
||||
// move area 和普通 movable area 分开展示,方便区分“能走”与“会触发切图”。
|
||||
if (!debugMode_) {
|
||||
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();
|
||||
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) {
|
||||
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;
|
||||
Vec2 cameraPos = camera->getPosition();
|
||||
|
||||
// 主视图平移完全交给底层
|
||||
// Camera,这里只保留地图层自己的静态校准和少量视差偏移。
|
||||
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_);
|
||||
float posX = 0.0f;
|
||||
float posY = static_cast<float>(mapOffsetY_);
|
||||
|
||||
// 远景层只做部分水平跟随;主 Camera
|
||||
// 已经负责完整位移,这里补上“少动一些”的差值。
|
||||
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);
|
||||
}
|
||||
@@ -301,6 +342,7 @@ void GameMap::AddObject(RefPtr<Actor> object) {
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
// 动态对象默认进 normal 层,并沿用 2D 地图里常见的“按 y 值排序”规则。
|
||||
object->SetZOrder(static_cast<int>(object->GetPosition().y));
|
||||
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 targetY = curPos.y + posOffset.y;
|
||||
|
||||
// X/Y
|
||||
// 分开判断,这样贴着边移动时不会因为一个方向越界而把另一个方向也一起锁死。
|
||||
bool isXValid = false;
|
||||
for (const auto& area : movableArea_) {
|
||||
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 {
|
||||
// moveArea_ 和配置里的 target 是一一对应的,命中后直接返回同索引的目标信息。
|
||||
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];
|
||||
|
||||
48
Game/src/Actor/GameMapTestScene.cpp
Normal file
48
Game/src/Actor/GameMapTestScene.cpp
Normal 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
|
||||
@@ -88,20 +88,30 @@ void GameTown::AddCharacter(RefPtr<Actor> actor, int areaIndex) {
|
||||
}
|
||||
|
||||
AddChild(mapIt->map);
|
||||
mapIt->map->Enter();
|
||||
cameraController_.SetMap(mapIt->map.Get());
|
||||
cameraController_.SetZoom(1.2f);
|
||||
cameraController_.SetTarget(actor.Get());
|
||||
cameraController_.SnapToDefaultFocus();
|
||||
|
||||
if (actor) {
|
||||
cameraController_.SetDebugEnabled(false);
|
||||
if (areaIndex == -2 && sariaRoomPos_.x >= 0.0f && sariaRoomPos_.y >= 0.0f) {
|
||||
actor->SetPosition(sariaRoomPos_);
|
||||
}
|
||||
mapIt->map->AddObject(actor);
|
||||
cameraController_.SetFocus(actor->GetPosition());
|
||||
} else {
|
||||
cameraController_.SetDebugEnabled(true);
|
||||
}
|
||||
|
||||
mapIt->map->Enter();
|
||||
|
||||
curMapIndex_ = mapIt->areaId;
|
||||
}
|
||||
|
||||
void GameTown::Update(float deltaTime) {
|
||||
Actor::Update(deltaTime);
|
||||
cameraController_.Update(deltaTime);
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -13,7 +13,6 @@ void GameWorld::onEnter() {
|
||||
return;
|
||||
}
|
||||
|
||||
SetScale(1.2f);
|
||||
initialized_ = InitWorld();
|
||||
}
|
||||
|
||||
|
||||
193
Game/src/Camera/GameCameraController.cpp
Normal file
193
Game/src/Camera/GameCameraController.cpp
Normal 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
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <frostbite2D/resource/sound_pack_archive.h>
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
#include "Actor/GameMapTestScene.h"
|
||||
#include "Actor/GameWorld.h"
|
||||
|
||||
using namespace frostbite2D;
|
||||
@@ -106,8 +107,8 @@ int main(int argc, char **argv) {
|
||||
[](std::string message) mutable {
|
||||
SDL_Log("后台资源加载成功");
|
||||
|
||||
auto gameWorld = MakePtr<GameWorld>();
|
||||
SceneManager::get().ReplaceScene(gameWorld);
|
||||
auto testMapScene = MakePtr<GameMapTestScene>();
|
||||
SceneManager::get().ReplaceScene(testMapScene);
|
||||
},
|
||||
[](std::exception_ptr error) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user