C语言:项目——从零编写小游戏(走迷宫)

目录


前言

作为一名游戏玩家,经过一段时间的C语言的学习后,我在想能不能通过编写一些小游戏来使学习过程变得没那么枯燥,同时用实战加深对所学知识的理解。

一、从Hello World开始

先写一个所有编程学习者最亲切的程序,打印“Hello World”。

#include <stdio.h> // 标准输入输出头文件

int main()
{
    printf("Hello world"); // 打印字符串"Hello world"
    return 0;              // 退出主函数,并返回0值
}

程序运行时我们发现,运行结果总是一闪而过,且每执行程序都会遗留上次运行的痕迹。

让我们通过下述语句来改进一下这个程序:
1、语句:system(“cls”); //表示对控制台窗口清屏,该函数包含在头文件stdlib.h中。
2、语句:getch(); //表示按任意键结束,该函数包含在头文件conio.h中。

二、一个移动的游戏角色

先声明一个游戏角色的结构体用于将角色属性存储在变量里。

struct Man
{
    int x; // 角色横坐标
    int y; // 角色纵坐标
    int v; // 角色移动速度
};

运动的原理是,先传递移动后的坐标给角色,再删除角色原来的痕迹,把光标移动到角色坐标位置,然后打印出角色符号。

man.x = willx; // 传递移动后的横坐标
man.y = willy; // 传递移动后的纵坐标

printf("\b ");                // 删除角色原来的痕迹
CursorJump(2 * man.x, man.y); // 把光标移动到角色的目标坐标
printf("@");                  // 打印角色符号

光标移动函数CursorJump()是一个自定义函数,其中调用的关键字包含在windows.h头文件内。

/*光标移动*/
void CursorJump(int x, int y)
{
    COORD pos; // 定义光标位置的结构体变量
    pos.X = x; // 横坐标设置
    pos.Y = y; // 纵坐标设置

    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台标准输出设备的句柄

    SetConsoleCursorPosition(handle, pos); // 设置控制台光标位置
}

三、交互的实现

游戏角色已经有了,下面就是让他根据我们的按键指令顺着方向移动。

char key = getch(); // 读取keycode
switch (key)
{
case UP: // 获取角色上移后的坐标
    willx = man.x;
    willy = man.y - man.v;
    break;
case DOWN: // 获取角色下移后的坐标
    willx = man.x;
    willy = man.y + man.v;
    break;
case LEFT: // 获取角色左移后的坐标
    willx = man.x - man.v;
    willy = man.y;
    break;
case RIGHT: // 获取角色右移后的坐标
    willx = man.x + man.v;
    willy = man.y;
    break;
default:
    break; // 其他按键则忽略处理
}

角色不会只执行一次操作,我们再用一个while(1)循环结构语句让程序无限循环起来。当然,也可以使用for(;;)语句,效果相同。

怎么样,是不是有了玩游戏的感觉了?不过这个游戏没有什么目的,也没有什么判断胜负的条件。下面我们就利用这个能控制它移动的游戏角色来做一个更有趣的游戏吧! 

四、在迷宫中探索  

还记得小时候再一些小人书和杂志上看见的迷宫游戏吗?好的,现在我们用C语言来编个走迷宫的游戏,重温一下童年吧。

首先,我们定义一个二维数组map,用它来保存迷宫的地图。其中,数组元素的下标是地图上的坐标点,数组元素的数值是地图在该坐标的地形信息。

/*初始化地图信息,通道为0,墙壁为1,目的地为2*/
int map[ROW][COL] =
    {
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 0, 0, 0, 0, 1, 0, 0, 2, 1},
        {1, 1, 1, 1, 0, 1, 0, 1, 0, 1},
        {1, 0, 0, 0, 0, 1, 0, 0, 0, 1},
        {1, 0, 1, 1, 1, 1, 1, 0, 1, 1},
        {1, 0, 0, 0, 0, 0, 1, 0, 1, 1},
        {1, 0, 1, 0, 1, 0, 0, 0, 0, 1},
        {1, 0, 1, 0, 1, 1, 1, 1, 0, 1},
        {1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};

然后,用双循环结构把地图打印在窗口上。这里我用用’  ‘表示通道,’■’表示墙壁,用’ #’表示目的地。

/*打印地图*/
for (int i = 0; i < ROW; i++)
{
    for (int j = 0; j < COL; j++)
    {
        CursorJump(2 * j, i);
        switch (map[i][j])
        {
        case 0: // 打印通道
            printf(" ");
            break;
        case 1: // 打印墙壁
            printf("■");
            break;
        case 2: // 打印目的地
            printf("#'");
            break;
        default:
            break;
        }
    }
}

在游戏循环中,我们再增加了一些用来判断移动后结果的语句,遇到通道就移动,遇到墙壁就停止,遇到目的地就游戏胜利。

/*判断交互条件*/
switch (map[willy][willx])
{
case 0: // 在通道中移动
    man.x = willx;                // 传递移动后的横坐标
    man.y = willy;                // 传递移动后的纵坐标
    printf("\b ");                // 删除角色原来的痕迹
    CursorJump(2 * man.x, man.y); // 把光标移动到角色的目标坐标
    printf("@");                  // 打印角色符号
    break;
case 1: // 撞墙
    break;
case 2: // 到达目的地
    CursorJump(2 * COL + 2, 0);
    printf("You win!\n"); // 游戏胜利
    getch(); // 暂停等待,按任意键结束
    return 0;
    break;
default:
    break;
}

哇噻!我们真的做出了一个完整的游戏了。当然,你还可以进行一些优化和内容填充,让它更有游戏性,比如:修改游戏地图、增加地形种类、增加关卡数量、优化UI界面等。

五、完整代码示例

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <windows.h>

#define ROW 10 // 游戏区行数
#define COL 10 // 游戏区列数

#define UP 72    // 方向键:上
#define DOWN 80  // 方向键:下
#define LEFT 75  // 方向键:左
#define RIGHT 77 // 方向键:右

/*声明游戏角色的结构体*/
struct Man
{
    int x; // 角色横坐标
    int y; // 角色纵坐标
    int v; // 角色移动速度
};

/*隐藏光标*/
void HideCursor()
{
    CONSOLE_CURSOR_INFO curInfo; // 定义光标信息的结构体变量
    curInfo.dwSize = 1;          // 光标尺寸,如果没赋值的话,隐藏光标无效
    curInfo.bVisible = FALSE;    // 将光标设置为不可见

    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台标准输出设备的句柄

    SetConsoleCursorInfo(handle, &curInfo); // 设置控制台光标信息
}

/*光标移动*/
void CursorJump(int x, int y)
{
    COORD pos; // 定义光标位置的结构体变量
    pos.X = x; // 横坐标设置
    pos.Y = y; // 纵坐标设置

    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台标准输出设备的句柄

    SetConsoleCursorPosition(handle, pos); // 设置控制台光标位置
}

/*初始化地图信息,墙壁为1,通道为0*/
int map[ROW][COL] =
    {
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 0, 0, 0, 0, 1, 0, 0, 2, 1},
        {1, 1, 1, 1, 0, 1, 0, 1, 0, 1},
        {1, 0, 0, 0, 0, 1, 0, 0, 0, 1},
        {1, 0, 1, 1, 1, 1, 1, 0, 1, 1},
        {1, 0, 0, 0, 0, 0, 1, 0, 1, 1},
        {1, 0, 1, 0, 1, 0, 0, 0, 0, 1},
        {1, 0, 1, 0, 1, 1, 1, 1, 0, 1},
        {1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};

/*初始化游戏角色*/
struct Man man = {1, 1, 1};

int main()
{
    /*初始化控制台窗口*/
    system("chcp65001"); // 设置编码格式为UTF-8
    system("cls");       // cmd窗口清屏
    HideCursor();        // 隐藏光标

    /*打印地图*/
    for (int i = 0; i < ROW; i++)
    {
        for (int j = 0; j < COL; j++)
        {
            CursorJump(2 * j, i);
            switch (map[i][j])
            {
            case 0: // 打印通道
                printf(" ");
                break;
            case 1: // 打印墙壁
                printf("■");
                break;
            case 2: // 打印目的地
                printf("#'");
                break;
            default:
                break;
            }
        }
    }

    /*打印游戏角色'@'*/
    CursorJump(2 * man.x, man.y);
    printf("@");

    /*游戏逻辑主体*/
    while (1)
    {
        /*获取角色控制指令*/
        int willx = 0;      // 暂存角色移动后的横坐标
        int willy = 0;      // 暂存角色移动后的纵坐标
        char key = getch(); // 读取keycode
        switch (key)
        {
        case UP: // 获取角色上移后的坐标
            willx = man.x;
            willy = man.y - man.v;
            break;
        case DOWN: // 获取角色下移后的坐标
            willx = man.x;
            willy = man.y + man.v;
            break;
        case LEFT: // 获取角色左移后的坐标
            willx = man.x - man.v;
            willy = man.y;
            break;
        case RIGHT: // 获取角色右移后的坐标
            willx = man.x + man.v;
            willy = man.y;
            break;
        default:
            break; // 其他按键则忽略处理
        }

        /*判断交互条件*/
        switch (map[willy][willx])
        {
        case 0: // 在通道中移动
            man.x = willx; // 传递移动后的横坐标
            man.y = willy; // 传递移动后的纵坐标

            printf("\b ");                // 删除角色原来的痕迹
            CursorJump(2 * man.x, man.y); // 把光标移动到角色的目标坐标
            printf("@");                  // 打印角色符号
            break;
        case 1: // 撞墙
            break;
        case 2: // 到达目的地
            CursorJump(2 * COL + 2, 0);
            printf("You win!\n"); // 游戏胜利
            getch(); // 暂停等待,按任意键结束
            return 0;
            break;
        default:
            break;
        }
    }
}

六、聪明的搬运工

不知道大家有没有玩过“推箱子”的游戏,这是在手机和电子字典上较流行的益智游戏,通过在上述走迷宫游戏的基础上稍微改造一下就能实现。

程序依然用数组map来保存地图,数组元素如果为空格则表示什么也没有,’b’表示箱子,’#’表示墙壁,’*’表示目的地,’i ‘表示箱子在目的地。我们以后每推一下箱子,不但要改变屏幕的显示,也要改变map相应元素的值。游戏的主循环依然是接收按键,移动坐标,判断交互条件。当接收一个方向键,需要判断小人前面一格的状态,如果是空地或目的地,则人物可以直接移动;如果是墙壁,则不可移动;如果是箱子或目的地上的箱子,则需要继续判断箱子前面一格的状态:如果前一格是空地或目的地,则人推箱子前进,否则不可移动。最后,再增加一个判断胜利的条件,用数组Des来记录全部目的地的坐标,我们每执行一步操作后,程序就要通过Des数组判断这些目的地上是否都有箱子了。

真棒啊!我们可以做游戏了,而且是一个趣味十足的游戏呢!当然,我们可以通过修改map数组来设计不同的游戏地图,我们还可以相互分享好的游戏地图呢。

尾声

就像学习音乐,不是要等到把全部乐理学完后才演奏一个完整的曲子,而是刚开始学时就有一些简单的曲子进行演奏,让你立刻就有成就感,很快就能卖弄出来在别人面前表现自己。其实,我觉得通过编写游戏来学习编程,把学习变成游戏,不失为学习计算机的一种好方法。

好了,编游戏就这么简单,希望兄弟们也尝试用C语言或其他的语言来做几个自己喜欢的小游戏。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年12月26日
下一篇 2023年12月26日

相关推荐

此站出售,如需请站内私信或者邮箱!