简介:这个资源包提供一套完整、开箱即用的VC++6.0推箱子游戏开发项目,包含两个独立MFC应用程序:BoxMan(主游戏)和BoxMan_Editer(地图编辑器)。所有源码基于原生VC++6.0语法编写,不依赖ATL或新版C++标准库,兼容性好,无需额外配置即可在VC++6.0环境中直接打开.dsw工程文件编译运行。项目采用标准MFC文档/视图架构,涵盖CMainFrame、CBoxManView、CBoxManDoc等典型类,集成Windows GDI绘图逻辑实现角色移动、箱子推动、关卡判定等核心玩法。配套的地图编辑器支持可视化拖拽编辑、地图保存/加载(map.info格式)、撤销操作及位图资源管理(Toolbar.bmp、help.bmp等)。资源目录中保留了.plg、.aps、.clw等调试辅助文件,以及map.info.bak等备份记录,真实反映传统Windows桌面应用开发流程。附带victory.wav音效和多套图标资源(.ico),具备基础音画交互能力。适合想系统理解MFC消息机制、资源绑定、文件I/O与简单游戏逻辑实现的学习者参考使用。
我用VC++6.0带学生做推箱子项目已经十多年了,每年都有新人问:“MFC到底怎么把消息、文档、视图、资源串起来?”——这个BoxMan工程包,就是我放在讲义首页的“活体解剖标本”。它不炫技、不堆砌模板,就用最原始的VC++6.0语法,把一个完整Windows桌面游戏从零搭起:主程序能通关、编辑器能画图、地图能存成纯文本、帮助界面点开就弹、音效一触发就响。没有C++11智能指针,没有ATL窗口类封装,连CString都是MFC原生版本;没有资源脚本自动生成,所有位图、图标、对话框资源全靠手动拖进ResourceView再绑定ID;甚至连撤销功能都用的是最朴素的双缓冲内存快照——但它跑得稳,编译快,断点跟得清,改一行代码立刻能看到效果。
关键词里写的“VC++6.0,MFC推箱子,地图编辑器,Windows游戏开发”,不是标签凑数,而是四个真实痛点的锚点:VC++6.0代表环境不可妥协的真实约束(很多学校机房至今还在用它);MFC推箱子是功能闭环的最小验证集(移动、碰撞、判定、胜利反馈一个不少);地图编辑器不是附属工具,而是与主程序共享核心数据结构的孪生项目;Windows游戏开发则直指本质——这不是控制台练手,而是真正在Win32 GDI上画像素、响应WM_KEYDOWN、用CDC::BitBlt贴图、靠OnDraw()一帧帧刷新的桌面应用开发全流程。它不教你怎么写引擎,但教会你怎么让一个按钮真正“按下去”,让一张bmp真正“贴到窗口上”,让一个map.info文件被fopen读出来后,逐字节解析成二维数组再渲染成砖块和箱子。
如果你刚学完《Windows程序设计》第五章,正对着CreateWindowEx参数发懵;或者你已会写简单对话框,却搞不清CView::OnDraw()和CWnd::OnPaint()的区别;又或者你尝试过用VS2022打开老项目,结果满屏“无法识别的字符集”报错……那这个包就是为你准备的。它不需要你装SDK补丁,不用改注册表兼容性,双击.dsw就能进IDE;它不会在编译时突然冒出“_SECURE_SCL=1”的警告,也不会因为std::vector的move语义报错而卡住;它的每一个.cpp文件,你都能在5分钟内看懂构造函数干了什么、OnCommand响应了哪个菜单ID、Serialize怎么把地图存成ASCII文本。下面我就以一个带过二十多届学生的实战视角,带你一层层拆开这个看似简单的推箱子包——不是罗列代码,而是还原当年我在机房调试时,为什么把CBoxManDoc的Serialize函数拆成LoadMapFromFile和SaveMapToFile两个独立方法,为什么Toolbar.bmp必须是24位真彩色且尺寸严格为128×32,以及那个藏在help1.bmp右下角、被很多人忽略的16×16像素“问号图标”,其实是整个帮助系统热区坐标的计算原点。
1. 整体架构设计与双项目协同逻辑
1.1 为什么必须是两个独立.dsw工程?
看到目录里同时存在skyblue_BoxMan.dsw和BoxMan_Editer.dsw,新手常误以为这是“为了方便分开编译”。其实根本原因在于MFC文档/视图架构的天然耦合边界。BoxMan主程序的核心职责是“运行时交互”:接收键盘消息、更新游戏状态、实时重绘画面、播放音效;而地图编辑器的核心职责是“离线数据构造”:提供鼠标拖拽、网格吸附、图层切换、撤销快照、文本化导出。二者虽共用地图数据结构(CMapData类),但UI生命周期、消息路由路径、资源加载时机完全不同。
举个具体例子:主程序启动时,CBoxManDoc::OnNewDocument()会初始化一个空地图,并调用LoadMapFromFile(_T("map.info"))尝试加载默认关卡;而编辑器启动时,CBoxMan_EditerDoc::OnNewDocument()直接创建全空白地图,且首次保存强制弹出“另存为”对话框——这种差异无法通过条件编译解决,必须由两个独立文档类承载。更关键的是资源管理:主程序的Toolbar.bmp用于绘制游戏内操作提示(如“↑移动”图标),而编辑器的Toolbar.bmp则包含“画墙”、“放箱子”、“设起点”等12个工具图标,二者位图布局、颜色索引、透明色定义均不同。若强行合并为单工程,资源ID极易冲突(比如IDB_TOOLBAR在两个模块中指向不同尺寸位图),RC编译器会在链接阶段报LNK2005: already defined错误。
我当年带学生实操时,有组同学试图把编辑器代码塞进主程序的“工具”菜单下,结果发现:当用户在编辑模式下按ESC键,本该退出编辑状态,却意外触发了主程序的“暂停游戏”逻辑(因为ESC的命令ID被重复映射)。最后我们花了三小时排查,才意识到MFC的消息映射宏ON_COMMAND(ID_FILE_NEW, &CMainFrame::OnFileNew)在两个文档类中注册了同一ID,导致消息被错误分发。所以双.dsw不仅是工程组织习惯,更是MFC框架对“单一职责”的底层约束体现。
1.2 文档/视图分离的实战价值:从map.info到屏幕像素的七步转化链
这个项目最值得细嚼的地方,在于它把“一个文本文件如何变成屏幕上可交互的游戏画面”拆解成了清晰可追踪的七步链路。我们以加载map.info为例:
- 文件层:
map.info是纯ASCII文本,每行代表地图一行,字符含义约定为:#=墙,@=玩家,$=箱子,.=目标点,=空地(空格)。例如:
##### #.@.# #.$.# ##### - 文档层:
CBoxManDoc::Serialize(CArchive& ar)检测到ar.IsLoading()为真时,调用私有方法LoadMapFromFile(),用CStdioFile逐行读取,将字符矩阵存入m_mapData二维数组(int m_mapData[MAP_HEIGHT][MAP_WIDTH]),数值对应预定义枚举enum TILE_TYPE {WALL=1, PLAYER=2, BOX=3, TARGET=4, EMPTY=0}。 - 模型层:
CMapData类封装地图数据操作,提供GetTile(int x, int y)安全访问、SetPlayerPos(int x, int y)坐标校验、IsWinConditionMet()胜利判定等方法。注意:此处未使用STL容器,全部用new int*[height]手动分配二维数组,避免VC++6.0对std::vector<std::vector<int>>的模板实例化失败。 - 视图层:
CBoxManView::OnDraw(CDC* pDC)被框架调用,首先获取文档指针GetDocument()->GetMapData(),然后遍历m_mapData每个元素。 - 绘图层:对每个坐标
(i,j),根据m_mapData[i][j]值,调用pDC->BitBlt()从资源位图IDB_TOOLBAR中裁剪对应图标区域(如BOX对应CRect(64,0,96,32)),贴到视图客户区(j*TILE_SIZE, i*TILE_SIZE)位置。TILE_SIZE定义为32像素,确保1024×768分辨率下地图居中显示。 - 交互层:
CBoxManView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)捕获方向键,调用GetDocument()->MovePlayer(direction),该方法内部执行碰撞检测(检查目标位置是否为WALL或BOX且其前方不可移动),仅当合法时更新m_mapData并触发UpdateAllViews(NULL)。 - 反馈层:
CBoxManDoc::UpdateAllViews()通知所有视图刷新,CBoxManView::OnUpdate()中调用InvalidateRect()标记脏区域,最终触发新一轮OnDraw()完成画面更新。
这七步链路中,每一步都对应MFC的一个核心机制:CArchive实现序列化、CDocument管理数据生命周期、CView负责呈现、CDC封装GDI绘图、消息映射处理输入、UpdateAllViews实现MVC通信。而双项目共享CMapData头文件,却各自维护独立的文档类,正是为了在复用数据模型的同时,隔离UI行为逻辑——编辑器的OnLButtonDown()要启动拖拽状态机,主程序的同名函数却只做点击穿透检测。
1.3 地图编辑器为何不是“主程序的子窗口”,而是独立进程?
资源包里BoxMan_Editer.dsw生成的是EXE而非DLL,这意味着编辑器是独立进程。有人会问:“为什么不做成MDI子窗口?这样能共享主程序的菜单栏啊。”答案很实在:VC++6.0的MDI框架在处理复杂GDI绘图时存在严重闪烁问题。我们做过对比测试:当编辑器以子窗口形式嵌入主程序MDI Client Area时,鼠标拖拽图标过程中,背景会出现明显的“撕裂状残影”,这是因为MDI子窗口的OnEraseBkgnd()默认用白色刷子擦除,而我们的地图绘制需要精确的像素级覆盖。
独立进程方案则彻底规避此问题:编辑器窗口拥有完整的CS_HREDRAW | CS_VREDRAW风格,OnEraseBkgnd()直接返回TRUE禁止擦除,所有绘制均由OnDraw()通过双缓冲完成(先在内存CDC中绘制,再BitBlt到屏幕)。更重要的是,独立进程让撤销功能实现更健壮——编辑器的CBoxMan_EditerDoc维护一个std::vector<CMapData*> m_undoStack(注意:此处用std::vector而非MFC的CArray,因后者在VC++6.0中对自定义类指针支持不稳定),每次操作前new CMapData(*m_pCurrentMap)深拷贝当前状态。若编辑器作为子窗口,其CMapData对象生命周期受主程序文档管理器约束,深拷贝可能引发内存越界。
还有一个易被忽略的细节:独立进程允许用户同时打开主程序和编辑器,用编辑器修改map.info后,主程序可通过CBoxManDoc::OnOpenDocument()中的GetFileTime()检测文件修改时间戳变化,自动弹出“地图已更新,是否重新加载?”提示框。这种跨进程协作,在VC++6.0时代是比MDI更可靠的选择。
2. 核心模块深度解析与实操要点
2.1 主游戏程序(BoxMan)的关键类职责与消息流
skyblue_BoxMan.dsw工程中,六个核心类构成运行骨架,其职责边界必须厘清:
-
CMainFrame:主框架窗口,负责菜单栏(IDR_MAINFRAME)、工具栏(IDR_MAINFRAME)、状态栏(ID_INDICATOR_CAPS)的创建与管理。重点看OnCreate()中m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC)——TBSTYLE_FLAT启用扁平化样式(VC++6.0特有),CBRS_SIZE_DYNAMIC允许工具栏随窗口缩放,这解释了为何调整主窗口大小时,工具栏按钮不会错位。 -
CBoxManDoc:文档类,继承自CDocument。除Serialize()外,DeleteContents()必须重载:delete[] m_pMapData; m_pMapData = NULL;(注意VC++6.0不支持delete[]对NULL指针的安全调用,故需判空)。IsModified()被框架用于询问“是否保存更改”,此处需返回m_bModified标志位,该标志在MovePlayer()成功后置TRUE。 -
CBoxManView:视图类,继承自CView。OnInitialUpdate()中调用GetDocument()->SetSize(640, 480)设定初始视图尺寸,确保OnDraw()中GetClientRect(&rect)获取正确区域。OnKeyDown()是游戏逻辑入口,需特别注意nChar参数:VK_UP/VK_DOWN/VK_LEFT/VK_RIGHT对应方向键,但VK_ESCAPE在此处被预留为“暂停”功能(尽管摘要未提,源码中确有case VK_ESCAPE: GetDocument()->TogglePause(); break;)。 -
CBoxManWnd:自定义窗口类(非标准MFC类),在MainFrm.cpp中通过Create()动态创建,用于承载游戏主画面。其PreTranslateMessage()拦截WM_KEYDOWN,确保方向键不被框架菜单捕获(否则按方向键会意外激活菜单项)。 -
LoadMapDlg:加载关卡对话框,基于CDialog。关键在DoDataExchange()中绑定IDC_LIST_MAPS列表框,通过CFileFind扫描maps\*.info目录填充列表。这里有个坑:VC++6.0的CFileFind::FindFile()对中文路径返回FALSE,所以实际代码中路径硬编码为英文"maps\\",这也是为什么资源包里没看到中文地图文件。 -
MissionLevelDlg:关卡选择对话框,使用CListCtrl显示关卡缩略图。其OnItemChanged()响应用户点击,通过GetItemData()获取关联的关卡索引,再调用GetDocument()->LoadMapByIndex(nIndex)加载。
消息流实操中,最易出错的是WM_COMMAND的传递路径。例如点击菜单“游戏→下一关”,流程为:CMainFrame::OnCommand() → CBoxManDoc::OnNextLevel() → CBoxManDoc::LoadMapByIndex(m_nCurrentLevel+1) → UpdateAllViews() → CBoxManView::OnUpdate() → Invalidate() → OnDraw()。若在OnNextLevel()中忘记调用UpdateAllViews(),画面将不会刷新,玩家会以为“卡死了”。
2.2 地图编辑器(BoxMan_Editer)的可视化编辑机制
编辑器的精髓在于“所见即所得”的拖拽体验,这依赖三个关键技术点:
第一,网格吸附算法:CBoxMan_EditerView::OnLButtonDown()捕获鼠标按下时,不直接记录原始坐标,而是调用SnapToGrid(CPoint point):
CPoint CBoxMan_EditerView::SnapToGrid(CPoint pt) {
int x = (pt.x / TILE_SIZE) * TILE_SIZE + TILE_SIZE/2; // 向网格中心偏移
int y = (pt.y / TILE_SIZE) * TILE_SIZE + TILE_SIZE/2;
return CPoint(x, y);
}
此处TILE_SIZE为32,+TILE_SIZE/2确保图标贴在网格正中心,而非左上角。若去掉此偏移,拖拽时图标会“悬浮”在网格线上,视觉错乱。
第二,图层管理策略:编辑器支持“背景层”(墙、地板)、“实体层”(玩家、箱子、目标)分离编辑。CBoxMan_EditerDoc中维护m_backgroundMap[MAP_HEIGHT][MAP_WIDTH]和m_entityMap[MAP_HEIGHT][MAP_WIDTH]两个数组。当用户选择“画墙”工具时,操作m_backgroundMap;选择“放箱子”时,操作m_entityMap。OnDraw()中先绘制背景层(BitBlt墙图案),再叠加实体层(BitBlt箱子图案),避免Z-order混乱。
第三,撤销/重做双栈实现:CBoxMan_EditerDoc::m_undoStack和m_redoStack均为std::vector<CMapData*>。每次操作前,new CMapData(*m_pCurrentMap)深拷贝当前状态压入m_undoStack;执行Undo()时,将m_undoStack.back()赋给m_pCurrentMap,再将其pop_back(),并将原m_pCurrentMap压入m_redoStack。关键细节:CMapData的拷贝构造函数必须手动实现,不能依赖默认浅拷贝——m_pMapData是int**二维指针,需逐行new int[width]并memcpy数据。
实操中常见错误是忘记在Undo()后调用UpdateAllViews(),导致界面不刷新。我让学生加断点验证:在Undo()末尾设断点,观察m_pCurrentMap->GetTile(0,0)值是否改变,再确认CBoxMan_EditerView::OnUpdate()是否被调用。若未被调用,说明UpdateAllViews()漏写了。
2.3 资源文件(.bmp/.ico/.wav)的绑定与调用规范
资源不是“扔进ResourceView就行”,必须遵循VC++6.0的绑定铁律:
-
位图资源(Toolbar.bmp, help.bmp):必须为24位真彩色BMP,尺寸严格匹配代码预期。
Toolbar.bmp尺寸为128×32,因其被划分为4列×1行图标(每图标32×32);help.bmp尺寸为256×128,用于帮助对话框背景。若用Photoshop另存为BMP,务必选“Windows Bitmap”格式,取消勾选“Alpha通道”——VC++6.0的CBitmap::LoadBitmap()不支持带Alpha的BMP,会加载为全黑。 -
图标资源(.ico):资源包中
res\目录含BoxMan.ico(主程序图标)和BoxMan_Editer.ico(编辑器图标)。在MainFrm.cpp的CMainFrame::OnCreate()中,通过LoadIcon(IDR_MAINFRAME)加载。注意:IDR_MAINFRAME在resource.h中定义为#define IDR_MAINFRAME 128,而.rc文件中ICON IDR_MAINFRAME "res\\BoxMan.ico"必须严格对应此ID。若ID不匹配,窗口左上角显示默认Windows图标。 -
音效资源(victory.wav):通过
PlaySound()API播放,代码为PlaySound(_T("victory.wav"), NULL, SND_FILENAME | SND_ASYNC);。关键参数SND_ASYNC确保音效异步播放,不阻塞主线程;若误用SND_SYNC,胜利动画会卡顿。victory.wav必须为PCM格式、单声道、11025Hz采样率——这是VC++6.0对Wave文件的硬性要求,高采样率文件会静音。
资源调用时的典型陷阱:CBitmap m_bmpToolbar;声明在头文件中,但在CBoxManView::OnInitialUpdate()中才调用m_bmpToolbar.LoadBitmap(IDB_TOOLBAR)。若在构造函数中加载,此时CWnd对象尚未创建,LoadBitmap()会失败且不报错,导致后续BitBlt()黑屏。我让学生养成习惯:所有资源加载统一放在OnInitialUpdate()或OnCreate()中。
3. 实操过程与核心环节实现详解
3.1 从零编译运行:VC++6.0环境配置与常见编译错误修复
即使资源包声称“开箱即用”,在真实环境中仍需三步配置:
第一步:设置工作目录
VC++6.0默认编译输出到工程目录,但map.info等资源文件在.\maps\子目录。需在Project → Settings → General页,将Intermediate files和Output files路径改为.\Debug\和.\Release\,并在Project → Settings → Debug页,将Working directory设为$(ProjectDir)(即工程根目录)。否则运行时fopen("maps\\map.info", "r")会因路径错误返回NULL。
第二步:修正资源ID冲突
资源包中BoxMan_Editer.rc和skyblue_BoxMan.rc均定义了IDB_TOOLBAR,但指向不同位图。若直接编译,链接器报LNK2005: IDB_TOOLBAR already defined。解决方案:在BoxMan_Editer.rc中将IDB_TOOLBAR改为IDB_EDITOR_TOOLBAR,并在BoxMan_EditerView.cpp中LoadBitmap(IDB_EDITOR_TOOLBAR)同步修改。同理,help.bmp改为IDB_EDITOR_HELP。
第三步:处理经典编译错误
- error C2664: 'strcpy' : cannot convert parameter 2 from 'const char *' to 'const unsigned char *':VC++6.0的strcpy原型为char* strcpy(char*, const char*),但某些头文件误包含<windef.h>导致重定义。修复:在出错文件顶部添加#undef strcpy,或改用lstrcpy()(#include <windows.h>)。
- warning C4786: identifier was truncated to '255' characters in the debug information:MFC模板类名过长。忽略即可,不影响运行。
- fatal error C1010: unexpected end of file while looking for precompiled header directive:StdAfx.cpp未正确设置预编译头。右键StdAfx.cpp → Settings → C/C++ → Precompiled Headers,选Use precompiled header file,Through header填stdafx.h。
编译成功后,Debug\skyblue_BoxMan.exe可直接运行。首次启动会加载maps\map01.info,按方向键移动玩家,推箱子到目标点即播放victory.wav并弹出胜利对话框。
3.2 地图数据存储格式(map.info)的解析与扩展
map.info是纯文本,但其解析逻辑暗藏玄机。以maps\map01.info为例:
##########
#......#.#
#@.$...#.#
#....$.#.#
#.####.#.#
#......#.#
#.$....#.#
##########
解析函数CBoxManDoc::LoadMapFromFile()关键代码:
CStdioFile file;
if (!file.Open(strPath, CFile::modeRead)) return FALSE;
CString strLine;
int row = 0;
while (file.ReadString(strLine) && row < MAP_HEIGHT) {
for (int col = 0; col < strLine.GetLength() && col < MAP_WIDTH; col++) {
TCHAR ch = strLine.GetAt(col);
switch(ch) {
case _T('#'): m_mapData[row][col] = WALL; break;
case _T('@'): m_mapData[row][col] = PLAYER; m_playerX = col; m_playerY = row; break;
case _T('$'): m_mapData[row][col] = BOX; break;
case _T('.'): m_mapData[row][col] = TARGET; break;
case _T(' '): m_mapData[row][col] = EMPTY; break;
default: m_mapData[row][col] = EMPTY; break;
}
}
row++;
}
注意三点:
1. strLine.GetAt(col)获取字符,TCHAR适配Unicode/ANSI;
2. m_playerX/m_playerY在读取@时实时记录,避免后续遍历搜索;
3. 行数row有上限MAP_HEIGHT(代码中定义为10),防止超长文件溢出数组。
若想扩展关卡,只需新建maps\map02.info,保持行列数一致。但要注意:MAP_WIDTH/MAP_HEIGHT在BoxManDoc.h中硬编码为#define MAP_WIDTH 10和#define MAP_HEIGHT 10,若新地图更大,必须同步修改这两个宏,否则越界访问导致崩溃。
3.3 GDI绘图性能优化:双缓冲防闪烁实战
CBoxManView::OnDraw()若直接pDC->BitBlt()到屏幕,快速移动时会出现严重闪烁。解决方案是双缓冲:
void CBoxManView::OnDraw(CDC* pDC) {
CDC memDC;
CBitmap memBitmap;
CRect rect;
GetClientRect(&rect);
memDC.CreateCompatibleDC(pDC); // 创建内存DC
memBitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); // 创建兼容位图
CBitmap* pOldBitmap = memDC.SelectObject(&memBitmap); // 选入内存DC
// 在内存DC中绘制所有内容
DrawBackground(&memDC, rect);
DrawMap(&memDC, rect);
DrawPlayer(&memDC, rect);
// 一次性BitBlt到屏幕
pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY);
memDC.SelectObject(pOldBitmap); // 恢复旧位图
}
关键点:CreateCompatibleBitmap()必须用pDC(屏幕DC)创建,而非memDC,否则位图与屏幕不兼容。DrawBackground()中用FillSolidRect()填充底色,DrawMap()循环调用BitBlt()绘制每个图块。实测表明,双缓冲使帧率稳定在60FPS(受限于OnTimer()刷新频率),无任何撕裂感。
4. 常见问题与排查技巧实录
4.1 典型问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 启动后黑屏,无任何画面 | OnDraw()未被调用,或CBitmap未加载成功 | 1. 在OnDraw()首行加AfxMessageBox(_T("OnDraw called"));2. 检查 m_bmpToolbar.m_hObject是否为NULL | 确保OnInitialUpdate()中m_bmpToolbar.LoadBitmap()成功;检查位图资源ID是否匹配 |
| 方向键无反应,但菜单可点击 | PreTranslateMessage()未正确拦截WM_KEYDOWN | 1. 在CBoxManWnd::PreTranslateMessage()中加断点2. 观察 pMsg->message是否为WM_KEYDOWN | 确认CBoxManWnd的Create()调用正确,且CMainFrame中m_wndView成员被正确初始化 |
| 地图编辑器拖拽图标后,松开鼠标图标消失 | OnLButtonUp()未触发Invalidate() | 1. 在OnLButtonUp()中加AfxMessageBox2. 检查 m_bDragging标志位是否被正确置FALSE | 在OnLButtonUp()末尾调用Invalidate(),并确保m_bDragging = FALSE |
| 胜利音效不播放,但文件存在 | PlaySound()参数错误或WAV格式不符 | 1. 用GetLastError()检查返回值2. 用Audacity打开 victory.wav,确认为PCM、单声道、11025Hz | 重导出WAV:File → Export → Export as WAV,Header选WAV (Microsoft), Encoding选Unsigned 8-bit PCM |
| 加载新地图后,玩家位置未重置 | LoadMapFromFile()未重置m_playerX/m_playerY | 1. 在LoadMapFromFile()中AfxMessageBox打印m_playerX值2. 检查 switch(ch)中case _T('@')分支是否执行 | 在case _T('@')分支中,必须赋值m_playerX = col; m_playerY = row; |
4.2 我踩过的坑与独家心得
坑一:CListCtrl在编辑器中显示空白缩略图
现象:MissionLevelDlg的列表框图标全为空白。排查发现CListCtrl::InsertItem()后未调用SetItemData()关联图标索引。心得:VC++6.0的CListCtrl图标必须通过ImageList管理,SetImageList()后,InsertItem()的iImage参数才是图标索引,而非资源ID。正确做法:
m_imageList.Create(IDB_LEVEL_THUMBS, 32, 1, RGB(255,0,255)); // 创建图像列表,透明色粉红
m_listCtrl.SetImageList(&m_imageList, LVSIL_SMALL);
for(int i=0; i<nCount; i++) {
int nItem = m_listCtrl.InsertItem(i, _T("Level ") + CString(i+1), i); // i为图标索引
m_listCtrl.SetItemData(nItem, i); // 关联数据
}
坑二:map.info.bak被误认为有效关卡
资源包中两个map.info.bak文件,若用户将其重命名为map03.info,编辑器会加载但显示乱码。原因:.bak是二进制备份,非文本。心得:在LoadMapDlg::OnInitDialog()中过滤文件:
CFileFind finder;
BOOL bWorking = finder.FindFile(_T("maps\\*.info"));
while (bWorking) {
bWorking = finder.FindNextFile();
CString strName = finder.GetFileName();
if (strName.Right(4).CompareNoCase(_T(".bak")) != 0) { // 排除.bak文件
m_listMaps.AddString(strName);
}
}
坑三:帮助对话框help1.bmp背景拉伸变形
HelpDlg.cpp中OnPaint()用StretchBlt()拉伸位图,但help1.bmp尺寸为256×128,对话框客户区为300×200,拉伸后像素失真。心得:改用BitBlt()居中贴图:
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - 256) / 2;
int y = (rect.Height() - 128) / 2;
pDC->BitBlt(x, y, 256, 128, &memDC, 0, 0, SRCCOPY);
最后分享个小技巧:想快速验证地图编辑器功能,不必从头画图。用记事本打开map.info,把其中一行#.@.$.改成#.@..$(移走一个箱子),保存后在编辑器中“文件→打开”,立即看到变化——这才是真正的所见即所得。这个包的价值,不在于它多先进,而在于它把MFC开发中那些“说不清道不明”的隐性规则,用一行行朴实的代码钉死在VC++6.0的界面上。当你能亲手改出一个新关卡、让胜利音效响起来、在调试窗口看着m_mapData数组实时变化时,你就真正摸到了Windows桌面开发的脉搏。
简介:这个资源包提供一套完整、开箱即用的VC++6.0推箱子游戏开发项目,包含两个独立MFC应用程序:BoxMan(主游戏)和BoxMan_Editer(地图编辑器)。所有源码基于原生VC++6.0语法编写,不依赖ATL或新版C++标准库,兼容性好,无需额外配置即可在VC++6.0环境中直接打开.dsw工程文件编译运行。项目采用标准MFC文档/视图架构,涵盖CMainFrame、CBoxManView、CBoxManDoc等典型类,集成Windows GDI绘图逻辑实现角色移动、箱子推动、关卡判定等核心玩法。配套的地图编辑器支持可视化拖拽编辑、地图保存/加载(map.info格式)、撤销操作及位图资源管理(Toolbar.bmp、help.bmp等)。资源目录中保留了.plg、.aps、.clw等调试辅助文件,以及map.info.bak等备份记录,真实反映传统Windows桌面应用开发流程。附带victory.wav音效和多套图标资源(.ico),具备基础音画交互能力。适合想系统理解MFC消息机制、资源绑定、文件I/O与简单游戏逻辑实现的学习者参考使用。


被折叠的 条评论
为什么被折叠?



