要让物体沿着路径移动,必须同时修改X/Y两个值,用两个连续插值动画行不行?
在单片机这种单线程设备,两个TICK会前后脚进行修改,具有相同的时间跨度,所以似乎也是可以的。但是在支持多线程的设备,各动画对象的tick就不一定了。显示时可能会偏离路径。另外我们可能还会有更复杂的动画,比如,我们的任务要奔跑,那需要同时修改XY,同时人物奔跑时还有手脚动作的帧动画,可能显示血条的单次动画,等等等等,这些东西要是分开做的话,其同步可能会令人崩溃。
所以,我们需要一个播放组合型动画的能力。这里,我们借用 C# 的故事板概念,实现类似功能:
AnimationGroup 之间串行处理
AnimationGroup 内部的Animation 并行处理
当然,我们不可能完整实现C# 的故事板,只是最核心的最精简的能力。尽可能封装好,将来可移植复用即可。
1、定义动画组 AnimationGroup
AnimationGroup.h
/*
* AnimationGroup.h
*
* Created on: Dec 26, 2023
* Author: YoungMay
*/
#ifndef SRC_ANICOMP_ANIMATIONGROUP_H_
#define SRC_ANICOMP_ANIMATIONGROUP_H_
#include "Animation.h"
class AnimationGroup {
public:
AnimationGroup();
virtual ~AnimationGroup();
void addItem(Animation *animation);
void tick(uint32_t t);
void start();
uint8_t isValid = 0;
uint32_t repeat = 1;
uint8_t id;
private:
ListNode *animationList;
};
#endif /* SRC_ANICOMP_ANIMATIONGROUP_H_ */
AnimationGroup.cpp
/*
* AnimationGroup.cpp
*
* Created on: Dec 26, 2023
* Author: YoungMay
*/
#include "AnimationGroup.h"
AnimationGroup::AnimationGroup() {
animationList = ListCreate();
}
AnimationGroup::~AnimationGroup() {
for (ListNode *node = animationList->next; node != animationList; node =
node->next) {
delete (Animation*) node->data;
}
ListDestory(animationList);
}
void AnimationGroup::addItem(Animation *animation) {
ListPushBack(animationList, (LTDataType) animation);
}
void AnimationGroup::tick(uint32_t t) {
isValid = 0;
for (ListNode *node = animationList->next; node != animationList; node =
node->next) {
if (((Animation*) node->data)->isValid) {
((Animation*) node->data)->tick(t);
}
isValid = isValid | ((Animation*) node->data)->isValid;
}
}
void AnimationGroup::start() {
isValid = 1;
for (ListNode *node = animationList->next; node != animationList; node =
node->next) {
((Animation*) node->data)->start();
}
}
注意tick里面的处理,每个animation都能执行到。所有animation结束后,该group结束。
2、定义故事板 AnimationStoryBoard
AnimationStoryBoard.h
/*
* AnimationStoryBoard.h
*
* Created on: Dec 26, 2023
* Author: YoungMay
*/
#ifndef SRC_ANICOMP_ANIMATIONSTORYBOARD_H_
#define SRC_ANICOMP_ANIMATIONSTORYBOARD_H_
#include "AnimationGroup.h"
class AnimationStoryBoard {
public:
AnimationStoryBoard();
virtual ~AnimationStoryBoard();
void addItem(AnimationGroup *group);
void tick(uint32_t t);
void start();
uint8_t isValid = 0;
private:
ListNode *animationGroupList;
};
#endif /* SRC_ANICOMP_ANIMATIONSTORYBOARD_H_ */
AnimationStoryBoard.cpp
/*
* AnimationStoryBoard.cpp
*
* Created on: Dec 26, 2023
* Author: YoungMay
*/
#include "AnimationStoryBoard.h"
AnimationStoryBoard::AnimationStoryBoard() {
animationGroupList = ListCreate();
}
AnimationStoryBoard::~AnimationStoryBoard() {
for (ListNode *node = animationGroupList->next; node != animationGroupList;
node = node->next) {
delete (AnimationGroup*) node->data;
}
ListDestory(animationGroupList);
}
void AnimationStoryBoard::addItem(AnimationGroup *group) {
ListPushBack(animationGroupList, (LTDataType) group);
}
void AnimationStoryBoard::tick(uint32_t t) {
if (!isValid)
return;
isValid = 0;
for (ListNode *node = animationGroupList->next; node != animationGroupList;
node = node->next) {
AnimationGroup *group = (AnimationGroup*) node->data;
if (group->isValid) {
group->tick(t);
isValid = 1;
if ((!group->isValid) && group->repeat > 1) {
group->repeat--;
group->start();
}
return;
}
}
}
void AnimationStoryBoard::start() {
isValid = 1;
for (ListNode *node = animationGroupList->next; node != animationGroupList;
node = node->next) {
((AnimationGroup*) node->data)->start();
}
}
注意tick里面的处理,只处理第一个未结束的animationGroup。
好了,现在可以设置BOSS的飞行路径。
分为两个group.先从屏幕外面飞下来,然后绕8字形循环飞行。
1、修改BOSS,添加一个故事板 animationStoryBoard
class EnemyT3: public EnemyBase {
public:
EnemyT3();
~EnemyT3();
uint8_t tick(uint32_t t);
void init();
uint8_t show(void);
uint8_t hitDetect(int x, int y, int damage);
bool sharp[5][9] = { { 0, 0, 1, 1, 1, 1, 1, 0, 0, }, { 0, 0, 0, 0, 1, 0, 0,
0, 0, }, { 1, 1, 1, 1, 1, 1, 1, 1, 1, },
{ 0, 0, 1, 0, 1, 0, 1, 0, 0, }, { 0, 0, 0, 0, 1, 0, 0, 0, 0, } };
private:
AnimationStoryBoard *animationStoryBoard;
IntervalAniTimer_t fireTimer = { 2000, 6000 };
void createBulletObject();
};
2、在BOSS初始化 init 函数中灌入数据:
void EnemyT3::init() {
damageAnimation.addItem(0, 0xa02000);
damageAnimation.addItem(1000, 0x604000);
explodeAnimation.addItem(0, 1);
explodeAnimation.addItem(200, 2);
explodeAnimation.addItem(400, 3);
explodeAnimation.addItem(600, 4);
explodeAnimation.addItem(800, 5);
explodeAnimation.addItem(1000, 6);
explodeAnimation.addItem(1200, 7);
explodeAnimation.addItem(1400, 100);
animationStoryBoard = new AnimationStoryBoard();
AnimationGroup *group1 = new AnimationGroup();
ContinuousAnimation *ani1X = new ContinuousAnimation();
ContinuousAnimation *ani1Y = new ContinuousAnimation();
ani1X->bindAddress = &baseInfo.x;
ani1Y->bindAddress = &baseInfo.y;
group1->addItem(ani1X);
group1->addItem(ani1Y);
ani1X->addItem(0, 20 * PlaneXYScale);
ani1Y->addItem(0, 0 * PlaneXYScale);
ani1X->addItem(2000, 20 * PlaneXYScale);
ani1Y->addItem(2000, 10 * PlaneXYScale);
animationStoryBoard->addItem(group1);
AnimationGroup *group2 = new AnimationGroup();
ContinuousAnimation *ani2X = new ContinuousAnimation();
ContinuousAnimation *ani2Y = new ContinuousAnimation();
ani2X->bindAddress = &baseInfo.x;
ani2Y->bindAddress = &baseInfo.y;
group2->addItem(ani2X);
group2->addItem(ani2Y);
ani2X->addItem(0, 20 * PlaneXYScale);
ani2Y->addItem(0, 10 * PlaneXYScale);
ani2X->addItem(5658, 12 * PlaneXYScale);
ani2Y->addItem(5658, 15 * PlaneXYScale);
ani2X->addItem(8658, 7 * PlaneXYScale);
ani2Y->addItem(8658, 15 * PlaneXYScale);
ani2X->addItem(10356, 5 * PlaneXYScale);
ani2Y->addItem(10356, 16 * PlaneXYScale);
ani2X->addItem(10956, 5 * PlaneXYScale);
ani2Y->addItem(10956, 14 * PlaneXYScale);
ani2X->addItem(12654, 7 * PlaneXYScale);
ani2Y->addItem(12654, 10 * PlaneXYScale);
ani2X->addItem(15654, 12 * PlaneXYScale);
ani2Y->addItem(15654, 10 * PlaneXYScale);
ani2X->addItem(21312, 20 * PlaneXYScale);
ani2Y->addItem(21312, 15 * PlaneXYScale);
ani2X->addItem(24312, 25 * PlaneXYScale);
ani2Y->addItem(24312, 15 * PlaneXYScale);
ani2X->addItem(26010, 27 * PlaneXYScale);
ani2Y->addItem(26010, 16 * PlaneXYScale);
ani2X->addItem(26610, 27 * PlaneXYScale);
ani2Y->addItem(26610, 14 * PlaneXYScale);
ani2X->addItem(28308, 25 * PlaneXYScale);
ani2Y->addItem(28308, 10 * PlaneXYScale);
ani2X->addItem(31308, 20 * PlaneXYScale);
ani2Y->addItem(31308, 10 * PlaneXYScale);
group2->repeat = 1000000;
animationStoryBoard->addItem(group2);
animationStoryBoard->start();
}
3、最后在BOSS的tick里面加上故事板的调用。
uint8_t EnemyT3::tick(uint32_t t) {
if (explodeState == 0)
baseInfo.y += t * baseInfo.speed;
if (baseInfo.y > 64 * PlaneXYScale)
baseInfo.visiable = 0;
if (fireTimer.tick(t))
createBulletObject();
animationStoryBoard->tick(t);
for (ListNode *node = animationList->next; node != animationList; node =
node->next) {
if (((Animation*) node->data)->isValid) {
((Animation*) node->data)->tick(t);
}
}
return 0;
}
好了。看看效果:
STM32学习笔记十七:WS2812制作像素游戏屏-飞行射击
故事板功能可以做得非常强大,通过各种animation和group的组合,可以实现复杂的动画。还可以自行扩展各种group的触发条件,如前置后置条件,同步异步等。
大致思路如此,要做成什么样就看需求了。
STM32学习笔记十八:WS2812制作像素游戏屏-飞行射击游戏(8)探索游戏多样性,范围伤害模式