C语言——实现贪吃蛇小游戏

简介: 本文介绍了一个基于Windows控制台的贪吃蛇游戏的实现方法。首先,需调整控制台界面以便更好地显示游戏。接着,文章详细描述了如何使用Win32 API函数如`COORD`、`GetStdHandle`、`GetConsoleCursorInfo`等来控制控制台的光标和窗口属性。此外,还介绍了如何利用`GetAsyncKeyState`函数实现键盘监听功能。文中还涉及了`<locale.h>`库的使用,以支持本地化字符显示。

准备工作

首先我们需要更改一下运行之后调用的控制台界面

如果运行之后出现的是上面的界面,就需要更改一下,鼠标右键点击控制台顶端,再点击设置


再启动就可以了,之后也可以自己自定义控制台的样式,例如颜色,字体,还是右键点击控制台顶端,再点击属性,就可以找到设置了。
之后我们可以通过一些命令来对控制台进行设置

system("mode con cols=100 lines=30");//设置窗口大小
system("title 贪吃蛇");//设置标题

以下是我们需要达到的最终效果

用到的win32API

COORD控制台坐标

COORD是一个结构体类型,在使用时需要包含的头文件是windows.h,定义的参数分别为x,y轴坐标

GetStdHandle

它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值)。这个句柄可以用于后续对设备进行操作或修改其属性。可以理解为就是一只手,通过这个手我们才能后续对设备进行操作

GetConsoleCursorInfo

用于检索有关指定的控制台屏幕缓冲区的光标的可见性和大小信息

示例:

int main() {
    HANDLE houtput = NULL;//获取句柄
    houtput = GetStdHandle(STD_OUTPUT_HANDLE);
    //定义光标信息结构体
    CONSOLE_CURSOR_INFO cursor_info = { 0 };
    //获取和houtput句柄相关的控制台上的光标信息,并存放在cursor_info中
    GetConsoleCursorInfo(houtput, &cursor_info);
    printf("%d\n", cursor_info.dwSize);
    
    return 0;
}

当我们把光标信息打印出来时,显示为25,也就是当前光标大小占整个字符的25%

//修改光标占比
cursor_info.dwSize = 50;
//设置和houtput句柄相关的控制台上光标信息
SetConsoleCursorInfo(houtput, &cursor_info);

可以加入上面的代码对光标进行修改,显示的就是占比50%的光标

还可以根据以下代码设置光标的位置

//定位光标位置
COORD pos = { 10,20 };
SetConsoleCursorPosition(houtput, pos);

GetAsyncKeyState

用于检测指定的键是否被按下或释放,接受一个虚拟键码作为参数,并返回一个short类型的值,如果指定的键被按下,则返回一个负数,表示该键此前被按下并一直保持按下状态;如果指定的键未被按下,则返回零。

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)  //把GetAsyncKeyState定义为宏,类似与函数
int main(){
   while (1) {
    //分别传入0,1,2的虚拟键码
    if (KEY_PRESS(0x30)) {
        printf("0\n");
    }
    else if (KEY_PRESS(0x31)) {
        printf("1\n");
    }
    else if (KEY_PRESS(0x32)) {
        printf("2\n");
    }
    }
return 0;
}

这样就能实现键盘监听的效果

<locale.h>本地化

用于改变程序的行为以适应不同的文化和语言环境,例如,中文的一个文字是宽字符,需要占用两个单字符

#include <stdio.h>
#include <locale.h>
int main() {
    char* ret = setlocale(LC_ALL, NULL);//c语言默认模式
    printf("%s\n", ret);
    ret = setlocale(LC_ALL, "");//简体中文模式
    printf("%s\n", ret);
    char a = 'a', b = 'b';
    printf("%c%c\n", a, b);
    //宽字符打印
    wchar_t w1 = L'你';
    wchar_t w2 = L'好';
    wprintf(L"%lc\n", w1);
    wprintf(L"%lc\n", w2);
    return 0;
}

设置本地化之后,就可以打印一些本地化的符号了,例如中文宽字符的打印,也可以明显的看出,宽字符要占用两个普通字符的宽度。

在后续的操作中需要注意,一个坐标能放一个普通字符,两个坐标才能放一个宽字符

主要部分

初始化成员

//蛇前进的方向
enum DIRECTTON {
    UP = 1,
    DOWN, 
    LEFT,
    RIGHT
};
//游戏状态
enum GAME_STATUS {
    OK,//正常
    KILL_BY_WALL,//撞墙
    KILL_BY_SELF,//咬到自己
    END_NORMAL//正常结束
};
//定义蛇,坐标定位蛇的位置
typedef struct SnakeNode {
    int x;
    int y;
    struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
//封装
typedef struct Snake {
    pSnakeNode psnake; //指向蛇头的指针
    pSnakeNode pFood;//指向食物的指针
    enum DIRECTTON dir;//蛇的方向
    enum GAME_STATUS status;//游戏的状态
    int food_weight;//一个食物的分数
    int score;//总分
    int sleep_time;
}Snake,*psnake;

这样我们就完成了所有成员变量的初始化

地图创建

void CreateMap()
{
    //上
    int i = 0;
    for (i = 0; i < 29; i++)
    {
        wprintf(L"%lc", WALL);
    }
    //下
    SetPos(0, 26);
    for (i = 0; i < 29; i++)
    {
        wprintf(L"%lc", WALL);
    }
    //左
    for (i = 1; i <= 25; i++)
    {
        SetPos(0, i);
        wprintf(L"%lc", WALL);
    }
    //右
    for (i = 1; i <= 25; i++)
    {
        SetPos(56, i);
        wprintf(L"%lc", WALL);
    }
    system("pause");
}

蛇的初始化

void InitSnake(psnake ps) {
    pSnakeNode cur = NULL;
    for (int i = 0; i < 5; i++) {
        cur = (pSnakeNode)malloc(sizeof(SnakeNode));
        if (cur == NULL) {
            perror("InitSnake");
            return;
        }
        cur->next = NULL;
        cur->x = POS_X + 2 * i;
        cur->y = POS_Y;
        if (ps->psnake == NULL) {//空链表
            ps->psnake = cur;
        }
        else {//非空链表
            cur->next = ps->psnake;
            ps->psnake = cur;
        }
    }
    cur = ps->psnake;
    while (cur) {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    //设置贪吃蛇的属性
    ps->dir = RIGHT;
    ps->score = 0;
    ps->food_weight = 10;
    ps->sleep_time = 200;
    ps->status = OK;
    getchar();
}

随机生成食物

void CreateFood(psnake ps)
{
    int x = 0;
    int y = 0;
    //生成x是2的倍数
    //x:2~54
    //y: 1~25
again:
    do
    {
        x = rand() % 53 + 2;
        y = rand() % 25 + 1;
    } while (x % 2 != 0);
    //x和y的坐标不能和蛇的身体坐标冲突
    pSnakeNode cur = ps->psnake;
    while (cur)
    {
        if (x == cur->x && y == cur->y)
        {
            goto again;
        }
        cur = cur->next;
    }
    //创建食物的节点
    pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (pFood == NULL)
    {
        perror("CreateFood()::malloc()");
        return;
    }
    pFood->x = x;
    pFood->y = y;
    pFood->next = NULL;
    SetPos(x, y);//定位位置
    wprintf(L"%lc", FOOD);
    ps->pFood = pFood;
}

吃到食物后再生成食物,蛇身增加

void EatFood(pSnakeNode pn, psnake ps)
{
    //头插法
    ps->pFood->next = ps->psnake;
    ps->psnake = ps->pFood;
    //释放下一个位置的节点
    free(pn);
    pn = NULL;
    pSnakeNode cur = ps->psnake;
    //打印蛇
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    ps->score += ps->food_weight;
    //重新创建食物
    CreateFood(ps);
}

完整源码

snake.h

#pragma once
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
enum DIRECTTON {
    UP = 1,
    DOWN,
    LEFT,
    RIGHT
};
enum GAME_STATUS {
    OK,//正常
    KILL_BY_WALL,//撞墙
    KILL_BY_SELF,//咬到自己
    END_NORMAL//正常结束
};
typedef struct SnakeNode {
    int x;
    int y;
    struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
typedef struct Snake {
    pSnakeNode psnake; //指向蛇头的指针
    pSnakeNode pFood;//指向食物的指针
    enum DIRECTTON dir;//蛇的方向
    enum GAME_STATUS status;//游戏的状态
    int food_weight;//一个食物的分数
    int score;//总分
    int sleep_time;
}Snake,*psnake;
void GameStart(psnake p);
void WelcomeToGame();
void CreateMap();
void InitSnake(psnake ps);
void CreateFood(psnake ps);
//游戏运行的逻辑
void GameRun(psnake ps);
void SnakeMove(psnake ps);
int NextIsFood(pSnakeNode pn, psnake ps);
void EatFood(pSnakeNode pn, psnake ps);
void NoFood(pSnakeNode pn, psnake ps);
void KillByWall(psnake ps);
void KillBySelf(psnake ps);
void GameEnd(psnake ps);

snake.c

#define _CRT_SECURE_NO_WARNINGS 1
#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)
#include "snake.h"
void SetPos(short x, short y)
{
    //获得标准输出设备的句柄
    HANDLE houtput = NULL;
    houtput = GetStdHandle(STD_OUTPUT_HANDLE);
    //定位光标的位置
    COORD pos = { x, y };
    SetConsoleCursorPosition(houtput, pos);
}
void WelcomeToGame()
{
    SetPos(40, 14);
    wprintf(L"欢迎来到贪吃蛇小游戏\n");
    SetPos(42, 20);
    system("pause");
    system("cls");
    SetPos(25, 14);
    wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
    SetPos(25, 15);
    wprintf(L"加速能够得到更高的分数\n");
    SetPos(42, 20);
    system("pause");
    system("cls");
}
void CreateMap()
{
    //上
    int i = 0;
    for (i = 0; i < 29; i++)
    {
        wprintf(L"%lc", WALL);
    }
    //下
    SetPos(0, 26);
    for (i = 0; i < 29; i++)
    {
        wprintf(L"%lc", WALL);
    }
    //左
    for (i = 1; i <= 25; i++)
    {
        SetPos(0, i);
        wprintf(L"%lc", WALL);
    }
    //右
    for (i = 1; i <= 25; i++)
    {
        SetPos(56, i);
        wprintf(L"%lc", WALL);
    }
    system("pause");
}
void InitSnake(psnake ps) {
    pSnakeNode cur = NULL;
    for (int i = 0; i < 5; i++) {
        cur = (pSnakeNode)malloc(sizeof(SnakeNode));
        if (cur == NULL) {
            perror("InitSnake");
            return;
        }
        cur->next = NULL;
        cur->x = POS_X + 2 * i;
        cur->y = POS_Y;
        if (ps->psnake == NULL) {//空链表
            ps->psnake = cur;
        }
        else {//非空链表
            cur->next = ps->psnake;
            ps->psnake = cur;
        }
    }
    cur = ps->psnake;
    while (cur) {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    //设置贪吃蛇的属性
    ps->dir = RIGHT;
    ps->score = 0;
    ps->food_weight = 10;
    ps->sleep_time = 200;
    ps->status = OK;
    getchar();
}
void GameStart(psnake ps)
{
    //设置窗口
    system("mode con cols=100 lines=30");
    system("title 贪吃蛇");
    HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
    //影藏光标操作
    CONSOLE_CURSOR_INFO CursorInfo;
    GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
    CursorInfo.bVisible = false; //隐藏控制台光标
    SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态
    //开始界面
    WelcomeToGame();
    //绘制地图
    CreateMap();
    //创建蛇
    InitSnake(ps);
    //创建食物
    CreateFood(ps);
}
void CreateFood(psnake ps)
{
    int x = 0;
    int y = 0;
    //生成x是2的倍数
    //x:2~54
    //y: 1~25
again:
    do
    {
        x = rand() % 53 + 2;
        y = rand() % 25 + 1;
    } while (x % 2 != 0);
    //x和y的坐标不能和蛇的身体坐标冲突
    pSnakeNode cur = ps->psnake;
    while (cur)
    {
        if (x == cur->x && y == cur->y)
        {
            goto again;
        }
        cur = cur->next;
    }
    //创建食物的节点
    pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (pFood == NULL)
    {
        perror("CreateFood()::malloc()");
        return;
    }
    pFood->x = x;
    pFood->y = y;
    pFood->next = NULL;
    SetPos(x, y);//定位位置
    wprintf(L"%lc", FOOD);
    ps->pFood = pFood;
}
void PrintHelpInfo()
{
    SetPos(64, 14);
    wprintf(L"%ls", L"不能穿墙,不能咬到自己");
    SetPos(64, 15);
    wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
    SetPos(64, 16);
    wprintf(L"%ls", L"按F3加速,F4减速");
    SetPos(64, 17);
    wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
    SetPos(64, 18);
    wprintf(L"%ls", L"制作");
}
void Pause()
{
    while (1)
    {
        Sleep(200);
        if (KEY_PRESS(VK_SPACE))
        {
            break;
        }
    }
}
int NextIsFood(pSnakeNode pn, psnake ps)
{
    return (ps->pFood->x == pn->x && ps->pFood->y == pn->y);
}
void EatFood(pSnakeNode pn, psnake ps)
{
    //头插法
    ps->pFood->next = ps->psnake;
    ps->psnake = ps->pFood;
    //释放下一个位置的节点
    free(pn);
    pn = NULL;
    pSnakeNode cur = ps->psnake;
    //打印蛇
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    ps->score += ps->food_weight;
    //重新创建食物
    CreateFood(ps);
}
void NoFood(pSnakeNode pn, psnake ps)
{
    //头插法
    pn->next = ps->psnake;
    ps->psnake = pn;
    pSnakeNode cur = ps->psnake;
    while (cur->next->next != NULL)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    //把最后一个结点打印成空格
    SetPos(cur->next->x, cur->next->y);
    printf("  ");
    //释放最后一个结点
    free(cur->next);
    //把倒数第二个节点的地址置为NULL
    cur->next = NULL;
}
void KillByWall(psnake ps)
{
    if (ps->psnake->x == 0 || ps->psnake->x == 56 ||
        ps->psnake->y == 0 || ps->psnake->y == 26)
    {
        ps->status = KILL_BY_WALL;
    }
}
void KillBySelf(psnake ps)
{
    pSnakeNode cur = ps->psnake->next;
    while (cur)
    {
        if (cur->x == ps->psnake->x && cur->y == ps->psnake->y)
        {
            ps->status = KILL_BY_SELF;
            break;
        }
        cur = cur->next;
    }
}
void SnakeMove(psnake ps)
{
    //创建一个结点,表示蛇即将到的下一个节点
    pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (pNextNode == NULL)
    {
        perror("SnakeMove()::malloc()");
        return;
    }
    switch (ps->dir)
    {
    case UP:
        pNextNode->x = ps->psnake->x;
        pNextNode->y = ps->psnake->y - 1;
        break;
    case DOWN:
        pNextNode->x = ps->psnake->x;
        pNextNode->y = ps->psnake->y + 1;
        break;
    case LEFT:
        pNextNode->x = ps->psnake->x - 2;
        pNextNode->y = ps->psnake->y;
        break;
    case RIGHT:
        pNextNode->x = ps->psnake->x + 2;
        pNextNode->y = ps->psnake->y;
        break;
    }
    //检测下一个坐标处是否是食物
    if (NextIsFood(pNextNode, ps))
    {
        EatFood(pNextNode, ps);
    }
    else
    {
        NoFood(pNextNode, ps);
    }
    //检测蛇是否撞墙
    KillByWall(ps);
    //检测蛇是否撞到自己
    KillBySelf(ps);
}
void GameRun(psnake ps)
{
    //打印帮助信息
    PrintHelpInfo();
    do
    {
        //打印总分数和食物的分值
        SetPos(64, 10);
        printf("总分数:%d\n", ps->score);
        SetPos(64, 11);
        printf("当前食物的分数:%2d\n", ps->food_weight);
        if (KEY_PRESS(VK_UP) && ps->dir != DOWN)
        {
            ps->dir = UP;
        }
        else if (KEY_PRESS(VK_DOWN) && ps->dir != UP)
        {
            ps->dir = DOWN;
        }
        else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)
        {
            ps->dir = LEFT;
        }
        else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)
        {
            ps->dir = RIGHT;
        }
        else if (KEY_PRESS(VK_SPACE))
        {
            Pause();
        }
        else if (KEY_PRESS(VK_ESCAPE))
        {
            //正常退出游戏
            ps->status = END_NORMAL;
        }
        else if (KEY_PRESS(VK_F3))
        {
            //加速
            if (ps->sleep_time > 80)
            {
                ps->sleep_time -= 30;
                ps->food_weight += 2;
            }
        }
        else if (KEY_PRESS(VK_F4))
        {
            //减速
            if (ps->food_weight > 2)
            {
                ps->sleep_time += 30;
                ps->food_weight -= 2;
            }
        }
        SnakeMove(ps);
        Sleep(ps->sleep_time);
    } while (ps->status == OK);
}
void GameEnd(psnake ps)
{
    SetPos(24, 12);
    switch (ps->status)
    {
    case END_NORMAL:
        wprintf(L"结束游戏\n");
        break;
    case KILL_BY_WALL:
        wprintf(L"撞到墙上,游戏结束\n");
        break;
    case KILL_BY_SELF:
        wprintf(L"撞到了自己,游戏结束\n");
        break;
    }
    //释放蛇身的链表
    pSnakeNode cur = ps->psnake;
    while (cur)
    {
        pSnakeNode del = cur;
        cur = cur->next;
        free(del);
    }
}

text.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void test()
{
    int ch = 0;
    do
    {
        system("cls");
        //创建贪吃蛇
        Snake snake = { 0 };
        GameStart(&snake);
        GameRun(&snake);
        //结束游戏
        GameEnd(&snake);
        SetPos(20, 15);
        printf("再来一局吗?(Y/N):");
        ch = getchar();
        while (getchar() != '\n');
    } while (ch == 'Y' || ch == 'y');
    SetPos(0, 27);
}
int main()
{
    setlocale(LC_ALL, "");
    srand((unsigned int)time(NULL));
    test();
    return 0;
}
相关文章
|
4天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
1天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2153 11
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
1天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1155 13
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
1月前
|
运维 Cloud Native Devops
一线实战:运维人少,我们从 0 到 1 实践 DevOps 和云原生
上海经证科技有限公司为有效推进软件项目管理和开发工作,选择了阿里云云效作为 DevOps 解决方案。通过云效,实现了从 0 开始,到现在近百个微服务、数百条流水线与应用交付的全面覆盖,有效支撑了敏捷开发流程。
19265 29
|
1月前
|
人工智能 自然语言处理 搜索推荐
阿里云Elasticsearch AI搜索实践
本文介绍了阿里云 Elasticsearch 在AI 搜索方面的技术实践与探索。
18805 20
|
1月前
|
Rust Apache 对象存储
Apache Paimon V0.9最新进展
Apache Paimon V0.9 版本即将发布,此版本带来了多项新特性并解决了关键挑战。Paimon自2022年从Flink社区诞生以来迅速成长,已成为Apache顶级项目,并广泛应用于阿里集团内外的多家企业。
17509 13
Apache Paimon V0.9最新进展
|
1月前
|
存储 人工智能 前端开发
AI 网关零代码解决 AI 幻觉问题
本文主要介绍了 AI Agent 的背景,概念,探讨了 AI Agent 网关插件的使用方法,效果以及实现原理。
18695 16
|
1月前
|
人工智能 自然语言处理 搜索推荐
评测:AI客服接入钉钉与微信的对比分析
【8月更文第22天】随着人工智能技术的发展,越来越多的企业开始尝试将AI客服集成到自己的业务流程中。本文将基于《10分钟构建AI客服并应用到网站、钉钉或微信中》的解决方案,详细评测AI客服在钉钉和微信中的接入流程及实际应用效果,并结合个人体验分享一些心得。
9913 9
|
3天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
|
2天前
|
缓存 前端开发 JavaScript
终极 Nginx 配置指南(全网最详细)
本文详细介绍了Nginx配置文件`nginx.conf`的基本结构及其优化方法。首先通过删除注释简化了原始配置,使其更易理解。接着,文章将`nginx.conf`分为全局块、events块和http块三部分进行详细解析,帮助读者更好地掌握其功能与配置。此外,还介绍了如何通过简单修改实现网站上线,并提供了Nginx的优化技巧,包括解决前端History模式下的404问题、配置反向代理、开启gzip压缩、设置维护页面、在同一IP上部署多个网站以及实现动静分离等。最后,附上了Nginx的基础命令,如安装、启动、重启和关闭等操作,方便读者实践应用。
148 77
终极 Nginx 配置指南(全网最详细)