STM32学习笔记十五:WS2812制作像素游戏屏-飞行射击游戏(5)探索动画之帧动画

发布时间:2024年01月01日

本章又是个重要的章节——动画。

动画,本质上时一系列静态的画面连续播放,欺骗人眼产生动画效果。这个原理自打十九世纪电影诞生开始,就从来没变过。

我们的游戏中也需要一些动画效果,比如,被击中时的受伤效果,击毁效果,血包的动画效果等等。这些动画分为两类:连续线性动画、离散的帧动画。

离散动画,就是在指定的时间点,将目标变量设定为特定的值。

连续动画,就是除了两个特定时间之外,通过插值算法为中间帧设定中间值。

这两者的时间轴都应不受系统处理能力的影响,所以,我们又想到了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制作像素游戏屏-飞行射击

STM32学习笔记十六:WS2812制作像素游戏屏-飞行射击游戏(6)探索动画之插值动画

文章来源:https://blog.csdn.net/vvind/article/details/135216189
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。