提瓦特幸存者番外篇特效实现(并非完美)
如果对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(¤t_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, ¤t_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...