STM32学习笔记十三:WS2812制作像素游戏屏-飞行射击游戏(3)探索数据管理

发布时间:2023年12月31日

这回,开始做敌机。

我们设定敌机有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.期待下一章。

STM32学习笔记十四:WS2812制作像素游戏屏-飞行射击游戏(4)探索碰撞检测

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