书写实验报告 调整开始界面 修复逃跑后重新进入战斗的问题

This commit is contained in:
2026-06-03 18:27:46 +08:00
parent ed7bb908cb
commit e0e9b5d7af
11 changed files with 268 additions and 43 deletions
+26
View File
@@ -61,6 +61,7 @@ target_compile_options(mana_core PRIVATE -Wall -Wextra -Wpedantic)
add_executable(mana_pet_world add_executable(mana_pet_world
src/app/LogicalViewport.cpp src/app/LogicalViewport.cpp
src/app/TitleMenu.cpp
src/app/main.cpp src/app/main.cpp
src/battle/BattleScene.cpp src/battle/BattleScene.cpp
) )
@@ -69,6 +70,7 @@ target_compile_options(mana_pet_world PRIVATE -Wall -Wextra -Wpedantic)
enable_testing() enable_testing()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/app/LogicalViewportTest.cpp")
add_executable(logical_viewport_test add_executable(logical_viewport_test
tests/app/LogicalViewportTest.cpp tests/app/LogicalViewportTest.cpp
src/app/LogicalViewport.cpp src/app/LogicalViewport.cpp
@@ -76,3 +78,27 @@ add_executable(logical_viewport_test
target_include_directories(logical_viewport_test PRIVATE src/app) target_include_directories(logical_viewport_test PRIVATE src/app)
target_compile_options(logical_viewport_test PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(logical_viewport_test PRIVATE -Wall -Wextra -Wpedantic)
add_test(NAME logical_viewport_test COMMAND logical_viewport_test) add_test(NAME logical_viewport_test COMMAND logical_viewport_test)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/app/TitleMenuTest.cpp")
add_executable(title_menu_test
tests/app/TitleMenuTest.cpp
src/app/TitleMenu.cpp
)
target_include_directories(title_menu_test PRIVATE src/app)
target_compile_options(title_menu_test PRIVATE -Wall -Wextra -Wpedantic)
add_test(NAME title_menu_test COMMAND title_menu_test)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/app/BattleEncounterTest.cpp")
add_executable(battle_encounter_test
tests/app/BattleEncounterTest.cpp
src/app/LogicalViewport.cpp
src/app/TitleMenu.cpp
src/battle/BattleScene.cpp
)
target_include_directories(battle_encounter_test PRIVATE src/app)
target_link_libraries(battle_encounter_test PRIVATE mana_core PkgConfig::RAYLIB)
target_compile_options(battle_encounter_test PRIVATE -Wall -Wextra -Wpedantic)
add_test(NAME battle_encounter_test COMMAND battle_encounter_test)
endif()
+33
View File
@@ -0,0 +1,33 @@
#include "TitleMenu.h"
namespace mana::app {
std::vector<TitleMenuItem> BuildTitleMenuItems(bool hasSave)
{
std::vector<TitleMenuItem> items;
if (hasSave) {
items.push_back({
TitleMenuAction::ContinueGame,
"继续游戏",
true,
});
}
items.push_back({
TitleMenuAction::NewGame,
"开始新游戏",
true,
});
items.push_back({
TitleMenuAction::Help,
"帮助",
true,
});
items.push_back({
TitleMenuAction::Quit,
"退出游戏",
true,
});
return items;
}
} // namespace mana::app
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <string>
#include <vector>
namespace mana::app {
enum class TitleMenuAction {
ContinueGame,
NewGame,
Help,
Quit
};
struct TitleMenuItem {
TitleMenuAction action = TitleMenuAction::ContinueGame;
std::string label;
bool enabled = true;
};
std::vector<TitleMenuItem> BuildTitleMenuItems(bool hasSave);
} // namespace mana::app
+19 -36
View File
@@ -17,6 +17,7 @@
#include "ScriptedInteractable.h" #include "ScriptedInteractable.h"
#include "SoundAssets.h" #include "SoundAssets.h"
#include "SpriteAnimation.h" #include "SpriteAnimation.h"
#include "TitleMenu.h"
#include "TmxMap.h" #include "TmxMap.h"
#include "TmxMapPool.h" #include "TmxMapPool.h"
#include "TmxWorld.h" #include "TmxWorld.h"
@@ -55,6 +56,8 @@ using mana::app::kLogicalScreenHeight;
using mana::app::kLogicalScreenWidth; using mana::app::kLogicalScreenWidth;
using mana::app::kMinimumWindowHeight; using mana::app::kMinimumWindowHeight;
using mana::app::kMinimumWindowWidth; using mana::app::kMinimumWindowWidth;
using mana::app::TitleMenuAction;
using mana::app::TitleMenuItem;
Camera2D LogicalUiCamera(const mana::app::LogicalViewport& viewport) Camera2D LogicalUiCamera(const mana::app::LogicalViewport& viewport)
{ {
@@ -184,12 +187,6 @@ enum class InventoryPage {
Crafting Crafting
}; };
enum class TitleMenuAction {
ContinueGame,
NewGame,
Help
};
enum class PauseMenuAction { enum class PauseMenuAction {
Resume, Resume,
Save, Save,
@@ -197,12 +194,6 @@ enum class PauseMenuAction {
Quit Quit
}; };
struct TitleMenuItem {
TitleMenuAction action = TitleMenuAction::ContinueGame;
std::string label;
bool enabled = true;
};
struct PauseMenuItem { struct PauseMenuItem {
PauseMenuAction action = PauseMenuAction::Resume; PauseMenuAction action = PauseMenuAction::Resume;
std::string label; std::string label;
@@ -2650,25 +2641,7 @@ std::optional<SaveState> LoadTitleSavePreview(const Runtime& rt)
std::vector<TitleMenuItem> BuildTitleMenuItems(const Runtime& rt) std::vector<TitleMenuItem> BuildTitleMenuItems(const Runtime& rt)
{ {
const bool hasSave = LoadTitleSavePreview(rt).has_value(); const bool hasSave = LoadTitleSavePreview(rt).has_value();
std::vector<TitleMenuItem> items; return mana::app::BuildTitleMenuItems(hasSave);
if (hasSave) {
items.push_back({
TitleMenuAction::ContinueGame,
"继续游戏",
true,
});
}
items.push_back({
TitleMenuAction::NewGame,
"开始新游戏",
true,
});
items.push_back({
TitleMenuAction::Help,
"帮助",
true,
});
return items;
} }
void DrawTitleButton(Runtime& rt, Font font, const TitleMenuItem& item, Rectangle bounds, bool selected) void DrawTitleButton(Runtime& rt, Font font, const TitleMenuItem& item, Rectangle bounds, bool selected)
@@ -2713,15 +2686,20 @@ void DrawTitleMenu(Runtime& rt, Font font)
DrawTextCn(font, "托诺里宠物世界", {86.0f, 70.0f}, 48.0f, Color{255, 255, 221, 255}); DrawTextCn(font, "托诺里宠物世界", {86.0f, 70.0f}, 48.0f, Color{255, 255, 221, 255});
DrawTextCn(font, "Tonori Pet World", {90.0f, 126.0f}, 24.0f, Color{245, 219, 132, 255}); DrawTextCn(font, "Tonori Pet World", {90.0f, 126.0f}, 24.0f, Color{245, 219, 132, 255});
const Rectangle menuPanel{74.0f, 244.0f, 500.0f, 338.0f}; const std::vector<TitleMenuItem> items = BuildTitleMenuItems(rt);
rt.selectedTitleSlot = std::clamp(rt.selectedTitleSlot, 0, static_cast<int>(items.size()) - 1);
const float rowStartY = 330.0f;
const float rowSpacing = 78.0f;
const float rowHeight = 62.0f;
const float lastRowBottom = rowStartY + static_cast<float>(std::max(0, static_cast<int>(items.size()) - 1)) * rowSpacing + rowHeight;
const Rectangle menuPanel{74.0f, 244.0f, 500.0f, std::max(338.0f, lastRowBottom + 32.0f - 244.0f)};
DrawWindowPanel(rt, menuPanel); DrawWindowPanel(rt, menuPanel);
DrawIcon(rt, "icon/map.png", {106.0f, 266.0f}, 30.0f); DrawIcon(rt, "icon/map.png", {106.0f, 266.0f}, 30.0f);
DrawTextCn(font, "开始界面", {146.0f, 265.0f}, 27.0f, Color{255, 255, 221, 255}); DrawTextCn(font, "开始界面", {146.0f, 265.0f}, 27.0f, Color{255, 255, 221, 255});
std::vector<TitleMenuItem> items = BuildTitleMenuItems(rt);
rt.selectedTitleSlot = std::clamp(rt.selectedTitleSlot, 0, static_cast<int>(items.size()) - 1);
for (int i = 0; i < static_cast<int>(items.size()); ++i) { for (int i = 0; i < static_cast<int>(items.size()); ++i) {
const Rectangle row{110.0f, 330.0f + static_cast<float>(i) * 78.0f, 420.0f, 62.0f}; const Rectangle row{110.0f, rowStartY + static_cast<float>(i) * rowSpacing, 420.0f, rowHeight};
DrawTitleButton(rt, font, items[static_cast<std::size_t>(i)], row, i == rt.selectedTitleSlot); DrawTitleButton(rt, font, items[static_cast<std::size_t>(i)], row, i == rt.selectedTitleSlot);
} }
@@ -2742,7 +2720,7 @@ void DrawTitleMenu(Runtime& rt, Font font)
} }
if (rt.titleNewGameConfirm) { if (rt.titleNewGameConfirm) {
DrawTextCn(font, "再次确认将覆盖当前存档", {112.0f, 558.0f}, 17.0f, Color{245, 219, 132, 255}); DrawTextCn(font, "再次确认将覆盖当前存档", {112.0f, lastRowBottom + 12.0f}, 17.0f, Color{245, 219, 132, 255});
} }
} }
@@ -4487,6 +4465,7 @@ void StartBattle(Runtime& rt, int petIndex, bool playerFirstAction = false)
RestoreActivePetForRetreat(rt); RestoreActivePetForRetreat(rt);
rt.battle.player = rt.team.pets.empty() ? MakePet("Lulea", 30, 7) : rt.team.pets.front(); rt.battle.player = rt.team.pets.empty() ? MakePet("Lulea", 30, 7) : rt.team.pets.front();
rt.battle.wild = rt.wildPets[petIndex].pet; rt.battle.wild = rt.wildPets[petIndex].pet;
ScheduleMonsterRespawn(rt, rt.wildPets[petIndex]);
RegisterPetSeen(rt.petJournal, rt.battle.wild.name); RegisterPetSeen(rt.petJournal, rt.battle.wild.name);
ApplyQuestEvent(rt.questRuntime, QuestEvent::SawPet(rt.battle.wild.name)); ApplyQuestEvent(rt.questRuntime, QuestEvent::SawPet(rt.battle.wild.name));
rt.battle.message = "野外宠物出现了"; rt.battle.message = "野外宠物出现了";
@@ -5734,6 +5713,10 @@ void ExecuteTitleMenuAction(Runtime& rt, TitleMenuAction action, bool enabled)
rt.titleNewGameConfirm = false; rt.titleNewGameConfirm = false;
rt.mode = Mode::TitleHelp; rt.mode = Mode::TitleHelp;
break; break;
case TitleMenuAction::Quit:
rt.titleNewGameConfirm = false;
rt.exitRequested = true;
break;
} }
} }
+72
View File
@@ -0,0 +1,72 @@
# ManaPetWorld AI Agent 实验报告
## 1. 如何提示 Agent 实现需求
我采用了"ai 骑着 Agent 跑"的提示方式。每次让 Agent 修改前,先说明要实现的游戏体验,利用 LLM 先生成提示词和计划。
## 2. 如何让多轮修改的进度可重用
第一,要求 Agent 在每轮开始前先读取项目说明和现有代码模式。使用 `codegraph` MCP,辅助完成这项任务
第二,把需求拆成可验收的小任务。使用 `plan` 模式帮助设计。
第三,人工调试。人工验收并使用调试模式人工定位具体问题。
## 3. 多次尝试后仍然困难的错误及解决过程
实验中最难处理的问题是地图重定位和传送落点错误。
- TMX 地图中的传送对象坐标配置错误。
- 世界地图 placement 与单张地图局部坐标换算不一致。
- Warp 目标读取时把瓦片坐标和像素坐标混用。
- 玩家重定位后朝向、碰撞或附近 Warp 状态没有同步更新。
- 地图名使用了旧资源名或别名,导致目标地图解析不稳定。
Agent 前几次只根据代码推断问题,定位不够准确。之后我改变提示方式,不再直接要求“修好传送”,而是要求 Agent 先增加调试模式。
最终采用的解决方式是让 AI 增加 `F3` 坐标调试模式。调试面板显示:
- 当前地图名 `Map`
- 玩家局部坐标 `Local`
- 世界坐标 `World`
- 当前世界坐标覆盖到的地图列表
- 附近传送点及其目标地图 `Warp`
有了这些信息后,我可以重新定位问题:先在错误现场按 `F3`,记录当前地图和坐标,再告诉 Agent 修改方向,例如“当前位置已经进入目标地图,但 World 坐标覆盖不对,优先检查 LocalToWorld 换算和 world placement”,或者“附近 Warp 目标名正确但落点偏移,优先检查 ReadWarpTarget 和 TMX 对象坐标单位”。这比单纯描述“传送错了”更有效。
## 4. 最终效果
- 支持 NPC 对话、野外宠物遭遇、战斗、背包、捕捉符和任务日志。
- 支持小地图、日志面板、任务面板等 UI 展示。
<p align="center">
<img src="../screenshot/image0.png" alt="开始界面" width="600">
</p>
<p align="center">
图1:开始界面
</p>
<p align="center">
<img src="../screenshot/image1.png" alt="任务和地图探索" width="600">
</p>
<p align="center">
图2:任务和地图探索
</p>
<p align="center">
<img src="../screenshot/image2.png" alt="战斗界面" width="600">
</p>
<p align="center">
图3:战斗界面
</p>
<p align="center">
<img src="../screenshot/image3.png" alt="背包界面" width="600">
</p>
<p align="center">
图4:背包界面
</p>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 KiB

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 712 KiB

+47
View File
@@ -0,0 +1,47 @@
#define main mana_pet_world_real_main
#include "../../src/app/main.cpp"
#undef main
#include <cassert>
#include <cmath>
#include <filesystem>
namespace {
void StartBattleHidesTriggeredMonster()
{
const std::filesystem::path savePath = std::filesystem::temp_directory_path() / "mana_pet_world_battle_encounter_test_save.txt";
std::filesystem::remove(savePath);
Runtime rt;
rt.currentMapName = "TestMap";
rt.savePath = savePath;
Entity wild;
wild.name = "TestMonster";
wild.objectKey = "TestMap#monster#1";
wild.pet = MakePet("Lulea", 20, 7);
wild.active = true;
wild.moving = true;
wild.respawnDelay = 42.0f;
rt.wildPets.push_back(wild);
StartBattle(rt, 0);
assert(rt.mode == Mode::Battle);
assert(rt.battlePetIndex == 0);
assert(!rt.wildPets[0].active);
assert(!rt.wildPets[0].moving);
assert(std::fabs(rt.wildPets[0].respawnTimer - wild.respawnDelay) < 0.001f);
assert(rt.monsterRespawnUntil.find(wild.objectKey) != rt.monsterRespawnUntil.end());
std::filesystem::remove(savePath);
}
} // namespace
int main()
{
StartBattleHidesTriggeredMonster();
return 0;
}
+41
View File
@@ -0,0 +1,41 @@
#include "TitleMenu.h"
#include <cassert>
using mana::app::BuildTitleMenuItems;
using mana::app::TitleMenuAction;
namespace {
void MenuWithoutSaveOffersNewGameHelpAndQuit()
{
const auto items = BuildTitleMenuItems(false);
assert(items.size() == 3);
assert(items[0].action == TitleMenuAction::NewGame);
assert(items[0].label == "开始新游戏");
assert(items[1].action == TitleMenuAction::Help);
assert(items[1].label == "帮助");
assert(items[2].action == TitleMenuAction::Quit);
assert(items[2].label == "退出游戏");
}
void MenuWithSaveKeepsContinueFirstAndQuitLast()
{
const auto items = BuildTitleMenuItems(true);
assert(items.size() == 4);
assert(items.front().action == TitleMenuAction::ContinueGame);
assert(items.front().label == "继续游戏");
assert(items.back().action == TitleMenuAction::Quit);
assert(items.back().label == "退出游戏");
}
} // namespace
int main()
{
MenuWithoutSaveOffersNewGameHelpAndQuit();
MenuWithSaveKeepsContinueFirstAndQuitLast();
return 0;
}