【链表的应用】C语言实现贪吃蛇小游戏

在开始游戏之前先介绍几个游戏中需要用到的Windows API函数:

BOOL SetConsoleCursorPosition( 
HANDLE hConsoleOutput,  // handle to screen buffer  
COORD dwCursorPosition  // new cursor coordinates 
);

设置标准输出上光标的位置为pos

HANDLE GetStdHandle(  
DWORD nStdHandle   // input, output, or error device 
);

获取标准输出的句句柄(⽤用来标识不不同设备的数值)
使⽤用范例

void SetPos(int x, int y) 
{ 
    COORD pos = { 0}; //用于存储坐标(xy)的结构体
    HANDLE hOutput = NULL; 
    pos.X = x; 
    pos.Y = y; 
    hOutput = GetStdHandle(STD_OUTPUT_HANDLE); // 获取标准输出的句句柄 ( ⽤用来标识不不同设备的数值 ) 
    SetConsoleCursorPosition(hOutput, pos); // 设置标准输出上光标的位置为 pos 
}

GetAsyncKeyState():是一个⽤用来判断函数调⽤用时指定虚拟键的状态,确定用户当前是否按下了了键盘上的一个键的函数

开始分析游戏的整体结构:

首先蛇身用一个链表来实现,蛇身的每一个片段都是一个结点,当蛇吃到了食物就相当于给链表头插了一个结点

typedef struct SnakeNode
{
    int x;
    int y;
    struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

整个游戏的维护也用一个结构体来维护

enum Direction
{
    UP=1,
    DOWN,
    LEFT,
    RIGHT
};

enum GameStatus
{
    OK,
    NORMAL_END,
    KILL_BY_WALL,
    KILL_BY_SELF
};

typedef struct Snake
{
    pSnakeNode _pSnake;     //用于维护头结点
    pSnakeNode _pFood;      //维护食物
    int _TotalScore;        //游戏总得分
    int _AddScore;          //当前食物的增加分数
    int _SleepTime;         //游戏睡眠时间,用于调节蛇的移动速度
    enum Direction _Dir;    //蛇的方向状态
    enum GameStatus _Status;//游戏的状态
}Snake,*pSnake;

整个游戏分为三个阶段:

    GameStart();//游戏准备阶段
    GameRun();  //玩游戏阶段
    GameEnd(;   //游戏结束

GameStart()

游戏准备阶段又可以分为以下几个阶段

void GameStart()
{
    WelcomeToGame(); //欢迎界面的打印
    CreateMap();     //地图的打印
    InitSnake(ps);   //蛇的初始化创建以及打印
    CreateFood(ps);  //食物的创建以及打印
}
WelcomeToGame()

由于游戏中需要频繁的对光标进行定位,所以我们需要使用上面所提到的函数

void SetPos(int x, int y)
{
    COORD pos = {0};
    HANDLE handle = NULL;
    pos.X = x;
    pos.Y = y;

    handle = GetStdHandle(STD_OUTPUT_HANDLE);

    SetConsoleCursorPosition(handle, pos);
}

计算机屏幕的布局按以下方式进行划分,所以利用SetPos()可以将光标定位在指定位置,然后作相应的打印操作,这点在地图的打印中会明显体现
这里写图片描述

//欢迎界面的打印
void WelcomeToGame()
{
    system("mode con cols=100 lines=30");//改变控制台窗口大小
    SetPos(38, 14);
    printf("欢迎来到贪吃蛇小游戏\n");
    SetPos(40, 28);
    system("pause");//需要分屏打印,按任意键打印下一页界面
    system("cls");//清屏

    SetPos(33, 14);
    printf("使用↑、↓、←、→键来控制蛇的的移动\n");
    SetPos(35, 15);
    printf("F1可以加快蛇的移动速度,F2可减速\n");
    SetPos(38, 16);
    printf("提示:速度越快得分越高\n");

    SetPos(40, 28);

    system("pause");
    system("cls");
}

这里写图片描述

CreateMap()

墙体的打印是把墙分成上、下、左、右四部分分开打印,每一部分借助循环和SetPos()多次定位,在每一次定位时打印一个事先定义好的图案,作为墙体#define WALL "■"

void CreateMap()
{
    int i = 0;
    //上边界
    for (i=0; i<=58; i+=2)
    {
        SetPos(i, 0);
        printf(WALL);
    }
    //下边界
    for (i=0; i<=58; i+=2)
    {
        SetPos(i, 29);
        printf(WALL);
    }
    //左边界
    for (i=1; i<=28; i++)
    {
        SetPos(0, i);
        printf(WALL);
    }
    //右边界
    for (i=1; i<=28; i++)
    {
        SetPos(58, i);
        printf(WALL);
    }
}
InitSnake()

蛇在这里的创建是使用一个带头结点的链表来维护的,因为需要频繁创建结点,所以我们先定义一个函数来用于次操作

pSnakeNode BuyNode()
{
    pSnakeNode NewNode = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (NULL == NewNode)
    {
        perror("BuyNode::malloc()");
        exit(EXIT_FAILURE);
    }

    NewNode->x = 0;
    NewNode->y = 0;
    NewNode->next = NULL;

    return NewNode;
}

蛇身的创建是创建出一个有五个结点的链表,第一个结点为头结点
实现定义了一个初始地址,用于定位蛇的最开始位置#define INIT_X 20
#define INIT_Y 10

void InitSnake(pSnake ps)
{
    pSnakeNode first = BuyNode();
    pSnakeNode cur = NULL;
    int i = 0;

    first->x = INIT_X;
    first->y = INIT_Y;

    for (i=0; i<4; i++)
    {
        cur = BuyNode();
        cur->x = first->x + 2;
        cur->y = first->y;
        cur->next = first;
        first = cur;
    }
    cur = first;
    while (cur)
    {
        SetPos(cur->x, cur->y);
        printf(FOOD);//初始化创建完后循环打印整条蛇
        cur = cur->next;
    }
    printf("\n");

    ps->_pSnake = first;//将整条蛇交给 ps->_pSnake 维护,即将头结点地址传过去
}
CreateFood()

食物的打印也是通过创建结点的方式来实现的,在地图内部,借助rand()创建两个随机值用于食物的坐标,然后在指定位置创建结点并进行打印

void CreateFood(pSnake ps)
{
    pSnakeNode FoodNode = BuyNode();
    FoodNode->y = rand()%28 + 1;
    do
    {
        FoodNode->x = rand()%55 + 2;
    }while (FoodNode->x % 2 != 0 );

    SetPos(FoodNode->x, FoodNode->y);
    printf(FOOD);

    ps->_pFood = FoodNode;//将食物传给ps维护
}

这里写图片描述

GameRun()

玩游戏的整体框架使用一个循环来完成,只要游戏的状态为OK则继续游戏,游戏状态发生变化,在GameEnd()中做相应提示,退出游戏

void GameRun(pSnake ps)
{
    do
    {
        if (GetAsyncKeyState(VK_UP) && ps->_Dir != DOWN)
        {
            ps->_Dir = UP;
        }
        else if (GetAsyncKeyState(VK_DOWN) && ps->_Dir != UP)
        {
            ps->_Dir = DOWN;
        }
        else if (GetAsyncKeyState(VK_LEFT) && ps->_Dir != RIGHT)
        {
            ps->_Dir = LEFT;
        }
        else if (GetAsyncKeyState(VK_RIGHT) && ps->_Dir != LEFT)
        {
            ps->_Dir = RIGHT;
        }
        else if (GetAsyncKeyState(VK_SPACE))
        {
            Pause();
        }
        else if (GetAsyncKeyState(VK_ESCAPE))
        {
            ps->_Status = NORMAL_END;
        }
        else if (GetAsyncKeyState(VK_F1))
        {
            if (ps->_SleepTime >= 40)
            {
                ps->_AddScore += 2;
                ps->_SleepTime -= 20;
            }

        }
        else if (GetAsyncKeyState(VK_F2))
        {
            if (ps->_SleepTime <= 360 )
            {
                if (ps->_SleepTime <= 260)
                {
                    ps->_AddScore -= 2;
                    ps->_SleepTime += 20;
                }
                else
                {
                    ps->_AddScore = 1;
                    ps->_SleepTime += 20;
                }
            }
        }
        Sleep(ps->_SleepTime);
        SnakeMove(ps);
        KillByWall(ps);
        KillBySelf(ps);
    }while (ps->_Status == OK);
}

循环中用GetAsyncKeyState()检测当前键盘按下的键
因为是蛇头走在最前面,用于改变方向,蛇不能立即反向,所以还要检测蛇的当前方向状态

  • 检测到VK_UP且当前状态不为DOWN:ps->_Dir = UP

  • 检测到VK_DOWN且当前状态不为UP:ps->_Dir = DOWN

  • 检测到VK_LEFT且当前状态不为RIGHT:ps->_Dir = LEFT

  • 检测到VK_RIGHT且当前状态不为LEFT:ps->_Dir = RIGHT

  • 检测到VK_ESCAPE:ps->_Status = NORMAL_END

  • 检测到VK_SPACE:游戏暂停(有一个死循环让游戏一直处于睡眠状态,当接下来再检测到VK_SPACE,跳出循环游戏继续)

void Pause()
{
    while (1)
    {
        Sleep(100);
        if (GetAsyncKeyState(VK_SPACE))
        {
            break;
        }
    }
}
  • 检测到VK_F1:游戏加速(睡眠时间缩短,食物得分增加)

  • 检测到VK_F2:游戏减速(睡眠时间增长,食物得分减少)

SnakeMove()

根据蛇当前的方向状态,做出不同的方向处理
我们把蛇每走一步都当作是一次链表的头插,如果下一个结点的位置刚好是食物,那我我们就新插入结点打印,即蛇身增长以截儿,如果下一个位置不是食物,依然打印新插入结点,但将蛇尾的结点先用空格打印后释放掉,相当于是蛇向前走了一步

void SnakeMove(pSnake ps)
{
    HelpInfor(ps);
    switch(ps->_Dir)
    {
    case UP:
        {
            pSnakeNode pNextNode = BuyNode();
            pNextNode->x = ps->_pSnake->x;
            pNextNode->y = ps->_pSnake->y - 1;
            if (IsFood(ps, pNextNode))
            {
                EatFood(ps, pNextNode);
            }
            else
            {
                EatSpace(ps, pNextNode);
            }
        }
        break;
    case DOWN:
        {
            pSnakeNode pNextNode = BuyNode();
            pNextNode->x = ps->_pSnake->x;
            pNextNode->y = ps->_pSnake->y + 1;
            if (IsFood(ps, pNextNode))
            {
                EatFood(ps, pNextNode);
            }
            else
            {
                EatSpace(ps, pNextNode);
            }
        }
        break;
    case LEFT:
        {
            pSnakeNode pNextNode = BuyNode();
            pNextNode->x = ps->_pSnake->x - 2;
            pNextNode->y = ps->_pSnake->y;
            if (IsFood(ps, pNextNode))
            {
                EatFood(ps, pNextNode);
            }
            else
            {
                EatSpace(ps, pNextNode);
            }
        }
        break;
    case RIGHT:
        {
            pSnakeNode pNextNode = BuyNode();
            pNextNode->x = ps->_pSnake->x + 2;
            pNextNode->y = ps->_pSnake->y;
            if (IsFood(ps, pNextNode))
            {
                EatFood(ps, pNextNode);
            }
            else
            {
                EatSpace(ps, pNextNode);
            }
        }
        break;
    default:
        break;
    }
}
帮助菜单的打印
void HelpInfor(pSnake ps)
{
    SetPos(69,7);
    printf("游戏的总得分为:%d ", ps->_TotalScore);
    SetPos(66,8);
    printf("当前游戏的食物得分为:%d ", ps->_AddScore);

    SetPos(62,12);
    printf("使用↑、↓、←、→键来控制蛇的的移动\n");
    SetPos(64,13);
    printf("F1可以加快蛇的移动速度,F2可减速\n");
    SetPos(69,14);
    printf("按下SPACE可以暂停游戏\n");
    SetPos(70,15);
    printf("按下ESC可以退出游戏\n");

}
判断下一个位置是否为食物
int IsFood(pSnake ps, pSnakeNode pn)
{
    return ((ps->_pFood->x == pn->x) && (ps->_pFood->y == pn->y));
}
如果下一个位置不是食物
void EatSpace(pSnake ps, pSnakeNode pn)
{
    pSnakeNode cur = NULL;
    pn->next = ps->_pSnake;
    ps->_pSnake = pn;

    cur = ps->_pSnake;

    SetPos(cur->x, cur->y);
    printf(FOOD);//打印新结点

    //找到最后一个结点的位置
    while (cur->next->next)
    {
        cur = cur->next;
    }

    SetPos(cur->next->x, cur->next->y);
    printf(" ");//用空格覆盖最后一个结点

    free(cur->next);//释放最后一个结点
    cur->next = NULL;
}
如果下一个位置是食物
void EatFood(pSnake ps, pSnakeNode pn)
{
    pSnakeNode cur = NULL;
    pn->next = ps->_pSnake;
    ps->_pSnake = pn;

    cur = ps->_pSnake;


    SetPos(cur->x, cur->y);
    printf(FOOD);
    cur = cur->next;

    ps->_TotalScore += ps->_AddScore;
    CreateFood(ps);//食物被吃后创建食物
}
KillByWall()

检测蛇头是否与墙重合,若重合,改变游戏的当前状态

void KillByWall(pSnake ps)
{
    if (ps->_pSnake->x == 0
        || ps->_pSnake->x == 58
        || ps->_pSnake->y == 0
        || ps->_pSnake->y == 29)
    {
        ps->_Status = KILL_BY_WALL;
    }
}
KillBySelf()

检测蛇头是否与蛇身任意结点重合(遍历整个结点与蛇头结点比较),若重合,改变当前游戏状态

void KillBySelf(pSnake ps)
{
    pSnakeNode cur = NULL;
    cur = ps->_pSnake->next;

    while (cur)
    {
        if ((ps->_pSnake->x == cur->x) && (ps->_pSnake->y == cur->y))
        {
            ps->_Status = KILL_BY_SELF;
        }
        cur = cur->next;
    }
}

GameEnd()

在本函数内先对游戏结束状态做出相应的提示
所有的结点(蛇身和食物)都是通过同台内存开辟出来的,所以在程序结束之前必须要对动态开辟出的内存进行释放,避免内存泄漏

void GameEnd(pSnake ps)
{
    pSnakeNode del = NULL;
    if (ps->_Status == NORMAL_END)
    {
        SetPos(23,14);
        printf("退出游戏\n");
        SetPos(70, 28);
        return;
    }
    else if (ps->_Status == KILL_BY_WALL)
    {
        SetPos(23,14);
        printf("很不幸!你撞墙而亡\n");
    }
    else if (ps->_Status == KILL_BY_SELF)
    {
        SetPos(23,14);
        printf("很不幸!你追尾了\n");
    }
    SetPos(70, 28);
    while (ps->_pSnake)
    {
        del = ps->_pSnake;
        ps->_pSnake = ps->_pSnake->next;
        free(del);
        del = NULL;
    }

    ps->_pSnake = NULL;//ps->_pSnake置空,避免访问已经释放的堆空间

    free(ps->_pFood);//释放未来得及吃的食物
    ps->_pFood = NULL;
}

这里写图片描述

这里写图片描述

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值