针对推箱子这类题目,数据维护(即:如何优雅地处理“移动后原来格子变回什么”以及“移动到新格子变成什么”)确实是最大的痛点。
针对上机考试(求稳、求快、逻辑不炸),我认为最佳实践不是用复杂的 if-else 去判断,而是采用 “双层地图法” (Two-Layer Approach)。
如果一定要坚持你偏好的 “单层地图法”(复合数值),也有对应的最佳辅助函数写法。
我把这两种方案都列出来,你可以根据自己的习惯选择。
方案一:双层地图法(强烈推荐,逻辑最稳)
这是我最推荐的方案。它的核心思想是:把“不动的”和“会动的”分开存。
- Layer 1 (Static): 存放 墙、目标点、空地。这些东西在游戏开始后永远不会变。
- Layer 2 (Dynamic): 存放 人、箱子。这些东西会跑来跑去。
1. 数据结构
// Layer 1: 背景层 (只存 0-空地, 1-墙, 3-目标)
// 初始化后只读,永远不修改!
std::vector<std::vector<int>> staticMap;
// Layer 2: 物体层 (存 0-无物体, 2-箱子, 4-人)
// 移动时只修改这个
std::vector<std::vector<int>> dynamicMap; 2. 为什么这是最佳实践?
当你移动人物时,你完全不需要关心脚下有没有目标点。
- 人走开?直接把
dynamicMap的该位置设为0。 - 人进入?直接把
dynamicMap的该位置设为4。 - 原来的目标点呢? 它安安静静地躺在
staticMap里,根本没人动它。
3. 代码实现 (Move Logic)
你看,代码里完全没有“恢复现场”的逻辑,清爽极了:
void MainWindow::movePlayer(int dx, int dy) {
int nx = px + dx;
int ny = py + dy;
// 1. 检查墙 (看 staticMap)
if (staticMap[nx][ny] == 1) return;
// 2. 检查箱子 (看 dynamicMap)
if (dynamicMap[nx][ny] == 2) {
// 推箱子逻辑...
// 只需要改 dynamicMap,不用管 staticMap
dynamicMap[nnx][nny] = 2; // 箱子移到新位置
dynamicMap[nx][ny] = 0; // 旧位置箱子没了
}
// 3. 移动人
dynamicMap[px][py] = 0; // 原位置清空 (不用管是不是目标点,staticMap记着呢)
dynamicMap[nx][ny] = 4; // 新位置设定人
px = nx; py = ny;
}4. 绘图逻辑 (Paint Logic)
绘图时,把两层叠加起来看就行了:
// 先画 staticMap (背景)
if (staticMap[i][j] == 3) drawTarget();
else if (staticMap[i][j] == 1) drawWall();
// 再画 dynamicMap (前景)
if (dynamicMap[i][j] == 2) {
// 进阶:如果 staticMap[i][j] == 3,说明箱子在目标上,画绿色箱子
if (staticMap[i][j] == 3) drawGreenBox();
else drawBrownBox();
}
else if (dynamicMap[i][j] == 4) drawPlayer();方案二:单层地图法 + 状态还原函数(符合你直觉的方案)
如果你觉得维护两个二维数组麻烦,坚持用一个 map(包含 0-6 所有状态),那么最佳实践是:封装“进入”和“离开”的逻辑,坚决不手写 if-else。
你需要写两个辅助函数:enterCell 和 leaveCell。
1. 辅助函数定义
// 当一个物体(objType) 离开 当前格子(currentVal) 后,格子变成了什么?
// objType: 4(人) 或 2(箱子)
// currentVal: 当前格子里的数值 (比如 6-人在目标, 4-人在空地)
int getAfterLeave(int currentVal) {
if (currentVal == 6 || currentVal == 5) return 3; // 只要是在目标上的,离开后变回目标(3)
return 0; // 否则变回空地
}
// 当一个物体(objType) 进入 目标格子(targetVal) 后,格子变成了什么?
// targetVal: 目标格原本的样子 (0-空地, 3-目标)
int getAfterEnter(int targetVal, int objType) {
if (targetVal == 3) {
// 如果进的是目标点
if (objType == 4) return 6; // 人(4) -> 人在目标(6)
if (objType == 2) return 5; // 箱子(2) -> 箱子在目标(5)
}
// 如果进的是空地
return objType; // 保持原样 (4或2)
}2. 代码实现 (Move Logic)
有了这两个函数,你的移动逻辑就变成了公式化的操作,不会出 bug:
void MainWindow::movePlayer(int dx, int dy) {
int nx = px + dx;
int ny = py + dy;
int &nextCell = map[nx][ny]; // 下一步的格子
int &currCell = map[px][py]; // 当前脚下的格子
// --- 撞墙 ---
if (nextCell == 1) return;
// --- 遇到箱子 (2或5) ---
if (nextCell == 2 || nextCell == 5) {
int nnx = nx + dx;
int nny = ny + dy;
int &farCell = map[nnx][nny]; // 箱子后面
// 箱子能推吗?(后面必须是 0-空地 或 3-目标)
if (farCell == 0 || farCell == 3) {
// 1. 箱子离开 nextCell,进入 farCell
farCell = getAfterEnter(farCell, 2); // 箱子进后面
nextCell = getAfterLeave(nextCell); // 箱子离开中间(此时nextCell暂时变回了空地或目标)
// 2. 人离开 currCell,进入 nextCell
// 注意:此时 nextCell 已经是箱子离开后的状态了,直接让人进去
nextCell = getAfterEnter(nextCell, 4);
currCell = getAfterLeave(currCell);
px = nx; py = ny;
}
return;
}
// --- 遇到空地或目标 (0或3) ---
if (nextCell == 0 || nextCell == 3) {
nextCell = getAfterEnter(nextCell, 4); // 人进新坑
currCell = getAfterLeave(currCell); // 人出旧坑
px = nx; py = ny;
}
}总结建议
- 如果你想脑子最省事:选 方案一(双层地图)。因为你不需要去想“离开后变什么”这个问题,逻辑天然解耦。只要判定
staticMap有没有墙,然后只改dynamicMap就行了。 - 如果你想代码最紧凑:选 方案二(单层地图),但务必把
getAfterLeave这种逻辑抽离出来。千万不要在movePlayer里写一堆if (val == 5) ... else if (val == 6) ...,考场上一紧张绝对会漏掉一种情况(比如箱子推走后,目标点没复原)。
鉴于只有 2 小时,我会稍微倾向于 方案一,因为它的 paintEvent 和 move 逻辑几乎不会发生“状态丢失”的 Bug。