01-游戏资产处理与程序动画框架
1 导入之前实现的类
首先复制之前实现Vector2类,Timer类,Atlas类到本工程中,它们都和植物明星大乱斗中的相同。
注意Timer的on_update方法,参数中的delta要改成float类型,还有set_wait_time也要改参数的类型为float。
2 实现裁剪绘图函数
在resources中enemy目录下以不同路径存储了不同招式下的敌人序列帧,而在player目录下,则是以精灵图的方式存储玩家动画。
之前我们实现的putimage_alpha中,直接渲染图片,这无法处理精灵图。所以这里我们重新封装一个支持裁切图片的绘图函数
新建util.h头文件,首先定义Rect结构体来描述矩形
struct Rect
{
int x, y; // 左上角点坐标
int w, h; // 宽度和高度
};
之后实现下述的绘图函数
inline void putimage_ex(IMAGE* img, const Rect* rect_dst, const Rect* rect_src = nullptr)
{
// 当无需裁切图片时,无需传入rect_src
static BLENDFUNCTION blend_func = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
AlphaBlend(GetImageHDC(GetWorkingImage()), rect_dst->x, rect_dst->y, rect_dst->w, rect_dst.h,
GetImageHDC(img), rect_src ? rect_src->x : 0, rect_src ? rect_src->y : 0,
rect_src ? rect_src->w : img->getwidth(), rect_src ? rect_src->h : img->getheight(), blend_func);
}
3 Animation类
接下来我们考虑一个锚点定位的问题。一般情况下,我们通常把玩家脚底的坐标记录为它们的位置,这是因为不同角色的身高不同,这样做可以避免渲染和数据的耦合,而且也更加方便了对地面碰撞的处理。
但是对于子弹等物体,它们一般是完全对称的,我们一般把它们的中心作为位置坐标。
我们的动画系统在处理这些游戏对象的定位时,就需要根据不同的锚点逻辑进行自动的偏移和调整了。
创建animation.h头文件,
class Animation
{
public:
enum class AnchorNode
{
Centered,
BottomCentered
};
public:
Animation()
{
}
~Animation()
{
}
private:
};
3.1 抽象Frame结构体
原本以图片变量作为每一帧,已经无法满足处理精灵图的需求了。所以我们定义Frame结构体
private:
struct Frame
{
Rect rect_src;
IMAGE *image = nullptr;
Frame() = default;
Frame(IMAGE* image, const Rect& rect_src)
: image(image), rect_src(rect_src) { }
~Frame() = default;
};
注意,帧是一个动画内部的概念,裁剪区域的生成是根据图片上的帧数量自动计算得到的。
无需对外暴露直接操纵每一帧的接口。所以我们这里将Frame定义为私有类型。
3.2 定义私有变量
现在,我们就可以为Animation类添加这样的私有成员
private:
Timer timer;
Vector2 position;
bool is_loop = true;
size_t idx_frame = 0;
std::vector<Frame> frame_list;
std::function<void()> on_finished;
AnchorNode anchor_node = AnchorNode::Centered;
我们有时会将动画作为游戏世界中事件触发的驱动力,比如子弹破碎动画播放结束后再将他们从场景中删除掉。所以我们使用了on_finished回调函数,来处理动画播放结束的逻辑。
在构造函数中,我们就需要对定时器进行设置了。
Animation()
{
timer.set_one_shot(false);
timer.set_callback(
[&]()
{
idx_frame++;
if (idx_frame >= frame_list.size())
{
idx_frame = is_loop ? 0 : frame_list.size() - 1;
if (!is_loop && on_finished)
on_finished();
}
});
}
3.3 外部配置函数
之后,我们编写下面这几个函数来方便在外部对Animation类进行设置
void reset()
{
timer.restart();
idx_frame = 0;
}
void set_anchor_mode(AnchorMode mode)
{
anchor_mode = mode;
}
void set_position(const Vector2& position)
{
this->position = position;
}
void set_loop(bool is_loop)
{
this->is_loop = is_loop;
}
void set_interval(float interval)
{
timer.set_wait_time(interval);
}
void set_on_finished(std::function<void()> on_finished)
{
this->on_finished = on_finished;
}
如何为一个动画添加帧呢?我们定义add_frame方法,用来添加武士零角色一整条的动画素材。除去图片指针之外,我们还需要额外传入一个数字来表示当前图片素材需要被切分成几帧进行显示。
void add_frame(IMAGE* image, int num_h)
{
int width = image->getwidth();
int height = image->getheight();
int width_frame = width / num_h;
for (int i = 0; i < num_h; i++) {
Rect rect_src;
rect_src.x = i * width_frame, rect_src.y = 0;
rect_src.w = width_frame, rect_src.h = height;
frame_list.emplace_back(image, rect_src);
}
}
另外,由于敌人动画我们使用的是图片帧序列进行存储,使用Atlas图集类来进行管理。所以我们还需要提供一个使用图集对象作为参数添加动画帧的函数重载。
void add_frame(Atlas* atlas)
{
for (int i = 0; i < atlas->get_size(); i++)
{
IMAGE *image = atlas->get_image(i);
int width = image->getwidth();
int height = image->getheight();
Rect rect_src;
rect_src.x = 0, rect_src.y = 0;
rect_src.w = width, rect_src.h = height;
frame_list.emplace_back(image, rect_src);
}
}
3.4 更新和渲染
然后便是动画的核心部分:更新和渲染
有了timer类作为动画播放的驱动力,在on_update中只需要调用timer类的对应方法即可。
渲染时,我们则需要根据锚点来计算目标矩形的位置。
void on_update(float delta)
{
timer.on_update(delta);
}
void on_render()
{
const Frame &frame = frame_list[idx_frame];
Rect rect_dst;
rect_dst.x = (int)position.x - frame.rect_src.w / 2;
rect_dst.y = (anchor_mode == AnchorMode::Centered)
? (int)position.y - frame.rect_src.h / 2 : (int)position.y - frame.rect_src.h;
rect_dst.w = frame.rect_src.w, rect_dst.h = frame.rect_src.h;
putimage_ex(frame.image, &rect_dst, &frame.rect_src);
}
与之前的Animation类相比,我们统一使用定时器来驱动,并且抽象了Frame的概念,来适配不同类型的游戏素材资源。使它们能够兼容到我们的动画类中,进行统一的播放处理。
评论
如果你已登录 GitHub,就可以直接在这里评论。