全网最详细用c语言实现植物大战僵尸游戏(上)-2

简介: 全网最详细用c语言实现植物大战僵尸游戏(上)

五、创建游戏启动菜单界面

在主函数中,启动菜单放在开始


bdce9ec82c89c043cd059cde946e6333_d513999bd2a04002a5fb3eab2e809b28.png

我们开始创建这个函数,开始加载菜单界面和俩个明暗菜单按钮,因为这个界面是要等待用户点击,所以是一个循环,像用户点击函数一样,我们接受鼠标信息,若鼠标左键按下状态在开始按钮区域中,则渲染高亮开始按钮图片,若鼠标左键弹起状态在开始按钮区域中,则结束循环。


void startUI()
{
  IMAGE imgBj, imgMenun1, imgMenun2;
  loadimage(&imgBj, "res/menu.png");
  loadimage(&imgMenun1, "res/menu1.png");
  loadimage(&imgMenun2, "res/menu2.png");
  int flag = 0;
  while (1)
  {
    BeginBatchDraw();
    putimage(0, 0, &imgBj);
    putimagePNG(475, 75, flag ? &imgMenun2 : &imgMenun1);
    ExMessage msg;
    if (peekmessage(&msg, EX_MOUSE))//判断鼠标消息,有返回真,无返回假
    {
      if (msg.message == WM_LBUTTONDOWN && msg.x > 474 && msg.x < 474 + 300 && msg.y>75 && msg.y < 75 + 140)
      {
        flag = 1;
      }
      else if (msg.message == WM_LBUTTONUP && msg.x > 474 && msg.x < 474 + 300 && msg.y>75 && msg.y < 75 + 140)
      {
        //return;
        EndBatchDraw();//点击到开始按钮,在跳出循环之前结束缓冲
        break;
      }
    }
    EndBatchDraw();//没有点击开始按钮,需要结束缓冲
  }
}


六、创建阳光球

创建阳光球结构体和一个结构体数组,表示10个阳光球,阳光球分为俩个部分,一种是自由落体,另一种是向日葵产生的抛物线运动的阳光球,这里引入一个工具文件vector2.cpp,将该文件添加到现有项里面去。


这个可以理解成c语言的结构体类型,红色框代表结构体成员,可以运用这个结构体进行直线,抛物线运动(贝塞尔曲线)


贝塞尔曲线函数:通过四个点和t(起始位置t=0,终点位置t=1)确定t时刻在曲线位置坐标


(斜)直线运动:通过俩个点(p1和p4)以及t,确定直线运动的t时刻位置坐标,t=t+speed,pcur=p1+t*(p4-p1)

0da024881aa4d1abb34d20803b4ac151_e97a644d336d41ebaaf164c79ee01881.png


9be36f32a97f40f1fb8193ff74bded89_2b43335639c9409fa8224a35413b69b3.png

enum{SUNSHINE_DOWN,SUNSHINE_GROUND,SUNSHINE_COLLECT,SUNSHINE_PRODUCT};
//0.阳光下降 1.阳光着陆 2.阳光收集 3.阳光生产
struct sunshineball
{
  int frameIndex;//当前显示图片帧的序号
  //int destY;//飘落的目标位置的y坐标
  bool use;//是否在使用(0代表没有被使用)
  int timer;//计时器,目的使阳光停留几秒
  float t;//贝塞尔曲线时间点0-1
  vector2 p1, p2, p3, p4;//贝塞尔曲线的四个点
  vector2 pcur;//当前时刻阳光球位置
  float speed;
  int status;//阳光球四个状态
};
struct sunshineball balls[10];
IMAGE imgSunshineball[29];
int sunshine;//阳光值


然后在初始化函数中初始化(加载)阳光球


memset(balls, 0, sizeof(balls));
  for (int i = 0; i < 29; i++)
  {
    sprintf_s(plantname, sizeof(plantname), "res/sunshine/%d.png", i + 1);
    loadimage(&imgSunshineball[i], plantname);
  }

因为阳光球在下落过程中,数据是不断变化的,我们在更新数据函数updategame()写创建阳光球函数和修改阳光球函数


void updategame()//修改游戏数据
{
  creatsunshine();//创建阳光
  updatesunshine();//修改阳光状态
}

创建阳光球函数怎么写呢?


阳光球分为俩个部分,一种是自由落体阳光球,另一种是向日葵产生的抛物线运动的阳光球。


6.1 创建自由落体阳光球

先遍历10个阳光球,找到一个没有使用过的,对该阳光球初始化,阳光球状态初始化下降状态,下降只要设置起始点和落地点即可,起始点的横坐标在260-800位置随机出现(随机函数可以看这篇博文随机数的生成),降落的纵坐标在4行草坪高度随机出现,dis()是计算俩个坐标之间的距离(可以看成俩个向量相减的模长),阳光球从起始位置时t=0,到目标位置t=1,所以可以以此计算阳光球的运动快慢,若speed=1.0/2,t=t+speed,则需要俩次阳光才能到目的地。


void creatsunshine()
{
    //创建自由落体阳光
  static int count = 0;
  static int fre = 200;
  count++;
  if (count >= fre)//每400帧获取一个阳光值
  {
    fre = rand() % 200;//200-399的随机值
    count = 0;
    //从阳光池中取一个可以使用的
    int ballmax = sizeof(balls) / sizeof(balls[0]);
    int i = 0;
    for (i = 0; i < ballmax && balls[i].use; i++);//找到未被使用的
    if (i >= ballmax)//未找到
    {
      return;
    }
    balls[i].use = true;//使用
    balls[i].frameIndex = 0;//第0个序列图片
    //balls[i].x = rand() % (800 - 260) + 260;
    //balls[i].destY = (rand() % 5) * 90 + 170;//目标位置
    //balls[i].y = 60;
    balls[i].timer = 0;
    //balls[i].xoff = 0;
    //balls[i].yoff = 0;
    balls[i].status = SUNSHINE_DOWN;
    balls[i].t = 0;
    balls[i].p1 = vector2(rand() % (800 - 260) + 260, 60);//把vector2看成一个结构体
    balls[i].p4 = vector2(balls[i].p1.x, (rand() % 5) * 90 + 170);
    int off = 2;
    float distance = dis(balls[i].p4.y - balls[i].p1.y);
    balls[i].speed = 1.0 / (distance / off);//下落速度
  }
     //创建向日葵生产阳光
}


6.2 创建向日葵生产阳光

遍历4行8列的植物数组,若是向日葵,则找一个一个没有用过的太阳球,对太阳球结构体修改成员状态,状态改为生产阳光,起始点为向日葵坐标,终点坐标是向日葵左右100-150作用,中间俩个坐标自己调节,

//向日葵生产阳光
  int ballmax = sizeof(balls) / sizeof(balls[0]);
  for (int i = 0; i < 4; i++)
  {
    for (int j = 0; j < 8; j++)
    {
      if (map[i][j].type == SUNFLOWER + 1)
      {
        map[i][j].timer++;//计时器
        if (map[i][j].timer > 100)
        {
          map[i][j].timer = 0;
          int k;
          for (k = 0; k < ballmax && balls[k].use; k++);
          if (k >= ballmax)
          {
            return;
          }
          balls[k].use = true;
          balls[k].p1 = vector2(map[i][j].x, map[i][j].y);
          int w =(100 + rand() % 50)*(rand()%2? 1:-1);
          balls[k].p4 = vector2(map[i][j].x+w,
            map[i][j].y+imgplant[SUNFLOWER][0]->getheight()- imgSunshineball[0].getheight()+10);//喷射下落位置
          balls[k].p2 = vector2(balls[k].p1.x + 0.3 * w, balls[k].p1.y - 50);
          balls[k].p3 = vector2(balls[k].p1.x + 0.7 * w, balls[k].p1.y - 50);
          balls[k].status = SUNSHINE_PRODUCT;
          balls[k].speed = 0.05;
          balls[k].t = 0;
        }
      }
    }
  }


七、收集阳光

收集阳光属于用户操作,收集阳光函数应在用户操作函数中,若鼠标弹起状态位置不是卡牌区域,判断是否在阳光球位置,若是在阳光球位置,改变阳光球的状态(阳光球有四种状态)为收集状态,阳光球的起始位置为点击位置,终点位置为工具栏的阳光图片。


void userclick()
{
  ExMessage msg;//这个结构体变量用于保存鼠标消息
  static int status= 0;
  if (peekmessage(&msg,EX_MOUSE))//判断鼠标消息,有返回真,无返回假
  {
  if (msg.message == WM_LBUTTONUP)//鼠标左键弹起
  {
    if (msg.x > 338 && msg.x < 338 + 65 * PLANT_COUNT && msg.y < 96)
    {
    }
    else
    {
      collectsunshine(&msg);//收集阳光
    }
  }
  else if (msg.message == WM_MOUSEMOVE && status == 1)//鼠标移动
  {
  }
  else if (msg.message == WM_LBUTTONDOWN)//鼠标左键按下
  {
    curplant = 0;
    status = 0;
  }
  }
}


void collectsunshine(ExMessage*msg)
{
  int count = sizeof(balls) / sizeof(balls[0]);
  int w = imgSunshineball[0].getwidth();//获取图片宽度
  int h = imgSunshineball[0].getheight();
  for (int i = 0; i < count; i++)//若鼠标左键弹起的位置在阳光里面,则阳光消失,且阳光值加25
  {
    if (balls[i].use)
    {
      //int x = balls[i].x;
        //int y = balls[i].y;
      int x = balls[i].pcur.x;
      int y = balls[i].pcur.y;
      if (msg->x > (x + 8) && msg->x<(x + w - 8) && msg->y >(y + 8) && msg->y < (y + h - 8))//鼠标左键弹起的位置在阳光里面
      {
        //balls[i].use = false;
        balls[i].status = SUNSHINE_COLLECT;
        //sunshine += 25;
        //mciSendString("play res/sunshine.mp3", 0, 0, 0);//播放音效(这个音乐需要上一个播放完才能播放下一个)
        PlaySound("res/sunshine.wav", NULL, SND_FILENAME |SND_ASYNC);
        //设置阳光偏移量
        //float destY = 0;
        //float destX = 262;
        //float angle = atan((balls[i].y - destY) / (balls[i].x - destX));
        //balls[i].xoff = 4 * cos(angle);
        //balls[i].yoff = 4 * sin(angle);
        balls[i].p1 = balls[i].pcur;//直线运动不用p2,p3.
        balls[i].p4 = vector2(262,0);
        balls[i].t = 0;
        float distance = dis(balls[i].p1 - balls[i].p4);
        float off = 8;
        balls[i].speed = 1.0 / (distance / off);//实际distance/off帧回到原点
        break;//收集该阳光球后,判断下次点击
      }
    }
  }
}


八、更新阳光球状态

前面我们已经有了四种阳光球状态,然后对这四种状态阳光球进行数据修改,


(1)如果正在使用阳光球处于下降状态,t=t+speed,pcur=p1+t*(p4-p1),通过t就可以得到下降状态的位置,此时阳光球状态变为落地,计时器归0;


(2)如果正在使用阳光球处于落地状态,使太阳球停顿一会再消失;


(3)如果正在使用阳光球处于收集状态,此时运动是斜直线,用疑似贝塞尔曲线确定阳光位置,t=1时,阳光球消失,计数器加25


(4)如果正在使用阳光球处于生产状态,此时运动状态是曲线,用贝塞尔曲线计算位置,t=1,到终点位置,变落地状态


void updatesunshine()
{
  int ballmax = sizeof(balls) / sizeof(balls[0]);
  for (int i = 0; i < ballmax; i++)
  {
    if (balls[i].use)//被使用状态
    {
      balls[i].frameIndex = (balls[i].frameIndex + 1) % 29;
      if (balls[i].status == SUNSHINE_DOWN)
      {
        struct sunshineball* sun = &balls[i];
        sun->t += sun->speed;
        sun->pcur = sun->p1 + sun->t * (sun->p4 - sun->p1);//疑似贝塞尔曲线
        if (sun->t >= 1)
        {
          sun->status = SUNSHINE_GROUND;
          sun->timer = 0;
        }
      }
      else if (balls[i].status == SUNSHINE_GROUND)
      {
        balls[i].timer++;
        if (balls[i].timer > 25)
        {
          balls[i].use = false;
          balls[i].timer = 0;
        }
      }
      else if (balls[i].status == SUNSHINE_COLLECT)//此时运动是斜直线
      {
        struct sunshineball* sun = &balls[i];
        sun->t += sun->speed;
        sun->pcur = sun->p1 + sun->t * (sun->p4 - sun->p1);//向量相减
        if (sun->t >= 1)
        {
          sun->use = false;
          sun->timer = 0;
          sunshine += 25;
        }
      }
      else if (balls[i].status == SUNSHINE_PRODUCT)
      {
        struct sunshineball* sun = &balls[i];
        sun->t += sun->speed;
        sun->pcur = calcBezierPoint(sun->t, sun->p1, sun->p2, sun->p3, sun->p4);
        if (sun->t >= 1)
        {
          sun->status = SUNSHINE_GROUND;
          sun->timer = 0;
        }
      }
    }
  }
}


上面我们设计,当阳光求回到工具栏中,阳光值增加25,我们的目的想使阳光值显示再工具栏上。


有俩方法,一种用图片呈现,另一种直接输出字体


这里我们输出文本,先在初始化函数中设置字体


//设置字体
  LOGFONT f;//当前字体结构体变量
  gettextstyle(&f);//获取当前字体
  f.lfHeight = 30;
  f.lfWidth = 15;
  strcpy(f.lfFaceName, "Segoe UI Black");//修改字体类型
  f.lfQuality = ANTIALIASED_QUALITY;//抗锯齿效果
  settextstyle(&f);//设置字体文本
  setbkmode(TRANSPARENT);//设置字体背景透明
  setcolor(BLACK);

然后渲染阳光值数字,要把十进制数字转化为字符串,再去渲染


   char scoretext[8];
  sprintf_s(scoretext, sizeof(scoretext), "%d", sunshine);//将阳光值存储到字符数组
  outtextxy(276, 67, scoretext);//输出字符数组里面的字符

接下来我们将阳光球渲染出来就可以看到效果了


//渲染阳光球
  int ballmax = sizeof(balls) / sizeof(balls[0]);
  for(int i=0;i<ballmax;i++)
  {
    //if (balls[i].use||balls[i].xoff)
    if(balls[i].use)
    {
      IMAGE* img = &imgSunshineball[balls[i].frameIndex];
      //putimagePNG(balls[i].x, balls[i].y, img);
      putimagePNG(balls[i].pcur.x, balls[i].pcur.y, img);
    }
  }

9f44515ad3c6003a368189884a525f6b_ea6192483c554b8ea55900f1b83decc0.png

后续精彩内容,请点击  植物大战僵尸(下) 进行跳转


完整的代码及素材链接:


植物大战僵尸源码及素材

相关文章
|
6天前
|
安全 C语言
四步手把手教你实现扫雷游戏(c语言)
四步手把手教你实现扫雷游戏(c语言)
|
6天前
|
程序员 C语言
实现三子棋游戏(C语言)----就是这么简单
实现三子棋游戏(C语言)----就是这么简单
|
6天前
|
C语言
C语言实现猜数字游戏
C语言实现猜数字游戏
|
7天前
|
C语言
C语言实现扫雷游戏
C语言实现扫雷游戏
12 0
|
10天前
|
C语言
循环的应用--猜数字游戏、关机程序【c语言篇】
循环的应用--猜数字游戏、关机程序【c语言篇】
23 0
TU^
|
11天前
|
存储 C语言
C语言实现扫雷游戏
扫雷游戏是一款十分经典的游戏,运用到C语言中的数组,循环和条件语句,随机数的生成,函数,递归等知识点。
TU^
27 1
|
12天前
|
人工智能 C语言
c语言:初步实现扫雷游戏
c语言:初步实现扫雷游戏
25 0
|
14天前
|
C语言
C语言初阶⑤(数组)扫雷游戏(分步实现+效果图)
C语言初阶⑤(数组)扫雷游戏(分步实现+效果图)
21 1
|
19天前
|
C语言
C语言实战演练之游戏框架
C语言实战演练之游戏框架
|
19天前
|
存储 算法 C语言
【C 言专栏】用 C 语言开发游戏的实践
【5月更文挑战第5天】本文探讨了使用C语言开发游戏的实践,包括选择适合的游戏类型(如贪吃蛇、俄罗斯方块),设计游戏框架、图形界面和逻辑,以及音效添加。文章还强调了性能优化、测试调试、跨平台挑战及未来发展趋势。对于热衷于C语言的开发者,这是一次挑战与乐趣并存的探索之旅。
【C 言专栏】用 C 语言开发游戏的实践