首页 > 其他分享 >冰球游戏

冰球游戏

时间:2024-07-04 20:56:45浏览次数:15  
标签:rotate 游戏 puck paddle 冰球 绘制

冰球游戏

游戏灵感和游戏规则

冰球游戏是我和朋友去电玩城,玩了几盘冰球后,我突发奇想要在电脑上模拟的一款游戏。

图:在电玩城打冰球

我先玩了几局游戏,掌握游戏的玩法和规则。

这个网站的冰球游戏给了我最初的灵感:

https://www.silvergames.com/en/air-hockey

而Youtube博主 javidx9Code it Yourself 系列游戏教程视频,给了我具体实现的启发
https://www.youtube.com/watch?v=8OK8_tHeCIA&list=PLrOv9FMX8xJE8NgepZR1etrsU63fDDGxO

b站也有搬运他的视频:
https://www.bilibili.com/video/BV1ET421171Z?p=12

他的开源引擎 olcPixelGameEngine ,给了我实现的代码基础

https://github.com/OneLoneCoder/olcPixelGameEngine

我也将把自己写的游戏开源,欢迎大家游玩和评论

https://github.com/wangqqiyue/Games/tree/master/ice_hockey

游戏分析


图:游戏元素分析

这个冰球游戏的几个元素:

  1. 场地:分为己方半场和对方半场,球场有两个球门,球进了对方球门就得分

  2. 球拍:双方都用球拍来击球

  3. 冰球:全场一个冰球,双方可以用球拍击打冰球,从而攻破对方球门,每局结束冰球都会重置

我就创建了4个类

  1. Field类,场地类,绘制场地

  2. Paddle类,球拍类,绘制球拍,控制球拍运动

  3. Puck类,冰球类,绘制球、控制球体运动

  4. IceHockey类,冰球游戏类,作为游戏整体的控制类,用于初始化游戏对象、控制游戏帧率、管理游戏比分、绘制背景动画、播放音乐等

碰撞模拟

这个游戏主要涉及的物理技术就是运动的球体间相互碰撞的模拟
所谓碰撞:指的是物体A以一定的速度和质量,在一定角度与具有一定速度和质量的物体B发生碰撞,从而产生二者运动状态的同时改变。

图:两个球体碰撞前后的状态变化

图:冲量碰撞公式

这个公式是从书上找到,参考了《游戏开发物理学》

图:《游戏开发物理学》

碰撞响应代码

转成实际代码就是:


void IceHockey::CollisionResponse(Paddle& paddle) {

	olc::vf2d vPaddle = paddle.v;
	olc::vf2d vRelative = puck.velocity - vPaddle;
	olc::vf2d vDis = puck.position - paddle.pos;
	olc::vf2d vNormal = vDis.norm();//碰撞垂向量
	float vRn = vRelative.dot(vNormal);
	float dis = vDis.mag();
	float sumR = paddle.outerR + puck.radius;
	//距离小于两者半径和,且相对移动速度为正
	if (dis < sumR && vRn<0.0f) {

		float j = -2.0f * vRn / vNormal.dot(vNormal);
		j /= (1.0f / paddle.mass + 1.0f / puck.mass);
		puck.velocity += j * vNormal * 1.0f / puck.mass;
		paddle.v -= j * vNormal * 1.0f / paddle.mass;

		PlaySound(NULL, 0, 0);//先停止其他声音
		PlaySound(bound_sound_file, NULL, SND_FILENAME | SND_ASYNC);

		//调整位置
		paddle.pos -= (sumR-dis)*vDis.norm();

	}

}

引入游戏AI

另外为了增强游戏可玩性,我还增加了一个AiPaddle类,即Ai球拍,它继承自Paddle类
它除了Paddle类的绘制功能外,还可以自行分析局势,自行移动以击打冰球

GIF:引入游戏AI后的表现

我采用的策略很简单,就是把AI的状态分成防御和进攻
AI根据冰球当前的位置来决定是防御还是进攻

  1. 如果冰球在对方半场: 防御
  2. 如果冰球在己方半场:进攻
    防御和进攻的具体策略可以看下面的代码

void IceHockey::AiResponseStrong(AiPaddle& paddle) {
	paddle.speedEasy = SPEED_MAX / 10;
	paddle.speedNormal = SPEED_MAX / 5;
	paddle.speedHard = SPEED_MAX/2;
	olc::vf2d nMove = { 0.0f,0.0f };
	olc::vf2d pCenter= { ScreenWidth() / 2.0f,ScreenHeight() / 2.0f };
	float disX = paddle.pos.x - puck.position.x;
	//默认方向是朝向球的方向
	nMove = (puck.position - paddle.pos).norm();
	srand(time(NULL));
	//如果球在对方半场
	if ((paddle.side == LEFT && puck.position.x -puck.radius > ScreenWidth()/2.0f) || (paddle.side==RIGHT  && puck.position.x+puck.radius < ScreenWidth() / 2.0f)) {
		//防守策略
		olc::vf2d pIntercept = (puck.position + paddle.posGoal ) /2.0f;
		if ((pIntercept - paddle.pos).mag() < 2*paddle.outerR) {
			return;
		}
		nMove = (pIntercept-paddle.pos).norm();
		nMove *= {0.9f + 0.2f * rand() / RAND_MAX, 0.9f + 0.2f * rand() / RAND_MAX};
		paddle.v = paddle.speedEasy * nMove;
		
		return;
	}

	//球在己方半场
	
	//进攻策略,当球未越过球拍时采用
	if ((paddle.side == LEFT && disX <= 0) || (paddle.side == RIGHT && disX >= 0)) {
		//如果球拍到球的和敌方球门的夹角较小,则大力进攻
		if ((puck.position - paddle.pos).norm().dot((paddle.posEnemyGoal - paddle.pos).norm()) > 0.7f) {
			if ((puck.position - paddle.pos).mag() <=  1.2f*(paddle.outerR + puck.radius)) {
				nMove = (paddle.posEnemyGoal - paddle.pos).norm();
				nMove *= 2.0f;
			}
			else {
				nMove = (puck.position - paddle.pos).norm();
			}
		}
		//绕道球后面
		else  {
			nMove = (puck.position - paddle.posEnemyGoal).norm() + (puck.position - paddle.pos).norm();
		}
		
		nMove *= {0.9f + 0.2f * rand() / RAND_MAX, 0.9f + 0.2f * rand() / RAND_MAX};
		paddle.v = paddle.speedNormal * nMove;
		//cout << "I'm attacking." << endl;
		return;
	}

	//防守策略,当球已越过球拍时采用
	olc::vf2d pIntercept = paddle.posGoal;//拦截点位置
	//cout << "I'm defensing." << endl;
	//如果球拍击球会导致球进入自己球门,则迂回绕开
	if (puck.velocity.mag()>paddle.speedEasy) {
		//球拍去拦截球
		nMove = (pIntercept - paddle.pos).norm();
	}
	else {
		nMove = (puck.position - paddle.pos).norm();
	}
	nMove *= {0.6f+0.8f*rand()/RAND_MAX, 0.6f+0.8f* rand() / RAND_MAX};
	paddle.v = paddle.speedEasy * nMove;
	return;
}

其他技术

为了让游戏更有趣,我还增加了背景音乐,背景图

背景音乐

背景音乐的播放用到了PlaySound函数,需要引入头文件<windows.h><mmsystem.h>
还要引入winmm库, `#pragma comment(lib,"winmm.lib")
PlaySound的用法, 可以参考微软文档
https://learn.microsoft.com/zh-cn/windows/win32/multimedia/the-playsound-function

背景图

我在使用 PixelGameEngine 时,发现 Sprite 绘制会让帧率大幅降低
于是查询 PixelGameEngine 的教程,发现还可以用 Decal 绘制图片
Decal 绘制用的是GPU资源,绘制起来很快

// Sprites live in RAM and are accessed and manipulated by the CPU. 
	//DrawSprite(0, 0, bgSprite.get());
	//A decal is a sprite that lives on the GPU
	// The GPU will draw the decal on top of whatever was drawn by the CPU first. 
	//SetDrawTarget(bgSprite.get());
	SetDecalMode(olc::DecalMode::MULTIPLICATIVE);
	DrawDecal({ 10,10 }, bgDecal.get());

GPU绘制Decal,会默认绘制在最上层
而我希望背景是在下层的,不希望遮住场地上的冰球和球拍等物体,但是目前还找不到解决办法
临时解决方案:在绘制Decal之前,SetDecalMode 把绘制模式设为 MULTIPLICATIVE
MULTIPLICATIVE模式指的是两个像素点绘制到一个位置时,最终颜色的处理方法,是把两种像素颜色和ALPHA值叠加

中点椭圆算法

在绘制球场时,我希望尽可能还原真实球场的比例和外表
我查询了真实球场,发现人家有一段弧形的围栏

图:真实球场比例
而我一开始绘制的,只有直角边,且PixelGameEngine自带的Draw函数里,也没有椭圆/圆弧的绘制函数
所以我只能自己查找资料实现弧线边框的绘制

图:中点椭圆算法公式

画椭圆函数DrawEllipse

//该函数使用当前画线样式绘制无填充的椭圆
	void PixelGameEngine::DrawEllipse(int32_t left, int32_t top, int32_t right, int32_t bottom, Pixel p) {
		/*left 椭圆外切矩形的左上角 x 坐标。
		top椭圆外切矩形的左上角 y 坐标。
		right椭圆外切矩形的右下角 x 坐标。
		bottom椭圆外切矩形的右下角 y 坐标。*/
		int a = (right - left) / 2;
		int b = (bottom-top) / 2;
		if (a <= 0 || b <= 0) {
			//非法值
			return;
		}
		int centerX = (left + right) / 2;
		int centerY = (top + bottom) / 2;
		int x, y;
		float d1, d2;
		x = 0;
		y = b;
		Draw(centerX+x, centerY+y, p);
		Draw(centerX - x, centerY + y, p);
		Draw(centerX + x, centerY - y, p);
		Draw(centerX - x, centerY - y, p);

		d1 = b * b - a * a * b + a * a / 4;//b^2-a^2b+a^2/4
		/*Region 1*/
		while (a * a * (y - 0.5f) > b * b * (x + 1)) {//a^2(y-1/2) > b^2(x+1)
			if (d1 < 0) {
				d1 += b * b * (2 * x + 3);//b^2(2x+3)
			}
			else {
				d1 += b * b * (2 * x + 3) + a * a * (-2 * y + 2);//b^2(2x+3)+a^2(-2y+2)
				y--;
			}
			x++;
			Draw(centerX + x, centerY + y, p);
			Draw(centerX - x, centerY + y, p);
			Draw(centerX + x, centerY - y, p);
			Draw(centerX - x, centerY - y, p);
		}
		//d2 = b^2(x+1/2)^2 + a^2(y-1)^2 -a^2b^2
		d2 = b * b * (x + 0.5f) * (x + 0.5f) + a * a * (y - 1) * (y - 1) - a * a * b * b;
		/*Region 2*/
		while (y > 0) {
			if (d2 < 0) {
				d2 += b * b * (2 * x + 2) + a * a * (-2 * y + 3);//b^2(2x+2) + a^2(-2y+3)
				x++;
			}
			else {
				d2 += a * a * (-2 * y + 3);//a^2(-2y+3)
			}
			y--;
			Draw(centerX + x, centerY + y, p);
			Draw(centerX - x, centerY + y, p);
			Draw(centerX + x, centerY - y, p);
			Draw(centerX - x, centerY - y, p);
		}
	}

画弧线函数DrawArc


//start提供x坐标,end 提供y坐标,d代表绘制的弧线在start-end连线的什么方向
void Field::DrawArc(olc::vf2d start , olc::vf2d end, olc::Pixel c, Direction d) {
	olc::vf2d center;
	olc::vi2d rotate;
	int x, y;
	int a, b;
	float d1, d2;

	a = abs(start.x - end.x);
	b = abs(start.y - end.y);
	center.x = start.x;
	center.y = end.y;
	x = 0;
	y = b;
	switch (d) {
	case NW:
		rotate.x = -1;
		rotate.y = -1;
	
		break;
	case NE:
		rotate.x = 1;
		rotate.y = -1;
		break;
	case SW:
		rotate.x = -1;
		rotate.y = 1;
		break;
	case SE:
		rotate.x = 1;
		rotate.y = 1;
		break;
	}
	p->Draw(center.x + rotate.x*x, center.y + rotate.y*y, c);


	d1 = b * b - a * a * b + a * a / 4;//b^2-a^2b+a^2/4
	/*Region 1*/
	while (a * a * (y - 0.5f) > b * b * (x + 1)) {//a^2(y-1/2) > b^2(x+1)
		if (d1 < 0) {
			d1 += b * b * (2 * x + 3);//b^2(2x+3)
		}
		else {
			d1 += b * b * (2 * x + 3) + a * a * (-2 * y + 2);//b^2(2x+3)+a^2(-2y+2)
			y--;
		}
		x++;
		p->Draw(center.x + rotate.x * x, center.y + rotate.y * y, c);
	}
	//d2 = b^2(x+1/2)^2 + a^2(y-1)^2 -a^2b^2
	d2 = b * b * (x + 0.5f) * (x + 0.5f) + a * a * (y - 1) * (y - 1) - a * a * b * b;
	/*Region 2*/
	while (y > 0) {
		if (d2 < 0) {
			d2 += b * b * (2 * x + 2) + a * a * (-2 * y + 3);//b^2(2x+2) + a^2(-2y+3)
			x++;
		}
		else {
			d2 += a * a * (-2 * y + 3);//a^2(-2y+3)
		}
		y--;
		p->Draw(center.x + rotate.x * x, center.y + rotate.y * y, c);
	}
}

绘制后

图:带圆弧的冰球场地

帧率控制

为了降低CPU占用率,我可以设置帧率上限

设置了帧率上限后,CPU占用率从30%多降到了10%以下。

标签:rotate,游戏,puck,paddle,冰球,绘制
From: https://www.cnblogs.com/luckydoog/p/18284542

相关文章

  • python实现扑克游戏 - 抽鬼牌 和 21点
    poker_gamespython实现扑克游戏:抽鬼牌和21点-PythonImplementationofPokerGames:DrawingGhostCardsandBlackjackpoker模块首先,定义一个扑克模块,后面的包括以后的扑克牌游戏,都可以调用这个模块这个模块可以实现:卡牌、扑克牌组发牌、洗牌玩家摸牌、出牌等......
  • 手把手教你如何用python写一个经典小游戏(仅需100行以内的代码)
    创作灵感小时候也就是大概十几年前的时候,智能触屏手机还未大量普及,移动网络还是2G,大部分人用的都是小灵通,里面只有几款经典的游戏,比如俄罗斯方块,贪吃蛇等。还记得以前自己玩的不亦乐乎。如今网络发展迅速,通讯设备越来越智能化,集成化,这些上世纪的经典游戏似乎早已淡忘人们的视......
  • 苹果Mac电脑能玩什么游戏 Mac怎么运行Windows游戏
    相对于Windows平台来说,Mac电脑可玩的游戏较少。虽然苹果设备的性能足以支持各种大型游戏,但由于系统以及苹果配套服务的限制,很多游戏无法在Mac系统中运行。不过,借助虚拟机软件,Mac电脑可以突破系统限制玩更多的游戏。接下来,一起来看看苹果Mac电脑能玩什么游戏,Mac怎么运行Windows......
  • 解决《植物大战僵尸》系列游戏缺失DLL文件的终极指南
    在享受《植物大战僵尸》这款经典塔防游戏的乐趣时,你可能会遇到因缺失DLL文件而导致的游戏启动失败或运行错误的问题。不要担心,这其实是一个相对常见的问题,且有多种解决方法可以帮助你快速回到与僵尸斗智斗勇的战场。本文将针对几种常见的DLL文件丢失问题,如msvcp100.dll、gdiplu......
  • 小白新手基于云数据库 Redis 搭建 游戏排行榜
    小白新手基于云数据库Redis搭建游戏排行榜免费试用搭建游戏排行榜搭建基础环境JDK、Maven部署游戏排行榜写在最后操作感受其他应用免费试用在开始搭建游戏排行榜之前,我们首先需要领取阿里云社区为我们准备的免费资源,比如云数据库Redis版免费试用点击【立即......
  • Zombie Voices Audio Pack(僵尸游戏音频包)
    僵尸声音音频包是600多个高质量声波的集合。它提供了僵尸主题游戏所需的一切,这要归功于它的20多个类别:攻击、咬、呼吸、窒息、损坏、死亡、进食、血腥、咕噜、大笑、疼痛、反应、尖叫、喉咙、呕吐、单词和句子。+我们的僵尸动画包带来的额外奖励,包括攻击、侦测、进食、地......
  • The Forest Enemy Pack(2D动画角色游戏模型)
    这个包包含14个适用于platformer和2drpg游戏的动画角色。动画总帧数:1785用于动画的所有精灵都具有透明背景,并准备有1500x1200和750x600两种尺寸。对于每个角色,你也可以找到具有单独身体部位的精灵表,这样你就可以轻松地制作自己的动画。它们有PNG和PSD格式。示例场景包含......
  • C语言编程-基于单链表实现贪吃蛇游戏
    基于单链表实现贪吃蛇游戏1.定义结构体参数蛇行走的方向蛇行走的状态蛇身节点类维护蛇的结构体型2.游戏运行前预备工作定位光标位置游戏欢迎界面绘制游戏地图(边界)初始化游戏中的蛇身创建食物3.游戏运行下一个位置是食物,就吃掉食物,释放该节点下一个位置不是......
  • steam游戏商城怎么共享游戏给好友?最详细的操作方法介绍
    在Steam平台上共享游戏给好友,实际上是通过Steam的家庭图书馆共享功能实现的。这允许你在一个家庭内与最多五位家庭成员共享你的游戏库,但他们必须使用同一台电脑。请注意,你不能直接将游戏共享给不在同一物理位置的好友。以下是启用家庭图书馆共享的步骤:1.登录Steam:首先,确保你......
  • 诺森德塔防游戏启动故障:msvcp110.dll文件缺失的高效解决策略
    《诺森德塔防》是一部以二战为背景的“肉鸽塔防”游戏,拥有着极为火爆的战场表现,让你能充分感受到收割成片敌人的快感,同时在玩法及策略性上都有着突出表现,然而最近很多用户都遇到了启动故障:msvcp110.dll文件缺失的问题,下面一起来看看解决方法介绍吧!重新安装MicrosoftVisualC......