本文主要是介绍STM32学习笔记十三:WS2812制作像素游戏屏-飞行射击游戏(3)探索数据管理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
这回,开始做敌机。
我们设定敌机有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 10000typedef 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.期待下一章。
STM32学习笔记十四:WS2812制作像素游戏屏-飞行射击游戏(4)探索碰撞检测
这篇关于STM32学习笔记十三:WS2812制作像素游戏屏-飞行射击游戏(3)探索数据管理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!