游戏项目 多态练习 超级玛丽demo8

噩梦难度警告!!!!

虚析构

保证多态场景下,子类对象通过父类指针销毁时,资源能被完整释放

new 的时候 是创建的子类 类型,所以会先执行 父类构造 再 执行子类构造

而 delete的时候 是用父类指针 来操作的 所以只会调用 父类析构。

这里就可能出现子类的内存泄漏

抽象类

只要有一个纯虚函数,类就成为抽象类。

抽象类 不允许实例化,否则会报错。

继承抽象类 的 子类必须把抽象函数 全部实现,否则子类也是抽象类。

内联函数

内联函数(Inline Function)是 C++ 中用于提升函数调用效率的特性,其核心思想是在编译阶段将函数体直接嵌入到每一处调用该函数的地方,从而避免普通函数调用时的压栈、跳转、返回等额外开销。

为什么需要内联函数?

普通函数调用时,程序需要执行一系列操作:保存当前执行状态(压栈)、跳转到函数地址、执行函数体、恢复之前的状态(出栈)并返回。这些操作对于频繁调用的短小函数(比如简单的加法、获取成员变量等)来说,额外开销可能远大于函数体本身的执行成本,导致效率浪费。

内联函数通过 "原地展开" 函数体,消除了这些调用开销,从而提升程序运行效率。

适用场景

内联函数适用于代码量少、被频繁调用的函数(如工具类函数、简单的成员变量访问器等)。对于复杂函数(如包含循环、多分支),即使声明为内联,编译器也可能不采纳,反而徒增代码体积。

加个 inline就可以

可以避免 入栈出栈的开销。

设计模式

链表

大一新手村都学过,但是实践最多就是 放几个数字 搞搞遍历,没有在项目中使用过,这里我们将在管理类里面使用俩表,把被管理的不同对象 全部储存在链表中。

而且这玩意 又现成写好的东西,我们直接用

#include<list>

选中变量名字 ctrl + f 可以在当前文档中 全部替换

超级玛丽demo8

之前我们的demo7 没有使用类,很多功能看起来非常乱。结构体的使用 也没有规范化

记得勾选当前文档,默认是所选内容。

CHero类 抽象类 父类

Mario 类子类

用枚举 或者 宏 会更清晰。

有时候 我们会自己看源码,但这些代码往往晦涩难懂,但这就是一种能力,学会看别人的代码。在企业里面上班,有很多东西要学,其中最重要的就是看比你厉害的程序员写的代码,我们可以借助ai 帮我们写注释 帮我们整理代码,去理解 去自己重做一遍,今天我们就来试试看。

使用ai帮助我们学习 编程 叫做vibe coding 是每个程序员的必备技能!!!

直接上传整个文件 带上深度思考。

豆包没有让我失望,果然字节的实力可以,豆包就能做这么多功能 我还说要用trae 试试看呢。

一、基础数据结构模块

Myrect.h

定义矩形 就是左上角坐标 加上wh 然后声明一个碰撞函数,参数是另一个矩形的地址。

MyRect.cpp

cpp 复制代码
#include "MyRect.h"  // 包含对应头文件

// 碰撞检测逻辑:判断两个矩形是否重叠
bool CMyRect::Collider(CMyRect* r)
{
    // 入参校验:若对方矩形无效(空指针或宽高<=0),则不碰撞
    if(r == 0 || r->m_w < 1 || r->m_h < 1 || m_w < 1 || m_h < 1)
        return false;
    
    // 计算当前矩形的左、右、上、下边界
    int L1 = m_x;                  // 左边界 = x坐标
    int R1 = m_x + m_w - 1;        // 右边界 = x + 宽度 - 1(因为包含自身像素)
    int T1 = m_y;                  // 上边界 = y坐标
    int B1 = m_y + m_h - 1;        // 下边界 = y + 高度 - 1

    // 计算对方矩形的边界
    int L2 = r->m_x;
    int R2 = r->m_x + r->m_w - 1;
    int T2 = r->m_y;
    int B2 = r->m_y + r->m_h - 1;

    // 碰撞条件:两个矩形不重叠的4种情况(当前左>对方右、当前右<对方左、当前上>对方下、当前下<对方上)
    // 若满足任意一种不重叠情况,则返回false(不碰撞),否则返回true(碰撞)
    if (L1 > R2 || R1 < L2 || T1 > B2 || B1 < T2)
        return false;
    return true;
}

这个函数 很简单 就是计算当前矩形的 上下左右边界(直接用 .h文件中的变量),计算得到的矩形的上下左右边界 这里要用-> 解引用+访问成员。

然后判断就好了。

碰撞了 返回true 没有碰撞 返回false。

二、游戏绘图模块

GameDraw.h

cpp 复制代码
#pragma once
#include <map>  // 用于存储图片资源的映射表

// 图片结构体:存储单张图片的宽、高和像素数据
struct BMP
{
    int w;       // 图片宽度(像素数)
    int h;       // 图片高度(像素数)
    char* _map;  // 像素数据数组(每个元素对应一个索引,关联到纹理集)
};

class CGD  // 游戏绘图类(负责图片管理、绘制、屏幕刷新)
{
    std::map<const char*, BMP> bmpmap;  // 图片资源映射表(键:图片名称,值:BMP结构体)

    char _ts[128];  // 纹理集(存储实际显示的字符,每个图片索引对应纹理集中的2个字符)
    int _tslen;     // 纹理集长度(图片索引的最大数量)
    char pint[(40 * 2 + 1) * 40 + 1];  // 输出缓冲区(存储最终要显示的字符,含换行)
    
    int client[1600];  // 屏幕像素缓冲区(max_w*max_h=40*40=1600,存储每个位置的图片索引)
    int max_w;  // 最大宽度(40)
    int max_h;  // 最大高度(40)
    int _w;     // 当前屏幕宽度(可通过SetWH设置)
    int _h;     // 当前屏幕高度(可通过SetWH设置)

public:
    ~CGD();  // 析构函数:释放图片资源
    void Init();  // 初始化:设置默认值
    void Begin();  // 开始绘制:清空屏幕缓冲区
    void End();    // 结束绘制:将缓冲区内容输出到屏幕
    bool SetTs(const char* ts);  // 设置纹理集
    bool Draw(const char* key, int x ,int y);  // 绘制图片(根据key在(x,y)位置绘制)
    bool SetWH(int w, int h);  // 设置屏幕宽高(需在max_w和max_h范围内)
    bool AddBmp(const char* key, int w, int h, const char* map);  // 添加图片资源
};

extern CGD g_gd;  // 全局绘图对象(整个程序共享)

这是最复杂的模块 ,好在之前我们已经写过一遍,不过这里我们把结构体都换成了类。

这个模块里有两个类 一个是 BMP 图片类 一个是 CGD绘制类

1 BMP很简单 就是储存一下 图片的宽高 像素数组的地址。

2 在绘制类里面 我们先用了 红黑树映射表

cpp 复制代码
 std::map<const char*, BMP> bmpmap;  // 图片资源映射表(键:图片名称,值:BMP结构体)

这个的作用就是 给图片起个名字 通过名字就可以使用图片了,更加清晰了,地面 墙 箱子 坦克飞机 比什么BMP1 BMP2 好用多了。

cpp 复制代码
 char _ts[128];  // 纹理集(存储实际显示的字符,每个图片索引对应纹理集中的2个字符)
    int _tslen;     // 纹理集长度(图片索引的最大数量)
    char pint[(40 * 2 + 1) * 40 + 1];  // 输出缓冲区(存储最终要显示的字符,含换行)

因为这个绘制类是个大类 我们在里面会放很多 整个程序中只用一次的东西。

比如这里 图素集 给了128个字节 可以放64个图素 还有图素集的大小。

由于我们是 缓冲绘制,这里相当于 让我们有一个40*40的缓冲区 去一次性绘制整个屏幕。

cpp 复制代码
    int client[1600];  // 屏幕像素缓冲区(max_w*max_h=40*40=1600,存储每个位置的图片索引)
    int max_w;  // 最大宽度(40)
    int max_h;  // 最大高度(40)
    int _w;     // 当前屏幕宽度(可通过SetWH设置)
    int _h;     // 当前屏幕高度(可通过SetWH设置)

这里还定义了client 客户端屏幕的大小 40*40 = 1600 貌似目前也是最大的世界大小。

cpp 复制代码
  int max_w;  // 最大宽度(40)
    int max_h;  // 最大高度(40)
    int _w;     // 当前屏幕宽度(可通过SetWH设置)
    int _h;     // 当前屏幕高度(可通过SetWH设置)

这里还另外定义了一个 _w 和 _h 我们可以任意定义 当前屏幕的大小, 但是要注意这一点 在其他函数定义的时候 使用的是_w _h draw()函数编写时候 要注意这一点。

cpp 复制代码
public:
    ~CGD();  // 析构函数:释放图片资源
    void Init();  // 初始化:设置默认值
    void Begin();  // 开始绘制:清空屏幕缓冲区
    void End();    // 结束绘制:将缓冲区内容输出到屏幕
    bool SetTs(const char* ts);  // 设置纹理集
    bool Draw(const char* key, int x ,int y);  // 绘制图片(根据key在(x,y)位置绘制)
    bool SetWH(int w, int h);  // 设置屏幕宽高(需在max_w和max_h范围内)
    bool AddBmp(const char* key, int w, int h, const char* map);  // 添加图片资源

这里的析构函数 我们要析构 之前设置的BMP中 所有的map 用堆储存的纹理。

std::map<const char*, BMP> bmpmap

记住我们是自己组出来的 一个红黑树 管理的 名字 + BMP 集合。

这玩意 使用的时候 会开堆 来储存图素,这个东西之前有讲,因为我们要图片有多大 就申请多大的内存,所以使用了堆,最开始我们是用的数组 会浪费内存 没记错的话这应该是 3.0 版本的 BMP

析构的时候就要析构掉。

所以我们要遍历 map 红黑树中所有的 BMP 来析构掉。

创建迭代器 可以使用for循环 结合 begin() end() 来遍历

cpp 复制代码
CGD::~CGD()
{
    // 遍历图片映射表
    std::map<const char*, BMP>::iterator it;
    for (it = bmpmap.begin(); it != bmpmap.end(); ++it)
    {
        // 释放像素数组(无论大小都删除,避免内存泄漏)
        if(it->second.w * it->second.h > 1)
            delete[] it->second._map;  // 数组形式删除
        else
            delete it->second._map;    // 单个元素删除(语法兼容)
    }    
}

这里有个麻烦点在于,delete 这玩意要知道是一个还是多个 所以我们要计算一下 w*h

看用 delete[] 还是 delete

cpp 复制代码
 void Init();  // 初始化:设置默认值
cpp 复制代码
// 初始化:设置默认参数
void CGD::Init()
{
    _tslen = 0;          // 纹理集长度初始化为0
    _ts[0] = "  "[0];    // 纹理集默认字符(空格)
    _ts[1] = "  "[1];
    _w = 0;              // 当前屏幕宽高初始化为0
    _h = 0;
    max_w = 40;          // 最大宽高固定为40
    max_h = 40;
}

初始化数据,其中 把纹理编号0 代表空格 给默认完成

这里空格 要用两字节 可以参考之前了

cpp 复制代码
void Begin();  // 开始绘制:清空屏幕缓冲区
cpp 复制代码
// 开始绘制:清空屏幕缓冲区(所有位置设为0,即透明)
void CGD::Begin()
{
    for (int i = 0; i < _h; ++i)  // 遍历每一行
    {
        for (int j = 0; j < _w; ++j)  // 遍历每一列
        {
            int ind = j + i * max_w;  // 计算索引
            client[ind] = 0;          // 清空为0
        }
    }
}

把所有索引编码 都清空清空为0,我认为 这里应该 是把 h_max 和 w_max 都清空会更好一点,让整个地图都清空而不是 眼前的屏幕

豆包也是这样说的。

cpp 复制代码
  void End();    // 结束绘制:将缓冲区内容输出到屏幕
cpp 复制代码
// 结束绘制:将缓冲区内容转换为字符串并输出到屏幕
void CGD::End()
{
    int len = 0;  // 输出缓冲区长度计数
    for (int i = 0; i < _h; ++i)  // 遍历每一行
    {
        for (int j = 0; j < _w; ++j)  // 遍历每一列
        {
            int ind = j + i * max_w;  // 屏幕缓冲区索引
            // 根据像素索引从纹理集中取2个字符,存入输出缓冲区
            pint[len++] = _ts[client[ind] * 2];
            pint[len++] = _ts[client[ind] * 2 + 1];
        }
        pint[len++] = '\n';  // 每行结束添加换行符
    }
    pint[len] = 0;  // 字符串结束符
    system("cls");  // 清屏
    std::cout << pint;  // 输出最终画面
}

print的屏幕缓冲区 我们计算好了的 不过上面 初始化的时候 我们是按照最大缓冲区来设计的。其实我们可以和 demo7一样 设置一个大一点的 地图,这里w_max 和 h_max 只是屏幕的最大的大小 而不是世界。

cpp 复制代码
 bool SetTs(const char* ts);  // 设置纹理集
cpp 复制代码
// 设置纹理集(纹理集是字符串,每个图片索引对应2个字符)
bool CGD::SetTs(const char* ts)
{
    if(ts == nullptr)  // 入参校验:纹理集为空则失败
        return false;
    int i = 0;
    while (1)
    {
        // 终止条件:遇到字符串结束符或剩余字符不足2个(因为每个索引对应2个字符)
        if (ts[i] == 0 || ts[i+1] == 0)
            break;
        _tslen += 1;  // 纹理集长度+1(增加一个索引)
        // 存储当前索引对应的2个字符
        _ts[_tslen * 2] = ts[i++];
        _ts[_tslen * 2 + 1] = ts[i++];
    }
    return true;
}

这个没啥好讲的 和 demo7 没有区别。

cpp 复制代码
 bool Draw(const char* key, int x ,int y);  // 绘制图片(根据key在(x,y)位置绘制)
cpp 复制代码
// 绘制图片:根据key找到图片,在(x,y)位置绘制
bool CGD::Draw(const char* key, int x, int y)
{
    if (key == 0)  // 入参校验:图片key为空则失败
        return false;

    // 在图片映射表中查找key对应的图片
    std::map<const char*, BMP>::iterator it;
    it = bmpmap.find(key);

    if (it == bmpmap.end())  // 未找到图片则失败
        return false;
    
    BMP* p = &it->second;  // 获取图片信息
    int s = p->w * p->h;   // 图片总像素数(宽*高)
    
    // 遍历图片的每个像素,绘制到屏幕缓冲区
    for (int i = 0; i < s; ++i)
    {
        // 只绘制在屏幕范围内的像素(x,y在0~_w-1和0~_h-1之间)
        if (x >= 0 && x < _w && y >= 0 && y < _h)
        {
            int ind = x + y * max_w;  // 计算屏幕缓冲区索引(行优先:y行x列)
            if(p->_map[i] != 0)       // 若像素值不为0,则更新缓冲区(0表示透明)
                client[ind] = p->_map[i];
        }
        // 更新下一个像素的坐标
        x += 1;                  // 同一行内x递增
        if (i % p->w == p->w - 1)  // 到达行尾时(i是当前行最后一个像素)
        {
            y += 1;              // 行数+1(换行)
            x -= p->w;           // x重置为行首(减去宽度)
        }
    }
    
    return true;
}

这里是在 的地带其中 查找对应BMP

cpp 复制代码
 it = bmpmap.find(key);

要使用的时候 直接用 BMP* p = &(t->second);

就可以操作 这个BMP了 拿到对应的宽高

其他的绘制 和之前一样 注意透明效果 遇到的不是0的BMP纹理 才绘制。

cpp 复制代码
 bool SetWH(int w, int h);  // 设置屏幕宽高(需在max_w和max_h范围内)
cpp 复制代码
bool CGD::SetWH(int w, int h)
{
    if (w < 1 || w > max_w || h < 1 || h > max_h)
        return false;  // 超出范围则失败
    _w = w;
    _h = h;
    return true;
}

最简单的函数了,不多说。

cpp 复制代码
 bool AddBmp(const char* key, int w, int h, const char* map);  // 添加图片资源
cpp 复制代码
// 添加图片资源到映射表
bool CGD::AddBmp(const char* key, int w, int h, const char* map)
{
    // 入参校验:key、宽高、像素数据无效则失败
    if (key == 0 || w < 1 || h < 1 || map == 0)
        return false;
    
    // 检查key是否已存在(避免重复添加)
    std::map<const char*, BMP>::iterator it;
    it = bmpmap.find(key);
    if (it != bmpmap.end())
        return false;

    // 构造BMP对象并复制像素数据
    BMP bmp;
    bmp.h = h;
    bmp.w = w;
    int s = w * h;
    bmp._map = new char[s];  // 分配像素数组内存
    for (int i = 0; i < s; ++i)
        bmp._map[i] = map[i];  // 复制像素数据
    
    // 添加到映射表
    bmpmap.insert(std::pair<const char*, BMP>(key, bmp));
    return true;
}

这里添加BMP的时候 先要创建好 map 和 w h

_map 就是BMP里的 const char*,这是记录数组首地址的

我们在使用的时候就要先把图片手写好再把首地址传进去。

这玩意是开堆的 释放实在 这个类析构的时候 释放掉。

这里 我们想把这个类定义出来 整个程序都使用 所以在头文件的时候就要导入出来。

下面是cpp文件的完整代码:

GameDraw.cpp

cpp 复制代码
#include "GameDraw.h"
#include <iostream>  // 用于屏幕输出

CGD g_gd;  // 定义全局绘图对象

// 析构函数:释放所有图片的像素数据
CGD::~CGD()
{
    // 遍历图片映射表
    std::map<const char*, BMP>::iterator it;
    for (it = bmpmap.begin(); it != bmpmap.end(); ++it)
    {
        // 释放像素数组(无论大小都删除,避免内存泄漏)
        if(it->second.w * it->second.h > 1)
            delete[] it->second._map;  // 数组形式删除
        else
            delete it->second._map;    // 单个元素删除(语法兼容)
    }    
}

// 初始化:设置默认参数
void CGD::Init()
{
    _tslen = 0;          // 纹理集长度初始化为0
    _ts[0] = "  "[0];    // 纹理集默认字符(空格)
    _ts[1] = "  "[1];
    _w = 0;              // 当前屏幕宽高初始化为0
    _h = 0;
    max_w = 40;          // 最大宽高固定为40
    max_h = 40;
}

// 设置纹理集(纹理集是字符串,每个图片索引对应2个字符)
bool CGD::SetTs(const char* ts)
{
    if(ts == nullptr)  // 入参校验:纹理集为空则失败
        return false;
    int i = 0;
    while (1)
    {
        // 终止条件:遇到字符串结束符或剩余字符不足2个(因为每个索引对应2个字符)
        if (ts[i] == 0 || ts[i+1] == 0)
            break;
        _tslen += 1;  // 纹理集长度+1(增加一个索引)
        // 存储当前索引对应的2个字符
        _ts[_tslen * 2] = ts[i++];
        _ts[_tslen * 2 + 1] = ts[i++];
    }
    return true;
}

// 绘制图片:根据key找到图片,在(x,y)位置绘制
bool CGD::Draw(const char* key, int x, int y)
{
    if (key == 0)  // 入参校验:图片key为空则失败
        return false;

    // 在图片映射表中查找key对应的图片
    std::map<const char*, BMP>::iterator it;
    it = bmpmap.find(key);

    if (it == bmpmap.end())  // 未找到图片则失败
        return false;
    
    BMP* p = &it->second;  // 获取图片信息
    int s = p->w * p->h;   // 图片总像素数(宽*高)
    
    // 遍历图片的每个像素,绘制到屏幕缓冲区
    for (int i = 0; i < s; ++i)
    {
        // 只绘制在屏幕范围内的像素(x,y在0~_w-1和0~_h-1之间)
        if (x >= 0 && x < _w && y >= 0 && y < _h)
        {
            int ind = x + y * max_w;  // 计算屏幕缓冲区索引(行优先:y行x列)
            if(p->_map[i] != 0)       // 若像素值不为0,则更新缓冲区(0表示透明)
                client[ind] = p->_map[i];
        }
        // 更新下一个像素的坐标
        x += 1;                  // 同一行内x递增
        if (i % p->w == p->w - 1)  // 到达行尾时(i是当前行最后一个像素)
        {
            y += 1;              // 行数+1(换行)
            x -= p->w;           // x重置为行首(减去宽度)
        }
    }
    
    return true;
}

// 开始绘制:清空屏幕缓冲区(所有位置设为0,即透明)
void CGD::Begin()
{
    for (int i = 0; i < _h; ++i)  // 遍历每一行
    {
        for (int j = 0; j < _w; ++j)  // 遍历每一列
        {
            int ind = j + i * max_w;  // 计算缓冲区索引
            client[ind] = 0;          // 清空为0
        }
    }
}

// 结束绘制:将缓冲区内容转换为字符串并输出到屏幕
void CGD::End()
{
    int len = 0;  // 输出缓冲区长度计数
    for (int i = 0; i < _h; ++i)  // 遍历每一行
    {
        for (int j = 0; j < _w; ++j)  // 遍历每一列
        {
            int ind = j + i * max_w;  // 屏幕缓冲区索引
            // 根据像素索引从纹理集中取2个字符,存入输出缓冲区
            pint[len++] = _ts[client[ind] * 2];
            pint[len++] = _ts[client[ind] * 2 + 1];
        }
        pint[len++] = '\n';  // 每行结束添加换行符
    }
    pint[len] = 0;  // 字符串结束符
    system("cls");  // 清屏
    std::cout << pint;  // 输出最终画面
}

// 设置屏幕宽高(需在1~max_w和1~max_h范围内)
bool CGD::SetWH(int w, int h)
{
    if (w < 1 || w > max_w || h < 1 || h > max_h)
        return false;  // 超出范围则失败
    _w = w;
    _h = h;
    return true;
}

// 添加图片资源到映射表
bool CGD::AddBmp(const char* key, int w, int h, const char* map)
{
    // 入参校验:key、宽高、像素数据无效则失败
    if (key == 0 || w < 1 || h < 1 || map == 0)
        return false;
    
    // 检查key是否已存在(避免重复添加)
    std::map<const char*, BMP>::iterator it;
    it = bmpmap.find(key);
    if (it != bmpmap.end())
        return false;

    // 构造BMP对象并复制像素数据
    BMP bmp;
    bmp.h = h;
    bmp.w = w;
    int s = w * h;
    bmp._map = new char[s];  // 分配像素数组内存
    for (int i = 0; i < s; ++i)
        bmp._map[i] = map[i];  // 复制像素数据
    
    // 添加到映射表
    bmpmap.insert(std::pair<const char*, BMP>(key, bmp));
    return true;
}

三、角色与障碍物模块

Hero.h(英雄基类)

cpp 复制代码
#pragma once

// 英雄动作枚举(状态常量)
#define H_IDLE		0  //  idle(静止)
#define H_MOVE		1  //  move(移动)
#define H_UP		2  //  up(跳跃上升)
#define H_DOWN		3  //  down(下落)

// 英雄状态结构体(存储位置、大小、动作等信息)
struct SX
{
    int x;        // x坐标
    int y;        // y坐标
    int w;        // 宽度
    int h;        // 高度
    int jp;       // 跳跃力度(可上升的帧数)
    int fx;       // 方向(4=左,5=默认,6=右)
    int state;    // 状态(8=上升中,5=地面,2=下落中)
    int curAct;   // 当前动作(对应H_IDLE等枚举)
};

class CHero  // 英雄基类(抽象角色行为)
{
protected:
    SX m_sx;          // 英雄状态数据
    const char* m_bmpKey;  // 当前使用的图片key(用于绘制)
    virtual void Idle();   // 静止动作(纯虚函数,子类实现)
    virtual void Move();   // 移动动作
    virtual void Up();     // 跳跃上升动作
    virtual void Down();   // 下落动作

public:
    virtual ~CHero();               // 析构函数
    virtual void Init();            // 初始化
    virtual void Run();             // 帧更新(处理动作逻辑)
    virtual void End();             // 结束清理
    SX* GetSx();                    // 获取状态指针(供外部访问)
    void SetBmpKey(const char* key); // 设置当前图片key
};

很明显 豆包在这里出错了 这里我们用的是宏 注释 给我们写的是枚举。

这才是枚举,忘了可以翻前面的笔记。

使用的时候直接用,c++11 又新特性 可以给枚举限定使用范围。

这里有三个量 我们必须理清楚

int fx:方向(朝向) 只有 左右和中立 对应数字456 move和 碰撞函数会使用

int state:物理状态(环境交互状态)

对应 上升 地面 和下降 258

Idle() 函数中,若 state 不是地面状态(即空中),会自动切换到下落动作

只有 state=5(地面)时,才能触发跳跃(切换到 H_UP 动作),避免空中二次跳跃

状态切换依据 :跳跃力度耗尽(curJp=0)时,从上升状态(8)切换到下落状态(2

int curAct:当前执行的动作(行为类型)

这里相当于是加花,分类方式不同而已,比如上面state 5 是地面 但是地面可以分为 不动和移动 这里就越高通过 curAct()来实现细分 和 加不同的动作姿态。

结构体打包英雄的所有属性

然后才定义这个 抽象类

注意抽象类 一定要 写虚析构函数!!!防止内存泄漏。

相当于封装 要定义一个属性的指针 来操作

这里的英雄图片 可以设置很多个 用于英雄在 跑,禁止,上升,下落 的图片。

这里只有这个 设置图片索引的函数 不是虚函数,因为都是传个索引进去而已。

Hero.cpp

cpp 复制代码
#include "Hero.h"

// 静止动作默认实现(空,子类重写)
void CHero::Idle()
{
}

// 移动动作默认实现(空,子类重写)
void CHero::Move()
{
}

// 跳跃上升动作默认实现(空,子类重写)
void CHero::Up()
{
}

// 下落动作默认实现(空,子类重写)
void CHero::Down()
{
}

// 析构函数(空,基类虚析构确保子类析构被调用)
CHero::~CHero()
{
}

// 初始化默认实现(空,子类重写)
void CHero::Init()
{
}

// 帧更新默认实现(空,子类重写)
void CHero::Run()
{
}

// 结束清理默认实现(空,子类重写)
void CHero::End()
{
}

// 获取状态指针(供外部修改或查询状态)
SX* CHero::GetSx()
{
    return &m_sx;
}

// 设置当前绘制的图片key
void CHero::SetBmpKey(const char* key)
{
    m_bmpKey = key;
}

ML.h(玩家英雄类头文件,继承 CHero)

cpp 复制代码
#pragma once
#include "Hero.h"

class CML : public CHero  // 玩家控制的英雄类(继承自CHero)
{
    const char* bmpkey[9];  // 存储不同动作对应的图片key(索引对应动作状态)
    int curJp;  // 当前跳跃进度(随帧数减少,控制上升高度)

protected:
    // 重写父类的动作函数(实现具体逻辑)
    virtual void Idle();
    virtual void Move();
    virtual void Up();
    virtual void Down();

public:
    virtual void Init();  // 初始化(加载图片、设置初始状态)
    virtual void Run();   // 帧更新(根据当前动作调用对应函数,更新绘制)
    virtual void End();   // 结束清理
};

我们发现 父类有8个虚函数 子类居然只有7个,没有重写父类的虚析构函数。

因为 这里 没有用过堆,就用子类默认的虚构函数就可以了。

这里相当于隐式重写了 虚构函数。

IdleMoveUpDown)是英雄状态机的核心实现

游戏中,角色的行为往往可以拆分为互斥的状态(比如 "静止" 时不会同时 "移动","跳跃上升" 时不会同时 "下落")。这种场景最适合用「状态机模式」处理:

他们是配合 run()函数 使用的

cpp 复制代码
virtual void Idle();
cpp 复制代码
// 静止动作逻辑(检测键盘输入切换状态)
void CML::Idle()
{
    // 检测A键(左移):切换到移动状态,方向左
    if (GetAsyncKeyState('A'))
    {
        m_sx.curAct = H_MOVE;
        m_sx.fx = 4;
    }
    // 检测D键(右移):切换到移动状态,方向右
    else if (GetAsyncKeyState('D'))
    { 
        m_sx.curAct = H_MOVE;
        m_sx.fx = 6;
    }

    // 重力逻辑:默认下落(y坐标+1)
    m_sx.y += 1;
    // 检测与下方障碍物的碰撞(方向2=下)
    if (g_zam.Peng(this, 2) == false)  // 未碰撞(空中)
    {
        m_sx.curAct = H_DOWN;  // 切换到下落状态
        m_sx.state = 2;
        return;
    }

    // 检测J键(跳跃):切换到上升状态
    if (GetAsyncKeyState('J'))
    {
        m_sx.curAct = H_UP;
        m_sx.state = 8;
        curJp = m_sx.jp;  // 初始化跳跃进度(使用预设的跳跃力度)
    }
}

从静止状态 如果 左 移动 或者右 移动就切换为 H_MOVE

如果 受重力后没有碰撞 就切换为 H_DOWN

如果跳跃 就切换为H_UP 注意这里会多一个跳跃力。

一共四种切换。

cpp 复制代码
virtual void Move();
cpp 复制代码
// 移动动作逻辑(处理左右移动和重力)
void CML::Move()
{
    // 根据方向移动x坐标
    if (m_sx.fx == 4)
        m_sx.x -= 1;  // 左移(x-1)
    else if (m_sx.fx == 6)
        m_sx.x += 1;  // 右移(x+1)
    
    // 检测与左右障碍物的碰撞(方向为当前fx)
    if (g_zam.Peng(this, m_sx.fx))
    {
        // 碰撞后切换到静止状态,方向重置为默认
        m_sx.curAct = H_IDLE;
        m_sx.fx = 5;
    }

    // 检测键盘输入更新方向
    if (GetAsyncKeyState('A'))
        m_sx.fx = 4;  // 持续按A则保持左向
    else if (GetAsyncKeyState('D'))
        m_sx.fx = 6;  // 持续按D则保持右向
    else
    {
        // 松开按键则切换到静止状态
        m_sx.curAct = H_IDLE;
        m_sx.fx = 5;
    }

这里的逻辑更加巧妙

根据朝向来 左右运动

检测左右碰撞 碰撞 变为 H_IDLE

按键可以改变move 方向 没有按AD 变为 H_IDLE

重力后没有碰撞 切换为H_DOWN

按跳跃 切换为 H_UP

cpp 复制代码
 virtual void Up();
cpp 复制代码
// 跳跃上升动作逻辑(上升阶段)
void CML::Up()
{
    // 左右移动(同Move)
    if (m_sx.fx == 4)
        m_sx.x -= 1;
    else if (m_sx.fx == 6)
        m_sx.x += 1;
    // 检测左右碰撞(碰撞后停止移动但继续上升)
    g_zam.Peng(this, m_sx.fx);

    // 上升逻辑:根据当前跳跃进度向上移动
    for (int i = 0; i < curJp; ++i)
    {
        m_sx.y -= 1;  // y-1(上升)
        if (g_zam.Peng(this, 8))  // 检测与上方障碍物碰撞(方向8=上)
        {
            break;  // 碰撞则停止上升
        }
    }
    curJp -= 1;  // 跳跃进度减1(每帧上升距离减少)
    if (curJp == 0)  // 跳跃进度为0时,切换到下落状态
    {
        m_sx.curAct = H_DOWN;
        m_sx.state = 2;
    }
}

跳跃的时候 也可以左右移动

但是同样检测左右碰撞 只检测水平方向 所以这里 fx这个量的作用就体现出来了

碰撞之后 英雄也会继续上升的

上升也要检测碰撞,这里就是用8作为方向来检测。

当跳跃力 为0切换为 H_DOWN 开始下降

cpp 复制代码
 virtual void Down();
cpp 复制代码
// 下落动作逻辑(下落阶段)
void CML::Down()
{
    // 左右移动(同Move)
    if (m_sx.fx == 4)
        m_sx.x -= 1;
    else if (m_sx.fx == 6)
        m_sx.x += 1;
    // 检测左右碰撞
    g_zam.Peng(this, m_sx.fx);

    // 更新方向(根据按键)
    if (GetAsyncKeyState('A'))
        m_sx.fx = 4;
    else if (GetAsyncKeyState('D'))
        m_sx.fx = 6;
    else
        m_sx.fx = 5;  // 无按键则默认方向

    // 下落逻辑(y+1)
    m_sx.y += 1;
    // 检测与下方障碍物碰撞(方向2=下)
    if (g_zam.Peng(this, 2))
    {
        // 碰撞后根据方向切换到移动或静止状态
        if(m_sx.fx == 4 || m_sx.fx == 6)
            m_sx.curAct = H_MOVE;
        else
            m_sx.curAct = H_IDLE;
        m_sx.state = 5;  // 状态设为地面
        return;
    }
}

下落的时候可以左右移动,可以改变移动方向

左右碰撞检测

向下发生碰撞 切换为H_MOVE (按下左右移动的情况)

切换为 H_IDLE

cpp 复制代码
virtual void Init();  // 初始化(加载图片、设置初始状态)
cpp 复制代码
// 初始化:加载图片资源、设置初始状态
void CML::Init()
{
    char index;  // 图片索引(关联到纹理集)

    // 添加下落状态的图片(左、中、右方向)
    index = 10;
    g_gd.AddBmp("LDown", 1, 1, &index);  // 左下落
    index = 11;
    g_gd.AddBmp("Down", 1, 1, &index);   // 中下落
    index = 12;
    g_gd.AddBmp("RDown", 1, 1, &index);  // 右下落

    // 添加移动/静止状态的图片
    index = 9;
    g_gd.AddBmp("LMove", 1, 1, &index);  // 左移
    index = 2;
    g_gd.AddBmp("Idle", 1, 1, &index);   // 静止
    index = 8;
    g_gd.AddBmp("RMove", 1, 1, &index);  // 右移

    // 添加跳跃上升状态的图片
    index = 5;
    g_gd.AddBmp("LUp", 1, 1, &index);    // 左跳
    index = 6;
    g_gd.AddBmp("Up", 1, 1, &index);     // 中跳
    index = 7;
    g_gd.AddBmp("RUp", 1, 1, &index);    // 右跳

    // 图片key与索引映射(方便根据状态快速获取key)
    bmpkey[0] = "LDown";
    bmpkey[1] = "Down";
    bmpkey[2] = "RDown";
    bmpkey[3] = "LMove";
    bmpkey[4] = "Idle";
    bmpkey[5] = "RMove";
    bmpkey[6] = "LUp";
    bmpkey[7] = "Up";
    bmpkey[8] = "RUp";

这里有点乱 感觉用个枚举会更好。

这里 bmp里的成员map 给个const char* 类型给我绕晕了,偏要用字符类型来存数字,追着豆包问了一条街,我想起来自己写demo7的时候 就是用int就行。 就把这玩意当int吧 我反正自己写用int。

这里就是天骄了 9个图片 代表向9个不同方向运动。

然后把索引都放到一个数组里面

然后就是 jp 和 curjp,jp相当于是属性 就是这个英雄的最大跳跃力 是固定不变的除非吃了特定的道具,而curjp是 每一帧画面会改变的,跳跃的格数会递减。第一帧为3格(最大跳跃力jp) 第二帧为2格 第三帧为1格。

然后这里就是初始化了 所有状态。

cpp 复制代码
 virtual void Run();   // 帧更新(根据当前动作调用对应函数,更新绘制)
cpp 复制代码
// 帧更新:根据当前动作执行逻辑,并更新绘制图片
void CML::Run()
{
    // 根据当前动作调用对应函数(状态机)
    switch (m_sx.curAct)
    {
    case H_IDLE: Idle(); break;
    case H_MOVE: Move(); break;
    case H_UP: Up(); break;
    case H_DOWN: Down(); break;
    }

    // 根据当前动作和方向设置绘制的图片key
    if (m_sx.curAct == H_MOVE)
    {
        if (m_sx.fx == 4)
            m_bmpKey = bmpkey[3];  // 左移图
        else
            m_bmpKey = bmpkey[5];  // 右移图
    }
    else if(m_sx.curAct == H_IDLE)
        m_bmpKey = bmpkey[4];  // 静止图
    else if (m_sx.curAct == H_UP)
    {
        if (m_sx.fx == 4)
            m_bmpKey = bmpkey[6];  // 左跳图
        else if (m_sx.fx == 6)
            m_bmpKey = bmpkey[8];  // 右跳图
        else
            m_bmpKey = bmpkey[7];  // 中跳图
    }
    else if (m_sx.curAct == H_DOWN)
    {
        if (m_sx.fx == 4)
            m_bmpKey = bmpkey[0];  // 左下落图
        else if (m_sx.fx == 6)
            m_bmpKey = bmpkey[2];  // 右下落图
        else
            m_bmpKey = bmpkey[1];  // 中下落图
    }

    // 绘制当前图片(位置为英雄的x,y)
    g_gd.Draw(m_bmpKey, m_sx.x, m_sx.y);
}

这个run 有两个功能

一个是 切换不同状态函数

一个是 绘制英雄图片(更改世界数组编码)

绘制的是哈偶还是基于了判断 分为了

不动 H_IDLE

移动 H_MOVE 左 右

上升 H_UP 直上 左上 右上

下降 H_DOWN 直线 左下 右下

一共9个图

cpp 复制代码
 virtual void End();   // 结束清理
cpp 复制代码
// 结束清理(空实现)
void CML::End()
{
}

我们的英雄 还没有弄血量死亡 所以这里先用个空函数,后面再加功能。

ML.cpp

cpp 复制代码
#include "ML.h"
#include "Hero.h"
#include <windows.h>  // 用于GetAsyncKeyState(检测键盘输入)
#include "GameDraw.h"  // 绘图对象
#include "ZAM.h"       // 障碍物管理

// 静止动作逻辑(检测键盘输入切换状态)
void CML::Idle()
{
    // 检测A键(左移):切换到移动状态,方向左
    if (GetAsyncKeyState('A'))
    {
        m_sx.curAct = H_MOVE;
        m_sx.fx = 4;
    }
    // 检测D键(右移):切换到移动状态,方向右
    else if (GetAsyncKeyState('D'))
    { 
        m_sx.curAct = H_MOVE;
        m_sx.fx = 6;
    }

    // 重力逻辑:默认下落(y坐标+1)
    m_sx.y += 1;
    // 检测与下方障碍物的碰撞(方向2=下)
    if (g_zam.Peng(this, 2) == false)  // 未碰撞(空中)
    {
        m_sx.curAct = H_DOWN;  // 切换到下落状态
        m_sx.state = 2;
        return;
    }

    // 检测J键(跳跃):切换到上升状态
    if (GetAsyncKeyState('J'))
    {
        m_sx.curAct = H_UP;
        m_sx.state = 8;
        curJp = m_sx.jp;  // 初始化跳跃进度(使用预设的跳跃力度)
    }
}

// 移动动作逻辑(处理左右移动和重力)
void CML::Move()
{
    // 根据方向移动x坐标
    if (m_sx.fx == 4)
        m_sx.x -= 1;  // 左移(x-1)
    else if (m_sx.fx == 6)
        m_sx.x += 1;  // 右移(x+1)
    
    // 检测与左右障碍物的碰撞(方向为当前fx)
    if (g_zam.Peng(this, m_sx.fx))
    {
        // 碰撞后切换到静止状态,方向重置为默认
        m_sx.curAct = H_IDLE;
        m_sx.fx = 5;
    }

    // 检测键盘输入更新方向
    if (GetAsyncKeyState('A'))
        m_sx.fx = 4;  // 持续按A则保持左向
    else if (GetAsyncKeyState('D'))
        m_sx.fx = 6;  // 持续按D则保持右向
    else
    {
        // 松开按键则切换到静止状态
        m_sx.curAct = H_IDLE;
        m_sx.fx = 5;
    }
    
    // 重力逻辑(同Idle)
    m_sx.y += 1;
    if (g_zam.Peng(this, 2) == false)  // 未碰撞(空中)
    {
        m_sx.curAct = H_DOWN;
        m_sx.state = 2;
        return;
    }

    // 跳跃检测(同Idle)
    if (GetAsyncKeyState('J'))
    {
        m_sx.curAct = H_UP;
        m_sx.state = 8;
        curJp = m_sx.jp;
    }
}

// 跳跃上升动作逻辑(上升阶段)
void CML::Up()
{
    // 左右移动(同Move)
    if (m_sx.fx == 4)
        m_sx.x -= 1;
    else if (m_sx.fx == 6)
        m_sx.x += 1;
    // 检测左右碰撞(碰撞后停止移动但继续上升)
    g_zam.Peng(this, m_sx.fx);

    // 上升逻辑:根据当前跳跃进度向上移动
    for (int i = 0; i < curJp; ++i)
    {
        m_sx.y -= 1;  // y-1(上升)
        if (g_zam.Peng(this, 8))  // 检测与上方障碍物碰撞(方向8=上)
        {
            break;  // 碰撞则停止上升
        }
    }
    curJp -= 1;  // 跳跃进度减1(每帧上升距离减少)
    if (curJp == 0)  // 跳跃进度为0时,切换到下落状态
    {
        m_sx.curAct = H_DOWN;
        m_sx.state = 2;
    }
}

// 下落动作逻辑(下落阶段)
void CML::Down()
{
    // 左右移动(同Move)
    if (m_sx.fx == 4)
        m_sx.x -= 1;
    else if (m_sx.fx == 6)
        m_sx.x += 1;
    // 检测左右碰撞
    g_zam.Peng(this, m_sx.fx);

    // 更新方向(根据按键)
    if (GetAsyncKeyState('A'))
        m_sx.fx = 4;
    else if (GetAsyncKeyState('D'))
        m_sx.fx = 6;
    else
        m_sx.fx = 5;  // 无按键则默认方向

    // 下落逻辑(y+1)
    m_sx.y += 1;
    // 检测与下方障碍物碰撞(方向2=下)
    if (g_zam.Peng(this, 2))
    {
        // 碰撞后根据方向切换到移动或静止状态
        if(m_sx.fx == 4 || m_sx.fx == 6)
            m_sx.curAct = H_MOVE;
        else
            m_sx.curAct = H_IDLE;
        m_sx.state = 5;  // 状态设为地面
        return;
    }
}

// 初始化:加载图片资源、设置初始状态
void CML::Init()
{
    char index;  // 图片索引(关联到纹理集)

    // 添加下落状态的图片(左、中、右方向)
    index = 10;
    g_gd.AddBmp("LDown", 1, 1, &index);  // 左下落
    index = 11;
    g_gd.AddBmp("Down", 1, 1, &index);   // 中下落
    index = 12;
    g_gd.AddBmp("RDown", 1, 1, &index);  // 右下落

    // 添加移动/静止状态的图片
    index = 9;
    g_gd.AddBmp("LMove", 1, 1, &index);  // 左移
    index = 2;
    g_gd.AddBmp("Idle", 1, 1, &index);   // 静止
    index = 8;
    g_gd.AddBmp("RMove", 1, 1, &index);  // 右移

    // 添加跳跃上升状态的图片
    index = 5;
    g_gd.AddBmp("LUp", 1, 1, &index);    // 左跳
    index = 6;
    g_gd.AddBmp("Up", 1, 1, &index);     // 中跳
    index = 7;
    g_gd.AddBmp("RUp", 1, 1, &index);    // 右跳

    // 图片key与索引映射(方便根据状态快速获取key)
    bmpkey[0] = "LDown";
    bmpkey[1] = "Down";
    bmpkey[2] = "RDown";
    bmpkey[3] = "LMove";
    bmpkey[4] = "Idle";
    bmpkey[5] = "RMove";
    bmpkey[6] = "LUp";
    bmpkey[7] = "Up";
    bmpkey[8] = "RUp";

    // 初始状态设置
    m_bmpKey = "Idle";       // 默认显示静止图片
    m_sx.curAct = H_IDLE;    // 初始动作:静止
    m_sx.w = 1;              // 宽高为1x1像素
    m_sx.h = 1;
    m_sx.jp = 3;             // 跳跃力度为3(上升3帧)
    m_sx.fx = 5;             // 默认方向
    m_sx.state = 5;          // 初始状态:地面
    curJp = 0;               // 跳跃进度初始为0
    m_sx.x = 0;              // 初始位置(0,0)
    m_sx.y = 0;
}

// 帧更新:根据当前动作执行逻辑,并更新绘制图片
void CML::Run()
{
    // 根据当前动作调用对应函数(状态机)
    switch (m_sx.curAct)
    {
    case H_IDLE: Idle(); break;
    case H_MOVE: Move(); break;
    case H_UP: Up(); break;
    case H_DOWN: Down(); break;
    }

    // 根据当前动作和方向设置绘制的图片key
    if (m_sx.curAct == H_MOVE)
    {
        if (m_sx.fx == 4)
            m_bmpKey = bmpkey[3];  // 左移图
        else
            m_bmpKey = bmpkey[5];  // 右移图
    }
    else if(m_sx.curAct == H_IDLE)
        m_bmpKey = bmpkey[4];  // 静止图
    else if (m_sx.curAct == H_UP)
    {
        if (m_sx.fx == 4)
            m_bmpKey = bmpkey[6];  // 左跳图
        else if (m_sx.fx == 6)
            m_bmpKey = bmpkey[8];  // 右跳图
        else
            m_bmpKey = bmpkey[7];  // 中跳图
    }
    else if (m_sx.curAct == H_DOWN)
    {
        if (m_sx.fx == 4)
            m_bmpKey = bmpkey[0];  // 左下落图
        else if (m_sx.fx == 6)
            m_bmpKey = bmpkey[2];  // 右下落图
        else
            m_bmpKey = bmpkey[1];  // 中下落图
    }

    // 绘制当前图片(位置为英雄的x,y)
    g_gd.Draw(m_bmpKey, m_sx.x, m_sx.y);
}

// 结束清理(空实现)
void CML::End()
{
}

四、管理类模块

HM.h(英雄管理类头文件)

cpp 复制代码
#pragma once
#include <vector>  // 用于存储英雄对象的动态数组
class CHero;

class CHM  // 英雄管理器(管理所有英雄对象的更新)
{
    std::vector<CHero*> m_HeroList;  // 英雄对象列表
public:
    bool AddHero(CHero* hero);  // 添加英雄到列表
    void Run();                 // 更新所有英雄(调用每个英雄的Run方法)
};

extern CHM g_hm;  // 全局英雄管理器对象

由于英雄这个类非常大所以我们都是用一个指针 来避免开销。

这里使用了 动态数组 顺序表来储存我们的英雄列表

这里的run()方法 是遍历所有英雄 全部都调用一遍 每个英雄自己的run方法。

HM.cpp

cpp 复制代码
#include "HM.h"
#include "Hero.h"

CHM g_hm;  // 全局英雄管理器

// 添加英雄到列表(入参为空则失败)
bool CHM::AddHero(CHero* hero)
{
    if (hero == 0)
        return false;
    m_HeroList.push_back(hero);  // 存入vector
    return true;  // 注意:原代码漏了return,实际应返回true
}

// 帧更新:调用所有英雄的Run方法
void CHM::Run()
{
    int len = m_HeroList.size();  // 获取英雄数量
    for(int i = 0; i < len; ++i)
    { 
        m_HeroList[i]->Run();  // 逐个更新
    }
}

ZA.h(障碍物基类头文件)

cpp 复制代码
#pragma once
#include "MyRect.h"  // 矩形碰撞
#include "GameDraw.h"  // 绘图
class CHero;

class CZA  // 障碍物基类(所有障碍物的父类)
{
protected:
    const char* m_key;  // 绘制用的图片key
    int m_x;            // x坐标
    int m_y;            // y坐标
    int m_w;            // 宽度
    int m_h;            // 高度

public:
    virtual ~CZA();                          // 析构函数
    void SetBmp(const char* key);            // 设置图片key
    void SetPos(int x, int y);               // 设置位置
    void SetWH(int w, int h);                // 设置宽高
    int GetX();                              // 获取x坐标
    int GetY();                              // 获取y坐标
    virtual CMyRect GetRect();               // 获取碰撞矩形
    virtual void Run();                      // 帧更新(绘制自身)
    virtual void Init();                     // 初始化
    virtual void End();                      // 结束清理
    virtual void ColliderFun(CHero* hero, int fx);  // 碰撞处理函数
};

只有几个set 函数不是虚函数 还有get函数 。这里把成员变量都设为保护类型了。

ZA.cpp

cpp 复制代码
#include "ZA.h"
#include "Hero.h"

// 析构函数(空)
CZA::~CZA()
{
}

// 设置绘制用的图片key
void CZA::SetBmp(const char* key)
{
    m_key = key;
}

// 获取碰撞矩形(返回当前障碍物的位置和大小)
CMyRect CZA::GetRect()
{ 
    CMyRect mr;
    mr.m_x = m_x;
    mr.m_y = m_y;
    mr.m_w = m_w;
    mr.m_h = m_h;
    return mr;
}

// 设置位置
void CZA::SetPos(int x, int y)
{
    m_x = x;
    m_y = y;
}

// 设置宽高
void CZA::SetWH(int w, int h)
{
    m_w = w;
    m_h = h;
}

// 获取x坐标
int CZA::GetX()
{
    return m_x;
}

// 获取y坐标
int CZA::GetY()
{
    return m_y;
}

// 帧更新默认实现(空,子类重写)
void CZA::Run()
{
}

// 初始化默认实现(空,子类重写)
void CZA::Init()
{
}

// 结束清理默认实现(空,子类重写)
void CZA::End()
{
}

// 碰撞处理默认实现(空,子类重写)
void CZA::ColliderFun(CHero* hero, int fx)
{
}

ZAM.h(障碍物管理类头文件)

cpp 复制代码
#pragma once
#include <list>  // 用于存储障碍物对象的链表
class CZA;
class CHero;

class CZAM  // 障碍物管理器(管理所有障碍物,检测碰撞)
{
    std::list<CZA*> m_ZAList;  // 障碍物列表
public:
    bool AddZA(CZA* za);       // 添加障碍物到列表
    bool Peng(CHero* hero, int fx);  // 检测英雄与障碍物的碰撞
    void Run();                // 更新所有障碍物(调用Run方法)
};

extern CZAM g_zam;  // 全局障碍物管理器对象

ZAM.cpp(障碍物管理实现)

cpp 复制代码
#include "ZAM.h"
#include "ZA.h"
#include "Hero.h"

CZAM g_zam;  // 全局障碍物管理器

// 添加障碍物到列表(入参为空则失败)
bool CZAM::AddZA(CZA* za)
{
    if(za == 0)
        return false;
    m_ZAList.push_back(za);  // 存入list
    return true;  // 原代码漏了return,实际应返回true
}

// 检测英雄与障碍物的碰撞(fx:碰撞方向)
bool CZAM::Peng(CHero* hero, int fx)
{
    if (hero == 0)  // 英雄为空则失败
        return false;
    
    // 获取英雄的状态(位置、大小)
    SX* p = hero->GetSx();
    CMyRect r1;  // 英雄的碰撞矩形
    r1.m_x = p->x;
    r1.m_y = p->y;
    r1.m_w = p->w;
    r1.m_h = p->h;

    // 遍历所有障碍物,检测碰撞
    std::list<CZA*>::iterator it;
    for (it = m_ZAList.begin(); it != m_ZAList.end(); ++it)
    {
        // 若当前障碍物与英雄碰撞
        if ((*it)->GetRect().Collider(&r1))
        {
            (*it)->ColliderFun(hero, fx);  // 调用障碍物的碰撞处理函数
            return true;  // 碰撞成功
        }
    }
    return false;  // 无碰撞
}

// 帧更新:调用所有障碍物的Run方法
void CZAM::Run()
{
    std::list<CZA*>::iterator it;
    for (it = m_ZAList.begin(); it != m_ZAList.end(); ++it)
    {
        (*it)->Run();
    }
}

这里使用了list 我们详细学习一下list

注意:list是双向链表,vector 是顺序表。

vector语法:

list语法:

五、具体障碍物实现

Wall.h(墙壁类头文件,继承 CZA)

cpp 复制代码
#pragma once
#include "ZA.h"

class CWall : public CZA  // 墙壁障碍物(继承自CZA)
{
public:
    CWall();  // 构造函数
    void Run();  // 帧更新(绘制自身)
    void Init();  // 初始化
    void End();   // 结束清理
    void ColliderFun(CHero* hero, int fx);  // 碰撞处理(阻挡英雄移动)
};

Wall.cpp

cpp 复制代码
#include "Wall.h"
#include "Hero.h"

// 构造函数:初始化图片key为空
CWall::CWall()
{
    m_key = 0;
}

// 帧更新:绘制墙壁
void CWall::Run()
{
    g_gd.Draw(m_key, m_x, m_y);  // 调用绘图对象绘制自身
}

// 初始化(空实现)
void CWall::Init()
{
}

// 结束清理(空实现)
void CWall::End()
{
}

// 碰撞处理:根据碰撞方向调整英雄位置(阻挡移动)
void CWall::ColliderFun(CHero* hero, int fx)
{
    SX* sx = hero->GetSx();  // 获取英雄状态
    // 计算墙壁的边界
    int L = m_x;               // 左边界
    int R = L + m_w - 1;       // 右边界
    int T = m_y;               // 上边界
    int B = T + m_h - 1;       // 下边界

    if(fx == 4)  // 英雄向左移动时碰撞(撞到墙壁右侧)
    {
        // 英雄x坐标调整到墙壁右侧+1(避免持续碰撞)
        sx->x = m_x + m_w - 1 + 1;  // 等价于 R + 1
    }
    else if (fx == 6)  // 英雄向右移动时碰撞(撞到墙壁左侧)
    {
        // 英雄x坐标调整到墙壁左侧 - 英雄宽度(避免持续碰撞)
        sx->x = L - sx->w;
    }
    else if (fx == 8)  // 英雄向上移动时碰撞(撞到墙壁下侧)
    {
        // 英雄y坐标调整到墙壁下侧+1
        sx->y = B + 1;
    }
    else if (fx == 2)  // 英雄向下移动时碰撞(撞到墙壁上侧)
    {
        // 英雄y坐标调整到墙壁上侧 - 英雄高度
        sx->y = T - sx->h;    
    }
}

主要就是 写了碰撞函数 这里墙壁只考虑 和英雄的碰撞。

这里只对碰撞后进行了处理,是否碰撞是Myrect 里的方法,需要啊英雄构造成一个矩形后再判断是否碰撞。

同样的 这里所有的障碍物都要构建出 矩形才能调用这个 碰撞检测,所以再父类ZA中,定义了GetRect方法 来返回一个矩形。

六、主程序入口

源.cpp(程序主函数)

cpp 复制代码
#include <iostream>
#include <time.h>    // 用于随机数种子
#include <windows.h> // 用于Sleep(延迟)
#include "GameDraw.h"
#include "ML.h"
#include "HM.h"
#include "Wall.h"
#include "ZAM.h"

void main()
{
    srand((int)time(0));  // 初始化随机数种子(当前时间)
    rand();  // 丢弃第一个随机数(提高随机性)

    // 初始化绘图对象
    g_gd.Init();
    // 设置纹理集(字符串中每个字符对对应图片索引的显示内容)
    // 索引0对应前2个字符,索引1对应接下来2个,以此类推
    g_gd.SetTs("���������I���J�����L���K");
    g_gd.SetWH(20, 20);  // 设置屏幕宽高为20x20

    // 创建玩家英雄并初始化
    CHero* p = new CML();
    p->Init();
    p->GetSx()->x = 0;  // 设置初始位置
    p->GetSx()->y = 0;
    g_hm.AddHero(p);  // 添加到英雄管理器

    CZA* za;  // 障碍物指针

    // 创建地面障碍物
    {
        za = new CWall;
        za->SetPos(0, 19);  // 位置(0,19)(屏幕底部)
        za->SetWH(20, 1);   // 宽20,高1(覆盖整个底部)
        za->SetBmp("di");   // 图片key为"di"
        g_zam.AddZA(za);    // 添加到障碍物管理器

        // 定义地面的像素数据(索引数组)
        char map[] =
        {
            1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,  // 20个像素,索引1和4交替
        };
        g_gd.AddBmp("di", 20, 1, map);  // 添加地面图片资源
    }

    // 创建柱子障碍物
    {
        za = new CWall;
        za->SetPos(12, 14);  // 位置(12,14)
        za->SetWH(3, 4);     // 宽3,高4
        za->SetBmp("zhu");   // 图片key为"zhu"
        g_zam.AddZA(za);     // 添加到障碍物管理器

        // 定义柱子的像素数据(3x4的网格)
        char map[] =
        {
            1,4,1,    // 第1行
            4,1,4,    // 第2行
            1,4,1,    // 第3行
            4,1,4,    // 第4行
        };
        g_gd.AddBmp("zhu", 3, 4, map);  // 添加柱子图片资源
    }

    // 游戏主循环
    while (1)
    {
        g_gd.Begin();    // 开始绘制(清空缓冲区)
        g_zam.Run();     // 更新所有障碍物(绘制障碍物)
        g_hm.Run();      // 更新所有英雄(处理动作、绘制英雄)
        g_gd.End();      // 结束绘制(输出画面)
        Sleep(200);      // 延迟200ms(控制帧率约5帧/秒)
    }

    system("pause");  // 程序结束暂停(实际不会执行,因循环是死循环)
}

最近玩了一款模拟汇编的编程的游戏,玩完之后再来看这个c++的大工程,感c++是真的好用和强大,再汇编语言中 一个循环都给你绕晕,用汇编语言是不可能写出这个游戏的。

相关推荐
心疼你的一切3 小时前
使用Unity引擎开发Rokid主机应用的全面配置交互操作
学习·游戏·unity·c#·游戏引擎·交互
2501_9291576821 小时前
NEOGEOCD模拟器+全游戏ISO+工具+特典美术+文档
游戏
da_vinci_x1 天前
2D角色动画进阶:Spine网格变形与序列帧特效的混合工作流
游戏·设计模式·设计师·photoshop·spine·游戏策划·游戏美术
大Mod_abfun3 天前
Unity游戏基础-2(初识场景~项目构建)
游戏·unity·游戏引擎
utmhikari3 天前
【测试人生】LLM赋能游戏自动化测试的一些想法
自动化测试·游戏·ai·大模型·llm·游戏测试
gopyer3 天前
180课时吃透Go语言游戏后端开发7:Go语言中的函数
开发语言·游戏·golang·go·函数
taulee013 天前
在云服务器搭建部署私人饥荒联机版游戏服务器 [2025.10.3][ubuntu 24.04][腾讯云2核2G服务器]
服务器·ubuntu·游戏
HELLOMILI3 天前
[UnrealEngine] 虚幻引擎UE5地形入门指南 | UE5地形教程(UE5 Terrain)
游戏·ue5·游戏引擎·虚幻·虚幻引擎·unreal engine
HELLOMILI4 天前
[UnrealEngine] 虚幻编辑器界面 | 虚幻界面详解 | UE5界面详解
游戏·ue5·编辑器·游戏引擎·虚幻·unreal engine