如何用C语言开发小游戏?零基础入门教程详解

C语言,作为一门经久不衰的系统级编程语言,其强大的底层控制能力和高效的性能使其成为学习计算机科学原理和开发小型、高性能程序的绝佳选择,虽然现代游戏引擎功能强大,但使用纯C语言从零开始构建一个小游戏,能够让你深刻理解游戏运行的核心机制图形渲染、用户输入处理、游戏逻辑循环、内存管理以及时间控制,这个过程不仅锻炼编程基本功,更能带来无与伦比的成就感,本教程将引导你一步步掌握C语言小游戏开发的核心技术栈和实现思路。

如何用C语言开发小游戏

C语言基础入门零基础【2026官方完整版】c语言程序设计基础计算机二级c语言二级考试c语言指针c语言大一期末速成c语言考研C语言专升本c语言入门零基础
加载中
C语言基础入门零基础【2026官方完整版】c语言程序设计基础计算机二级c语言二级考试c语言指针c语言大一期末速成c语言考研C语言专升本c语言入门零基础
392.1万7.7万2.4万
原视频地址

开发环境与工具准备

工欲善其事,必先利其器,C语言开发需要合适的编译器和必要的库支持,特别是对于图形界面。

  1. 选择编译器:

    • Windows: 推荐使用 MinGW-w64 (包含GCC编译器) 或 TDM-GCC,它们免费、开源且易于安装,集成开发环境(IDE)可以选择 Code::Blocks (自带MinGW) 或 Visual Studio (安装时选择“使用C++的桌面开发”工作负载,它支持C语言)。
    • Linux/macOS: 系统通常自带GCC或Clang编译器,在终端输入 gcc --versionclang --version 即可检查,常用IDE有 Code::Blocks, Eclipse CDT, 或者轻量级的编辑器如 VS Code (配合C/C++扩展)。
  2. 图形库的选择:
    标准C库(stdio.h, stdlib.h等)主要用于控制台输入输出,无法满足图形游戏的需求,我们需要借助第三方图形库:

    • SDL (Simple DirectMedia Layer): 强烈推荐! 跨平台(Windows, Linux, macOS, 甚至移动端)、开源、轻量级,它提供了对图形、声音、输入(键盘、鼠标、手柄)、线程等的抽象接口,隐藏了底层操作系统的复杂性,是学习游戏开发的理想起点。
    • 其他选项: Raylib (更现代、更易用的游戏库,基于OpenGL), Allegro, SFML (C++库,但有C接口)等,SDL因其广泛的社区支持和经典地位,是本教程的首选。
  3. 安装SDL:

    • 官网下载: 访问 www.libsdl.org
    • Windows: 下载 SDL2-devel-2.x.x-mingw.zip (对应MinGW) 或 SDL2-devel-2.x.x-VC.zip (对应Visual Studio),解压后,需要配置IDE的包含路径(include目录)和库路径(lib目录),并将SDL2.dll动态链接库文件放在你的可执行文件目录下或系统路径中。
    • Linux: 通常可以通过包管理器安装,Ubuntu/Debian: sudo apt-get install libsdl2-dev
    • macOS: 可以通过 Homebrew 安装: brew install sdl2
    • IDE配置: 务必在你的IDE项目中正确设置头文件包含路径和链接库(通常是 -lSDL2main -lSDL2SDL2.lib / SDL2main.lib)。

理解游戏核心循环

任何游戏的核心都是一个不断运行的循环,称为游戏循环主循环,其基本结构如下:

#include <SDL.h>
#include <stdbool.h> // 使用布尔类型
int main(int argc, char argv[]) {
    // 1. 初始化SDL
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        SDL_Log("SDL初始化失败: %s", SDL_GetError());
        return 1;
    }
    // 2. 创建窗口和渲染器
    SDL_Window window = SDL_CreateWindow("我的C语言小游戏", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, 0);
    if (!window) {
        SDL_Log("创建窗口失败: %s", SDL_GetError());
        SDL_Quit();
        return 1;
    }
    SDL_Renderer renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (!renderer) {
        SDL_Log("创建渲染器失败: %s", SDL_GetError());
        SDL_DestroyWindow(window);
        SDL_Quit();
        return 1;
    }
    // 3. 游戏状态初始化 ( 玩家位置、分数、关卡数据等)
    bool isRunning = true;
    SDL_Event event; // 用于接收事件
    // 4. 主游戏循环
    while (isRunning) {
        // 4.1 处理输入事件 (Input)
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                isRunning = false; // 用户点击关闭窗口
            }
            // 处理键盘、鼠标事件 ( event.type == SDL_KEYDOWN, event.key.keysym.sym)
            // ... (具体处理逻辑)
        }
        // 4.2 更新游戏状态 (Update)
        // 根据输入、时间流逝等更新玩家位置、敌人AI、碰撞检测、分数计算等
        // ... (具体更新逻辑)
        // 4.3 渲染 (Render)
        // 4.3.1 清屏 (通常用某种颜色填充背景)
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); // 黑色 (RGBA)
        SDL_RenderClear(renderer);
        // 4.3.2 绘制游戏对象 (玩家、敌人、背景、文字等)
        // 使用 SDL_RenderDraw... 系列函数绘制基本图形
        // 或使用 SDL_Texture / SDL_Surface 加载和绘制图像
        // ... (具体绘制逻辑)
        // 4.3.3 将渲染内容显示到屏幕 (双缓冲交换)
        SDL_RenderPresent(renderer);
        // (可选) 4.4 控制帧率 (Frame Rate Control)
        // 使用 SDL_Delay 或更精确的计时器(SDL_GetTicks)来确保游戏以期望的帧率运行
    }
    // 5. 清理资源
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

这个框架清晰地展示了游戏循环的四个关键阶段:

  1. Input (输入处理): 使用 SDL_PollEvent 轮询事件队列,处理用户的键盘、鼠标、窗口事件等。
  2. Update (状态更新): 根据输入、游戏规则、物理模拟(如果存在)以及时间的流逝,更新游戏世界中所有对象的状态(位置、生命值、动画帧等),进行关键的逻辑计算,如碰撞检测。
  3. Render (渲染):
    • 清屏: SDL_RenderClear 用背景色填充整个窗口。
    • 绘制: 使用SDL提供的绘图函数(SDL_RenderDrawLine, SDL_RenderDrawRect, SDL_RenderFillRect, SDL_RenderCopy用于贴图)或更高级的图形API(OpenGL/Vulkan,但SDL本身封装了基本2D绘制)来绘制当前帧的所有游戏对象。
    • 呈现: SDL_RenderPresent 将后台渲染缓冲区的内容交换到前台显示(双缓冲技术,避免画面撕裂)。
  4. Frame Rate Control (帧率控制 – 可选但重要): 在主循环末尾,通过 SDL_Delay 或计算帧时间差(SDL_GetTicks)来让程序等待一段时间,确保游戏运行在稳定的帧率(如60FPS),避免占用过多CPU资源。

核心模块详解与实践

  1. 图形绘制基础:

    如何用C语言开发小游戏

    • 基本图形: SDL提供了一系列函数绘制点、线、矩形(空心和实心)、圆(通过多边形近似或使用绘制像素点)。
    • 纹理(Textures)与表面(Surfaces):
      • SDL_Surface: 代表存储在内存中的像素数据(通常由 SDL_LoadBMP 加载位图文件创建)。
      • SDL_Texture: 代表存储在显卡显存中的像素数据,渲染效率远高于Surface,使用 SDL_CreateTextureFromSurface(renderer, surface) 将Surface转换为Texture。
      • 绘制纹理: 使用 SDL_RenderCopy(renderer, texture, &srcrect, &dstrect)srcrect 指定源纹理上的矩形区域(NULL代表整张纹理),dstrect 指定在屏幕上绘制的目标矩形区域(位置和大小)。
    • 颜色: 使用 SDL_SetRenderDrawColor(renderer, r, g, b, a) 设置后续绘制操作的画笔颜色(RGBA格式,0-255)。
    • 重要概念:双缓冲(Double Buffering): SDL_RenderPresent 实现了双缓冲,程序在后台缓冲区(back buffer)绘制当前帧,完成后一次性交换到前台缓冲区(front buffer)显示,确保画面平滑无闪烁。
  2. 处理用户输入:

    • 事件轮询: SDL_PollEvent(&event) 是核心函数,它从事件队列中取出一个事件(如果存在)填充到 event 结构体中,需要在循环中不断调用直到队列为空。
    • 事件类型: 检查 event.type 判断事件类型:
      • SDL_QUIT: 用户请求退出(点击窗口关闭按钮)。
      • SDL_KEYDOWN / SDL_KEYUP: 键盘按键按下/释放,通过 event.key.keysym.sym 获取具体的按键码(如 SDLK_UP, SDLK_SPACE, SDLK_a)。
      • SDL_MOUSEBUTTONDOWN / SDL_MOUSEBUTTONUP: 鼠标按键按下/释放,通过 event.button.button 获取按键(SDL_BUTTON_LEFT, SDL_BUTTON_RIGHT),event.button.xevent.button.y 获取鼠标位置。
      • SDL_MOUSEMOTION: 鼠标移动,通过 event.motion.x, event.motion.y 获取当前位置,event.motion.xrel, event.motion.yrel 获取相对上次事件的移动距离。
    • 键盘状态查询: const Uint8 state = SDL_GetKeyboardState(NULL); 获取当前所有键盘按键的状态数组,通过 state[SDL_ScanCode] 检查特定按键是否被按下(如 state[SDL_SCANCODE_W]),这种方式适合需要持续检测按键(如按住W键移动)的情况。
  3. 游戏状态管理:

    • 定义数据结构: 使用结构体(struct)来组织游戏对象的数据,一个简单的“方块”对象:
      typedef struct {
          float x, y;      // 位置
          float width, height; // 大小
          float velX, velY;    // 速度
          SDL_Color color; // 颜色
          // ... 其他属性 (生命值、状态标志等)
      } GameObject;
    • 初始化状态: 在游戏开始前,创建并初始化游戏对象(玩家、敌人、道具等)的实例,设置初始位置、速度、分数等。
    • 更新状态:Update 阶段,遍历所有活动对象:
      • 根据输入(如按键状态)更新玩家对象的速度或方向。
      • 根据速度更新对象的位置:x += velX deltaTime; y += velY deltaTime; (deltaTime是上一帧到现在的时间差,用于实现与帧率无关的平滑移动)。
      • 执行AI逻辑(敌人移动决策)。
      • 碰撞检测(Collision Detection): 检测对象之间是否发生碰撞,简单对象(方块、圆形)可以使用轴对齐包围盒(AABB)检测或圆形检测,这是游戏逻辑的关键部分,需要高效实现。
      • 处理碰撞后的逻辑(反弹、伤害、得分、销毁对象等)。
      • 更新计时器、动画帧等。
    • 管理对象生命周期: 创建新对象(如发射子弹)、销毁不再需要的对象(如被消灭的敌人),并小心管理内存(避免内存泄漏)。
  4. 时间管理:

    • 帧时间差(deltaTime): 这是游戏开发中极其重要的概念,它表示上一帧渲染完成到当前帧开始处理之间经过的时间(秒),使用 deltaTime 可以使物体的移动、旋转等变化与帧率无关,确保在不同性能的电脑上游戏体验一致。
    • 计算 deltaTime:
      Uint32 currentTime = SDL_GetTicks(); // 获取当前时间戳 (毫秒)
      float deltaTime = (currentTime - previousTime) / 1000.0f; // 转换为秒
      previousTime = currentTime;

      在Update阶段使用 deltaTime

      player.x += player.speed  deltaTime; // 速度单位变为 像素/秒
    • 固定时间步长(Fixed Timestep): 对于需要物理模拟稳定性的游戏(如使用简单的欧拉积分),可以在Update循环内部使用一个固定的小时间步长进行多次迭代更新,即使渲染帧率有波动,这比简单的 deltaTime 更复杂但更稳定。
  5. 资源管理与内存:

    • 加载资源: 在游戏初始化时或需要时加载图像(SDL_LoadBMP, IMG_Load – 需SDL_image库支持更多格式)、声音(Mix_LoadWAV – 需SDL_mixer库)等资源,创建对应的 SDL_TextureMix_Chunk
    • 释放资源: 至关重要! C语言没有自动垃圾回收,在游戏结束或对象销毁时,必须手动释放所有分配的内存、关闭文件句柄、销毁SDL对象(SDL_DestroyTexture, SDL_FreeSurface, Mix_FreeChunk, SDL_DestroyRenderer, SDL_DestroyWindow),使用 free() 释放 malloc/calloc 分配的内存,内存泄漏会导致程序占用内存不断增长最终崩溃。
    • 良好的习惯: 为每个 SDL_Create... 函数配对相应的 SDL_Destroy...,确保在 SDL_Quit 之前销毁所有依赖SDL的资源和对象。

实战案例:构建经典“贪吃蛇”游戏

让我们应用以上知识,规划一个简化版贪吃蛇游戏的核心实现步骤:

  1. 数据结构定义:

    typedef struct {
        int x, y; // 蛇身每一节的网格坐标
    } SnakeSegment;
    typedef struct {
        SnakeSegment body; // 动态数组存储蛇身
        int length;         // 当前蛇身长度
        int direction;      // 移动方向 (上:0, 右:1, 下:2, 左:3)
        // ... 其他 (颜色、是否增长标志等)
    } Snake;
    typedef struct {
        int x, y; // 食物网格坐标
    } Food;
  2. 初始化:

    • 初始化SDL窗口、渲染器。
    • 创建蛇对象:初始化body数组(例如初始长度为3节),设置初始位置和方向。
    • 随机生成食物位置(确保不在蛇身上)。
  3. 游戏循环:

    如何用C语言开发小游戏

    • Input:
      • 处理方向键 (SDLK_UP, SDLK_DOWN, SDLK_LEFT, SDLK_RIGHT) 改变蛇的 direction(注意不能直接反向移动)。
      • 处理退出事件。
    • Update:
      • 移动蛇:
        • 在蛇头前方(根据当前 direction)创建一个新蛇头节。
        • 如果蛇没有吃到食物(增长标志为假),则移除蛇尾最后一节,否则,只移除蛇尾的标记(即蛇身长度增加一节),并将增长标志重置。
      • 碰撞检测:
        • 边界碰撞: 检查新蛇头是否超出游戏网格边界 -> 游戏结束。
        • 自身碰撞: 检查新蛇头是否与蛇身的任何其他部分(除了尾部即将移除的那节)重叠 -> 游戏结束。
        • 食物碰撞: 检查新蛇头是否与食物位置重合 -> 吃到食物:设置蛇的增长标志为真,分数增加,随机生成新的食物位置(不在蛇身上)。
    • Render:
      • 清屏。
      • 绘制网格背景: (可选,帮助定位)
      • 绘制蛇: 遍历蛇的 body 数组,为每一节绘制一个矩形(如绿色)。
      • 绘制食物: 在食物坐标处绘制一个矩形(如红色)。
      • 绘制分数: (需要SDL_ttf库加载字体渲染文字,或使用基本图形拼数字)
      • 呈现。
  4. 结束与清理:

    • 游戏结束条件触发时,跳出主循环。
    • 释放蛇的 body 数组内存 (free(snake.body)).
    • 销毁SDL资源 (SDL_DestroyRenderer, SDL_DestroyWindow)。
    • 退出SDL (SDL_Quit)。

进阶优化与扩展思路

  1. 性能优化:

    • 纹理重用: 避免在每一帧都加载/销毁相同的纹理,在初始化时加载好,需要时直接绘制。
    • 批量绘制: 对于大量相同或相似的小对象(如粒子、砖块),考虑使用精灵图集(Sprite Sheet)和 SDL_RenderCopysrcrect 参数只绘制需要的部分。
    • 空间分区: 当对象数量很多时,使用空间数据结构(如四叉树、网格划分)优化碰撞检测等遍历操作。
    • 代码优化: 避免不必要的计算和内存分配(尤其是在循环内)。
  2. 提升游戏性:

    • 关卡设计: 增加不同难度关卡(蛇速度加快、障碍物出现)。
    • 道具系统: 添加不同类型的食物(加速、减速、穿墙等)。
    • 音效与音乐: 使用SDL_mixer库添加吃食物、碰撞、背景音乐等音效。
    • 动画效果: 实现简单的帧动画(通过切换纹理的不同区域 srcrect)或位置/颜色插值。
    • 保存游戏状态: 实现存档功能(将关键状态写入文件)。
  3. 跨平台注意事项:

    • 路径分隔符:Windows用,Linux/macOS用,SDL提供 SDL_GetBasePath() 获取程序运行目录,SDL_GetPrefPath(org, app) 获取适合存储配置/存档的路径。
    • 文件操作:使用标准C库 fopen 等时注意文件路径和权限,SDL也有文件IO抽象 (SDL_RWops)。

调试与常见问题

  • 利用 SDL_GetError(): 任何SDL函数调用失败后,立即调用 SDL_GetError() 获取错误信息并打印出来,这是定位SDL相关问题的首要方法。
  • 内存泄漏检测工具: Windows下可使用Visual Studio自带的内存诊断工具,Linux/macOS可使用 valgrind,养成良好的内存管理习惯是根本。
  • 检查返回值: 对可能失败的函数调用(如 SDL_CreateWindow, SDL_CreateRenderer, malloc, fopen)进行错误检查。
  • 简化问题: 当遇到复杂bug时,尝试注释掉部分代码,创建一个最小的可复现问题的例子。
  • 常见陷阱:
    • 忘记调用 SDL_RenderPresent,导致黑屏。
    • SDL_RenderClear 之前绘制,导致画面闪烁或内容被清除。
    • 内存泄漏(忘记 freeSDL_Destroy...)。
    • 野指针(使用已释放的内存)。
    • 碰撞检测逻辑错误(边界条件处理不当)。
    • 没有使用 deltaTime,导致游戏速度受帧率影响。

使用C语言配合SDL库开发小游戏,是一次深入计算机图形、实时系统、资源管理和逻辑设计的宝贵旅程,它让你亲自动手实现游戏的每一个核心模块,从处理用户按键到在屏幕上绘制像素,再到精确控制时间和逻辑状态,虽然挑战重重,但克服这些挑战带来的理解深度和掌控感是使用高级引擎难以比拟的,从简单的图形绘制开始,逐步实现输入响应、状态更新、碰撞检测,最终完成一个像贪吃蛇这样的完整游戏,你将建立起坚实的游戏编程基础和对C语言能力的强大信心,不断实践,勇于探索更复杂的机制和优化技巧,你就能用C语言创造出属于自己的独特游戏世界。

你已经准备好开始你的C语言游戏开发之旅了吗?你最想尝试制作的第一款小游戏是什么类型?或者在学习SDL/C游戏开发过程中遇到了哪些具体问题?欢迎在评论区分享你的想法和遇到的挑战!

首发原创文章,作者:王坚‌,如若转载,请注明出处:https://idctop.com/article/29110.html

(0)
Comm100好用吗?全渠道客服与AI聊天机器人深度测评
上一篇 2026年2月13日 16:29
国内大宽带高防服务器怎么样?租用高防服务器价格如何选,国内大宽带高防服务器如何选择?租用价格与防御能力解析
下一篇 2026年2月13日 16:35

相关推荐

  • 30岁转行游戏开发晚不晚?大龄程序员必看职业规划

    30岁开始游戏开发,是完全可行的,许多人误以为游戏开发是年轻人的专属领域,但成熟年龄带来独特优势,如更强的责任感、现实问题解决能力和职业经验,能让你更快上手并产出高质量作品,本教程基于多年行业经验,提供从零基础到专业开发者的完整路径,涵盖编程、工具使用、项目实战和职业规划,确保你高效入门并避免常见陷阱,让我们一……

    2026年2月11日
    21900
  • Java读取txt文件乱码怎么办?Java读取txt文件中文乱码解决方法

    关于java读取txt文件在服务器性能测评的语境下,“Java读取TXT文件”不仅仅是一个简单的IO操作,它是衡量服务器I/O吞吐量、内存管理效率以及JVM(Java虚拟机)垃圾回收机制稳定性的关键基准测试场景,对于运行高并发Web应用、日志分析系统或大数据处理平台的用户而言,服务器在处理大规模文本文件时的表现……

    2026年6月14日
    3000
  • 均衡型16GHadoop云主机多少钱?,负载均衡怎么配置?

    对于均衡型16G Hadoop云主机搭配独享型负载均衡的配置,市场主流方案的年费成本大致在数千元到上万元区间,具体取决于云厂商、带宽规格和存储类型,均衡型16G Hadoop云主机价格是多少?均衡型16G实例是云计算中针对通用计算负载设计的实例规格,内存16GB,适合运行Hadoop生态中的DataNode、N……

    2026年8月6日
    700
  • 小米5怎么刷开发版本?小米5开发版刷机教程

    小米5刷开发版本的核心价值在于获取系统最高权限与体验最新功能特性,这一操作虽能极大拓展设备可玩性,但必须建立在严谨的备份流程与正确的线刷操作之上,确保数据安全与系统稳定性,对于追求极致体验的用户而言,官方开发版不仅意味着更早接触安卓底层更新,更是刷入第三方ROM、使用Magisk进行模块化定制的必经之路,核心准……

    2026年3月11日
    12000
  • h5小游戏开发工具哪个好?免费h5游戏制作软件推荐

    在当前的移动互联网环境下,选择一款高效、稳定的H5小游戏开发工具,是项目成功上线与变现的关键前提,核心结论在于:优秀的开发工具不仅决定了代码的运行效率与游戏性能,更直接影响开发周期、跨平台兼容性以及后期的维护成本,对于开发者而言,最明智的策略是根据项目规模与技术储备,在成熟引擎与轻量级框架之间做出精准匹配,以实……

    2026年3月28日
    10300
  • arm嵌入式开发实例 pdf下载哪里有?arm嵌入式开发教程推荐

    ARM嵌入式开发的核心在于软硬件协同设计与工程实践能力的结合,获取一份高质量的arm嵌入式开发实例 pdf资料,对于工程师快速掌握从芯片架构到产品落地的全流程具有决定性意义,通过剖析真实的工程案例,开发者能够绕过枯燥的理论推导,直接获取解决中断处理、外设驱动及实时系统移植等关键问题的“通关密码”,从而显著缩短项……

    2026年3月24日
    9600
  • 共青团舆情监测怎么做?共青团舆情监测平台有哪些

    服务器测评在数字化转型加速的当下,服务器作为网站、应用及数据服务的核心基础设施,其稳定性、性能及安全性直接决定了业务的成败,对于企业开发者、初创团队及个人站长而言,选择一款高性价比、低延迟且售后完善的服务器产品,是构建稳定在线业务的第一步,本文基于实际测试数据与长期运行体验,对当前主流云服务器进行深度解析,并重……

    2026年6月17日
    2100
  • 云存储书籍哪本好?云存储技术入门指南

    关于云存储书籍在数字化阅读与知识管理日益普及的今天,云存储不仅是数据的容器,更是个人与机构知识资产的守护者,对于出版商、图书馆以及重度阅读爱好者而言,选择一款稳定、安全且高效的云存储服务至关重要,本次测评将聚焦于几款主流云存储平台在“书籍存储”场景下的表现,从数据安全性、读写速度、协作效率及性价比四个维度进行深……

    程序开发 2026年6月9日
    2700
  • 云主机如何部署jboss?jboss部署详细教程

    在云计算基础设施日益成熟的今天,JBOSS作为企业级Java应用服务器的核心组件,其部署环境的稳定性、性能吞吐量以及资源隔离性直接决定了上层业务系统的可靠性,对于追求高可用架构的企业而言,选择一款能够完美适配JBOSS运行环境的云主机,不仅是技术选型的问题,更是成本控制与运维效率的关键考量,本次测评将深入剖析当……

    2026年6月10日
    4400
  • 人脸识别门禁终端公司怎么选?人脸识别门禁系统多少钱

    2026年主流服务器硬件深度测评在智慧安防与数字化转型的浪潮中,人脸识别门禁系统已不再仅仅是简单的身份验证工具,而是演变为集高并发处理、低延迟响应及多重安全加密于一体的复杂物联网节点,对于【关于人脸识别门禁终端公司】而言,后端服务器的选型直接决定了前端设备的稳定性、识别准确率以及系统在高并发场景下的承载能力,2……

    2026年6月4日
    3900

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注