跳转至

03-碰撞检测与音乐播放

1 敌人与玩家碰撞

首先在Enemy类中编写子弹与敌人的碰撞检测

bool CheckBulletCollision(const Bullet& bullet)
{
    bool is_overlap_x = bullet.position.x >= position.x && bullet.position.x <= position.x + FRAME_WIDTH;
    bool is_overlap_y = bullet.position.y >= position.y && bullet.position.y <= position.y + FRAME_HEIGHT;
    return is_overlap_x && is_overlap_y;
}

然后是玩家与敌人的碰撞检测。原理是先找到敌人的监测点check_point,然后检查这个点是否在玩家的图片内。
bool CheckPlayerCollision(const Player& player)
{
    POINT check_point = {
        position.x + FRAME_WIDTH / 2,
        position.y + FRAME_HEIGHT / 2};
    const POINT &player_pos = player.GetPosition();
    bool is_overlap_x = check_point.x >= player_pos.x && check_point.x <= player_pos.x + player.FRAME_WIDTH;
    bool is_overlap_y = check_point.y >= player_pos.y && check_point.y <= player_pos.y + player.FRAME_HEIGHT;

    return is_overlap_x && is_overlap_y;
}

为此我们需要编写Player类的GetPosition方法
const POINT& GetPosition() const
{
    return position;
}

同时把Player的图片大小作为公开的变量
public:
    const int FRAME_WIDTH = 80;
    const int FRAME_HEIGHT = 80;

之后在帧循环中遍历所有的敌人,与玩家之间进行碰撞检测
// 检测敌人和玩家的碰撞
for(Enemy* enemy : enemy_list) {
    if(enemy->CheckPlayerCollision(player)) {
        MessageBoxW(GetHWnd(), L"扣“1”观看战败CG", L"游戏结束", MB_OK);
        running = false;
        break;
    }
}

如果检测到敌人与玩家的碰撞则弹出结束的弹窗。如果使用教程中提供的代码,
MessageBox(GetHWnd(), _T("扣“1”观看战败CG"), _T(L"游戏结束"), MB_OK);

在我的开发环境中弹窗中的中文会显示为乱码,

弹窗乱码,是 MinGW/MSYS2 环境下字符编码不匹配 导致的。在 Windows 上,MessageBox_T() 宏会根据项目的 Unicode 设置,将字符串转换为宽字符(UTF-16)或多字节字符(如 GBK),而 MinGW 默认的编码处理方式容易导致中文显示异常。

于是我修改为下述代码。直接调用 MessageBoxW,并使用 L"" 前缀明确指定宽字符串,避免 _T() 宏的编码歧义。

MessageBoxW(GetHWnd(), L"扣“1”观看战败CG", L"游戏结束", MB_OK);

其实更推荐的解决方法是在 CMakeLists.txt 或编译命令中添加:
add_compile_options(-finput-charset=UTF-8 -fexec-charset=GBK)

这告诉编译器,输入源文件是 UTF-8,输出可执行文件中的字符串是 GBK,与 Windows 系统默认编码一致。

2 子弹的显示和移动

首先在main函数中定义子弹集合

std::vector<Bullet> bullet_list(3);

编写更新子弹位置的函数
void UpdateBullets(std::vector<Bullet>& bullet_list, const Player& player)
{
    const double RADIAL_SPEED = 0.0045; //径向波动速度
    const double TANGENT_SPEED = 0.0055;    //切向速度
    double radian_interval = 2 * 3.14159 / bullet_list.size();
    POINT player_position = player.GetPosition();
    double radius = 100 + 25 * sin(GetTickCount() * RADIAL_SPEED);
    for (size_t i = 0; i<bullet_list.size(); i++) {
        double radian = GetTickCount() * TANGENT_SPEED + radian_interval * i;
        bullet_list[i].position.x = player_position.x + player.FRAME_WIDTH / 2 + (int)(radius * cos(radian));
        bullet_list[i].position.y = player_position.y + player.FRAME_HEIGHT / 2 + (int)(radius * sin(radian));
    }
}

在帧循环中调用这个函数
UpdateBullets(bullet_list, player);

遍历所有子弹,并调用Draw方法
for(const Bullet& bullet : bullet_list) {
    bullet.Draw();
}

3 敌人存活控制

首先在Enemy类中定义标记敌人存活状态的私有变量

bool alive = true;

编写相应的函数
void Hurt()
{
    alive = false;
}

bool CheckAlive()
{
    return alive;
}

之后在帧循环中添加碰撞检测和清除敌人的逻辑

//检测子弹和敌人的碰撞
for( Enemy* enemy : enemy_list)
{
    for(const Bullet& bullet : bullet_list)
    {
        if (enemy->CheckBulletCollision(bullet)) 
        {
            enemy->Hurt();
        }
    }
}

//移除生命值归零的敌人
for (size_t i = 0; i<enemy_list.size(); i++) {
    Enemy *enemy = enemy_list[i];
    if(!enemy->CheckAlive()) {
        std::swap(enemy_list[i], enemy_list.back());
        enemy_list.pop_back();
        delete enemy;
    }
}

4 得分系统

定义score变量

int score = 0;

子弹和敌人碰撞时加分

//检测子弹和敌人的碰撞
for( Enemy* enemy : enemy_list)
{
    for(const Bullet& bullet : bullet_list)
    {
        if (enemy->CheckBulletCollision(bullet)) 
        {
            enemy->Hurt();
            score ++;
        }
    }
}

游戏结束的弹窗中需要显示玩家的得分
// 检测敌人和玩家的碰撞
for(Enemy* enemy : enemy_list) {
    if(enemy->CheckPlayerCollision(player)) {
        static wchar_t text[128];
        swprintf_s(text, _countof(text), L"最终得分: %d", score);
        MessageBoxW(GetHWnd(), text, L"游戏结束", MB_OK);
        running = false;
        break;
    }
}

另外,我们在画面的左上角绘制显示玩家得分的文字信息
// 绘制玩家得分
void DrawPlayerScore(int score)
{
    static TCHAR text[64];
    _stprintf_s(text, _T("当前玩家得分: %d"), score);

    setbkmode(TRANSPARENT);
    settextcolor(RGB(255, 85, 185));
    outtextxy(10, 10, text);
}

在帧循环中调用这个函数
DrawPlayerScore(score);

5 音频播放

5.1 播放背景音乐

我们使用一种最简单轻巧的音乐播放实现方式,mciSendString
在代码中链接对应的库

#pragma comment(lib, "Winmm.lib")
#pragma comment(lib, "MSIMG32.LIB")

#pragma comment(lib, ...)是 C/C++ 中的一种预处理指令,用于在代码中指定链接器需要加载的库文件。这种方式可以简化项目配置,避免手动在 IDE 中添加依赖库。

但是#pragma comment(lib, ...)MSVC(Visual Studio 编译器)特有的编译指令,CMake 作为跨平台构建工具,并不会解析代码中的这个指令,也无法通过它自动链接对应的库文件。如果你仅依赖这个指令,在 MinGW/MSYS2 等非 MSVC 环境下编译会直接报 “未定义引用” 错误。

使用cmake的话,就要添加下面的语句

# 核心:链接Winmm库和MSIMG32库
# 对于Windows平台,直接用库名(无需后缀.lib)
if(WIN32)
    # 链接Winmm库(对应Winmm.lib)
    target_link_libraries(${PROJECT_NAME} PRIVATE winmm)
    # 链接MSIMG32库(对应MSIMG32.LIB)
    target_link_libraries(${PROJECT_NAME} PRIVATE msimg32)
endif()

注意在给 EasyX_test 这个目标使用 target_link_libraries 时,不能混用两种不同的语法格式(plain 纯列表格式和 keyword 关键字格式)。

# 纯列表格式(plain)
# 直接列出要链接的库,CMake 会默认按「库依赖」逻辑处理:
target_link_libraries(EasyX_test easyx opengl32)
# 关键字格式(keyword)
# (带 PRIVATE/PUBLIC/INTERFACE):显式指定依赖的传播范围,是现代 CMake 推荐的写法:
target_link_libraries(EasyX_test PRIVATE easyx opengl32)

\[ \underbrace{\text{mci}}_{\text{Media Control Interface,媒体控制接口}}\text{SendString} \]

mciSendString function (Windows) | Microsoft Learn

MCIERROR mciSendString(
   LPCTSTR lpszCommand,
   LPTSTR  lpszReturnString,
   UINT    cchReturn,
   HANDLE  hwndCallback
);

下述代码的含义就是把指定的bgm.mp3文件加载到程序中,并且在后续的程序中我们把这个音乐取名为bgm
mciSendString(_T("open mus/bgm.mp3 alias bgm"), NULL, 0, NULL);

在我的环境中需要写成
mciSendString(_T("open ..\\mus\\bgm.mp3 alias bgm"), NULL, 0, NULL);

当我们需要播放bgm时,我们只需要写下下面这行代码。这样就是和Windows系统说,我要播放先前以及已经准备好的名字为bgm的音乐,并让它从开头处循环播放。
mciSendString(_T("play bgm repeat from 0"), NULL, 0, NULL);

把这两行代码写在main函数的开头就能播放bgm了。

5.2 播放敌人受击音效

mciSendString(_T("open ..\\mus\\hit.mp3 alias bgm"), NULL, 0, NULL);

当子弹和敌人发生碰撞时便从头开始播放受击音效
mciSendString(_T("play hit from 0"), NULL, 0, NULL);

评论

如果你已登录 GitHub,就可以直接在这里评论。