贪吃蛇双人模式设计(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);//玩家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;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

NFT 智能合约实战-快速开始(1)NFT发展历史 | NFT合约标准(ERC-721、ERC-1155和ERC-998)介绍

文章目录 NFT 智能合约实战-快速开始(1)NFT发展历史国内NFT市场国内NFT合规性如何获得NFT?如何查询NFT信息?在 OpenSea 上查看我们的 NFT什么是ERC721NFT合约标准ERC-721、ERC-1155和ERC-998 对比ERC721IERC721.sol 接口内容关于合约需要接收 ERC721 资产 onERC721Received…

使用Leaflet-canvas-label进行个性化标注实践详解

目录 前言 一、leaflet-canvas-label属性 1、地图展示属性 2、Canvas文本标注属性 3、事件列表 二、属性设置实战 1、标注放大比例 2、字体颜色和方向偏移 3、标注文字透明色设置 4、标注显示层级 三、事件绑定 1、颜色改变 2、事件绑定解析 3、标记初始化的一个小…

数据结构(4):串

只需要掌握小题&#xff0c;在考纲中占比不大 1 串的定义 1.1 基本定义 字符串 数据结构三要数&#xff1a;逻辑结构、存储结构、运算 子串必须是连续的&#xff01; 空格也是一个字符&#xff01;每个空格字符占1B 1.2 串和线性表 2 串的基本操作 比值的操作&#xff01;&…

定个小目标之刷LeetCode热题(13)

今天来看看这道题&#xff0c;介绍两种解法 第一种动态规划&#xff0c;代码如下 class Solution {public int maxSubArray(int[] nums) {int pre 0, maxAns nums[0];for (int x : nums) {// 计算当前最大前缀和pre Math.max(pre x, x);// 更新最大前缀和maxAns Math.ma…

入门级的卷积神经网络训练识别手写数字-小白轻松上手-含数据集+pyqt界面

代码下载地址&#xff1a; https://download.csdn.net/download/qq_34904125/89374845 本代码是基于python pytorch环境安装的。 下载本代码后&#xff0c;有个requirement.txt文本&#xff0c;里面介绍了如何安装环境&#xff0c;环境需要自行配置。 或可直接参考下面博文…

arm开发板移植sshd

移植sshd 文章目录 移植sshd1、准备工作2、编译zlib3、编译openssl4、编译openssh5、其他旧版本6、部署测试7、多用户配置8、sshd_config示例 1、准备工作 准备openssh-9.5p1.tar.gz openssl-1.1.1w.tar.gz zlib-1.2.11.tar.gz 我在http://10.45.156.100/IG2100/IG2100.git …

向AI请教如何说不

面对父母的催婚&#xff0c;你可以采取以下几个步骤来进行沟通和表达自己的立场&#xff1a; 理解与尊重&#xff1a;首先&#xff0c;要理解父母催婚背后的关心和期望。他们可能出于对你未来幸福和生活稳定的考虑。表达对他们关心的感激&#xff0c;这有助于建立良好的沟通基础…

SAS:coalescec函数和cmiss函数的应用及拓展

背景&#xff1a;CRF中收集了每个受试者3个RACE方面的信息&#xff0c;SDTM SPEC规定了RACE的生成规则为&#xff1a;若收集了多个RACE&#xff0c;RACE“MULTIPLE”&#xff0c;详细的RACE信息记录在SUPPDM中&#xff1b;若仅收集到一个RACE&#xff0c;则RACE等于RACE1-RACE3…

程序猿大战Python——流程控制——while循环

程序里的循环 目标&#xff1a;了解循环语句的作用。 在程序中&#xff0c;有时候会遇到代码需要重复多次运行的情况。 例如&#xff0c;一起来完成&#xff1a; &#xff08;1&#xff09;在生活中做事没让媳妇儿满意&#xff0c;跟她承认错误&#xff0c;说10遍&#xff1a…

【Linux】进程7——进程地址空间

1.再谈fork 之前提到了fork之后对父子进程返回不同的id值&#xff0c;给父进程返回子进程的pid&#xff0c;给子进程返回0&#xff0c;所以对于一个id如何存储两个值的说法&#xff0c;在我们之前已经提到过了一个概念叫做写时拷贝&#xff0c;就是在子进程要想修改父进程的id…

JavaScript学习|JavaScript 引入方式、JavaScript 基础语法、JavaScript 对象、BOM、DOM、事件监听、事件绑定

JavaScript 能做什么 1.能够改变文本内容 2.能够改变图像的src属性值 3.能够进行表单验证等 JavaScript 引入方式 内部脚本 1.内部脚本:将 JS代码定义在HTML页面中&#xff0c;JavaScript代码必须位于<script>与</script>标签之间。在 HTML 文档中可以在任意地…

三维地图Cesium,加载一个模型,模型沿着给定的一组经纬度路线移动

目录 实现效果 实现思路 功能点 选择移动路线 加载模型和移动路线 重新运行 指定位置(经纬度点)开始移动 视角切换 到站提示 运行 停止 联动接口 完整代码 html js逻辑 trainOperation.js sourceData.js gitee仓库项目代码 疑问解答 实现效果 三维地图Cesiu…

热题系列章节5

169. 多数元素 给定一个大小为 n 的数组&#xff0c;找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1: 输入: [3,2,3] 输出: 3 示例 2: 输入: [2,2,1,1,1,2,2] 输出:…

shell编程(三)—— 运算符

和其他编程语言一样&#xff0c;bash也有多种类型的运算符&#xff0c;本篇对bash的相关运算符做简单介绍。 一、运算符 1.1 算术运算符 常见的算术运算符&#xff0c;如加&#xff08;&#xff09;、减&#xff08;-&#xff09;、乘&#xff08;*&#xff09;、除&#xf…

简单介绍一下vim

简单介绍一下vim 一、vim是什么&#xff1f;二、vim的优点三、vi/vim的使用命令模式输入模式底线命令模式 四、vi/vim 按键说明&#xff08;一&#xff09;命令模式可用的光标移动、复制粘贴、搜索替换等移动光标的方法:搜索替换的方法删除、复制与贴上的方法 &#xff08;二&a…

嵌入式Linux系统编程 — 3.5 utime、utimes、futimens、utimensat函数修改文件时间属性

目录 1 文件的时间属性简介 2 utime()函数 2.1 utime()函数简介 2.2 示例程序 3 utimes()函数 3.1 utimes()函数简介 3.2 示例程序 4 futimens()函数 4.1 futimens()函数简介 4.2 示例程序 5 utimensat()函数 5.1 utimensat()函数简介 5.2 示例程序 1 文件的时间…

如何从SD存储卡恢复已删除的图像

SD卡概述 在日常工作和学习中&#xff0c;SD卡经常被用作许多设备上的存储设备&#xff0c;例如数码相机&#xff0c;手机&#xff0c;摄像机&#xff0c;行车记录仪以及监控设备&#xff0c;用于为我们存储图像&#xff0c;视频&#xff0c;文档&#xff0c;音频和其他数据。…

html5实现个人网站源码

文章目录 1.设计来源1.1 网站首页页面1.2 个人工具页面1.3 个人日志页面1.4 个人相册页面1.5 给我留言页面 2.效果和源码2.1 动态效果2.2 目录结构 源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/139564407 ht…

12. Django 第三方功能应用

12. 第三方功能应用 因为Django具有很强的可扩展性, 所以延伸了第三方功能应用. 通过本章的学习, 读者能够在网站开发过程中快速实现API接口开发, 验证码生成与使用, 站内搜索引擎, 第三方网站实现用户注册, 异步任务和定时任务, 即时通信等功能.12.1 Django Rest Framework框…

【YOLOv5进阶】——修改网络结构(以C2f模块为例)

一、站在巨人的肩膀上 这里我们借鉴YOLOv8源码&#xff1a; 上期说到&#xff0c;对于网络模块定义详情在common.py这个文件&#xff0c;如Conv、CrossConv、C3f等。本期要修改的需要参考YOLOv8里的C2f模块&#xff0c;它定义在YOLOv8的module文件夹的block.py文件里&#xf…