这回,开始做敌机。
我们设定敌机有3中,小型,大型和BOSS,分别叫 EnemyT1 / EnemyT2 / EnemyT3。
先定义一个敌机基类:
EnemyBase.h
/*
* EnemyBase.h
*
* Created on: Dec 24, 2023
* Author: YoungMay
*/
#ifndef SRC_PLANE_ENEMYBASE_H_
#define SRC_PLANE_ENEMYBASE_H_
#include "../drivers/DList.h"
#include "PlaneDef.h"
class EnemyBase {
public:
EnemyBase();
virtual ~EnemyBase() {
}
virtual uint8_t tick(uint32_t t)=0;
virtual void init()=0;
virtual uint8_t show(void)=0;
ListNode *enemyBulletList;
PlaneObject_t baseInfo;
uint16_t HP;
};
#endif /* SRC_PLANE_ENEMYBASE_H_ */
然后是三种敌机:
EnemyT1.h
/*
* EnemyT1.h
*
* Created on: Dec 24, 2023
* Author: YoungMay
*/
#ifndef SRC_PLANE_EnemyT1_H_
#define SRC_PLANE_EnemyT1_H_
#include "EnemyBase.h"
class EnemyT1: public EnemyBase {
public:
EnemyT1();
~EnemyT1();
uint8_t tick(uint32_t t);
void init();
uint8_t show(void);
bool sharp[3][3] = {
{ 1, 0, 1 },
{ 1, 1, 1 },
{ 0, 1, 0 } };
};
#endif /* SRC_PLANE_EnemyT1_H_ */
?EnemyT1.cpp
/*
* EnemyT1.cpp
*
* Created on: Dec 24, 2023
* Author: YoungMay
*/
#include "EnemyT1.h"
EnemyT1::EnemyT1() {
HP = 100;
baseInfo.speed = 15;
baseInfo.width = 3;
baseInfo.height = 3;
getRainbowColor(&baseInfo.color, 400);
}
EnemyT1::~EnemyT1() {
// TODO Auto-generated destructor stub
}
void EnemyT1::init() {
}
uint8_t EnemyT1::tick(uint32_t t) {
baseInfo.y += t * baseInfo.speed;
return 0;
}
uint8_t EnemyT1::show(void) {
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.R, baseInfo.color.G, baseInfo.color.B);
}
}
return 0;
}
?EnemyT2.h
/*
* EnemyT2.h
*
* Created on: Dec 24, 2023
* Author: YoungMay
*/
#ifndef SRC_PLANE_EnemyT2_H_
#define SRC_PLANE_EnemyT2_H_
#include "EnemyBase.h"
class EnemyT2: public EnemyBase {
public:
EnemyT2();
~EnemyT2();
uint8_t tick(uint32_t t);
void init();
uint8_t show(void);
bool sharp[5][5] = {
{ 0, 1, 1, 1, 0 },
{ 0, 0, 1, 0, 0 },
{ 1, 1, 1, 1, 1 },
{ 0, 1, 0, 1, 0 },
{ 0, 1, 0, 1, 0 }
};
};
#endif /* SRC_PLANE_EnemyT2_H_ */
EnemyT3.h?
/*
* EnemyT3.h
*
* Created on: Dec 24, 2023
* Author: YoungMay
*/
#ifndef SRC_PLANE_EnemyT3_H_
#define SRC_PLANE_EnemyT3_H_
#include "EnemyBase.h"
class EnemyT3: public EnemyBase {
public:
EnemyT3();
~EnemyT3();
uint8_t tick(uint32_t t);
void init();
uint8_t show(void);
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, }
};
};
#endif /* SRC_PLANE_EnemyT3_H_ */
EnemyT2.cpp和EnemyT3.cpp 也几乎雷同。
是不是可以抽基类方法?可以!
是不是可以用宏定义?可以!
那为什么不呢?后面再说。
现在各种对象类型开始多起来了,我们需要考虑怎么管管这么多东西。
传统C中,数据都是直接铺在内存空间中,依赖各种地址各种指针进行管理,只要考虑空间是否足够,考虑再合适的时候释放空间。所以,最好的方式是计算好各类数据可能需要的地址空间,然后把各类数据放到指定的位置。要用的时候,按地址进行访问就行了。
但游戏中不确定数量的东西有可能无法计算,一旦预留太多则会浪费空间。这在单片机这种小内存设备上是致命的。
所以本次项目,我尝试使用C++的面向对象技术。面向对象思想更符合游戏的逻辑。额,其实吧,是我本人多年JAVA开发经验,身上每个细胞都是一个独立对象,对这太熟了,偷乐。而对C++的面向对象不太熟,借此机会学习一下。
言归正传。
我们先把数据结构的定义都搬到一起,以方便后面的各种引用。
PlaneDef.h
/*
* PlaneDef.h
*
* Created on: Dec 22, 2023
* Author: YoungMay
*/
#ifndef SRC_MENU_PLANEOBJECT_H_
#define SRC_MENU_PLANEOBJECT_H_
#include "../drivers/ws2812Frame.h"
#include "../drivers/tools.h"
#include "../drivers/keys.h"
#define PlaneXYScale 10000
typedef struct {
int x;
int y;
RGBColor_TypeDef color;
uint8_t speed;
uint8_t visiable = 0;
uint8_t width;
uint8_t height;
} PlaneObject_t;
typedef struct {
int x;
int y;
int speedX;
int speedY;
RGBColor_TypeDef color;
uint8_t visiable = 0;
} BulletObject_t;
typedef struct {
uint8_t type;
int x;
int y;
int speedX;
int speedY;
uint16_t hp;
uint8_t visiable = 0;
} EnemyObject_t;
typedef struct {
uint32_t lastTick = 0;
uint32_t defaultSpan = 100;
uint8_t tick(uint32_t tick) {
if (lastTick > tick) {
lastTick -= tick;
return 0;
} else {
lastTick = defaultSpan;
return 1;
}
}
} IntervalAniTimer_t;
#endif /* SRC_MENU_PLANEOBJECT_H_ */
定义一个EnemyManager类管理所有敌机。
EnemyManager.h
/*
* EnemyManager.h
*
* Created on: Dec 23, 2023
* Author: YoungMay
*/
#ifndef SRC_PLANE_ENEMYMANAGER_H_
#define SRC_PLANE_ENEMYMANAGER_H_
#include "../drivers/DList.h"
#include "PlaneDef.h"
#include "EnemyT1.h"
#include "EnemyT2.h"
#include "EnemyT3.h"
class EnemyManager {
public:
EnemyManager();
virtual ~EnemyManager();
uint8_t tick(uint32_t t);
void init();
uint8_t show(void);
ListNode *enemyList;
ListNode *bulletList;
private:
IntervalAniTimer_t createTimer = { 0, 2000 };
EnemyBase* createEnemyObject();
uint16_t enemyTypeProportion[3] = { 120, 20, 3 };
};
#endif /* SRC_PLANE_ENEMYMANAGER_H_ */
其中,我们用enemyList保存所有敌机实例,用bulletList管理所有敌机发射的子弹实例。
这样,我们在plane类的tick主函数中,?调用EnemyManager里面的tick方法,再由EnemyManager去调用每个enemy的tick方法。
后续,我们也是采用这一套逐级往下调用tick的方式,保证每个实例都能被执行tick,且在tick的入参中得到运行时间,已决定动作进行到哪里。
EnemyManager.cpp
/*
* EnemyManager.cpp
*
* Created on: Dec 23, 2023
* Author: YoungMay
*/
#include "EnemyManager.h"
EnemyManager::EnemyManager() {
enemyList = ListCreate();
}
EnemyManager::~EnemyManager() {
for (ListNode *cur = enemyList->next; cur != enemyList; cur = cur->next) {
delete ((EnemyObject_t*) (cur->data));
}
ListDestory(enemyList);
}
void EnemyManager::init() {
}
EnemyBase* EnemyManager::createEnemyObject() {
uint8_t ran = ran_seq(3, enemyTypeProportion);
switch (ran) {
case 0:
return new EnemyT1();
case 1:
return new EnemyT2();
case 2:
return new EnemyT3();
}
return NULL;
}
uint8_t EnemyManager::tick(uint32_t t) {
if (createTimer.tick(t)) {
EnemyBase *enemy = createEnemyObject();
ListPushBack(enemyList, (LTDataType) enemy);
}
for (ListNode *cur = enemyList->next; cur != enemyList; cur = cur->next) {
EnemyBase *enemy = ((EnemyBase*) (cur->data));
enemy->tick(t);
}
return 0;
}
uint8_t EnemyManager::show(void) {
ListNode *cur = enemyList->next;
while (cur != enemyList) {
EnemyBase *enemy = ((EnemyBase*) (cur->data));
if (!enemy->baseInfo.visiable) {
delete enemy;
ListErase(cur);
} else {
enemy->show();
}
cur = cur->next;
}
return 0;
}
注意1:其中用到一个工具方法:
每种敌机出现的机率不一样的。我们初定为120:20:3
enemyTypeProportion[3] = { 120, 20, 3 }
封装一个函数来实现它:
int ran_seq(int count, uint16_t *seqs) {
uint32_t i, sum = 0;
for (i = 0; i < count; i++) {
sum += seqs[i];
}
uint32_t ran = ran_max(sum);
sum = 0;
for (i = 0; i < count; i++) {
sum += seqs[i];
if (ran < sum) {
return i;
}
}
return 0;
}
注意2:
enemyList 由enemyManager来管理,在enemyManager构造函数中初始化。
bulletList却不是,它是在bulletManager中进行管理的。只是把地址传过去,以方便使用。
现在我们总结一下整个数据管理。
1、玩家数据最复杂,最个性化,所以有自己独立的类、属性等。自行管理。
2、差异较小的且无什操作的对象,我们放在管理类中对数据和方法进行统一管理。如星空背景、子弹,并不是每颗星星都是一个对象,而仅仅是BackGroundStar类中的一条数据。
3、介于二者之间的,属性具有一致性,但动作存在差异性的对象,划归不同的类,但放在同一个管理类中进行统一管理。如敌机,分3个类,但放在一个enemyList 中。回答上面问题,虽然三种敌机的实现基本一致,但是分开,是为了将来他们可以有不同的动作逻辑(tick)和不同的显示方法(show)。
4、不定数量的对象,放在队列中,该队列应放在该对象的Manager类中。跨队对象类型的数据访问,可以把队列地址传递给对方。
看看效果:
STM32学习笔记十三:WS2812制作像素游戏屏-飞行射击
似乎不太好,我们优化一下:
1、BOSS 飞行速度慢,开始出现时,只是冒出一点小头,我们要让他先冲出来,再减速。
EnemyT3.cpp
EnemyT3::EnemyT3() {
HP = 10000;
baseInfo.speed = 50;
baseInfo.width = 9;
baseInfo.height = 5;
getRainbowColor(&baseInfo.color, 600);
}
uint8_t EnemyT3::tick(uint32_t t) {
baseInfo.y += t * baseInfo.speed;
if (baseInfo.y > 5 * PlaneXYScale) {
baseInfo.speed = 1;
}
if (baseInfo.y > 64 * PlaneXYScale)
baseInfo.visiable = 0;
return 0;
}
2、BOSS虽然小几率出现,但是仍有可能短时间出现多个。所以,我们改为中小飞机按比例随机出现,而BOSS定时1分钟出现。为BOSS的出现单独设置一个定时器。
EnemyManager.h
class EnemyManager {
public:
EnemyManager();
virtual ~EnemyManager();
uint8_t tick(uint32_t t);
void init();
uint8_t show(void);
ListNode *enemyList;
ListNode *bulletList;
private:
IntervalAniTimer_t createTimer = { 0, 2000 };
IntervalAniTimer_t createBossTimer = { 30000,60000};
EnemyBase* createEnemyObject();
uint16_t enemyTypeProportion[3] = { 120, 20 };
};
EnemyManager.cpp
EnemyBase* EnemyManager::createEnemyObject() {
uint8_t ran = ran_seq(2, enemyTypeProportion);
switch (ran) {
case 0:
return new EnemyT1();
case 1:
return new EnemyT2();
}
return NULL;
}
uint8_t EnemyManager::tick(uint32_t t) {
if (createTimer.tick(t)) {
EnemyBase *enemy = createEnemyObject();
ListPushBack(enemyList, (LTDataType) enemy);
}
if (createBossTimer.tick(t)) {
EnemyBase *enemy = new EnemyT3();
ListPushBack(enemyList, (LTDataType) enemy);
}
for (ListNode *cur = enemyList->next; cur != enemyList; cur = cur->next) {
EnemyBase *enemy = ((EnemyBase*) (cur->data));
enemy->tick(t);
}
return 0;
}
数据管理都在各自的manager类里面了,但是跨manager的数据访问怎么办?我们做一个单例的数据仓库类,保留各种数据地址指针。当然,这个仓库可不光为了本飞行射击游戏使用,其他应用也是可以用的。
1、创建数据仓库DataBulk,这是个单例。
DataBulk.h
/*
* DataBulk.h
*
* Created on: Dec 27, 2023
* Author: YoungMay
*/
#ifndef SRC_DRIVERS_DATABULK_H_
#define SRC_DRIVERS_DATABULK_H_
#include "stddef.h"
#include "stdint.h"
class DataBulk {
private:
static DataBulk *p;
public:
DataBulk();
virtual ~DataBulk();
static DataBulk* GetInstance() //定义一个公有函数,可以获取这个唯一的实例,并且在需要时创建该实例。
{
if (p == NULL) //判断是否第一次调用
p = new DataBulk;
return p;
}
intptr_t data1;
intptr_t data2;
};
#endif /* SRC_DRIVERS_DATABULK_H_ */
在main.cpp里面进行初始化
/* USER CODE BEGIN 0 */
DataBulk *DataBulk::p = nullptr;
/* USER CODE END 0 */
现在还不知道需要哪些数据,先把玩家数据弄进去吧,毕竟最有可能用到。
在plane的init函数中,把地址存进去:
void Plane::init() {
。。。
DataBulk::GetInstance()->data1 = (intptr_t) &player1.baseInfo;
DataBulk::GetInstance()->data2 = (intptr_t) &player2.baseInfo;
}
宏定义简化访问:
#define Player1BaseInfo ((PlaneObject_t*)DataBulk::GetInstance()->data[0])
#define Player2BaseInfo ((PlaneObject_t*)DataBulk::GetInstance()->data[1])
OK.期待下一章。