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游戏是不存在这些问题的。
这可能跟我选择的实现这种功能的方式有关,但我目前还不知道还有什么其他的方式实现这个功能。
如果有大佬看见了我这个问题,也请求各位大佬能指出一条明路。
感谢大家的支持!