敲上瘾-CSDN博客 控制台程序设置_c语言控制程序窗口大小-CSDN博客 贪吃蛇小游戏_贪吃蛇小游戏csdn-CSDN博客
一、功能实现:
- 玩家1使用↓ → ← ↑按键来操作蛇的方向,使用右Shift键加速,右Ctrl键减速
- 玩家2使用W A S D按键来操作蛇的方向,使用左Alt键加速,C键减速
- 任意玩家点击空格键游戏暂停
- 若其中蛇a吃到蛇b的身体,则蛇a将变成食物,然后蛇a以初始状态进行复活
双人模式的主逻辑和单人模式差不多,就不在赘述,接下来就只讲一些要点,下面是头文件声明
头文件声明
二、食物节点的创建
双人模式相比单人模式需要把地图扩大,增加玩家们的博弈范围,除此之外就是把原来的食物个数增加。使玩家更有体验感。食物是用链表来维护的,所以在食物的创建上我们只需要在链表尾插上节点就行,10个为宜。
三、什么双线程
在写双人模式的时候有一个很要命的问题,就是如何让两条蛇的速度互不影响,因为当初我们是靠Sleep函数来控制速度的,而程序是一条一条逐一执行的,需要等一条蛇的程序执行结束,才轮到另一条蛇执行。这样的话它们的速度必然会互相干扰。在不了解双线程之前这个问题是很让人头疼的,几乎无法被解决。现在我们就来了解一下双线程:
通俗简单地说,双线程就像是一个人同时在做两件事情一样。想象一下,你在厨房里煮面条,同时在客厅里看电视。虽然你只有一双手,但你可以在等待面条煮熟的时候,趁机看一会电视。这样,你的时间就得到了更有效的利用,而不是只等在厨房里。
计算机中,双线程也是类似的。处理器就像是你的大脑,能够同时执行多个任务。有了双线程,处理器可以同时处理两个任务,提高了计算机的效率,让它能够更快地完成工作。
以下是一个简单的Windows下使用C语言创建并运行两个线程的示例代码:
#include <stdio.h>
#include <windows.h>
// 第一个线程函数
DWORD WINAPI Thread1Func(LPVOID lpParam)
{
for (int i = 0; i < 5; i++)
{
printf("Thread 1: Step %d\n", i);
// 线程休眠500毫秒
Sleep(500);
}
return 0;
}
// 第二个线程函数
DWORD WINAPI Thread2Func(LPVOID lpParam)
{
for (int i = 0; i < 5; i++)
{
printf("Thread 2: Step %d\n", i);
// 线程休眠700毫秒
Sleep(700);
}
return 0;
}
int main()
{
// 创建并启动第一个线程
HANDLE thread1 = CreateThread(NULL, 0, Thread1Func, NULL, 0, NULL);
if (thread1 == NULL)
{
printf("Error creating thread 1\n");
return 1;
}
// 创建并启动第二个线程
HANDLE thread2 = CreateThread(NULL, 0, Thread2Func, NULL, 0, NULL);
if (thread2 == NULL)
{
printf("Error creating thread 2\n");
return 1;
}
// 等待两个线程结束
WaitForSingleObject(thread1, INFINITE);
WaitForSingleObject(thread2, INFINITE);
// 关闭线程句柄
CloseHandle(thread1);
CloseHandle(thread2);
return 0;
}
1.首先,我们定义了两个线程函数 Thread1Func() 和 Thread2Func(),它们分别代表了两个不同线程的执行内容。这些函数的返回类型是 DWORD,参数类型是 LPVOID,表示线程函数的标准参数格式。
2.在 Thread1Func() 和 Thread2Func() 中,我们使用一个 for 循环输出线程执行的内容,并使用 printf() 函数进行输出。在每次循环结束后,线程使用 Sleep() 函数进行休眠,模拟一些处理过程。
3.在 main() 函数中,我们使用 CreateThread() 函数创建了两个线程。该函数接受多个参数,其中包括线程的安全属性、栈大小、线程函数、线程函数参数等。CreateThread() 返回一个指向新线程的句柄。
4.使用 WaitForSingleObject() 函数等待两个线程的结束。这样做可以确保主线程等待所有其他线程执行完毕后再继续执行。
5.最后,我们使用 CloseHandle() 函数关闭线程句柄,释放资源。
(1)、临界区
在进行多线程编程中,资源是共享的。
临界区通常用于多线程环境中,以确保对共享资源的互斥访问,防止多个线程同时修改该资源而导致数据不一致或错误。在实际应用中,当有共享资源需要被多个线程访问或修改时,我们通常会使用临界区或其他同步机制来保护这些资源。
如果示例代码中的两个线程需要访问共享资源,那么我们会在主函数中初始化临界区,并在线程函数中使用临界区的相关函数(如EnterCriticalSection() 和LeaveCriticalSection())来保护对共享资源的访问。在这种情况下,临界区的初始化和使用将成为必要的步骤。
(2)、锁
共享资源互斥访问是解决多线程在共用同资源时,导致不确定性的错误行为的一种机制。它可以使用锁来实现。当线程1执行到需要使用资源a时,获取到资源a的锁并给它上锁,那么线程2执行到资源a的时候不能使用,需要等待线程1把锁解开才能使用,并且也同样给资源a上锁。
上锁:EnterCriticalSection()
解锁:LeaveCriticalSection()
这样可以保证在同一时间内只有一个程序或线程对共享资源进行修改或操作,从而避免了竞态条件和数据不一致性的问题。
四、双线程处理
将两条蛇分开为两个线程执行,将两条蛇分成两个线程执行,避免速度的相互干扰。
void GameRun2(pSnake pu1, pSnake pu2)
{
pLSnake pm = (pLSnake)malloc(sizeof(LSnake));
pLSnake psk = pm;
if (!psk)
{
exit(-1);
}
psk->p1 = pu1;
psk->p2 = pu2;
CRITICAL_SECTION cs;
HANDLE thp1 = NULL, thp2 = NULL;
// 初始化临界区
InitializeCriticalSection(&cs);
// 创建线程
thp1 = CreateThread(NULL, 0, th1, (LPVOID)psk, 0, NULL);//玩家1
thp2 = CreateThread(NULL, 0, th2, (LPVOID)psk, 0, NULL);//玩家2
if (!thp1||!thp2)
{
exit(-1);
}
// 等待线程结束
WaitForSingleObject(thp1, INFINITE);
WaitForSingleObject(thp2, INFINITE);
// 销毁临界区
DeleteCriticalSection(&cs);
// 关闭线程句柄
CloseHandle(thp1);
CloseHandle(thp2);
}
thp1(),thp2()的实现主逻辑和单人模式差不多,这里不在细讲。下面主要来解决资源竞争的问题。
首先要思考的就是它们共用那些资源,比如printf函数,SetPos函数。这两个没处理处理好的话会导致在程序执行时打印信息会在屏幕发生错乱。如下:
解决方法也比较简单就是在双线程内每次使用到printf函数和SetPos函数都给它们们上锁,用完后再解锁。并且线程内的所有涉及到这两个函数的位置都需要上锁。
如下:
EnterCriticalSection(&cs);//上锁
SetPos(X2 + 6, 12);//坐标设置
printf("玩家2 蓝蛇");//打印信息
LeaveCriticalSection(&cs);//解锁
注意在上锁和解锁中要把printf函数和SetPos函数放在一起,这样才能保证在准确的坐标位置打印出信息 。
五、撞到对方身体处理
为了增加玩家的体验与单人模式不同的是当玩家撞到自己的时候,我们不设为游戏结束,示为正常行为,而当玩家1撞到玩家2的身体的时候,玩家1将变成食物,并且玩家1将以初始化的状态在随机位置(不完全随机)复活。而我们将任意蛇撞墙做为游戏结束的条件。接下来我们来分析一下玩家的复活。
六、玩家复活
(1)、蛇身变为食物
在玩家复活前自身需要变成食物,这个操作也比较简单,就是做一个链表的连接,需要把维护食物坐标的链表尾连接上维护蛇身的链表的头,再把食物输出。要注意的是因为玩家1的蛇头撞到玩家2才把玩家1置为食物的,所用玩家1的蛇头不能作为食物,在做链表连接的时候需要从头节点的下一位节点开始。
(2)、玩家的随机复活
虽然说是随机的但不是完全随机,还需要考虑以下这些问题:
- 复活位置横坐标必须是偶数
- 复活位置不能是有食物的位置
- 复活位置不能是对方玩家控制的蛇的位置
- 复活位置不能再地图之外
复活位置横坐标为什么必须是偶数?
因为我们打印的蛇身和食物以及地图边界都是宽字符,宽字符占用两个字符空间的大小,我们在前面已经统一把打印的首位置是横坐标为偶数的位置,所以这里同样要设为偶数,否则就会出现一半是食物一半是蛇的身体的情况。
因为考虑到这一点我们在初始化蛇的时候考虑把蛇的复活状态设置为竖直状态的五个节点,也就是蛇的节点的横坐标的是相同的,而只有纵坐标是不同的而且是依次递增的五个节点。那么我们需要做的就是生成两个随机数x和y,作为蛇的尾坐标,然后只让y++得到五个节点,并检查这五个节点是否满足要求。不满足要求需要重新生成随机数x和y再次检查,直到符合要求后把它做成链表进行蛇身的维护。
void Resurrect(pSnake pt,pSnake pn)//玩家复活
{
assert(pt);
pSnakeNode pff = pt->_pFood;
while (pff->next)
{
pff = pff->next;
}
pff->next = pt->_pSnake->next;//蛇身变成食物
PrintFood(pt->_pFood);//打印食物
pt->_pSnake = NULL;//重点!!
int x = 0, y = 0;//复活坐标
reset:
do
{
x = rand() % (X2 - 4) + 2;//2到X2-1
y = rand() % (Y2 - 1) + 1;//1到Y2-1
} while (x % 2 != 0);//复活位置横坐标设为偶数
for (int i = 0; i < 5; i++)
{
y += i;
pSnakeNode ph = pn->_pSnake;
while (ph)//检查复活位置是否与对方玩家相撞
{
if (ph->x == x && ph->y == y)
goto reset;
ph = ph->next;
}
pSnakeNode pfd = pt->_pFood;
while (pfd)//检查复活位置是否是食物
{
if (pfd->x == x && pfd->y == y)
goto reset;
pfd = pfd->next;
}
if ((y <= 0) || (y+12 >= Y2)//检查复活位置是否是地图外
|| (x <= 0) || (x + 12 >= X2 - 2))
goto reset;
}
//检查复活位置合法后做成链表进行维护
pSnakeNode pnew = NULL;
for (int i = 0; i < 5; i++)
{
pnew = (pSnakeNode)malloc(sizeof(SnakeNode));
if (!pnew)
{
exit(-1);
}
pnew->x = x;
pnew->y = y+i;
pnew->next = NULL;
if ((pt->_pSnake) == NULL)
{
pt->_pSnake = pnew;
}
else
{
pnew->next = pt->_pSnake;
pt->_pSnake = pnew;
}
}
pnew = pt->_pSnake;
while (pnew)
{
SetPos(pnew->x, pnew->y);
wprintf(L"%lc", BODY);
pnew = pnew->next;
}
pt->_status = OK;
pt->_food_weight = 10;
pt->_score = 0;
pt->_sleep_time = 200;
pt->_dir = RIGHT;
}