恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

C++/DirectX 11吃豆人源码解析:从渲染管线到鬼魂AI

  • 首页
  • 资讯中心
  • /
  • C++/DirectX 11吃豆人源码解析:从渲染管线到鬼魂AI

相关资讯

C++课设实战:EasyX还原超级马里奥游戏源码解析 2026/10/4 5:23:39
插件机制深度解析:从加载失败到激活异常的排查实战 2026/10/4 5:18:38
MS3D模型加载与骨骼动画:C++二进制解析实战 2026/10/4 5:18:38

最新资讯

趣博思AI问卷设计:把“提问”变成一种可设计的实验
STM32外接MRAM MR25H40CDF实战:从驱动到掉电保存
《GPT Image 2.5 最强“焚诀”直接拿去用,保姆级拆解教程》
从零配置Claude Code + DeepSeek V4(附cc-switch教程)
文生视频提示词完全指南:从废词到出片的五段式框架与平台适配
OpenShell:Windows命令行补全、高亮与配置实战指南

今日推荐

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

C++/DirectX 11吃豆人源码解析:从渲染管线到鬼魂AI

发布时间:2026/10/4 5:23:39
C++/DirectX 11吃豆人源码解析:从渲染管线到鬼魂AI 简介一份使用 C 与 DirectX 11 实现的吃豆人游戏完整工程面向游戏开发初学者及对经典街机复刻感兴趣的读者适合在 Visual Studio 2017 中配合 DirectXTK 编译运行。项目还原了 1980 年原版的操控手感玩家可通过方向键移动体验被幽灵追击、吃能量剂反击以及不同追击/逃跑阶段切换等核心玩法每个幽灵均具备独立的 AI且未复刻原版中 Pinky 与 Inky 的已知行为错误。地图采用分离立方体生成 2D 世界并对相邻立方体连接做了去重优化减少三角形数量并避免 Z-fighting 问题是理解网格生成与渲染优化的实用范例。压缩包共 61 个文件包含 18 个 C 头文件、15 个实现文件、8 个 HLSL 着色器、7 个 PNG 贴图与 GIF 演示以及 VS 工程配置和说明文档整体仅 6MB结构紧凑。已有 109 人浏览学习适合想要研究 DirectX 11 基础渲染、精灵动画与简单游戏状态机的开发者参考。1. 这个吃豆人项目值得你从零读一遍代码如果你以为吃豆人就是二维平面上一个黄色圆嘴吃豆子那这个项目会打破你的刻板印象。它是一个用 C 和 DirectX 11 实现的吃豆人游戏但不是简单地把像素精灵贴在屏幕上——整个地图是用 3D 立方体拼出来的 2D 游戏世界运行在一个完整的 D3D11 渲染管线里。换句话说它给了你一个同时学习游戏逻辑、渲染架构和性能优化的绝佳样本。这个项目以 1980 年原版为灵感保留了经典玩法移动、吃豆子、能量剂、被鬼追、反杀鬼。但它的底层实现和原版差了十万八千里。它用 DirectXTK 简化 DirectX 的日常操作用自制的地图生成器把二维数组变成三维场景还做了网格合并优化来减少三角形数量。对想深入 DirectX 11 的读者来说这份源码是最好的现成教材——它完整、能编译、有注释而且规模刚好适合逐个文件去读。如果你是游戏开发新手它能让你看到从地图数据到屏幕像素的完整链路如果你已经在用其他渲染 API这个项目也能提供一个不错的 D3D11 架构参考。2. 搭建环境与编译工程DirectXTK 依赖与 VS2017 解决方案的完整打开方式2.1 项目文件清单先搞清楚包里有什么在碰代码之前建议先花十分钟把资源文件过一遍。这个项目的结构不复杂但有些文件如果你不知道它是干嘛的很容易在中途迷失。Pacman.sln // 解决方案文件Visual Studio 2017 Pacman.vcxproj // 工程文件 Sources/ Game.cpp / Game.h // 主游戏类初始化窗口、游戏循环、状态管理 World.cpp / World.h // 世界地图从二维数组生成三维场景 Ghost.cpp / Ghost.h // 鬼魂的 AI 与状态切换 Character.cpp / Character.h // 吃豆人角色移动、碰撞、状态 Dots.cpp / Dots.h // 小豆子与能量剂的管理 ShaderManager.cpp/.h? // 着色器封装 Camera.cpp / Camera.h // 摄像机控制 StepTimer.h // 固定时间步长计时器微软官方模板 Global.h // 全局变量与常量定义 Caption.cpp / Caption.h // 界面上方文字信息 pch.cpp / pch.h // 预编译头文件 Resources/ pacman.png // 吃豆人精灵表 ghosts.png // 鬼魂精灵表 dot.png // 豆子贴图 caption.png // 界面字体贴图 resources.rc // Windows 资源脚本包含 directx.ico这里有一个很多人容易忽略的点ShaderManager在文件列表里没有对应的.cpp可能只有头文件也可能它的实现内联在头文件里。如果你打开工程发现某个.cpp文件找不到别慌——先检查是不是内联实现再看工程文件里的实际引用路径。2.2 安装 DirectXTKNuGet 是最快的路项目依赖 DirectXTK这个库负责封装 DirectX 11 里最繁琐的部分精灵批处理、字体渲染、纹理加载、数学库。原作者选择通过 NuGet 引入我的建议是不要绕开这个依赖。虽然你可以手动下载 DirectXTK 源码来编译但 NuGet 的方式最稳定、最省事版本锁定也最省心。在 Visual Studio 2017 里安装 DirectXTK 的步骤如下打开Pacman.sln解决方案。右键点击解决方案资源管理器中的项目名称选择“管理 NuGet 程序包”。在“浏览”标签页搜索DirectXTK注意是 DirectXTK不是 DirectXTK12后者是 DirectX 12 版本。选择最新稳定版点击“安装”。等待 NuGet 自动把依赖项写入packages.config和工程文件。// packages.config 的内容预期是这样的 ?xml version1.0 encodingutf-8? packages package idDirectXTK version1.0.5 targetFrameworknet46 / /packages如果你打开工程发现packages.config已经存在但没有实际安装直接右键项目 → “还原 NuGet 程序包”Visual Studio 会自动处理剩余的安装。另一个需要注意的细节是#include路径。NuGet 安装的 DirectXTK 头文件会放在packages\DirectXTK.X.X.X\Include目录工程文件已经配置好了这个路径。如果你手动下载源码不要只把.lib拷进项目目录头文件路径也必须配对。2.3 编译时的目标平台设置这是新手最容易踩的坑DirectX 11 是 64 位和 32 位都能跑的但 Visual Studio 默认的解决方案平台是x86还是x64取决于你打开工程时的环境。这个项目的.vcxproj里同时配置了Win32和x64两个平台但 DirectXTK 的 NuGet 包会自动把对应的.lib链接进来前提是你选对了配置管理器里的平台。我的建议是在编译之前直接确认两个地方解决方案配置Debug 或 Release推荐 Debug方便打断点调试。解决方案平台x64。原项目的主要开发环境是 x64如果你用 Win32 编译可能会遇到链接错误——这不是代码问题是 DirectXTK 库文件平台不匹配。设置方式是菜单栏 → 生成 → 配置管理器 → 在“活动解决方案平台”下拉框选择x64→ 关闭。2.4 首次编译的完整路径验证当一切配置就绪按F5或者在菜单栏选择“调试 → 开始执行不调试”等待编译完成。如果你之前从没碰过这个项目第一次编译可能会遇到 3 到 4 个错误绝大多数出在DirectXTK 版本不匹配症状找不到SpriteBatch或SpriteFont相关符号。平台设置不对症状LNK2019无法解析的外部符号。pch.h预编译头文件路径问题症状C1010致命错误。遇到这些不要慌往下看第 5 章我把这些坑的解法都写清楚了。提示如果你看到fopen安全错误C4996可以在Global.h或pch.h里加上#define _CRT_SECURE_NO_WARNINGS或者去工程属性 → C/C → 预处理器 → 预处理器定义里加上这一项。3. 游戏循环与渲染架构StepTimer、ShaderManager 与精灵渲染的协作链条3.1 主循环为什么用 StepTimer 而不是自己写帧率控制打开Game.cpp你首先会看到一个StepTimer类的实例。这是微软官方模板中的一个工具类它的价值在于把逻辑更新和渲染解耦。你可能会好奇直接用一个while(PeekMessage(...))循环每次循环都调用 Update 和 Render 不行吗可以但问题在于——不同显示器的刷新率不同如果 Update 的逻辑和帧率绑定游戏速度会在 60Hz 和 144Hz 显示器上表现不一样。StepTimer 用固定时间步长来解决这个问题。核心逻辑简化的示意代码// StepTimer.h 的核心思路 void Tick() { QPC() // 获取当前时间 m_totalTicks m_deltaTicks; // 累积时间 while (m_totalTicks m_targetElapsedTicks) { Update(); // 每帧只推进一次逻辑 m_totalTicks - m_targetElapsedTicks; } }这里的m_targetElapsedTicks通常设置为 1/60 秒也就是 60FPS 的逻辑帧率。实际渲染帧率可能高于或低于 60但游戏逻辑始终以 60FPS 的节奏推进这样鬼魂的移动速度、吃豆人的转向响应就不会因为硬件不同而忽快忽慢。在Game.cpp里你可能会发现一个细节这个项目没有显式的Update方法分离而是在Tick里同时处理了输入和世界逻辑更新。这是小项目的常见做法也没问题——因为逻辑简单不需要单独拆线程或架构。3.2 渲染管线SpriteBatch 如何把吃豆人画出来DirectX 11 最劝退新手的是渲染管线初始化设备、创建交换链、加载顶点缓冲、编辑着色器……每个环节都能卡住一周。这个项目用 DirectXTK 里的SpriteBatch和SpriteFont把这些工作压缩到了近乎简单操作的程度。SpriteBatch本质上是一个 2D 渲染器它替你管理了顶点缓冲、输入布局、像素着色器和纹理采样器。你的工作只剩三步加载纹理、指定绘制位置、绘制。来看Game.cpp里的实际调用方式// 在 Game.cpp 的 Update/Render 中简化后的渲染流程 void Game::Render() { m_spriteBatch-Begin(); // 绘制小豆子 for (auto dot : m_dots) { m_spriteBatch-Draw( m_dotTexture.Get(), // 纹理资源 dot.Position(), // 目标位置这是一个矩形区域 DirectX::Colors::White // 着色颜色 ); } // 绘制吃豆人 m_spriteBatch-Draw( m_pacmanTexture.Get(), // 精灵表纹理 pacman-GetSourceRect(), // 从精灵表中裁剪出当前帧 pacman-GetDestinationRect(), // 绘制到屏幕上的位置 DirectX::Colors::White ); m_spriteBatch-End(); }这段代码的逻辑揭示了一个关键信息精灵动画是通过剪裁源矩形实现的。GetSourceRect()返回的是精灵表pacman.png中当前动画帧对应的矩形区域GetDestinationRect()返回的是屏幕上要画到的目标位置。通过循环切换GetSourceRect()的坐标就实现了张嘴、闭嘴的动画效果。3.3 ShaderManager为什么你不需要自己编译着色器ShaderManager这个类在这个项目里很关键。如果你打开过 DirectX 11 的官方教程你会发现每一步都要创建顶点着色器和像素着色器并编译它们。但这里的ShaderManager把这些过程封装好了// ShaderManager.h 的关键接口简化 class ShaderManager { public: // 加载并编译一个 .cso/.hlsl 着色器文件 void LoadShader(LPCWSTR filename, ID3D11VertexShader** shader); // 绑定到渲染管线 void ApplyShaders(); };但这里有个坑项目文件列表里没有.hlsl或.cso着色器源文件。这不是遗漏而是 DirectXTK 的SpriteBatch内部已经内置了着色器。所以ShaderManager在这个项目里更像是一个占位或扩展点。如果你后续想加自己的 3D 模型、自定义后处理特效这个时候ShaderManager才会发挥作用。提示如果你想验证这点可以在ShaderManager.cpp里搜索CreateVertexShader或CompileShader你会发现调用频率很低——因为 SpriteBatch 把基础绘制全部接管了。3.4 摄像机与视图投影3D 场景的 2D 视角Camera.cpp和Camera.h负责视图矩阵和投影矩阵。它的存在揭示了这是 3D 场景的 2D 游戏——摄像机被放在一个俯视角度看着地面上的立方体方阵。你可能会好奇既然画面是 2D 的为什么不直接用一个正交投影矩阵答案原项目故意保留透视投影这样在特定视角下你能看到立方体的侧面获得一种类似 2.5D 的视觉层次感。这是设计决定不是缺陷。4. 地图生成与网格优化从分离立方体到合并网格4.1 二维数组到三维场景地图数据驱动世界生成这是整个项目里最值得细读的部分。World.cpp接收一个二维数组或类似布局的文本地图遍历每个格子在对应位置生成一个单位大小的立方体。每个立方体的顶点、法线、纹理坐标都会被写入顶点缓冲。这里的关键代码逻辑大致是这样的// World.cpp 的地图生成核心逻辑示意 // 假设 mapData 是一个 std::vectorstd::vectorint其中 1 表示墙0 表示路 for (int row 0; row rows; row) { for (int col 0; col cols; col) { if (mapData[row][col] 1) { // 在 (row, col) 位置生成一个立方体 CreateCube(row, col); } } }乱写的话一个中等尺寸的地图会产生上万甚至几十万个三角形。DirectX 11 处理这个数量级毫无压力但问题是——相邻的立方体共享的面是完全没有必要的它们会被渲染两遍甚至可能出现 z-fighting 闪烁。4.2 连接相邻立方体的优化策略减少三角形与消除肉眼可见的问题原项目特意做了一项优化合并相邻立方体之间的共享面。通俗说如果两个立方体紧挨着那么它们之间的那个面既看不见也不需要绘制。下图的情况你从俯视视角看一个 L 形墙体左侧立方体的右面和右侧立方体的左面是紧贴的这两个面完全不会从任何合法视角看到。优化思路是构建地图时检查每个立方体的上下左右前后六个方向如果相邻位置也有立方体就剔除当前立方体对应的那一个面。一个更直观的理解方式。假设在 (0, 0) 和 (1, 0) 有两个立方体第一个立方体有 6 个面上、下、左、右、前、后。第二个立方体有 6 个面上、下、左、右、前、后。如果两个立方体相邻左右关系那么第一个的右面和第二个的左面重合。这个重合面在最终渲染中不可见可以删掉。合并后只剩下 10 个面而不是 12 个面。这个优化只对静态地图有效。因为地图在游戏过程中不变化你可以提前在初始化时算好所有面的集合一次性生成顶点缓冲区。当玩家移动时渲染器不需要做任何修改。4.3 顶点缓冲的组织与摄像机视角验证在地图生成之后顶点数据被放入ID3D11Buffer。根据World.cpp的结构推断地图的顶点缓冲是紧凑排列的顶点位置、法线、纹理坐标在同一个结构中用D3D11_USAGE_IMMUTABLE标记——因为地图静态不需要每帧更新。一个值得验证的细节你可以在游戏运行时按某个键原项目有没有做这个扩展我不确定但你可以自己加切换摄像机的俯仰角或者增加一个自由视角模式来复现第 2.2 节提到的“底部视图”。当你把摄像机放到地图的底部向上看会发现底部是空的——不需要画的地方面积全被优化掉了。5. 避坑与常见问题排查DirectXTK 版本冲突与精灵表使用的典型踩坑记录5.1 LNK2019 无法解析的外部符号DirectXTK 版本与 x64 平台不配对现象工程编译到了百分之九十然后链接阶段报出一大堆LNK2019: unresolved external symbol public: __cdecl DirectX::SpriteBatch::SpriteBatch(...)。原因绝大多数情况下是平台不匹配。DirectXTK 的 NuGet 包默认安装时会把 x64 和 Win32 的.lib都放进lib目录。你的工程活动平台是Win32而代码里实际用到的库里只链接了 x64 版本的或者反过来。另一个常见原因是你安装了 DirectXTK 12 包DirectXTK12是基于 Direct3D 12 的不能用于 DirectX 11 工程。解决打开配置管理器确认活动平台是x64。重新安装 NuGet 包确认packages.config里的包 ID 是DirectXTK而不是DirectXTK12。如果仍然报错手动在工程属性 → 链接器 → 输入 → 附加依赖项里把完整的DirectXTK.lib路径写进去。5.2 纹理贴图花掉或发虚精灵表的源矩形坐标不对齐现象吃豆人身上出现莫名其妙的点状花纹或者鬼魂旁边跟着半截残影。原因精灵表pacman.png或ghosts.png的每一帧大小是固定的比如 32x32 像素但GetSourceRect()传入的坐标和精灵表实际布局不对齐。原版精灵表里可能包含 4 帧、8 帧甚至不同大小的动画帧如果写死 32x32 缩放就会截出错误区域。解决打开pacman.png和ghosts.png看尺寸确认每个动画帧的宽度和高度然后检查Character.cpp和Ghost.cpp里GetSourceRect()的偏移参数。一般正确的做法是定义一组常量如FRAME_WIDTH 24、FRAME_HEIGHT 24然后用当前帧索引计算源矩形的左上角 x、y 坐标// 计算源矩形的正确方式 void Character::UpdateAnimation() { int frameX (m_currentFrame % 4) * FRAME_WIDTH; // 10 行注释在精灵表里横向移动 int frameY m_direction * FRAME_HEIGHT; // 垂直方向对应四个朝向上下左右 m_sourceRect.left frameX; m_sourceRect.top frameY; m_sourceRect.right frameX FRAME_WIDTH; m_sourceRect.bottom frameY FRAME_HEIGHT; }核心教训如果你的精灵表里每个动画动画帧尺寸不是 32x32读代码时别瞎改源矩形——先去数像素。5.3 鬼魂半透明零散可见或消失渲染状态与着色顺序的冲突现象鬼魂在移动时偶尔闪烁或者消失尤其在转角的瞬间。原因你用了SpriteBatch::Begin()里的SpriteSortMode设置错误或者混入了ID3D11DepthStencilState的状态切换。D3D11 默认深度缓冲是开启的但 2D 精灵不需要深度测试。当SpriteBatch从普通状态切换到透明混合状态时如果没有正确设置深度掩码某些像素会被深度测试直接丢弃。解决在渲染精灵前使用SpriteBatch::Begin(SpriteSortMode_Deferred, ...)并清空深度缓冲// 每帧渲染前清空深度缓冲避免 3D 残留影响 2D 精灵 m_d3dContext-ClearDepthStencilView( m_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0 );另一个病根如果鬼魂的源矩形里包含透明像素半透明混合顺序是必要的。用SpriteSortMode_BackToFront能解决大多数“鬼影重叠”问题。5.4 摄像机视角偏移导致你看不到部分地图现象启动游戏后地图不是居中显示的左上角有一块被裁掉了。原因Camera.cpp里设置视图矩阵时的摄像机位置坐标不是整数导致纹理贴图在屏幕上出现亚像素级别的偏移看起来就像地图错位。解决把摄像机的初始位置对齐到整数像素坐标// 将摄像机位置取整避免采样精度的边缘偏移 float camX static_castfloat(static_castint(initialCamX)); float camY static_castfloat(static_castint(initialCamY));5.5Pinky和Inky的 AI 行为与预期不一致这不是 bug是记录在案的设计现象Pinky粉色鬼或者 Inky青色鬼在追逐模式下不走最短路径反而喜欢绕路。原因原作者在 README 或代码注释里特意提到Pinky 和 Inky 的行为在 1980 年原版中就有已知 bug原作者没有修复它们而是刻意保留了这些行为以贴近原版。这是设计决定不是你的代码问题。解决如果你希望它们表现得更“理性”可以调整它们的目标点计算先从吃豆人当前位置向前预测 4 格作为目标如果预测点被墙挡住就用吃豆人当前位置作为目标。 这种逻辑的实现方式在 Ghost.cpp 的UpdateTarget函数中。不过我的建议是保留原版行为——这些缺陷正是怀旧游戏体验的一部分。6. 鬼魂 AI 的实现与验证1980 年代行为模式的还原与边界6.1 四种鬼魂的差异化行为逻辑这是整个项目技术含量最高的部分。原作中四种鬼魂各自有独特的追踪策略它们的调参方式跟原版一模一样。具体来说鬼魂原版行为模式本项目实现要点Blinky红鬼直线追踪吃豆人当前位置目标点 吃豆人当前位置Pinky粉鬼追踪吃豆人前方 4 格目标点 吃豆人当前位置 方向向量×4Inky青鬼以 Blinky 为参考形成夹击目标点 吃豆人位置 (吃豆人位置 - Blinky位置) × 2Clyde橙鬼距离近时逃跑远时追踪距离大于 8 格时追踪吃豆人小于 8 格时回角落你会发现 Pinky 和 Inky 的逻辑里有变量名可能叫做targetTile或GetChaseTarget()。如果你在代码里没找到检查Ghost.cpp里的 Update 方法看到的是一个使用平局行列计算的return。6.2 鬼魂状态的切换机制追击与逃逸的完整闭环游戏里有能量剂机制吃下之后鬼魂进入逃跑状态。这个状态的切换在Ghost.cpp里会有一个枚举类型的成员变量。状态设计的合理之处在于每种状态都对应一组完全不同的行为规则。核心实现大致是// Ghost.cpp 中的状态切换逻辑示意代码 void Ghost::Update() { switch (m_state) { case GhostState::Chase: m_targetTile GetChaseTarget(); // 根据当前身份计算追踪目标点 break; case GhostState::Scatter: m_targetTile GetScatterCorner(); // 四角坐标散开状态 break; case GhostState::Frightened: // 进入惊吓状态反向、随机化移动方向 m_direction Reverse(m_direction); break; } }这里值得关注的是Frightened状态下鬼魂仍然需要移动但速度会减慢。鬼魂速度的关键参数通常是通过一个speedMultiplier设置的。如果你想知道为什么鬼魂在逃跑状态下有点慢半拍恐怕这是故意调出的设定。6.3 验证 AI 行为的两种方式把 AI 调完了怎么看效果提供两个实用的办法第一种加一条调试可视路径。在Game.cpp的渲染循环里用SpriteBatch画一个小方块来标出鬼魂当前目标点// 调试用画出当前追踪目标点 if (IsDebugMode) { m_spriteBatch-Draw( m_debugTexture.Get(), DirectX::SimpleMath::Vector2( ghost-GetTargetTile().x * TILE_SIZE, ghost-GetTargetTile().y * TILE_SIZE ), DirectX::Colors::Red ); }这样你能直观看到每个鬼魂正在往哪个方向走进而判断它的 AI 有没有按预想逻辑工作。第二种修改速度参数观察边界。吃豆人速度、鬼魂速度这些参数通常以常量的方式定义在Global.h或Character.h。试着把鬼魂速度调高 50%观察它们是否会在狭窄通道中互相卡住或者在交叉口出现逻辑异常。 这能帮你找到 AI 寻路逻辑的边界。6.4 收尾一个工程上值得养成的习惯把一个没打过包的代码包仔细读一遍会自然地发现某些类做了设计冗余某些辅助类是为后续扩展预留的。读取这个项目的全过程里最让我改不掉的习惯是拿到任意一份源码先从资源文件和工程文件的对应关系开始排查再对照代码的具体逻辑最后才编译运行看效果。你花的时间最少但定位问题最快。如果你也打算好好读这份吃豆人源码希望这个流程能帮到你。最后一件事编译成功后把Game::Update里吃豆人的移动速度参数调大一点点感受一下原版“速度稍快”的调校差异。这种 0.5 像素级别的参数手感恰恰是这个项目最花心思的地方。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号