C++终端游戏实战:用Dijkstra算法实现AI寻路与路径规划
1. 项目概述:为什么要在终端里用C++写游戏?
很多朋友一听到“游戏开发”,脑海里浮现的可能是Unity、Unreal Engine这些庞然大物,或者是用Python的Pygame库快速搭个图形界面。但今天我想聊点不一样的:用最纯粹的C/C++,在命令行终端(Terminal/Console)里开发游戏。这听起来可能有点“复古”甚至“简陋”,但我认为,这恰恰是深入理解计算机科学核心——特别是数据结构和算法——的绝佳练兵场。
我们这次实战项目的核心,是将Dijkstra算法这个经典的图论算法,融入到一个可交互的终端游戏中。你可能会问,Dijkstra不是用来找地图上两点间最短路径的吗,跟游戏有什么关系?关系大了。想象一下,你正在设计一个迷宫探险游戏,玩家控制角色,怪物AI需要自动寻路来追击玩家;或者在一个策略游戏中,单位需要计算到达资源点的最优路径以节省时间。这些场景的背后,都需要一个高效、可靠的路径规划算法作为支撑。在图形界面下,这些逻辑被华丽的贴图和流畅的动画所掩盖,而在终端里,每一行代码、每一个数据结构的选择、每一次算法的调用,都赤裸裸地决定了游戏的逻辑与性能。这就像在显微镜下观察引擎的每一个齿轮如何啮合,对于想夯实基础、理解底层原理的开发者来说,价值远超使用现成引擎的“拖拽式”开发。
这个项目适合谁呢?首先,当然是正在学习C++和数据结构的同学。课本上的链表、队列、图都是静态的、孤立的例子,而游戏是一个动态的、状态持续变化的系统,将数据结构应用于此,你能真切感受到“选择不同数据结构会极大影响程序效率”这句话的分量。其次,是对算法有浓厚兴趣,想知其然更知其所以然的开发者。通过实现Dijkstra并看到它实时计算出路径,你对贪心策略、松弛操作的理解会深刻得多。最后,即便是经验丰富的工程师,偶尔回归这种“极简”开发,也能帮助剥离繁杂的框架依赖,重新审视问题最本质的解决方案。
2. 核心思路与架构设计
2.1 游戏场景定义:一个简单的网格世界
为了聚焦于算法和数据结构本身,我们需要一个足够简单但又具备代表性的游戏场景。我选择了一个经典的网格化地图。我们可以用一个二维字符数组(或vector<vector<char>>)来表示整个游戏世界,比如:
‘.’代表可通行的空地。‘#’代表不可逾越的墙壁或障碍物。‘P’代表玩家(Player)的当前位置。‘G’代表目标点(Goal)或怪物(Ghost)的初始位置。‘*’可以代表算法计算出的最短路径。
游戏的核心循环是:在终端中绘制这个网格地图,等待玩家输入(如w/a/s/d控制上下左右移动),更新玩家位置,然后调用Dijkstra算法为“怪物”(或任何需要寻路的实体)计算从当前位置到玩家位置的最短路径,并让怪物沿着该路径移动一步。这个过程会循环进行,直到玩家到达目标或被抓到。
2.2 技术选型与工具链搭建
工欲善其事,必先利其器。虽然我们做的是终端游戏,但一个舒适的开发环境能极大提升效率。
编译器与构建工具:
- 编译器:首推MinGW-w64中的
g++。它在Windows上提供完整的GCC工具链,对C++标准支持良好,且与VSCode集成简单。你也可以使用MSVC(Visual Studio自带),但为了跨平台一致性,g++是更通用的选择。 - 构建系统:对于这种规模的项目,直接使用
Makefile是最清晰、最直接的方式。它定义了如何编译、链接你的源文件,管理起来比在IDE里点来点去更透明。一个基础的Makefile可能长这样:CXX = g++ CXXFLAGS = -std=c++17 -Wall -Wextra -O2 TARGET = maze_game SRCS = main.cpp game.cpp dijkstra.cpp OBJS = $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) $(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ clean: rm -f $(OBJS) $(TARGET)
集成开发环境(IDE):
- Visual Studio Code (VSCode)+C/C++扩展是绝配。它轻量、免费、插件生态丰富。你需要正确配置
c_cpp_properties.json(设置编译器路径和C++标准)以及tasks.json(配置构建任务,比如调用上面的make命令)。网上教程很多,核心是让VSCode能找到你的g++并理解你的项目结构。 - 为什么不直接用Visual Studio?VS当然强大,特别是其调试器。但对于这种强调底层和跨平台的小项目,VSCode+MinGW的组合更轻便,且强迫你更了解编译链接过程。如果你更熟悉VS,用它也完全没问题。
核心库的选择: 我们的目标是“纯净”的C++,所以应尽量避免大型图形或游戏库。我们将主要使用:
- C++标准库 (STL):这是我们数据结构的军火库。
vector,queue,priority_queue,pair,tuple等将是我们的主力。 - Windows.h / curses.h:为了在终端中实现“动画”效果(如清屏、光标定位、非阻塞输入),我们需要平台相关的终端控制库。在Windows上,可以使用
<windows.h>中的SetConsoleCursorPosition等函数。在Linux/macOS上,则可以使用ncurses库。为了简化,本文示例将主要给出逻辑核心代码,终端控制部分会抽象成几个函数。
注意:跨平台终端处理是个麻烦事。一个实用的建议是,在开发初期,可以先专注于核心算法和游戏逻辑的实现,用最简单的循环打印整个地图来观察状态。等核心功能稳定后,再专门封装一个
TerminalHelper类来处理不同平台的清屏、光标移动和键盘输入。
2.3 数据结构映射:从概念到代码
游戏中的每个元素都需要在内存中有其对应的表示,这就是数据结构设计的起点。
- 地图 (Map):使用
std::vector<std::vector<char>>或char grid[HEIGHT][WIDTH]。vector的版本更灵活(地图尺寸可运行时决定),而二维数组版本更简单直观。我倾向于使用vector,因为它能方便地使用grid[y][x]来访问(注意y是行,x是列)。 - 位置 (Position):用一个简单的
struct Point { int x; int y; }或者直接使用std::pair<int, int>。定义它时,重载==运算符和std::hash会非常有用,便于后续在容器中查找和比较。 - 游戏状态 (Game State):需要一个结构体或类来封装整个游戏的状态,例如:
class GameState { public: std::vector<std::vector<char>> map; Point playerPos; Point enemyPos; bool running; // ... 其他状态,如分数、步数 void render(); // 渲染到终端 void processInput(char cmd); // 处理输入 void updateAI(); // 更新AI(调用Dijkstra) }; - 图 (Graph) 的表示:这是Dijkstra算法的输入。我们的网格地图天然就是一个图:每个格子是一个节点,上下左右相邻的可通行格子之间有一条边(权值为1,因为移动一格代价相同)。我们通常采用邻接表或隐式建图。
- 隐式建图:对于网格这种结构规整的图,我们不需要预先构建一个庞大的邻接表数据结构。在Dijkstra算法运行时,当处理到某个节点
(x, y)时,我们直接检查其四个邻居(x+1,y),(x-1,y),(x,y+1),(x,y-1)。如果邻居坐标合法且不是墙,那么这个邻居就是当前节点的一条出边。这种方法节省内存,代码也简洁。
- 隐式建图:对于网格这种结构规整的图,我们不需要预先构建一个庞大的邻接表数据结构。在Dijkstra算法运行时,当处理到某个节点
3. Dijkstra算法在游戏寻路中的实现与优化
3.1 算法核心思想回顾与游戏化理解
Dijkstra算法解决的是带权非负单源最短路径问题。放在我们的游戏里:
- 源点 (Source):怪物当前的位置。
- 目标点 (Destination):玩家当前的位置。
- 图 (Graph):整个可通行的网格,每个格子是节点,相邻格子间的移动代价为1。
- 目标:找出从怪物位置到玩家位置,经过最少格子数(即最短路径)的走法。
算法的核心是贪心 + 动态规划。它维护两个关键集合:
- 已确定最短距离的节点集合 (S):算法已经找到了从源点到这些节点的绝对最短路径。
- 未确定节点的估计距离 (dist):一个数组(或映射),记录从源点到每个节点的当前已知最短距离估计值。
算法过程就像一场“波”的扩散:从源点开始,每次从“未确定”集合中挑选一个估计距离最小的节点,把它加入“已确定”集合(因为不可能有更短的路径了,这是权值非负的关键),然后“松弛”它的所有邻居——即检查如果经过这个新确定的节点去到它的邻居,会不会比已知的路径更短,如果是,就更新邻居的估计距离。
在游戏中,我们不仅需要知道最短距离是多少,还需要知道具体怎么走。因此,我们还需要一个predecessor(前驱)数组,记录到达每个节点的“上一个节点”是谁。当算法结束时,从目标点(玩家)反向追溯这个前驱链,就能得到完整的路径。
3.2 使用STL容器的高效C++实现
直接上代码,让我们看看如何用C++ STL优雅地实现它。我们将采用隐式建图和优先队列优化(这就是常说的“堆优化Dijkstra”)。
#include <vector> #include <queue> #include <climits> #include <unordered_map> #include <utility> struct Point { int x, y; bool operator==(const Point& other) const { return x == other.x && y == other.y; } }; // 为Point特化std::hash,用于unordered_map namespace std { template<> struct hash<Point> { size_t operator()(const Point& p) const { return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1); } }; } // 优先队列中使用的元素类型:{距离, 点} using PQElement = std::pair<int, Point>; // 方向数组:右,左,下,上 const std::vector<Point> directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; std::vector<Point> dijkstra(const std::vector<std::vector<char>>& grid, const Point& start, const Point& goal) { int rows = grid.size(); int cols = grid[0].size(); // 距离映射表,初始化为无穷大 std::unordered_map<Point, int, std::hash<Point>> dist; // 前驱映射表,记录路径 std::unordered_map<Point, Point, std::hash<Point>> prev; // 小顶堆优先队列 std::priority_queue<PQElement, std::vector<PQElement>, std::greater<PQElement>> pq; // 初始化 for (int y = 0; y < rows; ++y) { for (int x = 0; x < cols; ++x) { if (grid[y][x] != '#') { // 只关心可通行区域 dist[{x, y}] = INT_MAX; } } } dist[start] = 0; pq.push({0, start}); while (!pq.empty()) { auto [currentDist, current] = pq.top(); pq.pop(); // 如果当前取出的距离大于记录的距离,说明是旧数据,跳过 if (currentDist > dist[current]) { continue; } // 如果找到目标,提前退出(非必须,但游戏寻路中常见) if (current == goal) { break; } // 遍历四个方向的邻居 for (const auto& dir : directions) { Point neighbor = {current.x + dir.x, current.y + dir.y}; // 检查邻居是否在地图范围内且可通行 if (neighbor.x < 0 || neighbor.x >= cols || neighbor.y < 0 || neighbor.y >= rows || grid[neighbor.y][neighbor.x] == '#') { continue; } // 计算新的距离 int newDist = currentDist + 1; // 每一步代价为1 // 松弛操作 if (newDist < dist[neighbor]) { dist[neighbor] = newDist; prev[neighbor] = current; // 记录前驱 pq.push({newDist, neighbor}); } } } // 从目标点回溯构建路径 std::vector<Point> path; // 如果目标点不可达,返回空路径 if (dist.find(goal) == dist.end() || dist[goal] == INT_MAX) { return path; } for (Point at = goal; at != start; at = prev[at]) { path.push_back(at); } path.push_back(start); std::reverse(path.begin(), path.end()); // 反转得到从起点到终点的路径 return path; }代码关键点解析:
unordered_mapvsvector:这里用unordered_map<Point, int>来存储距离。因为我们的节点是二维坐标,如果用二维数组dist[rows][cols],访问是O(1),更高效。但使用unordered_map的代码更清晰,且能自动处理只存储可通行节点的问题。在性能敏感时,应改用二维向量。- 优先队列 (
priority_queue):这是堆优化Dijkstra的核心。我们使用std::greater作为比较函数,使其成为小顶堆,确保每次弹出的都是当前估计距离最小的节点。注意队列中元素是{距离, 点}。 if (currentDist > dist[current]) continue;:这是处理优先队列中“过时”条目(stale entry)的关键。因为同一个节点可能被多次加入队列(每次发现更短路径时),但只有距离最小的那次是有效的。这条语句能跳过无效的、旧的距离值,保证正确性。- 路径回溯:通过
prev映射表,我们从goal开始,不断查找前驱节点,直到回到start,然后反转列表,就得到了从起点到终点的路径。
3.3 性能考量与潜在优化
对于小地图(比如50x50),上述实现已经绰绰有余。但如果地图很大,或者需要每帧为多个实体计算路径,就需要考虑优化:
- 距离存储结构:将
unordered_map替换为二维std::vector<int>。访问从哈希查找的O(1)平均复杂度变为真正的O(1),常数时间更小。内存是连续的,对缓存友好。 - 优先队列的替代品:
std::priority_queue不支持修改队列中已有元素的优先级(我们通过插入新元素实现)。在极端性能要求下,可以考虑使用std::set(也是有序的,且能查找并修改)或手写斐波那契堆,但后者实现复杂,通常收益不大。 - 算法层面的替代:
- A* 算法:这是游戏AI寻路的实际标准。它在Dijkstra的基础上,增加了一个启发式函数(通常是到目标的曼哈顿距离或欧几里得距离估计)。这个函数引导算法优先探索更可能接近目标的方向,从而大幅减少需要探索的节点数。在我们的网格游戏中,将Dijkstra升级到A*几乎总是更好的选择,改动很小(只需修改优先队列的优先级为
f = g + h,其中g是当前距离,h是启发值)。 - 双向搜索:同时从起点和终点开始执行搜索,直到两个搜索区域相遇。这能有效减少搜索空间。
- A* 算法:这是游戏AI寻路的实际标准。它在Dijkstra的基础上,增加了一个启发式函数(通常是到目标的曼哈顿距离或欧几里得距离估计)。这个函数引导算法优先探索更可能接近目标的方向,从而大幅减少需要探索的节点数。在我们的网格游戏中,将Dijkstra升级到A*几乎总是更好的选择,改动很小(只需修改优先队列的优先级为
- 空间换时间——预计算:如果地图是静态的(障碍物不变),可以预先计算所有节点对之间的最短路径(例如使用Floyd-Warshall算法),存储起来。运行时寻路就是O(1)的查表操作。但这只适用于小地图或中等地图,因为空间复杂度是O(n²)。
实操心得:在游戏开发中,“够用就好”是重要的优化原则。不要过早优化。先用清晰的Dijkstra实现功能,用性能分析工具(如
gprof、Valgrind的callgrind)定位真正的瓶颈。很多时候,终端渲染或输入处理的效率可能比路径查找更值得关注。
4. 游戏主循环与系统集成
4.1 构建游戏主循环骨架
游戏主循环是驱动一切的核心,它通常遵循“输入-更新-渲染”的模式。
class MazeGame { private: GameState state; bool gameOver; public: MazeGame(int width, int height) : gameOver(false) { // 初始化地图,放置玩家、目标、墙壁 state.map = std::vector<std::vector<char>>(height, std::vector<char>(width, '.')); initializeMap(); // 自定义函数,生成地图 state.playerPos = {1, 1}; state.enemyPos = {width-2, height-2}; } void run() { while (!gameOver) { render(); char input = getNonBlockingInput(); // 非阻塞获取输入 if (input == 'q') { gameOver = true; break; } processInput(input); // 处理移动 updateAI(); // 更新怪物AI(调用Dijkstra) checkGameConditions(); // 检查胜负 // 简单延时,控制游戏速度 std::this_thread::sleep_for(std::chrono::milliseconds(200)); } showGameResult(); } void render() { // 清屏(平台相关) clearScreen(); // 复制一份地图用于显示 auto displayMap = state.map; // 标记玩家和怪物 displayMap[state.playerPos.y][state.playerPos.x] = 'P'; displayMap[state.enemyPos.y][state.enemyPos.x] = 'G'; // 计算并显示路径(可选,用于调试) auto path = dijkstra(state.map, state.enemyPos, state.playerPos); if (path.size() > 1) { // 排除起点自身 for (size_t i = 1; i < path.size(); ++i) { // 从索引1开始,不覆盖怪物位置 if (displayMap[path[i].y][path[i].x] == '.') { displayMap[path[i].y][path[i].x] = '*'; } } } // 打印地图 for (const auto& row : displayMap) { for (char cell : row) { std::cout << cell; } std::cout << '\n'; } std::cout << "WASD移动,Q退出" << std::endl; } void processInput(char cmd) { Point newPos = state.playerPos; switch (cmd) { case 'w': newPos.y--; break; case 's': newPos.y++; break; case 'a': newPos.x--; break; case 'd': newPos.x++; break; default: return; } // 检查移动是否合法(不撞墙) if (isValidPosition(newPos) && state.map[newPos.y][newPos.x] != '#') { state.playerPos = newPos; } } void updateAI() { auto path = dijkstra(state.map, state.enemyPos, state.playerPos); if (path.size() > 1) { // 如果存在路径且不止起点 // 怪物沿着路径向玩家移动一步(取路径中的下一个点) state.enemyPos = path[1]; // path[0]是怪物自己,path[1]是下一步 } // 如果path为空或只有一个点,说明怪物无法移动或已到达,可以不做处理 } void checkGameConditions() { if (state.playerPos == state.enemyPos) { gameOver = true; std::cout << "\n你被怪物抓住了!游戏结束。\n"; } // 可以添加到达目标点的胜利条件 // if (state.playerPos == goalPos) { ... } } // ... 其他辅助函数,如clearScreen, getNonBlockingInput, isValidPosition等 };4.2 终端交互的“坑”与技巧
在终端里做游戏,最大的挑战之一就是输入输出控制。
非阻塞输入:标准的
std::cin是阻塞的,程序会停在那里等待用户按键。对于游戏循环,我们需要非阻塞输入——有按键就读入,没有就继续。这在Windows和Unix-like系统上方法不同。- Windows: 使用
<conio.h>中的_kbhit()和_getch()。 - Linux/macOS: 使用
<termios.h>和<unistd.h>来修改终端模式(将标准输入设为非规范模式),然后使用read()。
注意:处理跨平台输入会引入大量条件编译 (
#ifdef _WIN32)。一个建议是,初期可以先用阻塞输入,每按一次键更新一次,这样逻辑简单。等游戏核心稳定后再去啃非阻塞输入这块硬骨头。- Windows: 使用
清屏与光标定位:
- 清屏:Windows下可以用
system(“cls”),Linux下用system(“clear”)。但频繁调用system有性能开销。更优的做法是使用ANSI转义序列(大多数现代终端都支持):std::cout << “\033[2J\033[1;1H”;。\033[2J清屏,\033[1;1H将光标移到左上角。 - 光标定位:同样可以用ANSI序列:
\033[row;colH。例如,要在第5行第10列打印,可以std::cout << “\033[5;10HX”;。这允许你只重绘变化的部分,而不是整个屏幕,从而实现更流畅的动画。
- 清屏:Windows下可以用
帧率控制:主循环中的
sleep是控制游戏速度最简单粗暴的方式。但要注意,sleep的精度不高,且会阻塞整个线程。更精细的做法是计算每一帧耗时,然后动态调整。
4.3 让游戏更有趣:扩展功能点
基础版本跑通后,可以尝试添加更多元素,深化对数据结构的运用:
- 多怪物与不同AI:用
std::vector<Point>存储多个怪物位置。可以为不同怪物赋予不同的行为模式:有的用Dijkstra紧追不舍,有的用随机游走,有的只在玩家进入一定范围(使用BFS计算距离)后才开始追击。这引入了行为树或状态机的简单概念。 - 可变地形与权值:让地图格子不仅有“可通过”和“不可通过”,还有“沼泽”(移动代价为2)、“公路”(移动代价为0.5)。Dijkstra算法能完美处理不同权值的边,只需在计算
newDist时加上边的权值即可。这让你思考如何设计地图数据结构和算法中的代价计算。 - 路径平滑与显示:算法计算出的路径是网格中心的连线,看起来是锯齿状的。可以尝试简单的路径平滑算法。在显示上,可以用不同的字符(如
>,v,<,^)根据路径方向来绘制箭头,视觉效果更好。 - 地图编辑器:单独写一个程序,允许你用鼠标或键盘交互式地放置墙壁、玩家、怪物,然后将地图保存为文件。主游戏程序再从文件读取。这涉及到文件I/O和更复杂的状态管理。
5. 调试、问题排查与性能分析实录
5.1 编译与链接常见问题
- **“undefined reference to
WinMain@16’”**: 这通常意味着你的程序被链接成了GUI子系统程序,但你没有提供WinMain入口函数。确保你的main函数是int main(),并且在编译链接时没有错误地指定了/SUBSYSTEM:WINDOWS`(MSVC)或类似选项。在g++中,这通常不是问题。 - “cannot find -lpdcurses” 或类似库错误: 如果你使用了
ncurses库,在Linux下需要用-lncurses链接。在Windows下使用pdcurses可能需要指定正确的库路径和文件名。仔细检查你的编译命令和库安装情况。 - C++标准不兼容: 确保你的编译器支持你代码中使用的C++特性(如C++17的结构化绑定
auto [dist, point] = …)。在g++中使用-std=c++17标志。
5.2 运行时逻辑错误排查
怪物不动或乱走:
- 检查地图边界:最常见的原因是
isValidPosition函数有误,或者方向数组directions导致邻居坐标越界。在访问grid[neighbor.y][neighbor.x]前,务必确保neighbor的x和y在[0, width)和[0, height)范围内。 - 检查Dijkstra返回值:在
updateAI中,打印path的大小和内容。如果path为空,说明起点或终点是墙,或者起点终点相同。如果path只有1个点(起点),说明怪物已经在玩家位置上。 - 检查路径回溯逻辑:确保
prev映射被正确填充。在Dijkstra函数中,可以在更新dist和prev后添加调试输出。
- 检查地图边界:最常见的原因是
路径显示不正确(如穿过墙壁):
- 验证地图数据:渲染时,确保用于显示的地图
displayMap是原始地图的副本,而不是引用。否则标记路径可能会永久修改地图数据。 - 检查Dijkstra的邻居有效性判断:确认在判断
grid[neighbor.y][neighbor.x] == ‘#’时,grid是原始的、不包含玩家和怪物的地图。最好使用一个专门存储地形信息的terrainGrid。
- 验证地图数据:渲染时,确保用于显示的地图
游戏循环卡死或反应迟钝:
- 非阻塞输入失效:如果使用了非阻塞输入但实现有误,可能会导致输入缓冲区混乱,程序无法响应。回退到阻塞输入进行测试。
- Dijkstra计算过慢:对于非常大的地图,每帧都计算完整路径可能导致卡顿。添加一个帧计数器,每N帧为怪物计算一次新路径,而不是每帧都计算。或者,仅在玩家移动后重新计算路径。
5.3 性能分析与优化实践
当你觉得游戏有点“卡”的时候,就需要请出性能分析工具了。
简单的计时:在Dijkstra函数开始和结束处使用
std::chrono高精度时钟测量耗时。#include <chrono> auto start = std::chrono::high_resolution_clock::now(); // ... 调用 dijkstra ... auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start); std::cout << “Dijkstra took ” << duration.count() << “ microseconds.\n”;这能让你快速知道算法是否是瓶颈。
使用性能分析工具:
- gprof (GNU Profiler): 在编译时加上
-pg标志,运行程序后会生成gmon.out文件,然后用gprof命令分析。它会告诉你每个函数被调用了多少次,耗时占比多少。这是定位“热点函数”的利器。 - Valgrind 的 Callgrind: 更强大的工具,能提供调用关系图和更细致的开销分析。使用
valgrind –tool=callgrind ./your_program运行,然后用kcachegrind可视化查看结果。
- gprof (GNU Profiler): 在编译时加上
优化实战案例:假设分析发现
dijkstra函数占用了95%的时间。优化步骤:- 第一步:更换距离容器。将
unordered_map<Point, int>改为vector<vector<int>> dist(rows, vector<int>(cols, INT_MAX))。这通常能带来数量级的提升,因为内存访问模式从间接、可能缓存不友好的哈希查找,变成了连续内存的直接访问。 - 第二步:考虑A*算法。如果地图很大且起点终点距离远,A*通过启发式函数能显著减少探索的节点数。在我们的网格游戏中,曼哈顿距离是一个很好的启发函数。
- 第三步:减少调用频率。怪物真的需要每帧都重新计算完整路径吗?也许可以每5帧计算一次,或者只在玩家移动超过一定距离后重新计算。
- 第一步:更换距离容器。将
5.4 内存管理注意事项
在这个规模的项目中,手动内存管理(new/delete)不是必须的,应优先使用STL容器(vector,queue等),它们会自动管理内存。但要注意:
- 避免不必要的拷贝:在函数传参时,对于大的地图数据,使用
const std::vector<std::vector<char>>&这样的常量引用,而不是值传递。 - 警惕循环引用:如果你的游戏对象之间互相用
shared_ptr指向对方,可能会导致内存无法释放。仔细设计对象所有权关系,优先使用unique_ptr或原始指针表示非拥有关系。
从零开始用C++在终端里实现一个融合了Dijkstra算法的游戏,这个过程就像亲手搭建一座微型的数字机械钟。你看到的不仅是时针分针的转动,更是背后每一个齿轮的精密咬合。它强迫你去思考坐标如何映射到内存、状态如何随时间变化、数据如何被高效地组织和访问。当看到怪物沿着你亲手实现的算法计算出的路径,一步步逼近玩家时,那种对代码的掌控感和对原理的理解深度,是调用现成游戏引擎API所无法比拟的。这个项目或许没有炫酷的画面,但它给予你的,是扎实的、可迁移的编程和算法能力,这才是应对更复杂软件工程的真正基石。
