C++ 用数学一次函数实现对象向任意方向移动 小游戏示例

C++ 用一次函数实现界面上的一个物体向任意方向移动

我之前写过的小例子普遍存在一个共同点,那就是他们所有对象的运动方向都是和X轴或Y轴平行的,这种运动方式非常单一且生硬。

反观市面上的2d游戏,也几乎都可以实现物体向任意方向移动。于是,我想到了使用初中学过的一次函数来实现这个效果。

整个的思路就是获取两个点的坐标,根据这两点算出一次函数(y = k * x + b)的函数关系式,每次循环这个物体的X轴自增,Y轴坐标根据函数解析式增加。

另外,我还在程序中加入了对象在不同状态时的动画效果,这样可以让画面看起来更生动,也更贴近真实的游戏。

运行效果:
效果

技术环节:
编译环境:Windows VS2019

需求:
子弹和怪物可在平面中向任意方向移动等

注意:
该小游戏只是一个半成品,可以说是我仅仅是为了实现“物体向任意方向移动”这个功能而写的,除核心的功能之外,几乎没有其他附加内容。
还请多多包含。

在代码中我写了详细的注释

代码:

#include <graphics.h>
#include <string>
#include <ctime>
#include <vector>

using namespace std;

//将两个字符串和一个int拼接后转为char*全局函数
const char* strtochar(const string stra, const string strb, int numi)
{
	string str1 = stra;
	string str2 = strb;
	//文件路径字符串+循环变量+字符串
	const string str3 = str1 + to_string(numi) + str2;
	char atemp[40];
	strncpy_s(atemp, str3.c_str(), str3.length());
	return atemp;
}

//我方机器人类
class OuRobot
{
private:
	PIMAGE RobotMoveR[8] = { 0 };	//机器人右移动素材
	PIMAGE RobotMoveL[4] = { 0 };	//机器人左移动素材
	PIMAGE RobotStop[7] = { 0 };	//机器人静止素材
	PIMAGE RobotMoveUD[5] = { 0 };	//机器人上下移动素材
	PIMAGE RobotFire[5] = { 0 };	//机器人发射子弹素材
	int roboti = 0;					//机器人图片循环使用变量
	float objX = 100.0, objY = 250.0;//对象坐标位置
	char key = 0;					//接收用于机器人移动的键值
	int state = 0;					//用于标记机器人当前状态
	int moveframe = 0;				//记录行走之后的帧数
	bool movesign = false;			//是否进入移动状态标记
	int width = 110, height = 100;	//对象的宽高
	mouse_msg msg;					//接收鼠标信息用于进入攻击状态
	bool leftDown = false;			//左键点击标记,左键点击设为true,右键点击设为false

public:
	//构造函数获取图片保存到图片数组
	OuRobot()
	{
		//机器人右运动素材
		for (roboti = 1; roboti <= 7; roboti++)
		{
			RobotMoveR[roboti] = newimage();	
			getimage(RobotMoveR[roboti], strtochar("素材\\机器人素材", ".png", roboti));	//从文件中获取图片
		}

		//机器人左运动素材
		for (roboti = 1; roboti <= 3; roboti++)
		{
			RobotMoveL[roboti] = newimage();
			getimage(RobotMoveL[roboti], strtochar("素材\\机器人后退素材", ".png", roboti));
		}

		//机器人上下移动素材
		for (roboti = 1; roboti <= 4; roboti++)
		{
			RobotMoveUD[roboti] = newimage();
			getimage(RobotMoveUD[roboti], strtochar("素材\\机器人上下移动素材", ".png", roboti));
		}

		//机器人静止素材
		for (roboti = 1; roboti <= 6; roboti++)
		{
			RobotStop[roboti] = newimage();
			getimage(RobotStop[roboti], strtochar("素材\\机器人静止素材", ".png", roboti));
		}

		//机器人发射子弹时素材
		for (roboti = 1; roboti <= 4; roboti++)
		{
			RobotFire[roboti] = newimage();
			getimage(RobotFire[roboti], strtochar("素材\\发射子弹", ".png", roboti));
		}

		roboti = 1;
	}

private:
	//输出机器人图片函数
	void PutRobotImg()
	{
		switch (state)
		{
		case 0:		//为0表示静止时,使用静止图片数组
			if (roboti > 6) roboti = 1;									//每一帧输出不同的机器人图片,i越界后重置为0
			putimage_withalpha(NULL, RobotStop[roboti], objX, objY);	//在对象坐标处显示机器人动态图片
			break;
		case 1:		//1表示右运动状态,使用右移动图片数组
			if (roboti > 7) roboti = 1;
			putimage_withalpha(NULL, RobotMoveR[roboti], objX, objY);
			break;	
		case 2:		//2表示左运动状态,使用左移动图片数组
			if (roboti > 3) roboti = 1;
			putimage_withalpha(NULL, RobotMoveL[roboti], objX, objY);
			break;
		case 3:		//3表示上下运动状态,使用上下移动图片数组
			if (roboti > 4) roboti = 1;
			putimage_withalpha(NULL, RobotMoveUD[roboti], objX, objY);
			break;
		case 4:		//4表示发射子弹状态,使用发射子弹图片数组
			if (roboti > 4) roboti = 1;	//因为发射子弹状态图片过高,所以对显示的坐标进行特殊处理
			putimage_withalpha(NULL, RobotFire[roboti], objX, objY - 120);
		}

		//每次帧循环变量+1,用于显示机器人序列帧
		roboti++;
	}

	//操控机器人移动函数
	void ContRobotMove()
	{
		key = getch();	//key接收一个键值

		if (key != 'w' && key != 'a' && key != 's' && key != 'd' && key != ' ')
			return;		//接收的不是特定键值无效并返回

		switch (key)
		{
		case 'w':		//根据按键移动机器人的位置
			objY -= 8;	
			
			state = 3;	//向上移动,机器人状态为3
			break;
		case 'a':
			objX -= 8;	

			state = 2;	//向左移动,机器人状态为2
			break;
		case 's':
			objY += 8;	

			state = 3;	//向下移动,机器人状态为3
			break;
		case 'd':
			objX += 8;
			
			state = 1;	//向右移动,机器人状态为1
			break;
		case ' ':		//按下空格键输出一次机器人,并暂停
			PutRobotImg();
			getch();	
			break;
		}

		leftDown = false;//机器人移动时将攻击状态取消,因为攻击状态点击触发的特殊,会一致将机器人状态设置位4
		movesign = true;//满足特定键值必定打开开关
	}

	//机器人静止移动规则
	void RStatMove()
	{
		//movesign为真代表机器人进入移动状态
		if (movesign)
		{
			moveframe++;		//帧数计数器开始自增

			if (moveframe >= 4)	//大于等于一定值时重置为0,且开关关闭
				moveframe = 0, movesign = false;

			return;
		}

		state = 0;			//机器人非移动状态时进入静止状态
	}

public:

	float GetObjX()		//获取机器人对象x坐标
	{
		return objX;
	}

	float GetObjY()		//获取机器人对象Y坐标
	{
		return objY;
	}

	int GetState()		//获取机器人当前的状态
	{
		return state;
	}

	int GetWidth()		//获取机器人宽度
	{
		return width;
	}

	int GetHeight()		//获取机器人高度
	{
		return height;
	}

	void MsgOper()		//鼠标操作攻击状态函数
	{
		//循环处理所有鼠标信息
		while (mousemsg())				//有鼠标消息则进入循环
		{
			msg = getmouse();			//获取一条鼠标消息

			if (msg.is_right())			//右键点击将左键点击状态设为假
				leftDown = false;

			if (msg.is_left() && msg.is_down())
				leftDown = true;
		}

		if (leftDown)					//机器人状态为4,开启状态
			state = 4, movesign = true;
	}

	//机器人行动总函数
	void Run()	
	{
		RStatMove();			//机器人移动规则

		if (kbhit())
			ContRobotMove();	//键盘按下时执行操控移动

		MsgOper();				//鼠标控制机器人攻击状态

		PutRobotImg();			//显示机器人图片
	}

};

//子弹类
class Bullet
{
private:
	PIMAGE Bullimg;						//子弹素材
	int msgX = 400.0, msgY = 481.0;		//记录当前鼠标的坐标
	float robX = 0, robY = 0;			//记录当前机器人坐标
	float X = 0, Y = 0, K = 0, B = 0;	//计算一次函数表达式使用参数,XY为子弹坐标
	bool sign = true;					//使部分代码只执行一次
	float laulocaX = 0.0, laulocaY = 0.0;//机器人发射子弹的位置

public:
	//构造函数获取图片素材
	Bullet()
	{
		Bullimg = newimage();
		getimage(Bullimg, "素材\\子弹.png");
	}

public:
	int GetbullX()		//获取子弹x坐标
	{
		return X;
	}

	int GetbullY()		//获取子弹y坐标
	{
		return Y;
	}

	//发射子弹函数
	void FireBullet(OuRobot& ourobot)
	{
		//微调子弹发射位置和机器人左上角位置的距离,并减少调用函数次数
		laulocaX = ourobot.GetObjX() + 42;
		laulocaY = ourobot.GetObjY() - 20;
		
		//两个点的参数:
		//1:ourobot.GetObjX  ourobot.GetObjY			a  b
		//2:msgX  msgY									c  d

		//编程解二元一次方程公式:k = (d - b) / (c - a)
		
		//因为编程中等号右边的所有变量都是当作一个数来计算的
		//等号右边不存在未知数的概念,所以想要在编程中正确计算(表示)未知数只能在等号左边计算

		if (sign)
		{
			mousepos(&msgX, &msgY);			//获取鼠标当前坐标
			K = (msgY - laulocaY) / (msgX - laulocaX);
			B = msgY - msgX * K;			//计算出子弹相应的一次函数路线
			X = laulocaX;					//X初始坐标为机器人的位置
			Y = laulocaY;
			sign = false;					//以上操作,每个子弹对象仅执行一次
		}
		
		//确定子弹的X、Y坐标,且根据鼠标在机器人的不同X位置确定X增减,且确定X每次循环增加或减小的值
		//根据机器人坐标和鼠标坐标的位置关系确定子弹X坐标的增减情况
		//这里有点问题,鼠标X坐标越接近机器人X坐标,K就会变的特别大,Y也就会随之增的特别大,子弹Y轴就会长的很快,这是非正常情况
		//而在鼠标X等于机器人X后,根据一次函数性质,Y会等于0,地图上此时不会出现子弹,如果对等于的情况单独进行处理,
		//X不再增大或减小,而Y每次增大一定值,但是貌似并没有什么用,且主要问题是Y的在K增大时增大非常快,问题得不到解决
		if (msgX < ourobot.GetObjX())
			X -= 18;
		else if ((msgX > ourobot.GetObjX()))
			X += 18;
		else
			Y += 18;

		Y = K * X + B;

		putimage_withalpha(NULL, Bullimg, X, Y);
	}
};

//怪物类
class Monster
{
protected:
	PIMAGE monsterimgDin[7] = { 0 };//恐龙图片数组
	PIMAGE monsterimgFir[7] = { 0 };//飞行怪图片数组
	int monX = 0, monY = 0;			//对象坐标
	int moni = 0;					//循环计数器
	bool sign = false;				//标记是否已初始化怪物坐标
	float K = 0.0, B = 0.0;			//计算一次函数使用
	int calccoun = 0;				//再次计算函数关系的帧间隔
	int width = 0, height = 0;		//怪物的宽高
	int health = 3;					//怪物的生命值
	int kind = 0;					//区分怪物的种类

public:
	//获取图片
	Monster()
	{
		//将两组图片分别读到两个数组中
		//因为两种怪物的不同点只有图片和宽高,没有不同的行为,所以不需要写成不同的类使用多态
		for (moni = 1; moni <= 6; moni++)
		{
			monsterimgDin[moni] = newimage();
			getimage(monsterimgDin[moni], strtochar("素材\\怪物", ".png", moni));
		}

		for (moni = 1; moni <= 6; moni++)
		{
			monsterimgFir[moni] = newimage();
			getimage(monsterimgFir[moni], strtochar("素材\\飞行怪物", ".png", moni));
		}

		moni = 1;	//循环变量置为1
	}

private:
	//显示怪物图片函数
	void PutMonImg()
	{
		kind == 1 ?		//根据不同种类的怪物显示不同的图片
		putimage_withalpha(NULL, monsterimgDin[moni], monX, monY) :
		putimage_withalpha(NULL, monsterimgFir[moni], monX, monY);

		moni++;		//要显示的图片的下标自增

		if (moni > 6) moni = 1;		//显示完毕一组图片后重置下标
	}

	//怪物初始坐标相关函数 等
	inline void Initcoor(OuRobot& ourobot)
	{
		//怪物的初始坐标为一个范围内的随机数
		monX = (rand() % (1030 - 1005 + 1)) + 1005;
		monY = (rand() % (520 - 20 + 1)) + 20;
		//怪物刚出现时执行一次计算函数关系,不然Y轴会出错
		K = (ourobot.GetObjY() - monY) / (ourobot.GetObjX() - monX);
		B = ourobot.GetObjY() - ourobot.GetObjX() * K;

		kind = (rand() % 2) + 1;	//设置怪物种类

		if (kind == 1) width = 52, height = 58;		//根据怪物种类确定怪物宽高
		else width = 104, height = 75;

		sign = true;				//该函数仅执行一次
	}

	//怪物移动相关函数
	void Move(OuRobot& ourobot)
	{
		//1:monX  monY
		//2:ourobot.GetObjX()  ourobot.GetObjY()

		//在计数器大于一定值时重新计算怪物和机器人之间的坐标
		//不启用这个计数器,每帧都计算新函数关系会出错,原因不明

		if (calccoun > 6)
		{
			K = (ourobot.GetObjY() - monY) / (ourobot.GetObjX() - monX);
			B = ourobot.GetObjY() - ourobot.GetObjX() * K;
			calccoun = 0;
		}
		
		calccoun++;

		monY = K * monX + B;				//通过K、B值计算出怪物原坐标到机器人坐标的函数关系式
		 
		if (monX > ourobot.GetObjX())
			monX -= 4;						//X坐标自减
		else if (monX < ourobot.GetObjX())
			monX += 4;
	}

public:

	int GetMonX()				//获取怪物的X坐标
	{
		return monX;
	}

	int GetMonY()				//获取怪物的Y坐标
	{
		return monY;
	}

	int GetMonW()				//获取怪物的宽度
	{
		return width;
	}

	int GetMonH()				//获取怪物的高度
	{
		return height;	
	}

	int GetHealth()				//获取怪物的生命值
	{
		return health;
	}

	void SetHealth(int minhea)	//设置怪物的生命值减少一个值
	{
		health -= minhea;
	}

	//怪物行动总函数
	virtual void Run(OuRobot& ourobot)
	{
		if(!sign)
			Initcoor(ourobot);		//怪物的初始坐标相关函数 等

		Move(ourobot);				//怪物移动相关函数
		
		PutMonImg();				//显示输出怪物图片
	}
};


//爆炸效果类
class Explode
{
private:
	PIMAGE explimg[9] = { 0 };	//图片数组
	int expi = 0;				//用于循环
	int X = 0, Y = 0;			//坐标	

public:
	Explode()
	{
		for (expi = 1; expi <= 8; expi++)
		{
			explimg[expi] = newimage();
			getimage(explimg[expi], strtochar("素材\\爆炸", ".png", expi));
		}
		expi = 1;
	}

	void SetCoor(int X, int Y)		//设置坐标函数
	{
		this->X = X;
		this->Y = Y;
	}

	//爆炸效果主要函数
	bool ExplResu()
	{
		//显示图片
		putimage_withalpha(NULL, explimg[expi], X, Y);

		expi++;

		if (expi > 8)		//所有图片播放完毕后返回true并将该对象删除
		{
			expi = 1;
			return true;
		}

		return false;
	}
};

//游戏类
class Game
{
private:
	OuRobot ourobot;			//我方机器人对象
	Bullet firebullt;			//用于插入的子弹对象
	vector<Bullet> firbuvec;	//子弹数组
	const int ATINT = 5;		//子弹攻击间隔常量(帧)
	int firbusize = 0;			//用于记录子弹数组的有效长度
	int counter = 8;			//用于子弹帧数计数器
	PIMAGE backimg = { 0 };		//背景图片
	Monster monster;			//用于插入的怪物对象
	vector<Monster> monstervec;	//怪物对象数组
	int moncount = 0;			//用于产生怪物帧数计数器
	int prod = 0;				//产生怪物的间隔
	int monvecsize = 0;			//记录怪物数组中怪物的数量
	Explode explode;			//用于插入的对象
	vector<Explode> explodevec;	//爆炸效果数组
	int score = 0;				//记录游戏得分
	
public:
	//获取背景图片和设置文字属性
	Game()
	{
		backimg = newimage();
		getimage(backimg, "素材\\背景.png");

		setbkmode(TRANSPARENT);		//文字背景透明
		setcolor(WHITE);			
		setfont(40, NULL, "楷体");
	}

private:
	//向数组中插入子弹规则
	void IntBullRule()		
	{
		//只有在机器人状态为4(攻击状态)的情况下才可以向数组中插入新的子弹

		if (ourobot.GetState() != 4)
			return;

		if (counter >= ATINT)
		{
			firbuvec.push_back(firebullt);//向数组中插入新的子弹
			counter = 0;		//将计数器重置为0
		}

		//帧数计数器在每次循环自增,大于一定值则向数组中插入一个新子弹
		counter++;
	}

public:
	//机器人子弹相关函数
	void Bulletcorr()			
	{
		IntBullRule();						//发射新子弹规则
		
		//遍历子弹数组中所有子弹,让子弹移动
		firbusize = firbuvec.size();		//使用一个变量记录数组长度,避免重复调用函数
		for (int i = 0; i < firbusize; i++)
			firbuvec[i].FireBullet(ourobot);//子弹移动主要函数

		//遍历子弹数组,删除越界的子弹
		for (vector<Bullet>::iterator it = firbuvec.begin(); it != firbuvec.end(); it++)
			if (it->GetbullX() >= 1000 || it->GetbullX() <= -12 || it->GetbullY() >= 600 || it->GetbullY() <= -12)
			{
				firbuvec.erase(it);
				break;
			}
	}

	//碰撞检测函数
	void CD()		
	{
		int OurObjX = ourobot.GetObjX(), OurObjY = ourobot.GetObjY();	//机器人坐标
		int OurObjW = ourobot.GetWidth(), OurObjH = ourobot.GetHeight();//机器人的宽高

		//遍历怪物数组
		//如果怪物碰到机器人,则删除这个怪物
		for (vector<Monster>::iterator it = monstervec.begin(); it != monstervec.end(); it++)
		{
			int MonObjX = it->GetMonX(), MonObjY = it->GetMonY();	//怪物坐标
			int MonObjW = it->GetMonW(), MonObjH = it->GetMonH();	//怪物宽高

			//判断机器人的矩形碰撞怪物矩形
			if ((((MonObjX <= OurObjX + OurObjW) && (MonObjX >= OurObjX)) || 
				((MonObjX + MonObjW >= OurObjX) && (MonObjX + MonObjW <= OurObjX + OurObjW))) &&
				((MonObjY + MonObjH >= OurObjY) && (MonObjY <= OurObjY + OurObjH)))
			{
				//碰到怪物则删除怪物,得分-1
				monstervec.erase(it), score--;			//删除这个怪物

				explode.SetCoor(MonObjX, MonObjY);		//设置对象的坐标,再这个对象插入到爆炸效果数组中
				explodevec.push_back(explode);
				break;
			}

			//遍历子弹数组
			for (vector<Bullet>::iterator bit = firbuvec.begin(); bit != firbuvec.end(); bit++)
			{
				//子弹宽高为12
				int BullCX = bit->GetbullX() + 6, BullCY = bit->GetbullY() + 6;		//子弹中心坐标
				
				//这里判断子弹的中心点和怪物的碰撞
				if ((BullCX >= MonObjX && BullCX <= MonObjX + MonObjW) &&
					(BullCY >= MonObjY && BullCY <= MonObjY + MonObjH))
				{
					//删除碰撞到怪物的子弹
					firbuvec.erase(bit);

					it->SetHealth(1);			//设置怪物的生命值减少一个值

					if(!it->GetHealth())		//如果怪物的生命值为0,则删除这个怪物
						monstervec.erase(it), score++;	//得分+1

					//设置对象的坐标,再这个对象插入到爆炸效果数组中
					explode.SetCoor(BullCX, BullCY);
					explodevec.push_back(explode);
					goto L1;
				}
			}
		}
	L1:;
	}

	//怪物相关函数
	void MonCoor()
	{
		moncount++;				//产生怪物间隔计数器自增
		prod = (rand() % (40 - 20 + 1)) + 20;		//每次产生怪物的帧间隔为一个随机数

		if (moncount >= prod)	//计数器大于一定值后,向数组中插入两个新的怪物
		{
			monstervec.push_back(monster);
			monstervec.push_back(monster);
			moncount = 0;		//将计数器重置为0
		}
		
		//让每一个怪物移动
		monvecsize = monstervec.size();
		for (int i = 0; i < monvecsize; i++)
			monstervec[i].Run(ourobot);
	}

	//爆炸效果函数
	inline void ExplodeResu()
	{
		//爆炸效果
		for (vector<Explode>::iterator it = explodevec.begin(); it != explodevec.end(); it++)
			if (it->ExplResu())
			{
				explodevec.erase(it);
				break;
			}
	}

	//游戏主循环
	void GameOper()
	{
		srand((unsigned)time(NULL));		//随机种子

		while (true)
		{
			putimage(0, -200, backimg);		//背景图片

			Bulletcorr();		//机器人子弹相关函数

			ourobot.Run();		//我方机器人行动

			MonCoor();			//怪物相关函数

			CD();				//碰撞检测

			ExplodeResu();		//爆炸效果相关函数

			xyprintf(10, 100, "得分:%d", score);

			delay_fps(30);		//控制游戏帧率
		}
	}
};


int main()
{
	setrendermode(RENDER_MANUAL);
	initgraph(1000, 600);			//初始化窗口为1000, 600
	setcaption("示例小游戏");		//设置窗口标题

	ege_enable_aa(true);			

	Game game;
	
	game.GameOper();

	getch();
	return 0;
}

不足之处:
虽然我实现了物体可以向任意方向移动的功能,但最终效果和我设想中的还是有一定差距的。

主要问题是,鼠标的X轴越接近机器人的X轴时(两个点的X轴坐标接近)时,子弹的移动速度就会变快,两个X位于同一个X轴上时,Y等于0,子弹在鼠标十分接近机器人X轴时不会显示。
我知道这是由一次函数的性质所导致的,但我设想中真正的“自由移动”,还有市面上2d游戏是不存在这些问题的。

这可能跟我选择的实现这种功能的方式有关,但我目前还不知道还有什么其他的方式实现这个功能。

如果有大佬看见了我这个问题,也请求各位大佬能指出一条明路。


感谢大家的支持!

原创文章 20 获赞 24 访问量 8349

猜你喜欢

转载自blog.csdn.net/qq_46239972/article/details/105394365