跳转至

03-资源加载和动画类实现

1 动画翻转

util.h中添加翻转图片的函数

inline void flip_image(IMAGE* src, IMAGE* dst)
{
    int w = src->getwidth();
    int h = src->getheight();

    Resize(dst, w, h);
    DWORD *src_buffer = GetImageBuffer(src);
    DWORD *dst_buffer = GetImageBuffer(dst);

    for (int x = 0; x < w; x++) {
        for (int y = 0; y < h; y++) {
            int idx_src = y * w + x;
            int idx_dst = y * w + w - 1 - x;
            dst_buffer[idx_dst] = src_buffer[idx_src];
        }
    }
}

再在main.cpp中实现翻转图集的函数

void flip_atlas(Atlas& src, Atlas& dst) 
{
    dst.clear();

    for (int i = 0; i < src.get_size(); i++) {
        IMAGE img_flipped;
        flip_image(src.get_image(i), &img_flipped);
        dst.add_image(img_flipped);
    }
}

2 资源加载

首先定义所有资源变量

IMAGE img_menu_background;                      // 主菜单背景图片

IMAGE img_VS;                             // VS 艺术字图片
IMAGE img_1P;                             // 1P 文本图片
IMAGE img_2P;                             // 2P 文本图片
IMAGE img_1P_desc;                        // 1P 键位描述图片
IMAGE img_2P_desc;                        // 2P 键位描述图片
IMAGE img_gravestone_left;                // 朝向左的墓碑图片
IMAGE img_gravestone_right;               // 朝向右的墓碑图片
IMAGE img_selector_tip;                   // 选角界面提示信息文本图片
IMAGE img_selector_background;            // 选角界面背景图

IMAGE img_1P_selector_btn_idle_left;      // 1P 向左选择按钮默认状态图片
IMAGE img_1P_selector_btn_idle_right;     // 1P 向右选择按钮默认状态图片
IMAGE img_1P_selector_btn_down_left;      // 1P 向左选择按钮按下状态图片
IMAGE img_1P_selector_btn_down_right;     // 1P 向右选择按钮按下状态图片

IMAGE img_2P_selector_btn_idle_left;      // 2P 向左选择按钮默认状态图片
IMAGE img_2P_selector_btn_idle_right;     // 2P 向右选择按钮默认状态图片
IMAGE img_2P_selector_btn_down_left;      // 2P 向左选择按钮按下状态图片
IMAGE img_2P_selector_btn_down_right;     // 2P 向右选择按钮按下状态图片

IMAGE img_peashooter_selector_background_left;   // 选角界面朝向左的豌豆射手背景图片
IMAGE img_peashooter_selector_background_right;  // 选角界面朝向右的豌豆射手背景图片
IMAGE img_sunflower_selector_background_left;    // 选角界面朝向左的龙日葵背景图片
IMAGE img_sunflower_selector_background_right;   // 选角界面朝向右的龙日葵背景图片

IMAGE img_sky;                            // 天空图片
IMAGE img_hills;                          // 山脉图片
IMAGE img_platform_large;                 // 大型平台图片
IMAGE img_platform_small;                 // 小型平台图片

IMAGE img_1P_cursor;                      // 1P 指示光标图片
IMAGE img_2P_cursor;                      // 2P 指示光标图片

Atlas atlas_peashooter_idle_left;         // 豌豆射手朝向左的默认动画图集
Atlas atlas_peashooter_idle_right;        // 豌豆射手朝向右的默认动画图集
Atlas atlas_peashooter_run_left;          // 豌豆射手朝向左的奔跑动画图集
Atlas atlas_peashooter_run_right;         // 豌豆射手朝向右的奔跑动画图集
Atlas atlas_peashooter_attack_ex_left;    // 豌豆射手朝向左的特殊攻击动画图集
Atlas atlas_peashooter_attack_ex_right;   // 豌豆射手朝向右的特殊攻击动画图集
Atlas atlas_peashooter_die_left;          // 豌豆射手朝向左的死亡动画图集
Atlas atlas_peashooter_die_right;         // 豌豆射手朝向右的死亡动画图集

Atlas atlas_sunflower_idle_left;          // 龙日葵朝向左的默认动画图集
Atlas atlas_sunflower_idle_right;         // 龙日葵朝向右的默认动画图集
Atlas atlas_sunflower_run_left;           // 龙日葵朝向左的奔跑动画图集
Atlas atlas_sunflower_run_right;          // 龙日葵朝向右的奔跑动画图集
Atlas atlas_sunflower_attack_ex_left;      // 龙日葵朝向左的特殊攻击动画图集
Atlas atlas_sunflower_attack_ex_right;     // 龙日葵朝向右的特殊攻击动画图集
Atlas atlas_sunflower_die_left;            // 龙日葵朝向左的死亡动画图集
Atlas atlas_sunflower_die_right;           // 龙日葵朝向右的死亡动画图集

IMAGE img_pea;                            // 豌豆图片
Atlas atlas_pea_break;                    // 豌豆破碎动画图集
Atlas atlas_sun;                          // 日光动画图集
Atlas atlas_sun_explode;                  // 日光爆炸动画图集
Atlas atlas_sun_ex;                       // 特殊日光动画图集
Atlas atlas_sun_ex_explode;               // 特殊日光爆炸动画图集
Atlas atlas_sun_text;                     // "日" 文本动画图集

Atlas atlas_run_effect;                   // 奔跑特效动画图集
Atlas atlas_jump_effect;                  // 跳跃特效动画图集
Atlas atlas_land_effect;                  // 落地特效动画图集

IMAGE img_1P_winner;                      // 1P 获胜文本图片
IMAGE img_2P_winner;                      // 2P 获胜文本图片
IMAGE img_winner_bar;

IMAGE img_avatar_peashooter;              // 豌豆射手头像图片
IMAGE img_avatar_sunflower;                // 龙日葵头像图片

定义load_game_resources()函数并在main函数中调用
void load_game_resources()
{
    AddFontResourceEx(_T("../resources/IPix.ttf"), FR_PRIVATE, NULL);

    loadimage(&img_menu_background, _T("../resources/menu_background.png"));

    loadimage(&img_VS, _T("../resources/VS.png"));
    loadimage(&img_1P, _T("../resources/1P.png"));
    loadimage(&img_2P, _T("../resources/2P.png"));
    loadimage(&img_1P_desc, _T("../resources/1P_desc.png"));
    loadimage(&img_2P_desc, _T("../resources/2P_desc.png"));
    loadimage(&img_gravestone_right, _T("../resources/gravestone.png"));
    flip_image(&img_gravestone_right, &img_gravestone_left);
    loadimage(&img_selector_tip, _T("../resources/selector_tip.png"));
    loadimage(&img_selector_background, _T("../resources/selector_background.png"));
    loadimage(&img_1P_selector_btn_idle_right, _T("../resources/1P_selector_btn_idle.png"));
    flip_image(&img_1P_selector_btn_idle_right, &img_1P_selector_btn_idle_left);
    loadimage(&img_1P_selector_btn_down_right, _T("../resources/1P_selector_btn_down.png"));
    flip_image(&img_1P_selector_btn_down_right, &img_1P_selector_btn_down_left);
    loadimage(&img_2P_selector_btn_idle_right, _T("../resources/2P_selector_btn_idle.png"));
    flip_image(&img_2P_selector_btn_idle_right, &img_2P_selector_btn_idle_left);
    loadimage(&img_2P_selector_btn_down_right, _T("../resources/2P_selector_btn_down.png"));
    flip_image(&img_2P_selector_btn_down_right, &img_2P_selector_btn_down_left);
    loadimage(&img_peashooter_selector_background_right, _T("../resources/peashooter_selector_background.png"));
    flip_image(&img_peashooter_selector_background_right, &img_peashooter_selector_background_left);
    loadimage(&img_sunflower_selector_background_right, _T("../resources/sunflower_selector_background.png"));
    flip_image(&img_sunflower_selector_background_right, &img_sunflower_selector_background_left);

    loadimage(&img_sky, _T("../resources/sky.png"));
    loadimage(&img_hills, _T("../resources/hills.png"));
    loadimage(&img_platform_large, _T("../resources/platform_large.png"));
    loadimage(&img_platform_small, _T("../resources/platform_small.png"));

    loadimage(&img_1P_cursor, _T("../resources/1P_cursor.png"));
    loadimage(&img_2P_cursor, _T("../resources/2P_cursor.png"));

    atlas_peashooter_idle_right.load_from_file(_T("../resources/peashooter_idle_%d.png"), 9);
    flip_atlas(atlas_peashooter_idle_right, atlas_peashooter_idle_left);
    atlas_peashooter_run_right.load_from_file(_T("../resources/peashooter_run_%d.png"), 5);
    flip_atlas(atlas_peashooter_run_right, atlas_peashooter_run_left);
    atlas_peashooter_attack_ex_right.load_from_file(_T("../resources/peashooter_attack_ex_%d.png"), 3);
    flip_atlas(atlas_peashooter_attack_ex_right, atlas_peashooter_attack_ex_left);
    atlas_peashooter_die_right.load_from_file(_T("../resources/peashooter_die_%d.png"), 4);
    flip_atlas(atlas_peashooter_die_right, atlas_peashooter_die_left);

    atlas_sunflower_idle_right.load_from_file(_T("../resources/sunflower_idle_%d.png"), 8);
    flip_atlas(atlas_sunflower_idle_right, atlas_sunflower_idle_left);
    atlas_sunflower_run_right.load_from_file(_T("../resources/sunflower_run_%d.png"), 5);
    flip_atlas(atlas_sunflower_run_right, atlas_sunflower_run_left);
    atlas_sunflower_attack_ex_right.load_from_file(_T("../resources/sunflower_attack_ex_%d.png"), 9);
    flip_atlas(atlas_sunflower_attack_ex_right, atlas_sunflower_attack_ex_left);
    atlas_sunflower_die_right.load_from_file(_T("../resources/sunflower_die_%d.png"), 2);
    flip_atlas(atlas_sunflower_die_right, atlas_sunflower_die_left);

    loadimage(&img_pea, _T("../resources/pea.png"));
    atlas_pea_break.load_from_file(_T("../resources/pea_break_%d.png"), 3);
    atlas_sun.load_from_file(_T("../resources/sun_%d.png"), 5);
    atlas_sun_explode.load_from_file(_T("../resources/sun_explode_%d.png"), 5);
    atlas_sun_ex.load_from_file(_T("../resources/sun_ex_%d.png"), 5);
    atlas_sun_ex_explode.load_from_file(_T("../resources/sun_ex_explode_%d.png"), 5);
    atlas_sun_text.load_from_file(_T("../resources/sun_text_%d.png"), 6);

    atlas_run_effect.load_from_file(_T("../resources/run_effect_%d.png"), 4);
    atlas_jump_effect.load_from_file(_T("../resources/jump_effect_%d.png"), 5);
    atlas_land_effect.load_from_file(_T("../resources/land_effect_%d.png"), 2);

    loadimage(&img_1P_winner, _T("../resources/1P_winner.png"));
    loadimage(&img_2P_winner, _T("../resources/2P_winner.png"));
    loadimage(&img_winner_bar, _T("../resources/winnner_bar.png"));

    loadimage(&img_avatar_peashooter, _T("../resources/avatar_peashooter.png"));
    loadimage(&img_avatar_sunflower, _T("../resources/avatar_sunflower.png"));

    mciSendString(_T("open ../resources/bgm_game.mp3 alias bgm_game"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/bgm_menu.mp3 alias bgm_menu"), NULL, 0, NULL);

    mciSendString(_T("open ../resources/pea_break_1.mp3 alias pea_break_1"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/pea_break_2.mp3 alias pea_break_2"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/pea_break_3.mp3 alias pea_break_3"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/pea_shoot_1.mp3 alias pea_shoot_1"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/pea_shoot_2.mp3 alias pea_shoot_2"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/pea_shoot_ex.mp3 alias pea_shoot_ex"), NULL, 0, NULL);

    mciSendString(_T("open ../resources/sun_explode.mp3 alias sun_explode"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/sun_explode_ex.mp3 alias sun_explode_ex"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/sun_text.mp3 alias sun_text"), NULL, 0, NULL);

    mciSendString(_T("open ../resources/ui_confirm.wav alias ui_confirm"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/ui_switch.wav alias ui_switch"), NULL, 0, NULL);
    mciSendString(_T("open ../resources/ui_win.wav alias ui_win"), NULL, 0, NULL);
}

由于我们使用了mciSendString()函数所以需要在CmakeLists.txt中添加额外的库文件链接

# 链接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()

3 Animation类

class Animation
{
public:
    Animation() = default;
    ~Animation() = default;

private:
};

增加下述私有变量
int timer = 0;          //计时器
int interval = 0;       //帧间隔
int idx_frame = 0;      //真索引
bool is_loop = true;    //是否循环
Atlas *atlas = nullptr;

之后我们实现了一些基本配置的方法
void reset()
{
    timer = 0;
    idx_frame = 0;
}

void set_atlas(Atlas* new_atlas)
{
    reset();
    atlas = new_atlas;
}

void set_loop(bool flag)
{
    is_loop = flag;
}

void set_interval(int ms)
{
    interval = ms;
}

之后再实现一些输出方法
int get_idx_frame()
{
    return idx_frame;
}

IMAGE* get_frame()
{
    return atlas->get_image(idx_frame);
}

bool check_finished()
{
    if(is_loop)
        return false;

    return (idx_frame == atlas->get_size() - 1);
}

3.1 更新方法on_update()

然后我们实现on_update()方法

void on_update(int delta)
{
    timer += delta;
    if( timer >= interval) {
        timer = 0;
        idx_frame++;
        if(idx_frame >=atlas->get_size()) {
            idx_frame = is_loop ? 0 : atlas->get_size() - 1;
        }
    }
}

3.2 渲染方法on_draw()

之后我们实现渲染方法on_draw(),在此之前我们要先再util.h实现putimage_alpha()

inline void putimage_alpha(int dst_x, int dst_y, IMAGE* img)
{
    int w = img->getwidth();
    int h = img->getheight();
    AlphaBlend(GetImageHDC(GetWorkingImage()), dst_x, dst_y, w, h,
               GetImageHDC(img), 0, 0, w, h, {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA});
}

void on_draw(int x, int y) const
{
    putimage_alpha(x, y, atlas->get_image(idx_frame));
}

3.3 死亡动画的播放

要想播放死亡动画,我们需要延后那些存在消失动画的物体被删除的时间。
当敌人生命值归零时,进入死亡状态,然后播放完死亡动画后,再从内存中删除。
这需要动画从而共勉提供一个动画播放结束的消息
我们可以通过已有的check_finished()方法检查动画是否播放结束,但是,我们可以使用一个更加优雅的概念:回调函数。
回调函数就是一个使用参数传递的函数对象,我们可以把它保存起来,然后在合适的时候调用它,这样就可以在函数内部的逻辑,在“合适的时候”才被执行。
以前面的场景为例,我们把删除敌人的逻辑定义为函数,以回调函数的形式保存在动画对象内部,当死亡动画播放结束后,再调用这个函数删除敌人对象。
这与c语言的函数指针类似,不过再cpp中我们可以使用一种全新的方式。

首先需要引入相关头文件

#include <functional>

我们在动画的私有成员中添加一个function类型的函数对象。
std::function<void()> callback;

function是一个模板类,尖括号中的内容表示这个函数对象存储的函数返回值为void并且没有任何参数。

之后我们在on_update()方法中,动画播放结束的分支语句中添加下述代码

if (!is_loop && callback) {
    callback();
}

另外,我们还需要编写一个为Animation类设置callback的方法
void set_callback(std::function<void()> callback_function)
{
    callback = callback_function;
}

这样,Animation类以及编写完成了

4 测试

4.1 动画播放测试

Menu_Scene中引入对应的头文件,并使用extern声明外部的动画图集等资源

extern Atlas atlas_peashooter_run_right;
extern SceneManager scene_manager;

随后我们在MenuScene类的私有成员中定义Animation对象

Animation animation_peashooter_run_right;

on_enter()中设置动画的属性
animation_peashooter_run_right.set_atlas(&atlas_peashooter_run_right);
animation_peashooter_run_right.set_interval(75);
animation_peashooter_run_right.set_loop(true);

on_update()中调用动画的on_update()方法
void on_update(int delta) {
    animation_peashooter_run_right.on_update(delta);
    std::cout << "主菜单正在运行" << std::endl;
}

这是,我们发现之前没有把时间变化量作为参数传递进来。所以,只能小幅重构一下之前的代码了
首先,在基类Scene中为虚方法on_update()添加参数
virtual void on_update(int delta) { }

除此之外,GameScene类和SelectorScene类也需要进行这样的修改。
而且,SceneManager类也需要修改。

现在回到main函数,我们需要把计算好的时间间隔传递给场景管理器

static DWORD last_tick_time = GetTickCount();
DWORD current_tick_time = GetTickCount();
DWORD delta_tick = current_tick_time - last_tick_time;
scene_manager.on_update(delta_tick);
last_tick_time = current_tick_time;

下面我们继续回到MenuScene中,在on_draw()方法内部调用动画对象的on_draw()方法

animation_peashooter_run_right.on_draw(100, 100);

4.2 回调函数测试

on_enter()方法中设置回调函数,这里使用了lambda函数作为参数

animation_peashooter_run_right.set_callback(
    []()
    {
        scene_manager.switch_to(SceneManager::SceneType::Game);
    }
);

为了便于观察,我们将动画的interval设置为300
animation_peashooter_run_right.set_interval(300);
animation_peashooter_run_right.set_loop(false);

这时运行程序,我们可以看到在可以放慢速度的动画播放结束后,游戏成功跳转到了GameScene阶段。

评论

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