VoidMatrix大佬项目提瓦特幸存者番外篇特效实现(并非完美)

提瓦特幸存者番外篇特效实现(并非完美)

如果对c++游戏开发感兴趣,请一定要关注VoidMatrix大佬!Voidmatrix的个人空间-Voidmatrix个人主页-哔哩哔哩视频

番外篇中作者并没有给出完整的教程:代码应该放在哪里(如何组织代码)。这让我一个新手猛然间醒悟,原来我根本就没有理解这个项目。我只是跟着敲代码,却很少思考过为什么要这么做!

实际上,术有万千而道不变!不要以为自己跟着敲了很多代码就是掌握了整个项目,你不去理解,不去深挖,实际上就是什么也没有学到!以下是我结合AI部分提示,交上一份难称及格的答卷,我希望有朝一日,我能更好地实现它。

一,翻转像素原理:

二,理解类之间的关系

Atlas类

Atlas类,图集类,作用是将文件中的图片加载(loadimage)到Atlas对象中,该对象封装了动画的序列帧,并提供GetFrame接口返回某帧图片(配合Animation类实现图片的渲染),提供FrameCount只读封装方法,返回序列帧总长(便于在Animation类中设置循环播放逻辑)。

当我在冥思苦想闪白效果该放到哪里的时候,AI已经给出了它的答案,Atlas类中。

ini 复制代码
// 图集类 - 支持自动翻转和白色剪影
class Atlas {
public:
    Atlas(LPCTSTR path, int num, bool auto_flip = false, bool gen_white = false) {
        TCHAR path_file[256];
        for (int i = 0; i < num; ++i) {
            _stprintf_s(path_file, path, i);
​
            IMAGE* frame = new IMAGE();
            loadimage(frame, path_file);
            frames.push_back(frame);
​
            if (auto_flip) {
                IMAGE* flipped = new IMAGE();
                FlipImageHorizontal(frame, flipped);
                flipped_frames.push_back(flipped);
            }
​
            if (gen_white) {
                IMAGE* white = new IMAGE();
                GenerateWhiteSilhouette(frame, white);
                white_frames.push_back(white);
​
                if (auto_flip) {
                    IMAGE* white_flipped = new IMAGE();
                    FlipImageHorizontal(white, white_flipped);
                    white_flipped_frames.push_back(white_flipped);
                }
            }
        }
    }
​
    ~Atlas() {
        for (auto frame : frames) delete frame;
        for (auto frame : flipped_frames) delete frame;
        for (auto frame : white_frames) delete frame;
        for (auto frame : white_flipped_frames) delete frame;
    }
​
    IMAGE* GetFrame(int index, bool flipped = false) {
        if (flipped && !flipped_frames.empty()) {
            return flipped_frames[index % flipped_frames.size()];
        }
        return frames[index % frames.size()];
    }
​
    IMAGE* GetWhiteFrame(int index, bool flipped = false) {
        if (flipped && !white_flipped_frames.empty()) {
            return white_flipped_frames[index % white_flipped_frames.size()];
        }
        return white_frames[index % white_frames.size()];
    }
​
    size_t FrameCount() const { return frames.size(); }
    //这个简单的函数体现了良好的面向对象设计思想,是管理动画资源时非常有用的工具方法。
​
private:
    static void FlipImageHorizontal(IMAGE* src, IMAGE* dst) {
        int width = src->getwidth();
        int height = src->getheight();
        dst->Resize(width, height);
​
        DWORD* src_buf = GetImageBuffer(src);
        DWORD* dst_buf = GetImageBuffer(dst);
​
        for (int y = 0; y < height; ++y) {
            for (int x = 0; x < width; ++x) {
                dst_buf[y * width + (width - 1 - x)] = src_buf[y * width + x];
            }
        }
    }
​
    static void GenerateWhiteSilhouette(IMAGE* src, IMAGE* dst) {
        int width = src->getwidth();
        int height = src->getheight();
        dst->Resize(width, height);
​
        DWORD* src_buf = GetImageBuffer(src);
        DWORD* dst_buf = GetImageBuffer(dst);
​
        for (int i = 0; i < width * height; ++i) {
            BYTE alpha = (src_buf[i] >> 24) & 0xFF;
            dst_buf[i] = (alpha << 24) | 0x00FFFFFF;
        }
    }
​
    std::vector<IMAGE*> frames;
    std::vector<IMAGE*> flipped_frames;
    std::vector<IMAGE*> white_frames;
    std::vector<IMAGE*> white_flipped_frames;
};

AI对Atlas类进行修改,使之可以选择是否生成翻转的图像,并提供对应的成员函数和成员变量,与Animation类相配合。

Animation类

Animation类,动画类,用Atlas图集和序列帧切换间隔以及是否翻转初始化。封装Play函数以循环渲染序列帧,提供SetFlipped函数判断渲染方向不同的图片。

ini 复制代码
// 动画类
class Animation {
public:
    Animation(Atlas* atlas, int interval)
        : atlas(atlas), interval_ms(interval) {}
​
    void Play(int x, int y, int delta, bool flipped = false, bool flash_effect = false) {
        timer += delta;
        if (timer >= interval_ms) {
            idx_frame = (idx_frame + 1) % atlas->FrameCount();
            timer = 0;
        }
​
        IMAGE* frame = flash_effect ?
            atlas->GetWhiteFrame(idx_frame, flipped) :
            atlas->GetFrame(idx_frame, flipped);
​
        putimage_alpha(x, y, frame);
    }
​
private:
    Atlas* atlas;
    int timer = 0;
    int idx_frame = 0;
    int interval_ms = 0;
};
Player类

Player类,玩家类,初始化时,创建Atlas类和Animation类的对象。便于后续渲染。有类方法ProcessEvent接受键盘指令处理运动逻辑(运动逻辑为内置的成员变量),提供setpos接口调整角色初始化位置。类成员move函数平衡斜向移动速度,并校正玩家位置处于游戏界面内。Draw函数绘制角色阴影,根据之前修改的运动逻辑信号调整角色朝向,之后调用Animation类中的函数Play实现角色绘制和帧率控制。

之所以在Player类中添加冰冻渲染,我猜可能是因为渲染只需要根据素材像素的情况做一些调整,它建立在素材之上,而Atlas中的都是从无到有的素材实现。

ini 复制代码
// 玩家类
class Player {
public:
    Player() {
        loadimage(&img_shadow, _T("提瓦特幸存者/img/shadow_player.png"));
        loadimage(&img_ice, _T("提瓦特幸存者/img/ice.png"));
        atlas = new Atlas(_T("提瓦特幸存者/img/player_left_%d.png"), PLAYER_ANIM_NUM, true, true);
        anim = new Animation(atlas, 45);
    }
​
    ~Player() {
        delete anim;
        delete atlas;
    }
​
    void ProcessEvent(const ExMessage& msg, bool useArrowKeys = false) {
        switch (msg.message) {
        case WM_KEYDOWN:
            if (useArrowKeys) {
                switch (msg.vkcode) {
                case VK_UP: is_move_up = true; break;
                case VK_DOWN: is_move_down = true; break;
                case VK_LEFT: is_move_left = true; break;
                case VK_RIGHT: is_move_right = true; break;
                }
            }
            else {
                switch (msg.vkcode) {
                case 'W': is_move_up = true; break;
                case 'S': is_move_down = true; break;
                case 'A': is_move_left = true; break;
                case 'D': is_move_right = true; break;
                }
            }
            break;
​
        case WM_KEYUP:
            if (useArrowKeys) {
                switch (msg.vkcode) {
                case VK_UP: is_move_up = false; break;
                case VK_DOWN: is_move_down = false; break;
                case VK_LEFT: is_move_left = false; break;
                case VK_RIGHT: is_move_right = false; break;
                }
            }
            else {
                switch (msg.vkcode) {
                case 'W': is_move_up = false; break;
                case 'S': is_move_down = false; break;
                case 'A': is_move_left = false; break;
                case 'D': is_move_right = false; break;
                }
            }
            break;
        }
    }
​
    void setpos(POINT pos) { player_pos = pos; }
​
    void move() {
        int dir_x = is_move_right - is_move_left;
        int dir_y = is_move_down - is_move_up;
        double len_dir = sqrt(dir_x * dir_x + dir_y * dir_y);
​
        if (len_dir != 0) {
            player_pos.x += static_cast<int>(PLAYER_SPEED * dir_x / len_dir);
            player_pos.y += static_cast<int>(PLAYER_SPEED * dir_y / len_dir);
        }
​
        // 边界检查
        player_pos.x = max(0, min(WINDOW_WIDTH - PLAYER_WIDTH, player_pos.x));
        player_pos.y = max(0, min(WINDOW_HEIGHT - PLAYER_HEIGHT, player_pos.y));
    }
​
    void RenderFrozenEffect() {
        static int counter = 0;
        static int anim_timer = 0;
        static int frozen_timer = 0;
        static const int THICKNESS = 5;
        static int highlight_pos_y;
        static bool is_frozen = false;
​
        if (!is_frozen && (++anim_timer % 3 == 0))
            counter = (counter + 1) % PLAYER_ANIM_NUM;
        if (++frozen_timer % 100 == 0) {
            is_frozen = !is_frozen;
            highlight_pos_y = -THICKNESS;
        }
​
        //// 绘制阴影
        //int shadow_x = position.x + (PLAYER_WIDTH - SHADOW_WIDTH) / 2;
        //int shadow_y = position.y + PLAYER_HEIGHT - 8;
        //putimage_alpha(shadow_x, shadow_y, &img_shadow);
​
        if (is_frozen) {
            IMAGE current_frame(*atlas->GetFrame(counter, facing_left));
            int width = current_frame.getwidth();
            int height = current_frame.getheight();
​
            highlight_pos_y = (highlight_pos_y + 2) % height;
​
            DWORD* ice_buf = GetImageBuffer(&img_ice);
            DWORD* frame_buf = GetImageBuffer(&current_frame);
​
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    int idx = y * width + x;
                    static const float RATIO = 0.25f;
                    static const float THRESHOLD = 0.84f;
                    DWORD ice_color = ice_buf[idx];
                    DWORD frame_color = frame_buf[idx];
​
                    if ((frame_color >> 24) & 0xFF) {
                        BYTE r = static_cast<BYTE>(GetBValue(frame_color) * RATIO + GetBValue(ice_color) * (1 - RATIO));
                        BYTE g = static_cast<BYTE>(GetGValue(frame_color) * RATIO + GetGValue(ice_color) * (1 - RATIO));
                        BYTE b = static_cast<BYTE>(GetRValue(frame_color) * RATIO + GetRValue(ice_color) * (1 - RATIO));
​
                        if ((y >= highlight_pos_y && y <= highlight_pos_y + THICKNESS) &&
                            (r / 255.0f * 0.2126f + g / 255.0f * 0.7152f + b / 255.0f * 0.0722f >= THRESHOLD)) {
                            frame_buf[idx] = 0xFFFFFFFF; // 纯白高亮
                            continue;
                        }
                        frame_buf[idx] = RGB(r, g, b) | (frame_color & 0xFF000000);
                    }
                }
            }
            putimage_alpha(player_pos.x, player_pos.y, &current_frame);
        }
        else {
            putimage_alpha(player_pos.x, player_pos.y, atlas->GetFrame(counter, facing_left));
        }
    }
​
    void SetFrozen(bool frozen) {
        is_frozen_state = frozen;
        if (frozen) frozen_timer = 0; // 重置计时器
    }
​
    bool IsFrozen() const { return is_frozen_state; }
​
    void StartFlash(int duration_ms) {
        is_flashing = true;
        flash_timer = duration_ms;
    }
​
    void Draw(int delta) {
        // 绘制阴影
        int pos_shadow_x = player_pos.x + (PLAYER_WIDTH / 2 - SHADOW_WIDTH / 2);
        int pos_shadow_y = player_pos.y + PLAYER_HEIGHT - 8;
        putimage_alpha(pos_shadow_x, pos_shadow_y, &img_shadow);
​
        // 更新朝向
        int dir_x = is_move_right - is_move_left;
        if (dir_x != 0) {
            facing_left = dir_x > 0;
        }
​
        if (IsFrozen()) {
            RenderFrozenEffect();
            return;
        }
​
        // 更新闪白状态
        if (is_flashing) {
            flash_timer -= delta;
            if (flash_timer <= 0) {
                is_flashing = false;
            }
        }
​
        // 绘制玩家
        bool show_white = is_flashing && ((flash_timer / FLASH_INTERVAL) % 2 == 0);
        anim->Play(player_pos.x, player_pos.y, delta, facing_left, show_white);
    }
​
private:
    const int PLAYER_WIDTH = 80;
    const int PLAYER_HEIGHT = 80;
    const int PLAYER_SPEED = 5;
    const int SHADOW_WIDTH = 32;
    static constexpr int FLASH_INTERVAL = 100;
​
    IMAGE img_shadow;
    POINT player_pos = { WINDOW_WIDTH / 2, WINDOW_HEIGHT / 2 };
    Atlas* atlas;
    Animation* anim;
​
    bool is_move_up = false;
    bool is_move_down = false;
    bool is_move_left = false;
    bool is_move_right = false;
    bool facing_left = false;
​
    bool is_flashing = false;
    int flash_timer = 0;
​
    // 添加冰冻特效所需资源
    IMAGE img_ice; // 冰冻纹理图
    bool is_frozen_state = false; // 冰冻状态控制
    int frozen_timer = 0;
};

三,全局变量与函数

arduino 复制代码
//实现透明通道混叠,必不可少
#pragma comment(lib, "MSIMG32.LIB")
​
// 全局常量
const int WINDOW_WIDTH = 1280;
const int WINDOW_HEIGHT = 720;
const int PLAYER_ANIM_NUM = 6;
bool running = true;
​
// 透明贴图函数
inline void putimage_alpha(int x, int y, IMAGE* img) {
    int w = img->getwidth();
    int h = img->getheight();
    AlphaBlend(GetImageHDC(NULL), x, y, w, h,
        GetImageHDC(img), 0, 0, w, h, { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA });
}

四,主函数

经测试,冰冻和闪白特效并不能同时触发。

scss 复制代码
int main() {
    initgraph(WINDOW_WIDTH, WINDOW_HEIGHT);
    Player player, player2;
    player2.setpos({ 100, 100 });
    player2.SetFrozen(true);
    // 测试闪白效果
    player.StartFlash(2000); // 2秒闪白
​
    IMAGE img_background;
    loadimage(&img_background, _T("提瓦特幸存者/img/background.png"));
​
    BeginBatchDraw();
    while (running) {
        DWORD start_time = GetTickCount();
​
        ExMessage msg;
        while (peekmessage(&msg)) {
            if (msg.message == WM_KEYDOWN && msg.vkcode == VK_ESCAPE) {
                running = false;
            }
            player.ProcessEvent(msg);
            player2.ProcessEvent(msg, true);
        }
        if (player.IsFrozen() && GetTickCount() > 1000) {
            player.SetFrozen(false);
        }
        player.move();
        player2.move();
​
        cleardevice();
        putimage(0, 0, &img_background);
        player.Draw(1000/144);  // 假设60FPS
        player2.Draw(1000/144);
        FlushBatchDraw();
​
        DWORD end_time = GetTickCount();
        DWORD delta_time = end_time - start_time;
        if (delta_time < 16) {
            Sleep(16 - delta_time);
        }
    }
​
    EndBatchDraw();
    return 0;
}

完整代码,博文全文上传至github仓库:github.com/kair998/Tiw...

相关推荐
晚雾也有归处1 小时前
链表(C++)
数据结构·c++·链表
勘察加熊人1 小时前
c++实现录音系统
开发语言·c++
李余博睿(新疆)2 小时前
青少年软件编程(C语言)等级考试试卷(三级)
c++
看到我,请让我去学习3 小时前
C语言快速入门-C语言基础知识
c语言·开发语言·c++·vscode
悄悄敲敲敲3 小时前
C++第13届蓝桥杯省b组习题笔记
c++·笔记·算法·蓝桥杯
Non importa4 小时前
【初阶数据结构】线性表之双链表
c语言·开发语言·数据结构·c++·考研·链表·学习方法
学习同学4 小时前
C++初阶知识复习 (31~45)
开发语言·c++
A1-295 小时前
C++的四种类型转换
开发语言·c++
噜啦噜啦嘞好5 小时前
c++的特性——多态
开发语言·c++
ydm_ymz6 小时前
初阶8 list
c语言·开发语言·数据结构·c++·list