Add camera zone lock
This commit is contained in:
@@ -178,12 +178,13 @@ mindmap
|
|||||||
|
|
||||||
- `scene/whitebox_scene.*`
|
- `scene/whitebox_scene.*`
|
||||||
- `scene/camera_controller.*`:玩家跟随、dead zone、平滑和世界边界 clamp。
|
- `scene/camera_controller.*`:玩家跟随、dead zone、平滑和世界边界 clamp。
|
||||||
|
- `CameraZoneLock`:玩家进入 battle zone 后摄像机锁定到 zone camera bounds,当前用 `F2` debug 清场解锁。
|
||||||
|
|
||||||
下一步:
|
下一步:
|
||||||
|
|
||||||
- 抽象 `GameplayScene` 作为通用玩法场景基类。
|
- 抽象 `GameplayScene` 作为通用玩法场景基类。
|
||||||
- `WhiteboxScene` 只保留第一关测试内容。
|
- `WhiteboxScene` 只保留第一关测试内容。
|
||||||
- 引入 battle zone 触发逻辑。
|
- 将 battle zone debug 清场替换为敌人清空自动解锁。
|
||||||
- 增加局内 reset 入口,用于卡状态、死亡重开和联机重同步。
|
- 增加局内 reset 入口,用于卡状态、死亡重开和联机重同步。
|
||||||
|
|
||||||
### 3. Stage
|
### 3. Stage
|
||||||
@@ -579,15 +580,14 @@ mindmap
|
|||||||
|
|
||||||
下一步建议按这个顺序做:
|
下一步建议按这个顺序做:
|
||||||
|
|
||||||
1. `CameraZoneLock`:让 battle zone 可以临时锁定摄像机范围,清场后恢复跟随。
|
1. `StageLayerSource`:让 `StageLayer` 同时支持色块占位和 sprite 资源路径。
|
||||||
2. `StageLayerSource`:让 `StageLayer` 同时支持色块占位和 sprite 资源路径。
|
2. `StageAssetSlots`:建立 `stage_01` 资源槽位和素材来源记录,做到素材可直接落位。
|
||||||
3. `StageAssetSlots`:建立 `stage_01` 资源槽位和素材来源记录,做到素材可直接落位。
|
3. `StageProps`:把后景/前景 props 统一到 layer 数据。
|
||||||
4. `StageProps`:把后景/前景 props 统一到 layer 数据。
|
4. `BattleZone`:把 `F2` debug 清场替换为敌人清空自动解锁。
|
||||||
5. `BattleZone`:在现有 zone 数据上做触发、锁镜头、清场解锁。
|
5. `EnemyActor`:做第一个敌人占位,接入出生点。
|
||||||
6. `EnemyActor`:做第一个敌人占位,接入出生点。
|
6. `CombatSystem`:把 hitbox/hurtbox 真正跑起来。
|
||||||
7. `CombatSystem`:把 hitbox/hurtbox 真正跑起来。
|
7. `SpriteAnimator`:把玩家动画从硬编码改成组件。
|
||||||
8. `SpriteAnimator`:把玩家动画从硬编码改成组件。
|
8. `InputConfig`:把默认键位/手柄映射迁移到可配置数据。
|
||||||
9. `InputConfig`:把默认键位/手柄映射迁移到可配置数据。
|
|
||||||
|
|
||||||
## 素材协作提示
|
## 素材协作提示
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
- `DebugOverlay` 已能显示碰撞、玩家框、battle zone、camera lock 范围和 spawn point。
|
- `DebugOverlay` 已能显示碰撞、玩家框、battle zone、camera lock 范围和 spawn point。
|
||||||
- `WhiteboxScene` 已支持 `F1` 切换 debug overlay。
|
- `WhiteboxScene` 已支持 `F1` 切换 debug overlay。
|
||||||
- `CameraController` 已接入:修复场景 `OnUpdate()` 不被调用导致摄像机不动的问题,并提供跟随、dead zone、平滑和世界边界 clamp。
|
- `CameraController` 已接入:修复场景 `OnUpdate()` 不被调用导致摄像机不动的问题,并提供跟随、dead zone、平滑和世界边界 clamp。
|
||||||
|
- `CameraZoneLock` 已接入:玩家进入 battle zone 后摄像机临时锁定到 zone camera bounds,`F2` 临时清场并恢复全关卡跟随。
|
||||||
|
|
||||||
## 2. 第一关场景目标
|
## 2. 第一关场景目标
|
||||||
|
|
||||||
@@ -250,7 +251,7 @@ game/assets/stage/stage_01/
|
|||||||
### 场景骨架实施顺序
|
### 场景骨架实施顺序
|
||||||
|
|
||||||
1. 已完成:`CameraController` 修复当前摄像机不会动的问题,实现玩家跟随、边界 clamp、dead zone 和平滑参数。
|
1. 已完成:`CameraController` 修复当前摄像机不会动的问题,实现玩家跟随、边界 clamp、dead zone 和平滑参数。
|
||||||
2. `CameraZoneLock`:让 battle zone 可以锁定摄像机范围,先只做 debug 触发和解锁占位。
|
2. 已完成:`CameraZoneLock` 让 battle zone 可以锁定摄像机范围,先用 `F2` 做 debug 清场和解锁占位。
|
||||||
3. `StageLayerSource`:扩展 `StageLayer`,让每个 rect 支持色块占位或 sprite 资源路径。
|
3. `StageLayerSource`:扩展 `StageLayer`,让每个 rect 支持色块占位或 sprite 资源路径。
|
||||||
4. `StageAssetSlots`:创建并记录 `stage_01` 资源槽位,不要求已有最终素材。
|
4. `StageAssetSlots`:创建并记录 `stage_01` 资源槽位,不要求已有最终素材。
|
||||||
5. `StageProps`:把 props 从旧 `WhiteboxProp` 迁移到统一 layer 数据,支持 back/front 两层。
|
5. `StageProps`:把 props 从旧 `WhiteboxProp` 迁移到统一 layer 数据,支持 back/front 两层。
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ struct BattleZoneDefinition {
|
|||||||
std::vector<SpawnPoint> enemySpawns;
|
std::vector<SpawnPoint> enemySpawns;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class BattleZoneDebugState {
|
||||||
|
Idle,
|
||||||
|
Active,
|
||||||
|
Cleared
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BattleZoneDebugInfo {
|
||||||
|
size_t zoneIndex = 0;
|
||||||
|
BattleZoneDebugState state = BattleZoneDebugState::Idle;
|
||||||
|
};
|
||||||
|
|
||||||
struct StageLayer {
|
struct StageLayer {
|
||||||
std::string id;
|
std::string id;
|
||||||
StageLayerRole role = StageLayerRole::LevelVisual;
|
StageLayerRole role = StageLayerRole::LevelVisual;
|
||||||
|
|||||||
@@ -37,15 +37,43 @@ void drawCross(const frostbite2D::Vec2& position,
|
|||||||
color);
|
color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BattleZoneDebugState resolveZoneState(
|
||||||
|
size_t zoneIndex, const std::vector<BattleZoneDebugInfo>* debugInfo) {
|
||||||
|
if (!debugInfo) {
|
||||||
|
return BattleZoneDebugState::Idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const BattleZoneDebugInfo& info : *debugInfo) {
|
||||||
|
if (info.zoneIndex == zoneIndex) {
|
||||||
|
return info.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BattleZoneDebugState::Idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
frostbite2D::Color zoneColor(BattleZoneDebugState state) {
|
||||||
|
switch (state) {
|
||||||
|
case BattleZoneDebugState::Active:
|
||||||
|
return frostbite2D::Color(1.0f, 0.82f, 0.15f, 0.95f);
|
||||||
|
case BattleZoneDebugState::Cleared:
|
||||||
|
return frostbite2D::Color(0.45f, 0.48f, 0.52f, 0.65f);
|
||||||
|
case BattleZoneDebugState::Idle:
|
||||||
|
default:
|
||||||
|
return frostbite2D::Color(0.95f, 0.25f, 0.25f, 0.9f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void DebugOverlay::Render(const LevelDefinition& level, const DebugFlags& flags,
|
void DebugOverlay::Render(const LevelDefinition& level, const DebugFlags& flags,
|
||||||
const frostbite2D::Rect* playerBounds) const {
|
const frostbite2D::Rect* playerBounds,
|
||||||
|
const std::vector<BattleZoneDebugInfo>*
|
||||||
|
battleZoneDebug) const {
|
||||||
if (flags.showCollision) {
|
if (flags.showCollision) {
|
||||||
drawCollision(level);
|
drawCollision(level);
|
||||||
}
|
}
|
||||||
if (flags.showBattleZones) {
|
if (flags.showBattleZones) {
|
||||||
drawBattleZones(level);
|
drawBattleZones(level, battleZoneDebug);
|
||||||
}
|
}
|
||||||
if (flags.showSpawnPoints) {
|
if (flags.showSpawnPoints) {
|
||||||
drawSpawnPoints(level);
|
drawSpawnPoints(level);
|
||||||
@@ -65,9 +93,12 @@ void DebugOverlay::drawActorBounds(const frostbite2D::Rect& bounds) const {
|
|||||||
drawOutline(bounds, frostbite2D::Color(1.0f, 0.78f, 0.1f, 0.9f));
|
drawOutline(bounds, frostbite2D::Color(1.0f, 0.78f, 0.1f, 0.9f));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DebugOverlay::drawBattleZones(const LevelDefinition& level) const {
|
void DebugOverlay::drawBattleZones(
|
||||||
for (const BattleZoneDefinition& zone : level.battleZones) {
|
const LevelDefinition& level,
|
||||||
drawOutline(zone.trigger, frostbite2D::Color(0.95f, 0.25f, 0.25f, 0.9f),
|
const std::vector<BattleZoneDebugInfo>* battleZoneDebug) const {
|
||||||
|
for (size_t i = 0; i < level.battleZones.size(); ++i) {
|
||||||
|
const BattleZoneDefinition& zone = level.battleZones[i];
|
||||||
|
drawOutline(zone.trigger, zoneColor(resolveZoneState(i, battleZoneDebug)),
|
||||||
3.0f);
|
3.0f);
|
||||||
drawOutline(zone.cameraBounds,
|
drawOutline(zone.cameraBounds,
|
||||||
frostbite2D::Color(0.25f, 0.55f, 1.0f, 0.75f), 2.0f);
|
frostbite2D::Color(0.25f, 0.55f, 1.0f, 0.75f), 2.0f);
|
||||||
|
|||||||
@@ -4,18 +4,22 @@
|
|||||||
#include "../data/level_definition.h"
|
#include "../data/level_definition.h"
|
||||||
|
|
||||||
#include <frostbite2D/types/type_math.h>
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace ns_game {
|
namespace ns_game {
|
||||||
|
|
||||||
class DebugOverlay {
|
class DebugOverlay {
|
||||||
public:
|
public:
|
||||||
void Render(const LevelDefinition& level, const DebugFlags& flags,
|
void Render(const LevelDefinition& level, const DebugFlags& flags,
|
||||||
const frostbite2D::Rect* playerBounds) const;
|
const frostbite2D::Rect* playerBounds,
|
||||||
|
const std::vector<BattleZoneDebugInfo>* battleZoneDebug) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void drawCollision(const LevelDefinition& level) const;
|
void drawCollision(const LevelDefinition& level) const;
|
||||||
void drawActorBounds(const frostbite2D::Rect& bounds) const;
|
void drawActorBounds(const frostbite2D::Rect& bounds) const;
|
||||||
void drawBattleZones(const LevelDefinition& level) const;
|
void drawBattleZones(
|
||||||
|
const LevelDefinition& level,
|
||||||
|
const std::vector<BattleZoneDebugInfo>* battleZoneDebug) const;
|
||||||
void drawSpawnPoints(const LevelDefinition& level) const;
|
void drawSpawnPoints(const LevelDefinition& level) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ void CameraController::Configure(const CameraControllerConfig& config) {
|
|||||||
config_ = config;
|
config_ = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CameraController::SetWorldBounds(const frostbite2D::Rect& worldBounds) {
|
||||||
|
config_.worldBounds = worldBounds;
|
||||||
|
}
|
||||||
|
|
||||||
void CameraController::SnapToTarget(frostbite2D::Camera& camera,
|
void CameraController::SnapToTarget(frostbite2D::Camera& camera,
|
||||||
const frostbite2D::Rect& targetBounds) {
|
const frostbite2D::Rect& targetBounds) {
|
||||||
camera.setPosition(clampToWorld(
|
camera.setPosition(clampToWorld(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ struct CameraControllerConfig {
|
|||||||
class CameraController {
|
class CameraController {
|
||||||
public:
|
public:
|
||||||
void Configure(const CameraControllerConfig& config);
|
void Configure(const CameraControllerConfig& config);
|
||||||
|
void SetWorldBounds(const frostbite2D::Rect& worldBounds);
|
||||||
void SnapToTarget(frostbite2D::Camera& camera,
|
void SnapToTarget(frostbite2D::Camera& camera,
|
||||||
const frostbite2D::Rect& targetBounds);
|
const frostbite2D::Rect& targetBounds);
|
||||||
void Update(frostbite2D::Camera& camera, const frostbite2D::Rect& targetBounds,
|
void Update(frostbite2D::Camera& camera, const frostbite2D::Rect& targetBounds,
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ namespace ns_game {
|
|||||||
void WhiteboxScene::onEnter() {
|
void WhiteboxScene::onEnter() {
|
||||||
frostbite2D::Scene::onEnter();
|
frostbite2D::Scene::onEnter();
|
||||||
level_ = CreateWhiteboxLevel();
|
level_ = CreateWhiteboxLevel();
|
||||||
|
battleZoneStates_.clear();
|
||||||
|
battleZoneStates_.resize(level_.battleZones.size());
|
||||||
|
activeBattleZoneIndex_ = -1;
|
||||||
|
|
||||||
player_ = frostbite2D::MakePtr<PlayerActor>();
|
player_ = frostbite2D::MakePtr<PlayerActor>();
|
||||||
player_->SetTopLeftPosition(level_.playerSpawn);
|
player_->SetTopLeftPosition(level_.playerSpawn);
|
||||||
@@ -22,6 +25,7 @@ void WhiteboxScene::onEnter() {
|
|||||||
|
|
||||||
void WhiteboxScene::Update(float deltaTime) {
|
void WhiteboxScene::Update(float deltaTime) {
|
||||||
frostbite2D::Scene::Update(deltaTime);
|
frostbite2D::Scene::Update(deltaTime);
|
||||||
|
updateBattleZones();
|
||||||
updateCamera(deltaTime);
|
updateCamera(deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +37,11 @@ bool WhiteboxScene::OnEvent(const frostbite2D::Event& event) {
|
|||||||
toggleDebugOverlay();
|
toggleDebugOverlay();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!keyEvent.isRepeat() &&
|
||||||
|
keyEvent.getKeyCode() == frostbite2D::KeyCode::F2) {
|
||||||
|
clearActiveBattleZone();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return frostbite2D::Scene::OnEvent(event);
|
return frostbite2D::Scene::OnEvent(event);
|
||||||
@@ -43,7 +52,10 @@ void WhiteboxScene::Render() {
|
|||||||
frostbite2D::Scene::Render();
|
frostbite2D::Scene::Render();
|
||||||
const frostbite2D::Rect playerBounds =
|
const frostbite2D::Rect playerBounds =
|
||||||
player_ ? player_->GetWorldBounds() : frostbite2D::Rect::Zero();
|
player_ ? player_->GetWorldBounds() : frostbite2D::Rect::Zero();
|
||||||
debugOverlay_.Render(level_, debugFlags_, player_ ? &playerBounds : nullptr);
|
const std::vector<BattleZoneDebugInfo> battleZoneDebug =
|
||||||
|
buildBattleZoneDebugInfo();
|
||||||
|
debugOverlay_.Render(level_, debugFlags_, player_ ? &playerBounds : nullptr,
|
||||||
|
&battleZoneDebug);
|
||||||
}
|
}
|
||||||
|
|
||||||
frostbite2D::Camera* WhiteboxScene::activeCamera() const {
|
frostbite2D::Camera* WhiteboxScene::activeCamera() const {
|
||||||
@@ -54,6 +66,22 @@ frostbite2D::Camera* WhiteboxScene::activeCamera() const {
|
|||||||
return renderer->getCamera();
|
return renderer->getCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<BattleZoneDebugInfo> WhiteboxScene::buildBattleZoneDebugInfo()
|
||||||
|
const {
|
||||||
|
std::vector<BattleZoneDebugInfo> debugInfo;
|
||||||
|
debugInfo.reserve(battleZoneStates_.size());
|
||||||
|
for (size_t i = 0; i < battleZoneStates_.size(); ++i) {
|
||||||
|
BattleZoneDebugState state = BattleZoneDebugState::Idle;
|
||||||
|
if (battleZoneStates_[i].cleared) {
|
||||||
|
state = BattleZoneDebugState::Cleared;
|
||||||
|
} else if (activeBattleZoneIndex_ == static_cast<int>(i)) {
|
||||||
|
state = BattleZoneDebugState::Active;
|
||||||
|
}
|
||||||
|
debugInfo.push_back(BattleZoneDebugInfo{i, state});
|
||||||
|
}
|
||||||
|
return debugInfo;
|
||||||
|
}
|
||||||
|
|
||||||
void WhiteboxScene::configureCamera() {
|
void WhiteboxScene::configureCamera() {
|
||||||
auto* camera = activeCamera();
|
auto* camera = activeCamera();
|
||||||
if (!camera) {
|
if (!camera) {
|
||||||
@@ -83,6 +111,41 @@ void WhiteboxScene::toggleDebugOverlay() {
|
|||||||
debugFlags_.showSpawnPoints = enabled;
|
debugFlags_.showSpawnPoints = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WhiteboxScene::clearActiveBattleZone() {
|
||||||
|
if (activeBattleZoneIndex_ < 0 ||
|
||||||
|
activeBattleZoneIndex_ >= static_cast<int>(battleZoneStates_.size())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
battleZoneStates_[static_cast<size_t>(activeBattleZoneIndex_)].cleared = true;
|
||||||
|
activeBattleZoneIndex_ = -1;
|
||||||
|
cameraController_.SetWorldBounds(level_.cameraBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WhiteboxScene::updateBattleZones() {
|
||||||
|
if (!player_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeBattleZoneIndex_ >= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frostbite2D::Rect playerBounds = player_->GetWorldBounds();
|
||||||
|
for (size_t i = 0; i < level_.battleZones.size(); ++i) {
|
||||||
|
BattleZoneRuntime& runtime = battleZoneStates_[i];
|
||||||
|
if (runtime.cleared ||
|
||||||
|
!level_.battleZones[i].trigger.intersects(playerBounds)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.triggered = true;
|
||||||
|
activeBattleZoneIndex_ = static_cast<int>(i);
|
||||||
|
cameraController_.SetWorldBounds(level_.battleZones[i].cameraBounds);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WhiteboxScene::updateCamera(float deltaTime) {
|
void WhiteboxScene::updateCamera(float deltaTime) {
|
||||||
if (!player_) {
|
if (!player_) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
#include <frostbite2D/scene/scene.h>
|
#include <frostbite2D/scene/scene.h>
|
||||||
#include <frostbite2D/types/type_math.h>
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace ns_game {
|
namespace ns_game {
|
||||||
|
|
||||||
@@ -20,13 +22,23 @@ public:
|
|||||||
void Render() override;
|
void Render() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct BattleZoneRuntime {
|
||||||
|
bool triggered = false;
|
||||||
|
bool cleared = false;
|
||||||
|
};
|
||||||
|
|
||||||
frostbite2D::Camera* activeCamera() const;
|
frostbite2D::Camera* activeCamera() const;
|
||||||
|
std::vector<BattleZoneDebugInfo> buildBattleZoneDebugInfo() const;
|
||||||
void configureCamera();
|
void configureCamera();
|
||||||
void toggleDebugOverlay();
|
void toggleDebugOverlay();
|
||||||
|
void clearActiveBattleZone();
|
||||||
|
void updateBattleZones();
|
||||||
void updateCamera(float deltaTime);
|
void updateCamera(float deltaTime);
|
||||||
|
|
||||||
LevelDefinition level_;
|
LevelDefinition level_;
|
||||||
DebugFlags debugFlags_;
|
DebugFlags debugFlags_;
|
||||||
|
std::vector<BattleZoneRuntime> battleZoneStates_;
|
||||||
|
int activeBattleZoneIndex_ = -1;
|
||||||
CameraController cameraController_;
|
CameraController cameraController_;
|
||||||
StageRenderer stageRenderer_;
|
StageRenderer stageRenderer_;
|
||||||
DebugOverlay debugOverlay_;
|
DebugOverlay debugOverlay_;
|
||||||
|
|||||||
Reference in New Issue
Block a user