贪吃蛇双人模式设计(2)

敲上瘾-CSDN博客
控制台程序设置_c语言控制程序窗口大小-CSDN博客
贪吃蛇小游戏_贪吃蛇小游戏csdn-CSDN博客

一、功能实现:

  1. 玩家1使用↓ → ← ↑按键来操作蛇的方向,使用右Shift键加速,右Ctrl键减速
  2. 玩家2使用W  A  S  D按键来操作蛇的方向,使用左Alt键加速,C键减速
  3. 任意玩家点击空格键游戏暂停
  4. 若其中蛇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);//玩家1thp2 = CreateThread(NULL, 0, th2, (LPVOID)psk, 0, NULL);//玩家2if (!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-1y = 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/348780.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络编程(三)UDP TFTP协议

文章目录 一、 UDP&#xff08;一&#xff09;概述&#xff08;二&#xff09;流程 二、收发函数&#xff08;一&#xff09;recvfrom&#xff08;二&#xff09;sendto 三、实现一个简单的udp服务端和客户端四、实现tftp客户端协议 一、 UDP &#xff08;一&#xff09;概述 …

shell条件测试和if语句

“C:\Users\Jessica\Desktop\6.12.assets\image-20240612094159578.png”# shell条件测试和if语句 一、条件测试 1、$? 返回码 判断命令或者脚本是否执行成功&#xff08;最近的一条&#xff09; 0 true 为真就是成功 成立 非0 false 失败或者异常 2、test命令 可以进行…

Docker 镜像源更换

实现 替换docker 镜像源 前提要求 安装 docker docker-compose 参考创建一键更换docker国内镜像源 Docker 镜像代理DaoCloud 镜像站百度云 https://mirror.baidubce.com南京大学镜像站

【Qt 学习笔记】Qt窗口 | 对话框 | 创建自定义对话框

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt窗口 | 对话框 | 创建自定义对话框 文章编号&#xff1a;Qt 学习笔记…

opencv 通过滑动条调整阈值处理、边缘检测、轮廓检测、模糊、色调调整和对比度增强参数 并实时预览效果

使用PySimpleGUI库创建了一个图形用户界面(GUI),用于实时处理来自OpenCV摄像头的图像。它允许用户应用不同的图像处理效果,如阈值处理、边缘检测、轮廓检测、模糊、色调调整和对比度增强。用户可以通过滑动条调整相关参数。 完整代码在文章最后,可以运行已经测试; 代码的…

怎么修改Visual Studio Code中现在github账号

git config --global user.name “你的用户名” git config --global user.email “你的邮箱” git config --global --list git push -u origin your_branch_name git remote add origin

又一个对标Sora的AI视频工具,Dream Machine,开始免费试用

新的AI视频工具又又来了-Dream Machine&#xff0c;开始免费试用。 地址&#xff1a;https://lumalabs.ai/dream-machine 该工具需要科学上网 先看一下官网的宣传视频 luma AI 发布 Dream Machine 我生成了几个视频&#xff0c;效果还可以 生成视频很简单 只需要输入描述就可…

KUKA机器人KRC5控制柜面板LED显示

对于KUKA机器人新系列控制柜KRC5控制柜来说&#xff0c;其控制柜面板LED布局如下图&#xff1a; 其中①②③④分别为&#xff1a; 1、机器人控制柜处于不同状态时&#xff0c;LED显示如下&#xff1a; 2、机器人控制柜正在运行时&#xff1a; 3、机器人控制柜运行时出现的故障…

编写函数isprime(int a),用来判断自变量a是否为素数,若是素数,函数返回整数1,否则返回0

int main() {int isprime(int x);int x;printf("请输入一个数\n");scanf("%d", &x);if (isprime(x)){printf("%d是素数\n",x);}else{printf("%d不是素数\n",x);} } int isprime(int a) {int i;for (i 2; i < a / 2; i){if (a%…

Linux_应用篇(17) FrameBuffer 应用编程

本章学习 Linux 下的 Framebuffer 应用编程&#xff0c; 通过对本章内容的学习&#xff0c; 大家将会了解到 Framebuffer 设备究竟是什么&#xff1f;以及如何编写应用程序来操控 FrameBuffer 设备。 本章将会讨论如下主题。 ⚫ 什么是 Framebuffer 设备&#xff1f; ⚫ LCD 显…

MySQL系列-语法说明以及基本操作(二)

1、MySQL数据表的约束 1.1、MySQL主键 “主键&#xff08;PRIMARY KEY&#xff09;”的完整称呼是“主键约束”。 MySQL 主键约束是一个列或者列的组合&#xff0c;其值能唯一地标识表中的每一行。这样的一列或多列称为表的主键&#xff0c;通过它可以强制表的实体完整性。 …

辽宁普通测径仪升级智能测径仪后都有哪些改进?

关键字: 普通测径仪, 智能测径仪, 测径仪升级, 测径仪特点, 智能测径仪优势, 目前多数厂家测径仪的数据处理方式是单片机计算出最终结果&#xff0c;然后传输到工控机后期处理。这样的电路系统对轧钢现场的高温、高粉尘和强电磁干扰的环境适应性很差&#xff0c;使得同一厂家、…

算法:模拟题目练习

目录 题目一&#xff1a;替换所有的问号 题目二&#xff1a;提莫攻击 题目三&#xff1a;N字形变换 题目四&#xff1a;外观数列 题目五&#xff1a;数青蛙 首先先解释一下模拟算法是什么&#xff0c;其实模拟算法就是题目让我们干什么我们就干什么&#xff0c;思路比较简…

如何对stm32查看IO功能。

有些同学对于别人的开发板的资源&#xff0c;或者IO口&#xff0c;或者串口等资源不知道怎么分配。 方法1、看硬石、野火、正点原子的开发板&#xff0c;看下他们的例子&#xff0c;那个资源用什么。自己多看几个原理图&#xff0c;多看几个视频&#xff0c;做一下笔记。以后依…

【iOS】JSONModel源码阅读笔记

文章目录 前言一、JSONModel使用二、JSONModel其他方法转换属性名称 三、源码分析- (instancetype)initWithDictionary:(NSDictionary*)dict error:(NSError **)err[self init]__setup____inspectProperties - (BOOL)__doesDictionary:(NSDictionary*)dict matchModelWithKeyMa…

ubuntu软件安装

目录 更新Ubuntu软件下载地址 1. 寻找国内镜像源 2. 备份Ubuntu默认的源地址 3. 更新源服务器列表 4. 更新源 更新Ubuntu软件下载地址 1. 寻找国内镜像源 所谓的镜像源&#xff1a;可以理解为提供下载软件的地⽅&#xff0c;⽐如 Android ⼿机上可以下载软件的 91 ⼿机助…

【快速上手】Win11家庭版升级专业版的3种方法!

在Win11电脑操作中&#xff0c;用户使用的是家庭版系统&#xff0c;现在用户想把家庭版升级为专业版&#xff0c;但不知道具体要怎么操作才能完成版本的升级操作&#xff1f;接下来小编介绍三种简单快速的方法&#xff0c;帮助大家轻松将Win11电脑系统升级为专业版本。 方法 1&…

WPF/C#:异常处理

什么是异常&#xff1f; 在C#中&#xff0c;异常是在程序执行过程中发生的特殊情况&#xff0c;例如尝试除以零、访问不存在的文件、网络连接中断等。这些情况会中断程序的正常流程。 当C#程序中发生这种特殊情况时&#xff0c;会创建一个异常对象并将其抛出。这个异常对象包…

【纯血鸿蒙】——响应式布局如何实现?

前面介绍了自适应布局&#xff0c;但是将窗口尺寸变化较大时&#xff0c;仅仅依靠自适应布局可能出现图片异常放大或页面内容稀疏、留白过多等问题。此时就需要借助响应式布局能力调整页面结构。 响应式布局 响应式布局是指页面内的元素可以根据特定的特征&#xff08;如窗口…

js实现一个数据结构——栈

栈的概念就不再赘述&#xff0c;无可厚非的先进后出&#xff0c;而JS又是高级语言&#xff0c;数组中的方法十分丰富&#xff0c;已经自带了push pop方法进行入栈出栈的操作。 1.基本实现 class Stack {constructor() {this.items [];}// 入栈push(item) {this.items.push(i…