本章又是个重要的章节——动画。
动画,本质上时一系列静态的画面连续播放,欺骗人眼产生动画效果。这个原理自打十九世纪电影诞生开始,就从来没变过。
我们的游戏中也需要一些动画效果,比如,被击中时的受伤效果,击毁效果,血包的动画效果等等。这些动画分为两类:连续线性动画、离散的帧动画。
离散动画,就是在指定的时间点,将目标变量设定为特定的值。
连续动画,就是除了两个特定时间之外,通过插值算法为中间帧设定中间值。
这两者的时间轴都应不受系统处理能力的影响,所以,我们又想到了tick。
我们先从简单的开始,先做个帧动画。设定飞机被击中时,变为红色,1秒后恢复,单次动画不重复。
1、先定义一个动画基类:
Animation.h
/*
* Animation.h
*
* Created on: Dec 25, 2023
* Author: YoungMay
*/
#ifndef SRC_ANICOMP_ANIMATION_H_
#define SRC_ANICOMP_ANIMATION_H_
#include "stdint.h"
#include "../drivers/DList.h"
#include "../drivers/tools.h"
typedef struct {
uint32_t time;
int value;
} AnimationData;
class Animation {
public:
Animation() {
dataList = ListCreate();
}
virtual ~Animation() {
ListDestory(dataList);
}
void addItem(uint32_t time, int value);
virtual int tick(uint32_t t)=0;
void start();
uint8_t isValid = 0;
int *bindAddress = NULL;
protected:
ListNode *dataList;
uint32_t totalTick;
};
#endif /* SRC_ANICOMP_ANIMATION_H_ */
其中
各时间点的数据,保存在链表dataList中。
bindAddress是绑定的数据地址,到了指定时刻,我们就修改它。
?2、再定义一个离散动画类:
DispersedAnimation.h
/*
* DispersedAnimation.h
*
* Created on: Dec 25, 2023
* Author: YoungMay
*/
#ifndef SRC_ANICOMP_DISPERSEDANIMATION_H_
#define SRC_ANICOMP_DISPERSEDANIMATION_H_
#include "Animation.h"
class DispersedAnimation: public Animation {
public:
DispersedAnimation();
~DispersedAnimation();
int tick(uint32_t t);
};
#endif /* SRC_ANICOMP_DISPERSEDANIMATION_H_ */
?DispersedAnimation.cpp
/*
* DispersedAnimation.cpp
*
* Created on: Dec 25, 2023
* Author: YoungMay
*/
#include "DispersedAnimation.h"
DispersedAnimation::DispersedAnimation() {
// TODO Auto-generated constructor stub
}
DispersedAnimation::~DispersedAnimation() {
// TODO Auto-generated destructor stub
}
int DispersedAnimation::tick(uint32_t t) {
totalTick += t;
if (((AnimationData*) dataList->prev->data)->time < totalTick) {
isValid = 0;
if (bindAddress != NULL)
*bindAddress = ((AnimationData*) dataList->prev->data)->value;
return ((AnimationData*) dataList->prev->data)->value;
}
if (((AnimationData*) dataList->next->data)->time > totalTick) {
if (bindAddress != NULL)
*bindAddress = ((AnimationData*) dataList->next->data)->value;
return ((AnimationData*) dataList->next->data)->value;
}
ListNode *node = dataList->next;
while (((AnimationData*) node->next->data)->time < totalTick) {
node = node->next;
}
if (bindAddress != NULL)
*bindAddress = ((AnimationData*) node->data)->value;
return ((AnimationData*) node->data)->value;
}
?动画类也有tick操作,我们把所有时间间隔都累加到了totalTick里面。
3、再看看怎么使用:
我们先在敌机的基类里面加上动画类 damageAnimation,让每个敌机都具备动画的能力。
class EnemyBase {
public:
EnemyBase();
virtual ~EnemyBase() {
}
virtual uint8_t tick(uint32_t t)=0;
virtual void init()=0;
virtual uint8_t show(void)=0;
virtual uint8_t hitDetect(int x, int y)=0;
ListNode *enemyBulletList;
PlaneObject_t baseInfo;
int HP;
void hurt() {
damageAnimation.start();
}
protected:
DispersedAnimation damageAnimation;
ListNode *animationList;
};
animationList是用于保存所有动画的链表。动画damageAnimation 其实是可以在外层如enemyManager或者plane里面进行定义和注入的,但因为他与敌机强相关且其他类也不会用,所以直接在敌机类里面定义比较满足封装思想。
基类构造类里面完成链表初始化:
EnemyBase::EnemyBase() {
baseInfo.x = ran_range(3 * PlaneXYScale, 29 * PlaneXYScale);
baseInfo.y = 0;
baseInfo.visiable = 1;
animationList = ListCreate();
ListPushBack(animationList, (LTDataType) &damageAnimation);
}
4、各种敌机本身颜色不一样,所以我们在各种敌机子类的初始化函数中,定义动画需要变得颜色:
void EnemyT1::init() {
damageAnimation.addItem(0, 0xa02000);
damageAnimation.addItem(1000, 0x208000);
damageAnimation.bindAddress = &baseInfo.color;
}
5、最后在敌机的tick函数里面,遍历动画链表:
uint8_t EnemyT1::tick(uint32_t t) {
baseInfo.y += t * baseInfo.speed;
if (baseInfo.y > 64 * PlaneXYScale)
baseInfo.visiable = 0;
if (fireTimer.tick(t)) {
createBulletObject();
}
for (ListNode *node = animationList->next; node != animationList; node =
node->next) {
if (((Animation*) node->data)->isValid) {
((Animation*) node->data)->tick(t);
}
}
return 0;
}
TIPS:由于什么时候执行tick无法确定,可能非常接近的时间点不会执行,直接就跳过了。所以用于做显示的动画可以接受,毕竟跳过就跳过了,显示最终效果即可,但如果用来修改某些影响流程的状态值的话,需要小心一些,需要有足够的时间间隔,确保能tick进去。
?同样的方法,我们再加上其他敌机类型的受伤效果,玩家被击中的效果等,不再累述。
飞机被击毁时,直接消失不见了,这不太合适,所以我们再给它加个击毁的动画。可以用类似前面焰火程序做个爆炸开来的样子。
还是用帧动画。
1、添加爆炸的动画explodeAnimation属性,添加爆炸阶段状态码explodeState。
class EnemyBase {
public:
EnemyBase();
virtual ~EnemyBase() {
}
virtual uint8_t tick(uint32_t t)=0;
virtual void init()=0;
virtual uint8_t show(void)=0;
virtual uint8_t hitDetect(int x, int y, int damage)=0;
ListNode *enemyBulletList;
PlaneObject_t baseInfo;
int HP;
protected:
DispersedAnimation damageAnimation;
DispersedAnimation explodeAnimation;
ListNode *animationList;
int explodeState = 0;
};
2、给explodeAnimation灌入数据
void EnemyT1::init() {
damageAnimation.addItem(0, 0xa02000);
damageAnimation.addItem(1000, 0x208000);
explodeAnimation.addItem(0, 1);
explodeAnimation.addItem(200, 2);
explodeAnimation.addItem(400, 3);
explodeAnimation.addItem(600, 4);
explodeAnimation.addItem(800, 100);
}
3、根据状态码explodeState显示不同的爆炸形态
const int8_t Explode_X[] = { -1, 0, 1, 1, 1, 0, -1, -1 };
const int8_t Explode_Y[] = { -1, -1, -1, 0, 1, 1, 1, 0 };
uint8_t EnemyT1::show(void) {
if (explodeState) {
for (uint8_t j = 0; j < 8; j++) {
ws2812_pixel(
baseInfo.x / PlaneXYScale + Explode_X[j] * explodeState,
baseInfo.y / PlaneXYScale + Explode_Y[j] * explodeState,
240, 20, 0);
}
if (explodeState == 100) {
baseInfo.visiable = 0;
}
} else {
for (uint8_t y = 0; y < baseInfo.height; y++) {
for (uint8_t x = 0; x < baseInfo.width; x++) {
if (sharp[y][x])
ws2812_pixel(
x + baseInfo.x / PlaneXYScale - baseInfo.width / 2,
y + baseInfo.y / PlaneXYScale - baseInfo.height / 2,
(baseInfo.color >> 16) & 0xff,
(baseInfo.color >> 8) & 0xff,
baseInfo.color & 0xff);
}
}
}
return 0;
}
4、原来血量为0时就直接消失了,现在还要再保留一下显示爆炸,而且这段时间也不能再动了。所以,对原来消失的和tick的逻辑做点小改动。
uint8_t EnemyT1::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();
}
for (ListNode *node = animationList->next; node != animationList; node =
node->next) {
if (((Animation*) node->data)->isValid) {
((Animation*) node->data)->tick(t);
}
}
return 0;
}
uint8_t EnemyT1::hitDetect(int x, int y, int damage) {
if (explodeState)
return 0;
int a = (x - baseInfo.x) / 100;
int b = (y - baseInfo.y) / 100;
int c = 180; // 1.5 * 10000 / 100 // 碰撞圈子略大一点,
uint8_t res = (a * a + b * b < c * c) ? 1 : 0;
if (res) {
HP -= damage;
if (HP < 0) {
explodeState = 1;
explodeAnimation.start();
} else
damageAnimation.start();
}
return res;
}
嗯,还有补充T2和T3的爆炸效果。T2炸的范围更大一点,而T3可以爆出两朵大花。
好了,我们看看效果:
STM32学习笔记十五:WS2812制作像素游戏屏-飞行射击