feat: 实现游戏摄像机控制器并优化地图系统
重构地图系统,增加摄像机控制器管理相机行为。主要变更包括: - 新增 GameCameraController 类,支持跟随目标和调试模式 - 重构 GameMap 类,分离相机逻辑到控制器 - 优化地图资源加载和同步逻辑 - 改进动画系统的事件处理 - 添加地图测试场景用于快速验证
This commit is contained in:
@@ -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