C 实现植物大战僵尸(二)

news2025/1/1 5:20:22

C 实现植物大战僵尸(二)

前文链接,C 实现植物大战僵尸(一)

五 制作启动菜单

启动菜单函数

void startUI()
{
    IMAGE imageBg, imgMenu1, imgMenu2;
    loadimage(&imageBg, "res/menu.png");
    loadimage(&imgMenu1, "res/menu1.png");
    loadimage(&imgMenu2, "res/menu2.png");
    bool mouseStatus = false; //0 表示鼠标未移动至开始游戏位置
    while (1) 
    {
        BeginBatchDraw();
        putimage(0, 0, &imageBg);
        //根据鼠标是否移动至游戏开始位置, 显示不同的图片
        putimagePNG(UI_LEFT_MARGIN, UI_TOP_MARGIN, mouseStatus ? &imgMenu2 : &imgMenu1);

        ExMessage msg;
        if (peekmessage(&msg)) //监听鼠标事件
        {
            //当鼠标移动至开始游戏位置, 界面高亮
            if (msg.x > UI_LEFT_MARGIN && msg.x < UI_LEFT_MARGIN + UI_WIDTH
                && msg.y > UI_TOP_MARGIN && msg.y < UI_TOP_MARGIN + UI_HIGHT)
            {
                putimagePNG(UI_LEFT_MARGIN, UI_TOP_MARGIN, &imgMenu2);
                //表示鼠标移动至开始游戏位置, 如果一直不移动鼠标则一直高亮
                mouseStatus = true;

                //当鼠标点击时, 进入游戏
                if (msg.message == WM_LBUTTONDOWN)
                    return;
            }
            else mouseStatus = false;
        }
        EndBatchDraw();
    }
}

提醒

不能把 startUI 放在 gameInit 前, gameInit 包含了创建游戏图形窗口

int main()
{
    gameInit(); //不能把 startUI 放在 gameInit 前, gameInit 包含了创建游戏图形窗口
    startUI();
    updateWindow(); //窗口视图展示

    int timer = 0; //用以计时 20 毫秒更新一次
    while (1)
    {
        userClick(); //监听窗口鼠标事件
        timer += getDelay();

        if (timer > 20)
        {
            updateWindow(); //更新窗口视图
            updateGame(); //更新游戏动画帧
            timer = 0;
        }
    }

    system("pause");
    return 0;
}

效果展示

鼠标移动至开始冒险模式时,会变成高亮效果,当点击开始开始冒险模式时,进入游戏

image-20241227115451230

六 创建和显示随机阳光

相关数据结构

//阳光球在飘落过程中 X 坐标不变
typedef struct SunShineBall
{
    int x;              //当前 X 轴坐标
    int y;              //当前 Y 轴坐标
    int frameId;        //当前图片帧编号
    int destination;    //飘落目标位置 Y 坐标
    bool used;          //是否在使用
    int timer;          //统计飘落目标位置后的帧次数
}SunShineBall;
#define MAX_BALLS_NUM 10
#define SUM_SHINE_PIC_NUM 29
SunShineBall balls[MAX_BALLS_NUM];
IMAGE imgSunShineBall[SUM_SHINE_PIC_NUM];

在更新游戏数据的函数中,创建阳光球并且更新阳光球数据

void updateGame() 
{
    for (int i = 0; i < GRASS_GRID_ROW; ++i)
    {
        for (int j = 0; j < GRASS_GRID_COL; ++j)
        {
            if (plants[i][j].type >= 0)
            {
                if (imgPlant[plants[i][j].type][++plants[i][j].frameId] == NULL)
                    plants[i][j].frameId = 0;
            }
        }
    }

    createSunshine();
    updateSunshine();
}

核心实现是 createSunshine (创建阳光球) 和 updateSunshine (更新阳光球数据)

void createSunshine() 
{
    static int callCnt = 0;
    static int randomCallCnt = 400;
    if (callCnt++ >= randomCallCnt) 
    {
        randomCallCnt = 200 + rand() % 200;
        callCnt = 0;

        //从阳光池中取一个可用阳光
        for (int i = 0; i < MAX_BALLS_NUM; ++i)
        {
            //找到一个未使用的阳光, 则进行初始化
            if (!balls[i].used)
            {
                //只允许阳光掉落在草地范围内任意位置
                balls[i].x = GRASS_LEFT_MARGIN + 
                    (rand() % (GRASS_GRID_COL * GRASS_GRID_WIDTH));
                balls[i].y = GRASS_TOP_MARGIN;
                balls[i].frameId = 0;
                //目标点在中间三行
                balls[i].destination = GRASS_TOP_MARGIN + 
                    GRASS_GRID_ROW + (rand() % (3 * GRASS_GRID_HIGHT));
                balls[i].used = true;
                balls[i].timer = 0;
                break;
            }
        }
    }
}

void updateSunshine()
{
    for (int i = 0; i < MAX_BALLS_NUM; ++i) 
    {
        if (balls[i].used)
        {
            if (balls[i].y < balls[i].destination)
            {
                balls[i].y += 2; //每次移动两个像素
                //修改当前图片帧编号, 并在到达 SUM_SHINE_PIC_NUM 时重置图片帧为 0
                balls[i].frameId = ++balls[i].frameId % SUM_SHINE_PIC_NUM;
            }
            else //当阳光下落至目标位置时, 停止移动
            {
                if (balls[i].timer < 100) ++balls[i].timer;
                else balls[i].used = false;
            }
        }
    }
}

在 gameInit 函数中加载阳光图片

void gameInit() 
{
    //加载背景图片
    loadimage(&imgBg, "res/map0.jpg");
    loadimage(&imgBar, "res/bar5.png");

    //加载植物卡片
    char name[64];
    //将二维指针数组内存空间置零
    memset(imgPlant, 0, sizeof(imgPlant));
    memset(plants, -1, sizeof(plants));
    memset(balls, 0, sizeof(balls));

    for (int i = 0; i < PLANT_CNT; ++i)
    {
        //获取植物卡片相对路径名称
        sprintf(name, "res/Cards/card_%d.png", i + 1);
        loadimage(&imgCards[i], name);

        for (int j = 0;i < MAX_PICTURE_NUM; ++j)
        {
            //获取动态植物素材相对路径名称
            sprintf(name, "res/Plants/%d/%d.png", i, j + 1);
            if (fileExist(name)) {
                imgPlant[i][j] = new IMAGE;
                loadimage(imgPlant[i][j], name);
            }
            else break;
        }
    }

    //加载阳光图片
    for (int i = 0; i < SUM_SHINE_PIC_NUM; ++i)
    {
        sprintf(name, "res/sunshine/%d.png", i + 1);
        loadimage(&imgSunShineBall[i], name);
    }

    //配置随机种子
    srand(time(NULL));

    //创建游戏图形窗口
    initgraph(WIN_WIDTH, WIN_HIGHT, 1);
}

在 updateWindow 函数中渲染阳光球

void updateWindow() 
{
    //使用双缓冲, 解决输出窗口闪屏
    BeginBatchDraw();

    //渲染背景图至窗口
    putimage(0, 0, &imgBg);
    putimagePNG(250, 0, &imgBar);

    //渲染植物卡牌
    for (int i = 0;i < PLANT_CNT;++i)
        putimage(PIC_LEFT_MARGIN + i * PIC_WIDTH, 6, &imgCards[i]);

    //渲染种植植物
    for (int i = 0; i < GRASS_GRID_ROW; ++i)
    {
        for (int j = 0; j < GRASS_GRID_COL; ++j) 
        {
            if (plants[i][j].type >= 0)
            {
                putimagePNG(GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5, //微调植物种植位置
                            GRASS_TOP_MARGIN + i * GRASS_GRID_HIGHT + 10,
                            imgPlant[plants[i][j].type][plants[i][j].frameId]);
            }
        }
    }

    //渲染随机阳光
    for (int i = 0; i < MAX_BALLS_NUM; ++i)
    {
        if (balls[i].used) 
            putimagePNG(balls[i].x, balls[i].y, &imgSunShineBall[balls[i].frameId]);
    }

    //渲染当前拖动的植物
    if (currIndex >= 0)
    {
        IMAGE* currImage = imgPlant[currIndex][0];
        putimagePNG(currX - currImage->getwidth() / 2,
            currY - currImage->getheight() / 2, currImage);
    }
    
    EndBatchDraw(); //结束双缓冲
}

效果展示

阳光球会在游戏开始的 400 帧后,开始从随机位置(只能是草坪)下落,之后阳光球会在 200 帧加上一个 200 内随机帧的时间内下落

image-20241227115654402

七 收集阳光并显示阳光值

int sunShineVal = 50; //全局变量阳光值

核心函数

#include <mmsystem.h>
#pragma commet(lib, "winmm.lib")
//加上音效头文件, 如果有 mciSendString 外部符号 ERROR 请按下方链接解决

void collectSunShine(ExMessage* msg)
{
    IMAGE* imgSunShine = NULL;
    for (int i = 0; i < MAX_BALLS_NUM; ++i)
    {
        //阳光球在使用中
        if (balls[i].used)
        {
            //找到对应的阳光球图片
            imgSunShine = &imgSunShineBall[balls[i].frameId];
            //判断鼠标移动的位置是否处于当前阳光球的位置
            if (msg->x > balls[i].x && msg->x < balls[i].x + imgSunShine->getwidth()
                && msg->y > balls[i].y && msg->y < balls[i].y + imgSunShine->getheight()) 
            {
                mciSendString("play res/audio/sunshine.mp3", 0, 0, 0);
                balls[i].used = false;
                sunShineVal += 25;
            }
        }
    }
}

在 userClick 函数中调用,收集阳光值

image-20241227121614596

在 gameInit 函数中设置阳光值字体

void gameInit() 
{
    //加载背景图片
    loadimage(&imgBg, "res/map0.jpg");
    loadimage(&imgBar, "res/bar5.png");

    //加载植物卡片
    char name[64];
    //将二维指针数组内存空间置零
    memset(imgPlant, 0, sizeof(imgPlant));
    memset(plants, -1, sizeof(plants));
    memset(balls, 0, sizeof(balls));

    for (int i = 0; i < PLANT_CNT; ++i)
    {
        //获取植物卡片相对路径名称
        sprintf(name, "res/Cards/card_%d.png", i + 1);
        loadimage(&imgCards[i], name);

        for (int j = 0;i < MAX_PICTURE_NUM; ++j)
        {
            //获取动态植物素材相对路径名称
            sprintf(name, "res/Plants/%d/%d.png", i, j + 1);
            if (fileExist(name)) {
                imgPlant[i][j] = new IMAGE;
                loadimage(imgPlant[i][j], name);
            }
            else break;
        }
    }

    //加载阳光图片
    for (int i = 0; i < SUM_SHINE_PIC_NUM; ++i)
    {
        sprintf(name, "res/sunshine/%d.png", i + 1);
        loadimage(&imgSunShineBall[i], name);
    }

    //配置随机种子
    srand(time(NULL));

    //创建游戏图形窗口
    initgraph(WIN_WIDTH, WIN_HIGHT, 1);

    //设置字体
    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); //设置字体颜色
}

在 updateWindow 函数中渲染阳光值

void updateWindow() 
{
    //使用双缓冲, 解决输出窗口闪屏
    BeginBatchDraw();

    //渲染背景图至窗口
    putimage(0, 0, &imgBg);
    putimagePNG(250, 0, &imgBar);

    //渲染植物卡牌
    for (int i = 0;i < PLANT_CNT;++i)
        putimage(PIC_LEFT_MARGIN + i * PIC_WIDTH, 6, &imgCards[i]);

    //渲染种植植物
    for (int i = 0; i < GRASS_GRID_ROW; ++i)
    {
        for (int j = 0; j < GRASS_GRID_COL; ++j) 
        {
            if (plants[i][j].type >= 0)
            {
                putimagePNG(GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5, //微调植物种植位置
                            GRASS_TOP_MARGIN + i * GRASS_GRID_HIGHT + 10,
                            imgPlant[plants[i][j].type][plants[i][j].frameId]);
            }
        }
    }

    //渲染随机阳光
    for (int i = 0; i < MAX_BALLS_NUM; ++i)
    {
        if (balls[i].used) 
            putimagePNG(balls[i].x, balls[i].y, &imgSunShineBall[balls[i].frameId]);
    }


    //渲染当前拖动的植物
    if (currIndex >= 0)
    {
        IMAGE* currImage = imgPlant[currIndex][0];
        putimagePNG(currX - currImage->getwidth() / 2,
            currY - currImage->getheight() / 2, currImage);
    }

    //渲染阳光值
    char scoreText[8];
    sprintf(scoreText, "%d", sunShineVal);
    outtextxy(277, 67, scoreText);

    EndBatchDraw(); //结束双缓冲
}

效果展示

阳光球在下落过程中,或到达目标点后停留的 100 帧内。若鼠标移动至对应阳光球的位置,则该阳光被收集(会触发对应的音效和左上角阳光值增加 25)

image-20241227120832551

vs 中 mciSendString 添加音效报错无法找到的外部符号

八 创建僵尸并实现行走

相关数据结构

#define MAX_ZOMBIE_NUM 10
#define MAX_ZOMBIE_PIC_NUM 22
typedef struct Zombie {
    int x;              //当前 X 轴坐标
    int y;              //当前 Y 轴坐标
    int frameId;        //当前图片帧编号
    int speed;
    bool used;          //是否在使用
};
Zombie zombies[MAX_ZOMBIE_NUM];
IMAGE imgZombies[MAX_ZOMBIE_PIC_NUM];

在更新游戏数据的函数中,创建僵尸并且更新僵尸数据

void updateGame() 
{
    for (int i = 0; i < GRASS_GRID_ROW; ++i)
    {
        for (int j = 0; j < GRASS_GRID_COL; ++j)
        {
            if (plants[i][j].type >= 0)
            {
                if (imgPlant[plants[i][j].type][++plants[i][j].frameId] == NULL)
                    plants[i][j].frameId = 0;
            }
        }
    }

    createSunshine();
    updateSunshine();
    createZombie();
    updateZombie();
}

核心函数

void createZombie()
{
    //延缓函数调用次数并增加些随机性
    static int zombieCallCnt = 0;
    static int randZombieCallCnt = 500;
    if (zombieCallCnt++ < randZombieCallCnt) return;
    randZombieCallCnt = 300 + rand() % 200;
    zombieCallCnt = 0;

    for (int i = 0; i < MAX_ZOMBIE_NUM; ++i)
    {
        //找一个未在界面的僵尸初始化
        if (!zombies[i].used)
        {
            zombies[i].x = WIN_WIDTH;
            //出现在草地的任意一格上
            zombies[i].y = GRASS_TOP_MARGIN +
                (rand() % GRASS_GRID_ROW) * GRASS_GRID_HIGHT;
            zombies[i].frameId = 0;
            //僵尸的移动速度
            zombies[i].speed = 1;
            zombies[i].used = 1;
            break;
        }
    }
}

void updateZombie() 
{
    //延缓函数调用次数
    static int CallCnt = 0;
    if (++CallCnt < 3) return;
    CallCnt = 0;

    for (int i = 0; i < MAX_ZOMBIE_NUM; ++i)
    {
        if (zombies[i].used)
        {
            //僵尸行走
            zombies[i].x -= zombies[i].speed;
            //僵尸更换图片帧
            zombies[i].frameId = ++zombies[i].frameId % MAX_ZOMBIE_PIC_NUM;
            
            //目前先这样写待优化
            if (zombies[i].x < 170)
            {
                printf("GAME OVER !");
                MessageBox(NULL, "over", "over", 0);
                exit(0);
            }
        }
    }
}

在 gameInit 中加载图片

//加载僵尸图片
for (int i = 0; i < MAX_ZOMBIE_PIC_NUM; ++i)
{
    sprintf(name, "res/zm/0/%d.png", i + 1);
    loadimage(&imgZombies[i], name);
}

在 updateWindow 中渲染僵尸

//渲染僵尸
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i)
{
    if (zombies[i].used) 
    {
        IMAGE* img = &imgZombies[zombies[i].frameId];
        //该位置 + img->getheight(), 因为 zombies[i].y 是草地格子的高度, +5 像素是微调
        putimagePNG(zombies[i].x, zombies[i].y + img->getheight() + 5,
            img);
    }
}

效果展示

僵尸会随机从游戏窗口右边任意草格子产生,并行走至左边房屋处。当僵尸行走至左边房屋处时,游戏将结束,并弹出提示窗口 over ,点击后程序退出

image-20241228105218658

image-20241228105458665

九 实现阳光球飞跃

在阳光球结构体中增加成员

typedef struct SunShineBall
{
    int x;              //当前 X 轴坐标
    int y;              //当前 Y 轴坐标
    int frameId;        //当前图片帧编号
    int destination;    //飘落目标位置 Y 坐标
    bool used;          //是否在使用
    int timer;          //统计飘落目标位置后的帧次数

    float xOffset;      //阳光球飞跃过程中每次 X 轴偏移量
    float yOffset;      //阳光球飞跃过程中每次 Y 轴偏移量
}SunShineBall;

在创建阳光球时进行初始化(有进行 memset 其实不初始化也是 0,仅为了规范)

注意更改 createSunshine 的判断条件,if (!balls[i].used && balls[i].xOffset == 0) 在飞跃状态时不能对其进行初始化

void createSunshine() 
{
    static int sunCallCnt = 0;
    static int randSunCallCnt = 400;
    if (++sunCallCnt < randSunCallCnt) return;
    randSunCallCnt = 200 + rand() % 200;
    sunCallCnt = 0;

    //从阳光池中取一个可用阳光
    for (int i = 0; i < MAX_BALLS_NUM; ++i)
    {
        //找到一个未使用的阳光, 则进行初始化
        if (!balls[i].used && balls[i].xOffset == 0)
        {
            //只允许阳光掉落在草地范围内(不允许左一格)
            balls[i].x = GRASS_LEFT_MARGIN + GRASS_GRID_WIDTH +
                (rand() % GRASS_GRID_COL) * GRASS_GRID_WIDTH;
            balls[i].y = GRASS_TOP_MARGIN;
            balls[i].frameId = 0;
            //目标点在中间三行
            balls[i].destination = GRASS_TOP_MARGIN + 
                GRASS_GRID_HIGHT + (rand() % (3 * GRASS_GRID_HIGHT));
            balls[i].used = true;
            balls[i].timer = 0;
            //对阳光球飞跃过程中的 X, Y 进行初始化
            balls[i].xOffset = 0;
            balls[i].yOffset = 0;
            break;
        }
    }
}

在收集阳光球时,计算阳光球飞跃过程中的 X, Y 偏移量

void collectSunShine(ExMessage* msg)
{
    IMAGE* imgSunShine = NULL;
    for (int i = 0; i < MAX_BALLS_NUM; ++i)
    {
        //阳光球在使用中
        if (balls[i].used)
        {
            //找到对应的阳光球图片
            imgSunShine = &imgSunShineBall[balls[i].frameId];
            //判断鼠标移动的位置是否处于当前阳光球的位置
            if (msg->x > balls[i].x && msg->x < balls[i].x + imgSunShine->getwidth()
                && msg->y > balls[i].y && msg->y < balls[i].y + imgSunShine->getheight()) 
            {
                mciSendString("play res/audio/sunshine.mp3", NULL, 0, NULL);
                balls[i].used = false;
                //计算阳光球飞跃过程中的 X, Y 偏移量
          		const float angle = atan((float)(balls[i].y - 0) / (float)(balls[i].x - 262));
                balls[i].xOffset = 16 * cos(angle);
                balls[i].yOffset = 16 * sin(angle);
            }
        }
    }
}

主要内容,是在更新阳光球游戏数据时,else if (balls[i].xOffset) 需要不断调整 balls[i].xballs[i].y 的值(不断调整阳光球的位置坐标)

void updateSunshine()
{
    for (int i = 0; i < MAX_BALLS_NUM; ++i) 
    {
        if (balls[i].used)
        {
            if (balls[i].y < balls[i].destination)
            {
                balls[i].y += 2; //每次移动两个像素
                //修改当前图片帧编号, 并在到达 SUM_SHINE_PIC_NUM 时重置图片帧为 0
                balls[i].frameId = ++balls[i].frameId % SUM_SHINE_PIC_NUM;
            }
            else //当阳光下落至目标位置时, 停止移动
            {
                if (balls[i].timer < 100) ++balls[i].timer;
                else balls[i].used = false;
            }
        }
        else if (balls[i].xOffset) //阳光球处于飞跃状态
        {
            if (balls[i].y > 0 && balls[i].x > 262)
            {
                //不断调整阳光球的位置坐标
                const float angle = atan((float)(balls[i].y - 0) / (float)(balls[i].x - 262));
                balls[i].xOffset = 16 * cos(angle);
                balls[i].yOffset = 16 * sin(angle);
                balls[i].x -= balls[i].xOffset;
                balls[i].y -= balls[i].yOffset;
            }
            else
            {
                //阳光球飞至计分器位置, 则将 xOffset 置 0, 且加上 25 积分
                balls[i].xOffset = 0;
                balls[i].yOffset = 0;
                sunShineVal += 25;
            }
        }
    }
}

最后不要忘记飞跃阳光球的渲染条件,修改 updateWindow 函数

image-20241228183659124

优化使用 mciSendString 收集阳光球卡顿

方法一好处在于节省资源,不会有线程的频繁创建和销毁;方法二好处是简便(原理同样是开异步线程)

方法一 : 单独开一个线程死循环

static bool isEnd = false;
/* sunShineMusic 加减也可换为使用 mutex */ 
long sunShineMusic = 0;
HANDLE sunShineThread = NULL;

DWORD WINAPI PlayMusic(LPVOID lpParam)
{
    while (1) 
    {
        if (sunShineMusic)
        {
            mciSendString("play res/audio/sunshine.mp3", NULL, 0, NULL);
            InterlockedDecrement(&sunShineMusic);
            /* 这里也可使用异步 notify 的方式 */
            Sleep(100);
        }
        if (isEnd) break;
    }
    return 0;
}

在收集阳光时把 sunShineMusic InterlockedIncrement

void collectSunShine(ExMessage* msg)
{
    IMAGE* imgSunShine = NULL;
    for (int i = 0; i < MAX_BALLS_NUM; ++i)
    {
        //阳光球在使用中
        if (balls[i].used)
        {
            //找到对应的阳光球图片
            imgSunShine = &imgSunShineBall[balls[i].frameId];
            //判断鼠标移动的位置是否处于当前阳光球的位置
            if (msg->x > balls[i].x && msg->x < balls[i].x + imgSunShine->getwidth()
                && msg->y > balls[i].y && msg->y < balls[i].y + imgSunShine->getheight()) 
            {
                InterlockedIncrement(&sunShineMusic);
                balls[i].used = false;
                sunShineVal += 25;
            }
        }
    }
}

程序退出时,把线程资源清除

int main()
{
    gameInit(); //不能把 startUI 放在 gameInit 前, gameInit 包含了创建游戏图形窗口
    startUI();
    updateWindow(); //窗口视图展示

    sunShineThread = CreateThread(NULL, 0,
        (LPTHREAD_START_ROUTINE)PlayMusic, (LPVOID)NULL, 0, NULL);
    int timer = 0; //用以计时 20 毫秒更新一次
    while (1)
    {
        userClick(); //监听窗口鼠标事件
        timer += getDelay();

        if (timer > 20)
        {
            updateWindow(); //更新窗口视图
            updateGame(); //更新游戏动画帧
            timer = 0;
        }
    }
    
    isEnd = true;
    //等待线程退出
    WaitForSingleObject(sunShineThread, INFINITE);
    if (sunShineThread)
    	//关闭线程
        CloseHandle(sunShineThread);

    system("pause");
    return 0;
}

方法二 : 使用 PlaySound API

注意 SND_ASYNC 参数,可参考 API 之 playsound,同样是以异步线程方式去播放音频

void collectSunShine(ExMessage* msg)
{
    IMAGE* imgSunShine = NULL;
    for (int i = 0; i < MAX_BALLS_NUM; ++i)
    {
        //阳光球在使用中
        if (balls[i].used)
        {
            //找到对应的阳光球图片
            imgSunShine = &imgSunShineBall[balls[i].frameId];
            //判断鼠标移动的位置是否处于当前阳光球的位置
            if (msg->x > balls[i].x && msg->x < balls[i].x + imgSunShine->getwidth()
                && msg->y > balls[i].y && msg->y < balls[i].y + imgSunShine->getheight()) 
            {
                PlaySound("res/audio/sunshine.wav", NULL, SND_FILENAME | SND_ASYNC);
                balls[i].used = false;
                sunShineVal += 25;
            }
        }
    }
}

解决 VS 告警太多,屏蔽该告警

例告警序号为 8888

一 直接在代码中添加 #pragma warning(disable:8888)

二 进入项目属性,通过 C/C++ -> Advanced -> Disable Specific Warnings 设置,输入8888 来屏蔽

image-20241228122134738

效果展示

在阳光球下落或落至草地目标点未消失之前,将鼠标移至阳光球上时,阳光球将会飞跃至左上角的计分板,然后阳光值积分会增加 25 (GIF 动图如下)
超过 CSDN 图片大小限制了 。。。感兴趣可以访问 如下链接

https://lucky-1331733286.cos.ap-guangzhou.myqcloud.com/images/202412281852663.gif

遇到的小问题

反三角函数 atan 的函数是浮点数类型(之前没用过),千万不要写成这样 const float angle = atan()(balls[i].y - 0) / (balls[i].x - 262)); ,将会导致阳光球在飞跃过程先在 X 轴平移一段再飞跃

在 debug 时,直接把对应的 xOffset,yOffset,x 和 y 打印了出来,如下

image-20241228133351839

才发现了问题的原因,解决方法就是如上代码,直接强转即可 const float angle = atan((float)(balls[i].y - 0) / (float)(balls[i].x - 262));

原因很好理解,跟数值在计算机中如何存储有关,感兴趣的可以去翻 C 进阶 — 数据在内存中的存储

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

sqlserver镜像设置

本案例是双机热备&#xff0c;只设置主体服务器&#xff08;主&#xff09;和镜像服务器&#xff08;从&#xff09;&#xff0c;不设置见证服务器 设置镜像前先检查是否启用了 主从服务器数据库的 TCP/IP协议 和 RemoteDAC &#xff08;1&#xff09;打开SQL Server配置管理器…

springboot503基于Sringboot+Vue个人驾校预约管理系统(论文+源码)_kaic

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装个人驾校预约管理系统软件来发挥其高效地信息处理的作用&am…

游戏引擎学习第61天

回顾并计划接下来的事情 我们现在的目标是通过创建一个占位符版本的游戏来展示我们所做的工作。这个版本的游戏包含了许多基本要素&#xff0c;目的是快速构建一些东西&#xff0c;进行测试&#xff0c;并观察代码结构的形成。这些代码的实施是为了理解系统如何工作&#xff0…

探索PyTorch:从入门到实践的demo全解析

探索PyTorch:从入门到实践的demo全解析 一、环境搭建:PyTorch的基石(一)选择你的“利器”:安装方式解析(二)步步为营:详细安装步骤指南二、基础入门demo:点亮第一盏灯(一)张量操作:深度学习的“积木”(二)自动求导:模型学习的“幕后英雄”三、数据处理demo:喂饱…

hiprint结合vue2项目实现静默打印详细使用步骤

代码地址是&#xff1a;vue-plugin-hiprint: hiprint for Vue2/Vue3 ⚡打印、打印设计、可视化设计器、报表设计、元素编辑、可视化打印编辑 本地安装包地址&#xff1a;electron-hiprint 发行版 - Gitee.com 1、先安装hipint安装包在本地 2、项目运行npm&#xff08;socket.…

Docker Container 可观测性最佳实践

Docker Container 介绍 Docker Container&#xff08; Docker 容器&#xff09;是一种轻量级、可移植的、自给自足的软件运行环境&#xff0c;它在 Docker 引擎的宿主机上运行。容器在许多方面类似于虚拟机&#xff0c;但它们更轻量&#xff0c;因为它们不需要模拟整个操作系统…

GXUOJ-算法-第二次作业

1.矩阵连&#xff08;链&#xff09;乘 问题描述 GXUOJ | 矩阵连乘 代码解答 #include<bits/stdc.h> using namespace std;const int N50; int m[N][N]; int p[N]; int n;int main(){cin>>n;//m[i][j] 存储的是从第 i 个矩阵到第 j 个矩阵这一段矩阵链相乘的最小…

OpenCV计算机视觉 02 图片修改 图像运算 边缘填充 阈值处理

目录 图片修改&#xff08;打码、组合、缩放&#xff09; 图像运算 边缘填充 ​阈值处理 上一篇文章&#xff1a; OpenCV计算机视觉 01 图像与视频的读取操作&颜色通道 图片修改&#xff08;打码、组合、缩放&#xff09; # 图片打码 import numpy as np a cv2.imre…

不修改内核镜像的情况下,使用内核模块实现“及时”的调度时间片超时事件上报

一、背景 之前的博客 不修改内核镜像的情况下&#xff0c;使用内核模块实现高效监控调度时延-CSDN博客 里&#xff0c;我们讲了不修改内核镜像高效监控每次的调度时延的方法。这篇博客里&#xff0c;我们对于调度时间片也做这么一个不修改内核镜像的改进。关于调度时间片过长的…

Flink定时器

flink的定时器都是基于事件时间&#xff08;event time&#xff09;或事件处理时间&#xff08;processing time&#xff09;的变化来触发响应的。对一部分新手玩家来说&#xff0c;可能不清楚事件时间和事件处理时间的区别。我这里先说一下我的理解&#xff0c;防止下面懵逼。…

使用 OpenCV 绘制线条和矩形

OpenCV 是一个功能强大的计算机视觉库&#xff0c;它不仅提供了丰富的图像处理功能&#xff0c;还支持图像的绘制。绘制简单的几何图形&#xff08;如线条和矩形&#xff09;是 OpenCV 中常见的操作。在本篇文章中&#xff0c;我们将介绍如何使用 OpenCV 在图像上绘制线条和矩形…

【Artificial Intelligence篇】AI 前沿探秘:开启智能学习的超维征程

目录 一、人工智能的蓬勃发展与智能学习的重要性: 二、数据的表示与处理 —— 智能学习的基石: 三、构建一个简单的感知机模型 —— 智能学习的初步探索: 四、神经网络 —— 开启超维征程的关键一步: 五、超维挑战与优化 —— 探索智能学习的深度: 六、可视化与交互 —— …

大数据的尽头是数据中台吗?

大数据的尽头是数据中台吗&#xff1f; 2018年末开始&#xff0c;原市场上各种关于大数据平台的招标突然不见&#xff0c;取而代之的是数据中台项目&#xff0c;建设数据中台俨然成为传统企业数字化转型首选&#xff0c;甚至不少大数据领域的专家都认为&#xff0c;数据中台是…

珞珈一号夜光遥感数据地理配准,栅格数据地理配准

目录 一、夜光数据下载&#xff1a; 二、夜光遥感数据地理配准 三、计算夜光数据值 四、辐射定标 五、以表格显示分区统计 五、结果验证 夜光数据位置和路网位置不匹配&#xff0c;虽然都是WGS84坐标系&#xff0c;不匹配&#xff01;&#xff01;&#xff01;不要看到就直接…

3.若依前端项目拉取、部署、访问

因为默认RuoYi-Vue是使用的Vue2,所以需要另外去下载vue3来部署。 拉取代码 git clone https://gitee.com/ys-gitee/RuoYi-Vue3.git 安装node才能执行npm相关的命令 执行命令npm install 如果npm install比较慢的话&#xff0c;需要添加上国内镜像 npm install --registrhttp…

【Java】线程相关面试题 (基础)

文章目录 线程与进程区别并行与并发区别解析概念含义资源利用执行方式应用场景 创建线程线程状态如何保证新建的三个线程按顺序执行wait方法和sleep方法的不同所属类和使用场景方法签名和参数说明调用wait方法的前提条件被唤醒的方式与notify/notifyAll方法的协作使用示例注意事…

手机租赁平台开发全攻略打造高效便捷的租赁服务系统

内容概要 手机租赁平台开发&#xff0c;简单说就是让用户能轻松租赁各类手机的高效系统。这一平台不仅帮助那些想要临时使用高端手机的人们节省了不少资金&#xff0c;还为商家开辟了新的收入渠道。随着智能手机的普及&#xff0c;很多人并不需要长期拥有一部手机&#xff0c;…

【视觉惯性SLAM:十一、ORB-SLAM2:跟踪线程】

跟踪线程是ORB-SLAM2的核心之一&#xff0c;其主要任务是实时跟踪相机的位姿变化和场景的变化&#xff0c;以维持地图的更新和相机轨迹的估计。ORB-SLAM2的跟踪线程通过多种方式&#xff08;参考关键帧跟踪、恒速模型跟踪、重定位跟踪、局部地图跟踪&#xff09;处理跟踪丢失、…

浙江肿瘤医院病理库存储及NAS共享存储(磁盘阵列)方案-Infortrend普安科技

Infortrend金牌代理-燊通智联信息科技发展&#xff08;上海&#xff09;有限公司与院方多轮沟通&#xff0c;详细讨论性能与容量要求&#xff0c;最终决定采用GSe统一存储设备&#xff0c;与现有病理系统服务器无缝对接&#xff0c;每台设备配1.92T SSD作缓存加速原数据读写&am…

解决GPT公式复制到Word之后乱码问题

chat辅助确实很有用。不论是出文稿还是代码。如何把chatgpt中的公式直接复制到word中且保持原样格式呢&#xff1f;下面的方法经过我的验证确实好用&#xff0c;成功解决了最近的论文报告写公式的问题。 一、首先复制chatgpt里面的公式 二、粘贴在下面网站 网站&#xff1a;Mat…