简介:以《水浒传》为题材的街机游戏完整源码包,内含C++游戏客户端与服务端源码、客户端资源以及库存/前控制代码,适合想从零到一掌握游戏开发流程的开发者反复研读。压缩包共510个文件,大小约461MB;369个png图片、30个mp3、6个wav音频构成美术与音效资源,16个cpp与22个h文件为核心逻辑实现,另有obj、lib、dll等编译依赖产物及ini、db配置数据文件。目前已有2086人学习/下载。透过完整源码,可深入研究角色行为、物理碰撞、图形渲染、服务器并发处理、状态同步、数据库交互等游戏开发关键技术;配合客户端素材与服务端工程,还能理解从网络通信、数据持久化到UI展现的完整项目组织方式,便于按模块拆解学习或在此基础上扩展新的玩法原型。对想进入街机或网络游戏开发领域的开发者,是一份覆盖前后端全链路的实用参考。
1. HGE水浒传源码能给你什么:不只是渲染,是一整套2D街机玩法框架
很多人听到“HGE”第一反应是“老掉牙的DirectX引擎”,但真正打开过一套完整HGE街机源码的人都知道,它把2D游戏最头疼的帧动画、输入缓冲区、状态机、碰撞检测全部摊开放在你面前。这份水浒传街机游戏源码就是这类样本:它虽然挂着古典题材,但内部用的是正宗HGE引擎那一套——贴图精灵、键盘/手柄输入、连续技判定、受击硬直,全都有迹可循。想学2D格斗或者横版动作游戏逻辑的人,读它比读上千页引擎教程快得多;想把它跑起来二次开发的人,则需要先摸清它的资源库存和前控制代码是怎么协作的。这篇文章就按“看懂骨架、跑起工程、改出自己想要的东西”的顺序,把这条落地路径讲透。
2. 拆源码骨架:HGE引擎初始化、主循环与资源库存的组织方式
这套水浒传源码不是孤立的几个渲染函数,它是一个典型HGE游戏工程的完整副本:入口在WinMain里,先创建HGE句柄,再注册帧回调、渲染回调和系统参数,最后启动引擎。要理解它,必须先绕过“看文件列表”的惯性,直接抓住两个锚点:引擎怎么被唤醒,以及那些贴图、音效、招式数据到底放在哪一层。
2.1 HGE引擎跑一个街机窗口的最小代码
通常这套源码的main.cpp就这么短,核心动作只有“创建HGE、设置回调、启动引擎”三步。最小骨架如下:
#include <hge.h> HGE* hge = NULL; // HGE引擎全局句柄 HTEXTURE texPlayer = NULL; // 测试贴图,后文会换成角色序列帧 bool FrameFunc() { // ESC退出 if (hge->Input_GetKeyState(HGEK_ESCAPE)) return true; // 这里放玩家输入、AI更新、碰撞检测等“逻辑层”代码 // 返回false表示继续运行,true表示请求退出 return false; } bool RenderFunc() { hge->Gfx_BeginScene(); hge->Gfx_Clear(HGERGB(16, 16, 32)); hge->Sprite_Render(texPlayer, 100.0f, 200.0f); hge->Gfx_EndScene(); return false; } int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { hge = hgeCreate(HGE_VERSION); hge->System_SetState(HGE_FRAMEFUNC, (void*)FrameFunc); hge->System_SetState(HGE_RENDERFUNC, (void*)RenderFunc); hge->System_SetState(HGE_WINDOWED, true); hge->System_SetState(HGE_SCREENWIDTH, 800); hge->System_SetState(HGE_SCREENHEIGHT, 600); hge->System_SetState(HGE_TITLE, "HGE Shuihu Demo"); if (hge->System_Initiate()) { hge->System_Start(); } hge->System_Shutdown(); hge->Release(); return 0; }逻辑说明:FrameFunc是每一帧的游戏逻辑入口,HGE会在一个稳定步长里反复调用它;RenderFunc负责所有绘制,绘制前必须先Gfx_BeginScene,结束前必须Gfx_EndScene,否则画面刷新会出问题。参数说明:HGE_SCREENWIDTH和HGE_SCREENHEIGHT决定窗口客户区尺寸,街机源码里通常会设为800x600或者640x480,并打开HGE_WINDOWED方便调试。常见做法是再设一个HGE_FPS控制逻辑帧率,比如锁定60,这会影响后文出招判定的时间敏感度。
2.2 资源库存:贴图、音效、招式数据放哪
“库存”在街机源码里并不是RPG背包,而是一套统一的数据装载层。这套水浒传源码里,角色贴图、背景音效、打击音、出招表数据全部以“库存项”的方式注册到一个数组或链表里。我一般会把它理解为“游戏资源池”:
struct SpriteAnim { HTEXTURE tex; // 纹理句柄 int frameCount; // 动画总帧数 float fps; // 播放速度(帧/秒) float frameWidth; // 单帧宽 float frameHeight; // 单帧高 }; struct FightStock { std::vector<SpriteAnim> anims; // 每个角色、每个动作对应一段动画 std::vector<Effect> sounds; // 打击音、脚步音、必杀音效 std::vector<HitBox> moves; // 出招判定框、伤害值、硬直时间 };逻辑说明:anims负责把一张序列帧大图按等宽高切出来,moves则记录每一招的攻击发生帧、攻击持续帧、收招帧,这是格斗游戏最核心的时间线。参数说明:fps不是随便写的,普通挥拳往往是12到15,轻脚更快,必杀技可能只有8到10;如果你的帧率参数和引擎逻辑帧率对不上,动画就会显得忽快忽慢。调试这类源码时,先改fps比改贴图坐标更有效。
2.3 主循环与帧率控制:HGE_FRAME和HGE_RENDER的回调关系
因为HGE把逻辑和渲染拆成了两个回调,很多首次接触的人会误以为它们是同一帧顺序执行的。实际上,HGE内部先调用FrameFunc处理状态,再调用RenderFunc画结果。如果逻辑帧率低于渲染刷新率,引擎会跳帧而不是拖慢渲染;反之,如果FrameFunc里做了太重的计算,渲染也会被压住。这套源码里经常看到全局变量g_GameTime在FrameFunc里累加一秒的帧数,用来做倒计时。
float g_GameTime = 0.0f; bool FrameFunc() { g_GameTime += hge->Timer_GetDelta(); // 累加真实流逝时间 // 2秒后自动触发一次测试攻击 if (g_GameTime > 2.0f) { TriggerAttack(); g_GameTime = 0.0f; } return false; }这里特别提醒:Timer_GetDelta返回的是距离上一帧的真实时间,单位秒。不要用它直接当动画帧索引,否则在高刷屏上,动画速度会变快。老HGE源码里有不少基于“每帧固定增长”的计数,那才是按逻辑帧率走的。阅读时遇到fTime += delta和frameIndex++混用,要分清哪个受帧率影响,这是后面调连招手感的关键。
3. 前控制代码实战:按键输入、角色状态机与连招判定的最小实现
标题里特别点出“前控制代码”,指的就是玩家从按下按键到角色做出动作这一段逻辑,它比渲染代码更容易被忽视,却直接决定一个街机游戏像不像街机。新手容易一上来就写if (GetKey(K)) attack,结果碰撞、硬直、动画同步全乱。HGE水浒传源码里这块通常分三层:输入采集、状态迁移、判定结算。
3.1 输入缓冲区与按键映射
街机面板只有摇杆和几个按键,HGE用Input_GetKeyState扫描按键状态,但直接读状态不够做连招,因为“左→右+A”这种连续输入需要记住过去一段时间内的按键序列。常见做法是维护一个环形输入缓冲区:
#define INPUT_HISTORY 16 struct InputFrame { int keys; // 位标记,bit0=左,bit1=右,bit2=上,bit3=下,bit4=A,bit5=B float timestamp; // 按下的时间点 }; InputFrame g_inputHist[INPUT_HISTORY]; int g_inputHead = 0; void RecordInput() { InputFrame f; f.keys = 0; if (hge->Input_GetKeyState(HGEK_LEFT)) f.keys |= (1 << 0); if (hge->Input_GetKeyState(HGEK_RIGHT)) f.keys |= (1 << 1); if (hge->Input_GetKeyState(HGEK_UP)) f.keys |= (1 << 2); if (hge->Input_GetKeyState(HGEK_DOWN)) f.keys |= (1 << 3); if (hge->Input_GetKeyState(HGEK_A)) f.keys |= (1 << 4); if (hge->Input_GetKeyState(HGEK_B)) f.keys |= (1 << 5); f.timestamp = hge->Timer_GetTime(); g_inputHist[g_inputHead] = f; g_inputHead = (g_inputHead + 1) % INPUT_HISTORY; }逻辑说明:把每一帧的按键位掩码和时间戳写进循环数组,后面做“输入匹配”时直接从最新帧往回扫,不丢历史。参数说明:INPUT_HISTORY取16是因为格斗游戏一个必杀技的手势输入最多也就五六帧,16足以覆盖200毫秒内的操作。如果你想要更宽松的判定,比如给新手玩家“搓招”容错,可以把历史拉长到24甚至32,但别超过30,否则会吞掉上一次出招的尾帧。
3.2 角色状态机:待机、攻击、跳跃、受击
有了输入,后续控制代码的核心就是“角色状态机”。状态不允许任意跳转,比如跳跃后必须先落地才能再跳,攻击中被打要先进入受击状态。源码里通常用枚举加Switch,这是最直白、最好调的结构:
enum PlayerState { PS_IDLE = 0, PS_WALK, PS_JUMP, PS_FALL, PS_ATTACK_A, // 轻攻击 PS_ATTACK_B, // 重攻击 PS_HIT, // 受击 PS_DOWN // 倒地 }; void UpdatePlayer(Player* p) { switch (p->state) { case PS_IDLE: if (p->inputBuf & (1 << 4)) p->state = PS_ATTACK_A; else if (p->inputBuf & (1 << 0)) p->state = PS_WALK; break; case PS_ATTACK_A: // 攻击动画播完才允许回待机 if (p->animTimer > p->animDuration_A) { p->state = PS_IDLE; } break; case PS_HIT: // 受击硬直结束回待机 if (p->hitTimer <= 0.0f) p->state = PS_IDLE; break; } }逻辑说明:状态开关只处理“允许发生的迁移”,比如待机时可攻击,跳跃中不可直接拖拽到待机。参数说明:animDuration_A要跟2.2节里的SpriteAnim::fps和frameCount匹配,否则会出现“动画还挂在出拳姿势,逻辑已经回待机”的割裂感。老源码里这种参数常被写死在角色表里,改它时要连动画资源一起改。
3.3 连招与必杀:用帧计数器串起出招表
前控制代码最出彩的地方是出招表。通常每个角色有一张技能表,每个技能定义了一串方向和按键的组合序列,以及命中的窗口时间。匹配不是等整个输入缓冲串,而是顺序匹配:
struct MoveDef { int sequence[6]; // 输入序列,如:下、下前、前 + A int seqLen; int hitDamage; float activeFrames; }; bool MatchMove(const MoveDef& mv, const InputFrame* hist, int histLen, int* outStep) { int matched = 0; for (int i = histLen - 1; i >= 0 && matched < mv.seqLen; i--) { if (hist[i].keys & mv.sequence[matched]) { matched++; } } *outStep = matched; return matched == mv.seqLen; }逻辑说明:从最新输入往回扫,按出招表的顺序倒着匹配,这样做的好处是允许玩家“预输入”——必杀技按键还没完全结束时,只要历史窗口里有那串序列就能触发。参数说明:sequence里的每个元素是一个位掩码,比如“下+B”就是(1<<3) | (1<<5)。这里有个坑:HGE键盘扫描不支持“斜方向”的独立按键,所以“下前”必须用相邻两帧的方向位来判断,否则会漏招。
读这套源码时,建议先注释掉所有出招表匹配,只保留基础按键攻击,跑通后再一行行放开。这样你能清楚感受到输入历史窗口对连招手感的影响,而不是被一堆keyDown和keyUp回调搅乱视线。
4. 从源码到可执行文件:用VS2008把HGE水浒传编译跑通的参数配置
拿到源码后,第一个实际问题是“这工程能编译吗”。HGE时代的主流开发环境是Visual C++ 2003/2005/2008,DirectX SDK版本也比较老。现在装新版本VS再直接打开往往报错一堆。可复现的办法是新建一个空工程,把源码文件加进去,再手动配HGE和DirectX的include、lib路径。这里每一步都很死,照着做就行。
4.1 环境准备:DirectX SDK、HGE库、Visual Studio工程设置
先准备三样东西:HGE引擎库(头文件hge.h、静态库hge.lib、运行时hge.dll),DirectX SDK(至少带d3d9、dinput8、dsound),以及Visual Studio 2008或2010速成版。新版本VS不是不能用,但老工程里常见的stricmp、strcpy等函数会在SDK冲突上绕圈子,所以能装2008就装2008。工程类型选“Win32 Application”,不要选控制台。
# 假设你的目录布局 C:\HGE_SDK\ include\hge.h lib\hge.lib lib\hgehelp.lib dll\hge.dll C:\DXSDK\ Include\ Lib\在VS里打开:项目属性 → C/C++ → 常规 → 附加包含目录,填入上面两个include路径;链接器 → 常规 → 附加库目录,填入lib路径。注意DirectX的Include要放在HGE之前还是之后?如果装了新版Windows SDK,d3d9.h存在版本冲突,我一般把DirectX SDK放在最前面,HGE放在后面,避免dinput.h两个头文件互相打架。
4.2 编译命令与工程选项:字符集、入口、多字节
HGE源码基本都是ANSI字符串,HGE_TITLE也是char*。VS默认的“Unicode字符集”会直接导致System_SetState(HGE_TITLE, L"...")类型不匹配。老工程翻车最常见就是这里。项目属性 → 常规 → 字符集 → 改成“使用多字节字符集”。
如果是命令行编译,可以用这样一组开关:
cl.exe /O2 /MT /D WIN32 /D _WINDOWS /D NOMINMAX /I C:\HGE_SDK\include /I C:\DXSDK\Include ^ main.cpp player.cpp fight.cpp ^ /link hge.lib hgehelp.lib d3d9.lib d3dx9.lib dsound.lib dinput8.lib dxguid.lib winmm.lib ^ /SUBSYSTEM:WINDOWS参数说明:/MT静态链接C运行库,避免部署目标机器缺msvcr90.dll;/SUBSYSTEM:WINDOWS表示这是一个窗口程序,入口是WinMain,如果你看到链接器报“无法解析的外部符号 _main”,多半就是没设这个开关或者把入口定成了main。NOMINMAX是防止Windows头文件里min/max宏跟HGE自己的工具函数冲突,这话说起来玄学,但遇到一些莫名其妙模板报错时,加它通常能救场。
4.3 运行参数与资源路径修正
编译成功后,大概率启动黑屏或找不到资源。原因往往不在代码逻辑,而是资源目录问题。HGE源码里的资源路径很多是resource/hero.png这样的相对路径,而运行时的当前工作目录可能不在源码根目录。如果你是从VS里F5启动,当前目录是工程文件所在目录,不是可执行文件所在目录,路径对不上。
解决办法是把所有资源路径改成相对可执行文件目录,或者在初始化里先切目录:
void ChangeWorkingDirToExe() { char path[MAX_PATH]; GetModuleFileNameA(NULL, path, MAX_PATH); char* slash = strrchr(path, '\\'); if (slash) *slash = '\0'; SetCurrentDirectoryA(path); }逻辑说明:GetModuleFileName拿到exe完整路径,截到最后一个反斜杠,再把当前目录切到exe所在目录。这样无论从VS启动还是双击运行,resource/都能被正确找到。参数说明:如果资源被打包进HGE的.tex或.dat文件里,你就不能用这个方式,而要用HGE的Resource_AttachPack先装载资源包。老源码里经常两种方式并存,调试时先看资源加载函数是Texture_Create还是Sprite_Load加路径,前者走磁盘,后者才走打包。
5. 避坑排查:老HGE源码在Windows 10以上版本的5类翻车现场
HGE当年跑在XP和Win7上很稳,但到了Win10/Win11的高DPI、多显卡、新DirectX环境下,问题一个接一个。下面五条是我实际翻车后才总结出的排查顺序,每一条都是“现象→原因→解决”三步走,你可以直接在调试时当清单用。
5.1 启动直接黑屏或闪退,连设置窗口都没看到
现象:运行exe后窗口一闪而过,或者黑屏停在启动画面。原因:HGESystem_Initiate()在线创建Direct3D9设备失败,常见于集成显卡对旧格式D3DFMT_X8R8G8B8支持不佳,或者窗口模式下色深设置不对。解决:将初始化参数从全屏改成窗口,并强制使用自动适配格式。在System_SetState里加一行hge->System_SetState(HGE_WINDOWED, true);,再检查HGE_SCREENBPP是否为32,不要设为16。如果闪退发生在进入主循环之前,建议在System_Initiate()返回false的分支里写一个MessageBoxA(NULL, hge->System_GetErrorMessage(), "HGE Error", MB_OK);,这一步能立刻把黑匣子打开。
5.2 碰撞检测不跟手,攻击判定框明显偏后
现象:击中人时角色动画已经挥了很久,或者没碰到却掉血。原因:判定框数据是手工调的,但贴图的分辨率和缩放比例跟设计图不一致。水浒传这种街机源码里的角色往往是从大图等比缩放的,HitBox写在逻辑坐标系里,而精灵绘制尺寸用的是屏幕像素。解决:先打印moves[i].hitRect和sprite.GetWidth(),确认两者的坐标系单位一致。老HGE源码里贴图分辨率通常比逻辑分辨率高,比如设计时角色120像素高,实际贴图200像素,判定框要除以缩放比例。最省事的方法是把所有判定框数据统一写到等级包配置文件里,不要散落在角色源文件里,否则每次换图都要重调。
5.3 出招经常串招或被吞招
现象:明明搓出了“下后+A”,角色却只放了个普通拳。原因:输入历史窗口太短,或者匹配函数里用了“完全相等”而不是“包含”判定。有些玩家输入时“下”和“后”之间的时间间隔大于该技能的窗口,导致配套的“下后”只匹配到了一个方向。解决:把匹配逻辑从“每个技能一个独立窗口”改成“全局输入历史+技能序列匹配”,并把窗口放宽到150毫秒。代码里不要用if(keys == MOVE_DOWN_BACK),要用(keys & MOVE_DOWN) && (keys & MOVE_BACK)这种位与判定。再不行就把INPUT_HISTORY从16调成24,这能显著提高新手出招成功率,代价是连续两个同类技能会有一瞬的重叠,但街机里手感优先。
5.4 音乐播放崩或将进游戏时卡顿
现象:游戏能跑,但背景音乐响到第四秒就卡一下,或者某些打击音效触发后整个音频变为杂音。原因:HGE音频模块基于DirectSound,老源码喜欢每帧都调用Effect_Play加载同一个音效,而不是先存入缓存。卡顿是磁盘读取造成的,杂音则可能是声道数或采样率不匹配。解决:把音效做成预加载池,在游戏开始时调用hge->Effect_Load加载到内存,之后播放只用Effect_Play(handle)。如果依然卡,检查音频格式是否为16位单声道,HGE对44.1kHz双声道支持尚可,但48kHz的mp3转出来的wav会让DirectSound的混音缓冲区措手不及。
5.5 帧率不受控,角色动作时快时慢
现象:高刷新率屏幕上角色动作比原版快,低配机器上又明显慢半拍。原因:有些源码直接把逻辑放进RenderFunc或者在FrameFunc里用真实时间驱动动画,而不是用“逻辑时间步长”。解决:为动画帧索引维护一个固定增量,而不是两端都依赖Timer_GetDelta。这一条在避坑清单里最贵,却最不显眼。我的习惯是用一个独立逻辑时钟,每帧只增加1.0f / HGE_FPS,动画frameIndex按fixedStep来走,渲染插值另算。这样动作速度跟机器刷新率彻底解耦,后期移植到Unity或Godot时也是同一套思路。
6. 二开技巧:加一套调试输出与自定义必杀技的验证方法
前面说的都是把源码跑通,但真正验证你读没读透,是能不能在不破坏手感的前提下加一个新必杀技。我常用两个技巧:一是把调试信息画在游戏自己画面上,二是用驱动表方式加招。快速验证一个自定义必杀技,不需要大改控制代码,只要三步。
第一步,在渲染回调里临时打印角色状态和输入缓冲区。HGE没有自带的调试字体,可以用贴图字模,也可以用Gfx_CreateFont加载HGE字体文件。考虑到资源完整性,直接用已有的字体资源显示一条调试字符串即可:
bool RenderFunc() { hge->Gfx_BeginScene(); // ...现有渲染 char buf[256]; sprintf(buf, "state=%d hitTimer=%.2f stock=%d", g_player.state, g_player.hitTimer, g_player.stock[0]); hge->Gfx_RenderText(font, buf, 10, 10); hge->Gfx_EndScene(); return false; }这行的价值是让你在屏幕上实时看到状态机迁移,不用靠断点去猜。第二步,定义一个新的MoveDef,比如武松的“醉酒晕拳”,使用“下后下前+B”:
MoveDef drunkFist = { { (1<<3), ((1<<2)|(1<<3)), (1<<2), (1<<1), (1<<5) }, 5, 35, 24.0f };第三步,在角色攻击处理函数里加一条调用:如果当前不是必杀技状态,就检测drunkFist是否匹配,匹配则强制进入必杀状态并播放专用资源。这个过程中你自然会遇到几个分界线:出招表的判定是否吞掉普通攻击、必杀技硬直时间要不要跳过、库存里的hitDamage参数是否覆盖了基础攻击。调整时要记住,街机手感的关键是“预输入窗口”和“攻击发生帧”,不是伤害值。
我个人的习惯是每次拿到老HGE源码都先跑通再拆,拆完再装回去,最后用上面这套加法把角色改成不一样的玩法。十几年前写这套源码的人大概率也没想到今天还有人会折腾HGE,但它的学习价值并不因此打折。真正动手调过一遍出招表之后,再看现代引擎里的Animator状态机,很多概念都是相通的。希望帮到你。
本文还有配套的精品资源,点击获取