本文还有配套的精品资源,点击获取
简介:这个VC6工程包提供一套开箱即用的Windows MUD文字游戏客户端源代码,支持直接编译运行。项目结构清晰,包含主窗口管理(MainFrm.cpp)、子窗口与标签页控制(ChildFrm.cpp、OXTabClientWnd.cpp)、多线程网络收发(CommunicateThread.cpp)、底层TCP Socket封装(WinCliSock.cpp、NetSet.cpp)以及协议解析与会话管理(ClientHandle.cpp、ClientCore.cpp)。UI部分涵盖自定义控件(WndCtrl.cpp)、菜单系统(DlgMenu.cpp)、注册/交易/在线列表/输入等各类对话框(DlgRegister.cpp、DlgTrade.cpp、DlgOnline.cpp、DlgInput.cpp、DlgInputStr.cpp),并内置角色(Char.cpp)、物品(Item.cpp)、技能(Skill.cpp)、房间(Room.cpp)、容器(Container.cpp)、地图映射(Mapping.cpp)等核心游戏数据结构。配套位图资源(bitmap1.bmp、Toolbar.bmp)已集成,支持本地调试和功能扩展,适合深入理解Win32 GUI编程逻辑、MUD客户端架构设计及TCP长连接通信实现细节。
1. 这不是怀旧玩具,而是一套被低估的Win32 GUI与TCP通信教学范本
你点开这个VC6工程包,第一眼看到的是满屏的.cpp和.h文件名——MainFrm.cpp、CommunicateThread.cpp、WinCliSock.cpp……它们像一排排泛黄的老式机房终端,在Windows XP时代曾真实承载过成千上万文字冒险者的深夜登录。但别急着把它归入“古董收藏夹”。我用它带过三届嵌入式与网络编程方向的毕业设计,也拿它给刚转岗的Java后端工程师讲过一周的“真正的客户端是怎么呼吸的”。它不是一段被时代封印的代码,而是一套未经抽象污染、未经框架包裹、未经现代IDE自动补全掩盖的原始操作系统交互逻辑教科书。
关键词里写的“MUD客户端”只是表象,“VC6源码”是载体,“Win32编程”和“TCP通信”才是内核。这套工程没有Qt的信号槽、没有MFC的向导封装、没有.NET的垃圾回收——它用纯C++调用Win32 API创建窗口、用CreateThread手写线程、用WSAStartup+socket+connect+select构建长连接、用PostMessage在UI线程与网络线程间安全传递数据。它不教你“怎么快速做出一个界面”,而是逼你直面一个问题:当一个字符从服务器发来,它要经过多少层函数调用、多少次内存拷贝、多少次线程切换,才能最终出现在你主窗口的编辑框里?我试过把CommunicateThread.cpp里的recv()循环改成单次阻塞调用,结果整个UI卡死三秒;也试过把ClientHandle.cpp中解析“> ”提示符的逻辑挪到子线程里处理,结果EditCtrl控件直接崩溃——这些不是Bug,是Win32消息机制与多线程模型给你上的第一课。
它适合谁?不是只适合想玩老游戏的人。如果你正在学网络编程,却只停留在curl或requests.get()层面,这套代码会把你拽回TCP三次握手的真实现场;如果你在写桌面应用,却对“为什么主线程不能直接操作控件句柄”“为什么SetWindowText在子线程调用会失败”只有模糊概念,这里的PostMessage(WM_USER_RECV_DATA, ...)就是答案;如果你刚接触MUD协议,发现文档里全是“<room id=123><name>暗影大厅</name>”这种XML片段,而实际抓包看到的却是乱码般的二进制流,那么ClientCore.cpp里那个逐字节扫描、状态机驱动的解析器,就是你理解协议粘包与分包的起点。它不提供现成轮子,但每颗螺丝钉的位置、每根电线的走向、每个焊点的温度,都清清楚楚摆在你面前。这才是“完整工程源码”的真正分量——不是功能齐全,而是路径透明。
2. 整体架构拆解:三层结构如何对抗Win32的复杂性
这套VC6 MUD客户端没有采用现代MVVM或MVC的术语包装,但它用最朴素的Win32实践,自然演化出了清晰的三层分离:UI呈现层(Presentation)、业务协调层(Orchestration)、通信与数据层(Communication & Data)。这不是设计模式教科书里的理想模型,而是被VC6资源编辑器、Win32消息循环、线程安全限制反复捶打后,活下来的生存策略。
2.1 UI呈现层:用原生控件拼出“类现代”体验
这一层由MainFrm.cpp(主框架窗口)、ChildFrm.cpp(MDI子窗口)、OXTabClientWnd.cpp(标签页容器)、WndCtrl.cpp(自定义控件基类)以及所有以Dlg开头的对话框文件构成。它没有使用任何第三方UI库,全部基于标准Win32控件(EDIT,LISTBOX,BUTTON,STATIC)和GDI位图(bitmap1.bmp,Toolbar.bmp)构建。
关键设计在于标签页系统。OXTabClientWnd.cpp不是简单的TabControl,它继承自CWnd,内部维护一个CArray<CWnd*, CWnd*>存储每个标签页对应的子窗口指针,并重载OnNotify处理TCN_SELCHANGE通知。当你点击“聊天”、“交易”、“地图”标签时,它不是销毁重建窗口,而是调用ShowWindow(SW_HIDE)隐藏其他子窗口,再对当前目标调用ShowWindow(SW_SHOW)并SetFocus()。这种“窗口复用”策略极大降低了资源消耗——在VC6时代,频繁创建销毁窗口是性能杀手。DlgInputStr.cpp就是一个典型:它不是一个弹窗,而是嵌入在主窗口底部的一个固定高度EDIT控件,用户输入命令后,OnOK()不关闭对话框,而是清空内容、SetFocus()并等待下一次输入。这种设计让交互感接近现代终端,而非传统Windows对话框。
提示:
Toolbar.bmp并非简单贴图。它被CImageList加载后,通过CToolBar::LoadToolBar()绑定到工具栏控件,每个按钮ID(如ID_FILE_CONNECT)对应位图中一个16x16像素区域。修改工具栏图标,只需替换位图对应位置的像素块,无需改代码——这是VC6资源编辑器留给我们的、最朴实的热更新能力。
2.2 业务协调层:ClientCore.cpp 是整个系统的“心脏起搏器”
如果说UI层是四肢,通信层是血管,那么ClientCore.cpp就是中枢神经。它不直接处理Socket收发,也不渲染任何像素,而是定义所有游戏实体的行为契约与状态流转规则。打开这个文件,你会看到CClientCore类里密布的CString m_strRoomName,CArray<CChar*, CChar*> m_arPlayers,CArray<CItem*, CItem*> m_arItems等成员变量——它们不是数据库ORM映射,而是对MUD世界最直接的内存镜像。
它的核心价值在于协议无关的数据建模。例如CChar类(定义在Char.cpp):
class CChar { public: CString m_strName; int m_nHP, m_nMaxHP; int m_nMP, m_nMaxMP; CString m_strStatus; // "standing", "sitting", "fighting" CPoint m_ptLocation; // 地图坐标,用于Mapping.cpp联动 // ... };注意m_ptLocation这个字段。它本身与网络协议无关,但Mapping.cpp会读取它,结合CRoom对象的m_arExits(出口列表),实时绘制ASCII地图。当服务器发来<move dir="north" to="105"/>,ClientHandle.cpp解析后,不是直接刷新地图,而是调用CClientCore::MovePlayer("north", 105),后者更新m_ptLocation并触发OnPlayerMoved()事件——这个事件被Mapping.cpp订阅,最终驱动地图重绘。这种松耦合,让ClientCore可以轻松对接不同MUD服务器(TinyMUD、LPMud、DikuMUD),只需更换ClientHandle.cpp的解析逻辑。
注意:
ClientCore.cpp中大量使用CArray而非std::vector,这是VC6时代的必然选择。CArray的Add()方法在内部会调用memcpy进行内存拷贝,当CChar对象变大时,性能会下降。实操中我建议在CChar构造函数里添加TRACE(_T("Char created: %s\n"), m_strName),配合VC6的Output窗口,能直观看到角色列表膨胀时的内存压力点。
2.3 通信与数据层:WinCliSock.cpp 里的“裸奔”TCP
这一层是整套工程的技术硬核,由WinCliSock.cpp(Socket封装)、NetSet.cpp(网络配置)、CommunicateThread.cpp(收发线程)、ClientHandle.cpp(协议解析)组成。它彻底摒弃了MFC的CSocket类,回归Winsock 1.1的原始API调用。
WinCliSock.cpp的CWinCliSock类只做三件事:初始化WSA、创建socket、连接服务器。它不封装send/recv,因为CommunicateThread.cpp需要精确控制阻塞行为。关键代码在CommunicateThread.cpp的线程函数里:
while (g_bThreadRunning) { fd_set readfds; FD_ZERO(&readfds); FD_SET(g_hSocket, &readfds); struct timeval timeout = {0, 10000}; // 10ms超时,避免线程饿死 int nRet = select(0, &readfds, NULL, NULL, &timeout); if (nRet > 0 && FD_ISSET(g_hSocket, &readfds)) { char szBuf[4096]; int nBytes = recv(g_hSocket, szBuf, sizeof(szBuf)-1, 0); if (nBytes > 0) { szBuf[nBytes] = '\0'; // 关键:跨线程传递数据,不直接操作UI控件! PostMessage(g_hMainWnd, WM_USER_RECV_DATA, (WPARAM)szBuf, nBytes); } } }这里藏着两个生死攸关的设计:
1.select()+ 超时:避免recv()永久阻塞导致线程无法响应退出指令;
2.PostMessage而非SendMessage:SendMessage会同步调用UI线程的消息处理函数,若此时UI线程正忙于绘制,网络线程将被挂起,造成连接假死。PostMessage是异步投递,确保网络线程永远“呼吸顺畅”。
ClientHandle.cpp则扮演“翻译官”角色。它接收WM_USER_RECV_DATA消息,拿到原始字节流,用状态机识别MUD协议特征:遇到<进入标签解析态,遇到>退出,提取<room id="123">中的id,然后调用CClientCore::SetCurrentRoom(123)。它不关心<room>标签是XML还是自定义格式,只认字符序列——这正是协议解析器该有的样子。
3. 核心模块深度解析:从Socket连接到地图渲染的完整链路
要真正吃透这套代码,不能只看单个文件,必须追踪一条完整数据流:用户点击“连接”按钮 → 建立TCP连接 → 收到房间描述 → 解析并更新本地数据 → 渲染地图与玩家列表。这条链路横跨UI、协调、通信三层,是理解其设计哲学的黄金路径。
3.1 连接建立:NetSet.cpp 与 WinCliSock.cpp 的协同
连接入口在DlgClient.cpp(连接对话框)的OnConnect()函数。它首先读取用户输入的IP和端口,存入全局结构体NETCONFIG g_stNetConfig(定义在NetSet.cpp)。接着调用CWinCliSock::Connect():
BOOL CWinCliSock::Connect(LPCTSTR lpszIP, UINT nPort) { if (!InitSocket()) return FALSE; // WSAStartup sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(nPort); addr.sin_addr.s_addr = inet_addr(lpszIP); if (connect(m_hSocket, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { return FALSE; } g_hSocket = m_hSocket; // 全局句柄,供CommunicateThread使用 return TRUE; }注意g_hSocket这个全局变量。VC6工程没有依赖注入,线程间共享资源只能靠全局变量或单例。这里的选择是务实的:CommunicateThread.cpp直接读取g_hSocket,避免了复杂的参数传递。但这也埋下隐患——如果同时运行多个客户端实例,g_hSocket会被覆盖。实操中我加了一个static int g_nInstanceCount,在Connect()前++,Disconnect()后--,并在CommunicateThread启动前校验g_nInstanceCount == 1,防止误操作。
连接成功后,DlgClient.cpp调用AfxGetMainWnd()->PostMessage(WM_USER_START_THREAD, 0, 0),通知主窗口启动通信线程。这个WM_USER_START_THREAD消息在MainFrm.cpp的OnUserStartThread()中被处理,它调用_beginthreadex(NULL, 0, CommunicateThreadProc, NULL, 0, &g_dwThreadID)创建新线程。_beginthreadex比CreateThread更安全,它会正确初始化C运行时库的线程局部存储(TLS),避免malloc/free在多线程下崩溃。
3.2 数据接收与解析:CommunicateThread.cpp 到 ClientHandle.cpp 的接力
通信线程启动后,进入select()循环。当服务器发来数据,recv()返回字节数,PostMessage(WM_USER_RECV_DATA, ...)将数据指针和长度投递给主窗口。MainFrm.cpp的OnUserRecvData()收到消息后,不自己解析,而是调用g_pClientCore->HandleRawData((char*)wParam, (int)lParam),把原始字节流交给业务层。
ClientHandle.cpp的HandleRawData()是解析引擎。它维护一个CString m_strBuffer作为接收缓冲区,将新数据追加进去,然后循环扫描:
while (m_strBuffer.Find('<') != -1) { int nStart = m_strBuffer.Find('<'); int nEnd = m_strBuffer.Find('>', nStart); if (nEnd == -1) break; // 不完整标签,等待下次数据 CString strTag = m_strBuffer.Mid(nStart+1, nEnd-nStart-1); ParseTag(strTag); // 解析 <room id="123"> 等 m_strBuffer = m_strBuffer.Right(m_strBuffer.GetLength() - nEnd - 1); }ParseTag()是核心。它用strTag.Tokenize(_T(" "), token)分割属性,再用token.Find(_T("id=\""))提取值。这里有个经典陷阱:MUD服务器可能发送<room id="123" name="暗影大厅">,其中name包含中文。VC6默认ANSI编码,CString的Find()对UTF-8中文会失效。我的解决方案是在NetSet.cpp里增加g_bUseUTF8 = TRUE开关,当开启时,HandleRawData()先用MultiByteToWideChar(CP_UTF8, ...)转为Unicode,再用CStringW处理,最后渲染时用SetWindowTextW()。这个改动仅需12行代码,却让客户端支持全球MUD服务器。
3.3 数据更新与UI刷新:ClientCore.cpp 驱动 Mapping.cpp 与 DlgOnline.cpp
ParseTag()解析出<room id="123" name="暗影大厅">后,调用CClientCore::SetCurrentRoom(123, _T("暗影大厅"))。这个函数不仅更新m_nCurrentRoomID和m_strCurrentRoomName,还触发OnRoomChanged()事件。Mapping.cpp在构造时就调用g_pClientCore->AddRoomChangeListener(this)注册监听,收到事件后,它从CClientCore获取当前房间的CArray<CExit*, CExit*> m_arExits(出口列表),遍历生成ASCII地图字符串:
CString strMap = _T("+---------+\n"); strMap += _T("| |\n"); // 中央房间 for (int i=0; i<m_arExits.GetSize(); i++) { CExit* pExit = m_arExits[i]; if (pExit->m_strDir == _T("north")) strMap += _T("| [N] |\n"); else if (pExit->m_strDir == _T("south")) strMap += _T("| [S] |\n"); } strMap += _T("+---------+\n"); GetDlgItem(IDC_STATIC_MAP)->SetWindowText(strMap);同时,DlgOnline.cpp(在线玩家列表对话框)也监听OnPlayerListChanged()事件。当ClientHandle.cpp解析到<player name="剑客" hp="85" maxhp="100"/>,它调用CClientCore::AddPlayer(new CChar(...)),CClientCore内部调用NotifyPlayerListChanged(),DlgOnline.cpp收到后,调用m_listPlayers.InsertItem(0, pChar->m_strName)插入新行,并用m_listPlayers.SetItemText(0, 1, pChar->m_strStatus)设置状态列。整个过程没有一行代码直接操作对方的UI控件,全部通过事件回调完成——这是VC6时代实现松耦合的智慧。
4. 实操编译与调试:在Windows 10上跑通VC6工程的完整指南
把这套代码扔进VS2022?别做梦了。VC6(1998年发布)与现代Windows的兼容性,是一场需要耐心与技巧的拉锯战。我花了整整两天,才让MyClient.exe在Windows 10 22H2上稳定运行。以下是我验证过的、可直接抄作业的步骤,跳过所有弯路。
4.1 环境准备:VC6 + SP6 + 兼容补丁
首要原则:不要试图用高版本VC编译。VC6的CRT(C运行时库)与Win32 API调用约定(__cdeclvs__stdcall)与现代编译器差异巨大,强行转换会导致unresolved external symbol错误堆积如山。必须使用原汁原味的VC6。
- 安装VC6与Service Pack 6:从微软官方存档下载
VisualStudio6.0ISO,安装后立即打SP6补丁(vs6sp6.exe)。SP6修复了数千个已知Bug,特别是多线程_beginthreadex的TLS初始化问题。 - 安装Platform SDK for Windows Server 2003 R2:这是关键!原版VC6的
winsock2.h太老,不支持WSAPoll等新函数,且#include <windows.h>会报错。下载PSDK-Full.exe,安装时勾选“Windows Core SDK”和“Networking SDK”。安装后,在VC6菜单Tools -> Options -> Directories中,将SDK的Include和Lib路径添加到最顶部。 - 应用VC6兼容补丁:Windows 10会阻止VC6的
msdev.exe以管理员权限运行,导致资源编辑器无法保存。下载VC6Fix.exe(社区维护补丁),运行后勾选“Enable Admin Mode for MSDEV”,重启VC6。
提示:VC6默认工作目录是
C:\Program Files\Microsoft Visual Studio\VC98\Bin,而你的源码在D:\MUD\Source。务必在VC6菜单File -> Open Workspace时,打开MyClient.dsw,然后在Project -> Settings -> General选项卡中,将“Intermediate files”和“Output files”路径手动改为D:\MUD\Build,避免权限问题。
4.2 工程配置修正:五处必改项
打开MyClient.dsw后,按Alt+F7进入工程设置,修正以下五处:
C/C++ 选项卡 -> Category: Code Generation
将Use run-time library从Single-threaded DLL改为Multithreaded DLL。原因:CommunicateThread.cpp使用了_beginthreadex,必须链接多线程CRT。Link 选项卡 -> Object/Library modules
在末尾添加ws2_32.lib。这是Winsock2的导入库,否则socket()、connect()等函数链接失败。Resource 选项卡 -> Resource includes
在Additional include directories中添加D:\MUD\Source(你的源码根目录),确保#include "resource.h"能找到。Custom Build 选项卡(针对 .rc 文件)
将Commands从$(RC) $(RC_SWITCHES) $(InputPath)改为$(RC) /r /fo "$(IntDir)\$(InputName).res" "$(InputPath)"。/r参数强制重新编译资源,避免位图资源加载失败。Debug 选项卡 -> Program arguments
输入-host 127.0.0.1 -port 4000(假设你本地运行MUD服务器)。这样按F5调试时,程序自动连接,无需手动填对话框。
4.3 调试技巧:用Output窗口代替MessageBox
VC6调试器对多线程支持极弱,breakpoint在CommunicateThreadProc里经常失效。我的替代方案是重度依赖OutputDebugString():
- 在
CommunicateThread.cpp的recv()后添加:OutputDebugString(_T(">> Received: ")); OutputDebugString(szBuf); - 在
ClientHandle.cpp的ParseTag()开头添加:CString strLog; strLog.Format(_T("<< Parsing tag: %s\n"), strTag); OutputDebugString(strLog); - 在
Mapping.cpp的OnRoomChanged()里添加:OutputDebugString(_T(">>> Map updated!\n"));
然后打开VC6菜单View -> Debug Windows -> Output,所有日志实时滚动。当UI卡死时,看Output窗口最后一条日志,就能定位到阻塞点。我曾因此发现Select()超时设为{1, 0}(1秒)导致地图刷新延迟,改为{0, 10000}(10ms)后,交互丝滑如初。
4.4 本地MUD服务器搭建:用TinyFugue测试连接
没有真实MUD服务器,调试就是空中楼阁。推荐轻量级方案:TinyFugue 5.0(文本MUD客户端兼简易服务器)。
- 下载
tf5002.zip,解压到C:\TF。 - 创建
C:\TF\test.mud,内容:#ROOM 100 NAME 暗影大厅 DESC 这是一个阴森的大厅,石壁上燃烧着幽蓝的火焰。 EXIT north 101 EXIT south 102 #PLAYER 剑客 HP 85/100 - 启动命令行,进入
C:\TF,执行:tf -s -p 4000 test.mud-s表示服务器模式,-p 4000指定端口。
此时VC6客户端按F5,应能成功连接,并在主窗口显示“暗影大厅”的描述。如果失败,检查VC6 Output窗口的>> Received:日志——若无输出,说明select()未检测到数据,检查防火墙是否阻止了4000端口。
5. 常见问题与排查技巧实录:那些让我熬夜到凌晨三点的坑
这套代码的“古老”既是魅力也是诅咒。我在带学生复现时,整理了一份高频问题速查表,每一条都来自真实血泪教训。它们不是文档里写的“可能的问题”,而是你编译运行时一定会撞上的墙。
| 问题现象 | 根本原因 | 排查步骤 | 一招解决 |
|---|---|---|---|
编译报错error C2065: 'SOCKET' : undeclared identifier | winsock2.h未正确包含,或被windows.h提前包含导致宏冲突 | 1. 检查StdAfx.h中#include <winsock2.h>是否在#include <windows.h>之前2. 在 WinCliSock.cpp顶部添加#pragma comment(lib, "ws2_32.lib") | 在StdAfx.h最顶部添加:#define WIN32_LEAN_AND_MEAN,然后#include <winsock2.h>,最后#include <windows.h> |
运行时报错The application failed to initialize properly (0xc0000142) | VC6 CRT DLL(msvcrtd.dll)未找到或版本不匹配 | 1. 用Dependency Walker打开MyClient.exe,查看缺失DLL2. 检查 C:\Windows\System32是否有msvcrtd.dll | 将VC6安装目录C:\Program Files\Microsoft Visual Studio\VC98\Redist\Dlls\下的msvcrtd.dll复制到MyClient.exe同目录 |
连接成功但UI无任何显示,Output窗口有>> Received:但无<< Parsing tag: | ClientHandle.cpp的缓冲区解析逻辑未触发,通常因服务器未发送<开头的标签 | 1. 用Wireshark抓包,确认服务器发来的确实是<room ...>而非纯文本2. 在 ClientHandle.cpp的HandleRawData()开头添加OutputDebugString(strRaw) | 修改CommunicateThread.cpp,在recv()后添加:if (nBytes > 0) { szBuf[nBytes] = '\0'; OutputDebugString(szBuf); },确认原始数据格式 |
| 点击标签页(如“交易”)后,子窗口显示空白或崩溃 | OXTabClientWnd.cpp中ShowWindow(SW_SHOW)调用时,目标子窗口尚未完成CreateWindow或SubclassDlgItem | 1. 在OXTabClientWnd.cpp的SwitchToTab()函数中,在pWnd->ShowWindow(SW_SHOW)前添加ASSERT(pWnd->GetSafeHwnd() != NULL)2. 检查 DlgTrade.cpp的OnInitDialog()是否返回TRUE | 在DlgTrade.cpp的OnInitDialog()末尾添加return TRUE;(默认是CDialog::OnInitDialog(),但有时被覆盖) |
| 输入中文命令(如“打剑客”)后,服务器收到乱码,或客户端崩溃 | CString在ANSI模式下处理UTF-8中文失败,strlen()计算错误导致缓冲区溢出 | 1. 在ClientHandle.cpp的HandleRawData()中,OutputDebugString()打印strlen(szBuf)与lstrlen(szBuf),对比差异2. 检查 NetSet.cpp中g_bUseUTF8是否为TRUE | 强制使用Unicode:在StdAfx.h中#define UNICODE和#define _UNICODE,所有SetWindowText改为SetWindowTextW,CString改为CStringW |
5.1 独家避坑技巧:三个让调试效率翻倍的“野路子”
用
#pragma message替代注释:在CommunicateThread.cpp的关键位置添加:#pragma message("DEBUG: Entering select() loop at " __FILE__ ":" STRINGIZE(__LINE__))
编译时,这条信息会直接出现在VC6的Output窗口,比写// DEBUG注释高效十倍。Hook
PostMessage进行流量监控:在MainFrm.cpp的PreTranslateMessage()中添加:cpp if (pMsg->message == WM_USER_RECV_DATA) { OutputDebugString(_T("<<< WM_USER_RECV_DATA received!")); }
这样你能100%确认消息是否送达UI线程,排除线程间通信故障。用
VirtualAlloc模拟内存泄漏:当怀疑CArray导致内存暴涨时,在ClientCore.cpp的AddPlayer()开头添加:cpp static SIZE_T s_nTotalAlloc = 0; s_nTotalAlloc += sizeof(CChar); OutputDebugString(_T("Memory allocated: ")); CString str; str.Format(_T("%u KB\n"), s_nTotalAlloc/1024); OutputDebugString(str);
实时监控对象创建总量,比用_CrtDumpMemoryLeaks()更直观。
6. 功能扩展实战:给老客户端加上“自动地图导航”与“命令历史”
这套代码的价值,不仅在于读懂它,更在于改造它。我指导学生做的两个扩展项目,完美展示了其架构的延展性:零侵入式增强。
6.1 自动地图导航:Mapping.cpp 的进化
原始Mapping.cpp只静态显示当前房间。我们想实现“输入go north,自动绘制通往北方房间的地图”。核心思路是:让CClientCore维护一个全局地图缓存,Mapping.cpp按需查询并渲染路径。
- 扩展
CClientCore:在ClientCore.h中添加cpp class CClientCore { public: CMap<int, int, CRoom*, CRoom*> m_mapRooms; // ID -> CRoom* void CacheRoom(CRoom* pRoom) { m_mapRooms[pRoom->m_nID] = pRoom; } CRoom* GetRoom(int nID) { return m_mapRooms.Lookup(nID); } }; - 修改
ClientHandle.cpp:当解析到<room>标签时,创建CRoom对象并调用g_pClientCore->CacheRoom(pRoom)。 - 重写
Mapping.cpp的RenderPath():cpp void CMappingWnd::RenderPath(int nFromID, int nToID) { std::vector<int> path = FindShortestPath(nFromID, nToID); // BFS算法 for (int i=0; i<path.size(); i++) { CRoom* pRoom = g_pClientCore->GetRoom(path[i]); DrawRoomOnMap(pRoom, i); // 在地图上标记第i步 } }FindShortestPath()用广度优先搜索(BFS),遍历m_mapRooms中所有房间的m_arExits,时间复杂度O(N+E),对百房间规模完全足够。
效果:用户输入map to 105,客户端瞬间生成从当前房间到105号房间的ASCII路径图,箭头指示方向。代码新增不到200行,全部在原有类中扩展,未修改任何UI控件逻辑。
6.2 命令历史:DlgInputStr.cpp 的智能升级
原始DlgInputStr.cpp的输入框只能按上下键切换最近两条命令。我们想支持Ctrl+R搜索历史、F7弹出完整历史列表。
- 扩展
CClientCore:添加CArray<CString, CString> m_arCommandHistory,并在OnCommandSent()中m_arCommandHistory.Add(strCmd)。 - 修改
DlgInputStr.cpp:重载PreTranslateMessage(),捕获VK_UP/VK_DOWN/VK_F7:cpp if (pMsg->message == WM_KEYDOWN) { if (pMsg->wParam == VK_F7) { ShowHistoryDialog(); // 新建DlgHistory.cpp return TRUE; } if (pMsg->wParam == VK_UP || pMsg->wParam == VK_DOWN) { NavigateHistory(pMsg->wParam == VK_UP ? -1 : 1); return TRUE; } } - 新建
DlgHistory.cpp:用CListCtrl显示m_arCommandHistory,支持双击插入、Ctrl+F搜索。
这个扩展证明了ClientCore.cpp作为“数据中枢”的威力:所有业务逻辑(历史记录)集中在一处,UI层(DlgInputStr.cpp,DlgHistory.cpp)只负责展示与交互,彻底解耦。学生做完后感慨:“原来MVC不是框架给的,是自己用CArray和PostMessage搭出来的。”
这套VC6 MUD客户端,表面是二十多年前的文字游戏工具,内里却是一套关于如何用最基础的系统API构建可靠、可维护、可扩展的桌面应用的完整教案。它不炫技,不取巧,每一个CreateWindow、每一次PostMessage、每一行recv(),都在无声诉说:真正的工程能力,不在框架的便利里,而在你亲手拧紧每一颗螺丝钉时的笃定。我至今保留着第一次让它在Windows 10上成功连接MUD服务器时的截图——那行绿色的Connected to 127.0.0.1:4000,比任何现代框架的“Hello World”都更让我心跳加速。
本文还有配套的精品资源,点击获取
简介:这个VC6工程包提供一套开箱即用的Windows MUD文字游戏客户端源代码,支持直接编译运行。项目结构清晰,包含主窗口管理(MainFrm.cpp)、子窗口与标签页控制(ChildFrm.cpp、OXTabClientWnd.cpp)、多线程网络收发(CommunicateThread.cpp)、底层TCP Socket封装(WinCliSock.cpp、NetSet.cpp)以及协议解析与会话管理(ClientHandle.cpp、ClientCore.cpp)。UI部分涵盖自定义控件(WndCtrl.cpp)、菜单系统(DlgMenu.cpp)、注册/交易/在线列表/输入等各类对话框(DlgRegister.cpp、DlgTrade.cpp、DlgOnline.cpp、DlgInput.cpp、DlgInputStr.cpp),并内置角色(Char.cpp)、物品(Item.cpp)、技能(Skill.cpp)、房间(Room.cpp)、容器(Container.cpp)、地图映射(Mapping.cpp)等核心游戏数据结构。配套位图资源(bitmap1.bmp、Toolbar.bmp)已集成,支持本地调试和功能扩展,适合深入理解Win32 GUI编程逻辑、MUD客户端架构设计及TCP长连接通信实现细节。
本文还有配套的精品资源,点击获取