feat(地图系统): 实现角色移动约束和地图切换功能

添加地图移动区域检测和角色移动约束逻辑
引入地图切换请求队列机制,支持延迟处理角色传送
在CharacterObject中实现地图边界检测和位置约束应用
This commit is contained in:
2026-04-05 12:04:07 +08:00
parent 2b0cfc6ce5
commit 6cd1b42fef
6 changed files with 117 additions and 4 deletions

View File

@@ -15,6 +15,8 @@
namespace frostbite2D { namespace frostbite2D {
class GameMap;
/// @brief 角色系统的主聚合对象。 /// @brief 角色系统的主聚合对象。
/// ///
/// 这个类可以理解为“角色外壳”: /// 这个类可以理解为“角色外壳”:
@@ -170,6 +172,9 @@ private:
void CommitPendingActionContext(const std::string& defaultRequestedActionId, void CommitPendingActionContext(const std::string& defaultRequestedActionId,
const std::string& defaultSourceActionId, const std::string& defaultSourceActionId,
CharacterStateId defaultSourceStateId); CharacterStateId defaultSourceStateId);
GameMap* FindOwningMap() const;
void ApplyMapMovementConstraints(const CharacterWorldPosition& previousPosition);
void QueueMapTransitionIfNeeded();
void SyncActorPositionFromWorld(); void SyncActorPositionFromWorld();
bool SetActionStrict(const std::string& actionName, bool SetActionStrict(const std::string& actionName,
const char* phase, const char* phase,

View File

@@ -44,6 +44,7 @@ public:
Vec3 CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const; Vec3 CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const;
/// 检查当前位置是否进入 town move area用于切图/传送判定。 /// 检查当前位置是否进入 town move area用于切图/传送判定。
MapMoveArea CheckIsItMoveArea(const Vec3& curPos) const; MapMoveArea CheckIsItMoveArea(const Vec3& curPos) const;
bool TryGetMoveAreaTarget(const Vec3& curPos, MapMoveArea& outTarget) 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;

View File

@@ -3,6 +3,7 @@
#include "world/GameTown.h" #include "world/GameTown.h"
#include <frostbite2D/scene/scene.h> #include <frostbite2D/scene/scene.h>
#include <map> #include <map>
#include <optional>
namespace frostbite2D { namespace frostbite2D {
@@ -13,18 +14,28 @@ public:
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void Update(float deltaTime) override;
void AddCharacter(RefPtr<Actor> actor, int townId); void AddCharacter(RefPtr<Actor> actor, int townId);
void MoveCharacter(RefPtr<Actor> actor, int townId, int area); void MoveCharacter(RefPtr<Actor> actor, int townId, int area);
void RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area);
static GameWorld* GetWorld(); static GameWorld* GetWorld();
private: private:
struct PendingCharacterMove {
RefPtr<Actor> actor;
int townId = -1;
int area = -1;
};
bool InitWorld(); bool InitWorld();
void ProcessPendingCharacterMove();
std::map<int, std::string> townPathMap_; std::map<int, std::string> townPathMap_;
std::map<int, RefPtr<GameTown>> townMap_; std::map<int, RefPtr<GameTown>> townMap_;
RefPtr<Actor> mainActor_; RefPtr<Actor> mainActor_;
std::optional<PendingCharacterMove> pendingCharacterMove_;
int curTown_ = -1; int curTown_ = -1;
bool initialized_ = false; bool initialized_ = false;
}; };

View File

@@ -1,5 +1,8 @@
#include "character/CharacterObject.h" #include "character/CharacterObject.h"
#include "map/GameMap.h"
#include "world/GameWorld.h"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <cmath>
#include <frostbite2D/core/application.h> #include <frostbite2D/core/application.h>
#include <sstream> #include <sstream>
#include <utility> #include <utility>
@@ -34,6 +37,15 @@ const char* NonEmptyOrPlaceholder(const std::string& value, const char* placehol
return value.empty() ? placeholder : value.c_str(); return value.empty() ? placeholder : value.c_str();
} }
Vec3 ToWorldVector(const CharacterWorldPosition& position) {
return Vec3(static_cast<float>(position.x), static_cast<float>(position.y),
static_cast<float>(position.z));
}
int32 RoundWorldCoordinate(float value) {
return static_cast<int32>(std::lround(value));
}
} // namespace } // namespace
bool CharacterObject::Construction(int jobId) { bool CharacterObject::Construction(int jobId) {
@@ -147,6 +159,53 @@ void CharacterObject::SyncActorPositionFromWorld() {
SetZOrder(worldPosition.y); SetZOrder(worldPosition.y);
} }
GameMap* CharacterObject::FindOwningMap() const {
Actor* node = GetParent();
while (node) {
if (auto* map = dynamic_cast<GameMap*>(node)) {
return map;
}
node = node->GetParent();
}
return nullptr;
}
void CharacterObject::ApplyMapMovementConstraints(
const CharacterWorldPosition& previousPosition) {
GameMap* map = FindOwningMap();
if (!map) {
return;
}
Vec3 previousWorldPos = ToWorldVector(previousPosition);
Vec3 worldOffset(static_cast<float>(motor_.position.x - previousPosition.x),
static_cast<float>(motor_.position.y - previousPosition.y),
static_cast<float>(motor_.position.z - previousPosition.z));
Vec3 resolvedWorldPos = map->CheckIsItMovable(previousWorldPos, worldOffset);
motor_.position.x = RoundWorldCoordinate(resolvedWorldPos.x);
motor_.position.y = RoundWorldCoordinate(resolvedWorldPos.y);
motor_.position.z = RoundWorldCoordinate(resolvedWorldPos.z);
}
void CharacterObject::QueueMapTransitionIfNeeded() {
GameMap* map = FindOwningMap();
if (!map) {
return;
}
GameMap::MapMoveArea target;
if (!map->TryGetMoveAreaTarget(ToWorldVector(motor_.position), target)) {
return;
}
GameWorld* world = GameWorld::GetWorld();
if (!world) {
return;
}
world->RequestMoveCharacter(RefPtr<Actor>(this), target.town, target.area);
}
void CharacterObject::PushCommand(const CharacterCommand& command) { void CharacterObject::PushCommand(const CharacterCommand& command) {
commandBuffer_.Submit(command); commandBuffer_.Submit(command);
} }
@@ -244,7 +303,10 @@ void CharacterObject::OnUpdate(float deltaTime) {
inputRouter_.EmitCommands(commandBuffer_); inputRouter_.EmitCommands(commandBuffer_);
currentIntent_ = commandBuffer_.BuildIntent(); currentIntent_ = commandBuffer_.BuildIntent();
stateMachine_.Update(*this, commandBuffer_, currentIntent_, deltaTime); stateMachine_.Update(*this, commandBuffer_, currentIntent_, deltaTime);
CharacterWorldPosition previousPosition = motor_.position;
motor_.Update(deltaTime); motor_.Update(deltaTime);
ApplyMapMovementConstraints(previousPosition);
QueueMapTransitionIfNeeded();
SyncActorPositionFromWorld(); SyncActorPositionFromWorld();
SetFacing(motor_.facing); SetFacing(motor_.facing);
} }

View File

@@ -95,8 +95,8 @@ bool GameMap::LoadMap(const std::string &mapName) {
InitTile(); InitTile();
InitBackgroundAnimation(); InitBackgroundAnimation();
InitMapAnimation(); InitMapAnimation();
// InitVirtualMovableArea(); InitVirtualMovableArea();
// InitMoveArea(); InitMoveArea();
return true; return true;
} }
@@ -405,15 +405,24 @@ 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 {
MapMoveArea target;
if (TryGetMoveAreaTarget(curPos, target)) {
return target;
}
return MapMoveArea();
}
bool GameMap::TryGetMoveAreaTarget(const Vec3& curPos, MapMoveArea& outTarget) const {
// moveArea_ and townMovableAreaTargets share the same index mapping. // moveArea_ and townMovableAreaTargets share the same index mapping.
int currentX = RoundWorldCoordinate(curPos.x); int currentX = RoundWorldCoordinate(curPos.x);
int currentY = RoundWorldCoordinate(curPos.y); int currentY = RoundWorldCoordinate(curPos.y);
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(MakeIntegerWorldPoint(currentX, currentY))) { if (moveArea_[i].containsPoint(MakeIntegerWorldPoint(currentX, currentY))) {
return mapConfig_.townMovableAreaTargets[i]; outTarget = mapConfig_.townMovableAreaTargets[i];
return true;
} }
} }
return MapMoveArea(); return false;
} }
const std::vector<GameMap::MapMoveArea>& GameMap::GetMoveAreaInfo() const { const std::vector<GameMap::MapMoveArea>& GameMap::GetMoveAreaInfo() const {

View File

@@ -20,6 +20,11 @@ void GameWorld::onExit() {
Scene::onExit(); Scene::onExit();
} }
void GameWorld::Update(float deltaTime) {
Scene::Update(deltaTime);
ProcessPendingCharacterMove();
}
bool GameWorld::InitWorld() { bool GameWorld::InitWorld() {
townPathMap_ = game::loadTownList(); townPathMap_ = game::loadTownList();
if (townPathMap_.empty()) { if (townPathMap_.empty()) {
@@ -77,6 +82,26 @@ void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
AddChild(townIt->second); AddChild(townIt->second);
} }
void GameWorld::RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area) {
if (!actor) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: ignore move request without actor");
return;
}
pendingCharacterMove_ = PendingCharacterMove{actor, townId, area};
}
void GameWorld::ProcessPendingCharacterMove() {
if (!pendingCharacterMove_) {
return;
}
PendingCharacterMove move = *pendingCharacterMove_;
pendingCharacterMove_.reset();
MoveCharacter(move.actor, move.townId, move.area);
}
GameWorld* GameWorld::GetWorld() { GameWorld* GameWorld::GetWorld() {
return dynamic_cast<GameWorld*>(SceneManager::get().GetCurrentScene()); return dynamic_cast<GameWorld*>(SceneManager::get().GetCurrentScene());
} }