X-Ray游戏项目,是由于要做设计模式的课程设计,而我偏爱于做游戏,所以干脆就做一个简单的弹幕射击游戏来作为课程设计好了。
游戏的开发使用的是Cocos2d-x来进行的,没有用到很多cocos2d-x的功能,对于我来说,它的功能太强大,也就意味着复杂度会有所提高,所以在设计最初的时候,就避免使用过多的cocos2d-x的特性,这样我的游戏设计,就算以后有新的引擎,也只要稍微改动下代码,就能直接用在别的引擎上开发了。
废话不多说,先来看一批截图吧,由于图片资源部分取之于网络,而且我美工技术有限,所以大家不必纠结这个了!!!
由于时间限制,就开发了一关。而且对于游戏中各个敌机的数量,攻击力,飞行速度等,我都是简单地配置了下,所以可玩性,手感可能不是很好,暂时没有那么多的时间来慢慢的调节这些数据。对于Boss,我给它做了很多不同的弹幕类型,上图中只能看到几个。
游戏至今还存在一个严重的bug,系统运行的时候,偶尔会出现一个迭代器错误,实在不知道为什么会有这样的错误,暂时放在这里,等以后知识充足了,在来改进。
好了,下面来反思下游戏开发中的路程吧。
确定游戏实体
在游戏中,我们需要一个玩家,并且能够通过键盘来控制玩家的行为,所以为此需要一个Player类,用于对玩家进行抽象。
玩家需要与敌人进行交战,所以为此,我们还需要抽象一个Enemy类,用来表示所有敌人对象的基类。并且,在此类的基础上,派生出不同的具体敌人类,如进行翻滚攻击的CircleGuyPlane,不顾一切向玩家进行冲撞的RushGuyPlane,还有就是能够发射涡旋形弹幕子弹的TwoEnemyPlane,除此之外,还有一个ThreeGuyPlane,这个敌机,向玩家不停的发射扇形的弹幕。
在有了玩家,有了敌人之后,我们还需要一个Boss,抽象一个Boss类,用来表示游戏中每个关卡出现的Boss对象。
有了具有AI思想的对象之后,我们还需要让玩家和敌人,boss互相进行攻击,所以需要一个子弹类Bullet,不同的子弹可以从这个类进行继承,然后实现不同的子弹类。
为了使对象和子弹之间不是那么的耦合,并且能够实现复杂的子弹发射方法,我们再在子弹的基础上抽象一个Weapon类。玩家和敌人,只需要调用Weapon类中的fire方法,来发射子弹即可,不同的发射方式,可以通过派生不同的Weapon来进行定义。
总结如下:
Player类:模拟玩家,控制玩家行为的类
Enemy: 所有敌人飞机的基类
Boss: 每一个关卡的Boss抽象
Bullet:游戏中发生的子弹的抽象
Weapon:游戏中用于对子弹进行统一创建发射的抽象
设计问题
问题1:上面确定的实体类,如何进行统一的管理,使得查询,删除等更容易实现?
问题2:不同实体之间如何进行通信?
问题3:当满屏的对象时,如何保证效率?
问题4:如何有组织的控制地方的敌人,而不是随机的出现,进行攻击?
问题5:实体具有不同的状态,在不同的状态下,需要完成不同的任务,如何弹性的实现这样的功能?
解决问题及分析
问题1:
通过将所有的上述实体类,继承与一个GameObject,并且在GameObject中为每一个对象生成一个独立的ID,然后创建一个GameObjectManager来对所有的这些GameObject进行管理。
为此,我们使用单例模式来设计GameObjectManager,部分实现代码如下
//File:GameObjectManager.h class GameObjectManager:public Interface, public Root { private: GameObjectManager(); ~GameObjectManager(); ...... //Singleton getter public: static GameObjectManager* sharedObjectManager() ; } |
GameObjectManager* GameObjectManager::sharedObjectManager() { static GameObjectManager objmag ; return &objmag ; }// end for shareObjectManager |
方案优点:能够统一有效的对所有游戏中的对象进行管理,提供方便的接口让其他使用者来创建或者销毁对象。
方案缺点:功能太过庞杂,不符合设计原则中的单一责任原则,应该将这个类继续细化成完成不同工作的类。
问题2:实体之间经常需要进行通信,为了能够降低不同实体对象之间的耦合关系,使用事件驱动机制来构造一个消息通信方式。
首先,定义一个Message结构,如下:
//File: Message.h /* * Define the message structure */ typedef struct MESSAGE { int msg_type ; //The message type _int64 send_id ; //The id of the sender _int64 reci_id ; //The id of the reciever unsigned int dely_tim ; //The delay time void* extra_info ; //The extra infomation int extra_info_size ; //The size of the extra info in byte }*LPMESSAGE ;
|
在Message结构中,给定了发送者的ID,接受者的ID,消息类型,是否需要延迟发送,以及发送消息的额外信息和它的大小。
有了Message之后,不同的实体,只要对接受的消息进行解析,然后做相应的工作即可。
而如何进行发送和接受?
为此,抽象一个中间层MessageDispatch,这个类用于对延迟消息进行缓存,对立即消息,进行立刻发送。这个类需要知道接受者ID所对应的对象是谁,所以,在一定程度上,它需要和GameObjectManager进行交互,来获取ID所对应的对象,然后将消息传递给对象的消息处理函数。MessageDispatch的结构如下:
//File: MessageDispatch.h /* * Define the class */ class MessageDispatch:public Interface { private: MessageDispatch(); ~MessageDispatch();
//Singleton instance public: static MessageDispatch* sharedMessageDispatch() ;
//life method public: bool init() ;
void update(float dt) ;
void pause();
void resume();
void reset();
void destroy();
public: /* * biref : This DispatchMessage will dispatch the immediately message or delay-message */ void dispatchMessage(int msg_type, _int64 send_id , _int64 reci_id, int delay_time, void* extra_info, int extra_info_size) ;
void dispatchMessage(MESSAGE msg);
/* * brief : This DispatchDelayMessages will dispatch the delay message which need to be send right now. We should call this method * at every game loop. */ void dispatchDelayMessages() ;
private: std::list<MESSAGE*> m_PriorityQueue ; //Used to store the delayed message long long m_nCurFrame ; };
|
这个类也是使用单例设计方式,来实现的,最主要的函数调用为dispatchMessage和dispatchDelayMessage。以下是部分实现:
//File: MessageDispatch.cpp Method: dispatchMessage { GameObject* obj = GameObjectManager::sharedObjectManager()->getObject(msg.reci_id);
if(msg.dely_tim == NO_DELAY_TIME) { obj->handleMessage(msg); } else { msg.dely_tim += m_nCurFrame ; MESSAGE * _msg = new MESSAGE; memcpy(_msg, &msg,sizeof(msg)); if(_msg->extra_info_size == 0) _msg->extra_info = NULL ; else { _msg->extra_info = (void*)malloc(msg.extra_info_size) ; memcpy(_msg->extra_info,msg.extra_info, msg.extra_info_size ); }// end if...else... m_PriorityQueue.push_back(_msg); } }
//------------------------------------------------------------------------------------- Method: dispatchDelayMessage { std::list<MESSAGE*>::iterator it = m_PriorityQueue.begin();
for(; it != m_PriorityQueue.end() ; ) { MESSAGE* msg = *it ; std::list<MESSAGE*>::iterator temp = it ; it ++ ; if(msg->dely_tim == m_nCurFrame)// Check if meet the delay time { //Check if the message is going to be sent to render system if(msg->reci_id == RenderSystem::sharedRenderSystem()->getID()) { RenderSystem::sharedRenderSystem()->handleMessage(*msg);
}// end for render system else if(msg->reci_id == GameObjectManager::sharedObjectManager()->getID())//Check if the message is going to be sent to game object manager { GameObjectManager::sharedObjectManager()->handleMessage(*msg); }else { //Check if the message is going to be sent to the object GameObject* obj = GameObjectManager::sharedObjectManager()->getObject(msg->reci_id);
//Check if the object exist if(obj != NULL) obj->handleMessage(*msg); }// end if...else....
if(msg->extra_info) { free(msg->extra_info); msg->extra_info = NULL ; }// end if
if(msg) { delete msg ; msg = NULL ; }// end if
//And remove the message from the prority queue m_PriorityQueue.erase(temp); }// end if }// end for }
|
以下是不同实体对象之间进行交互的示意图:
方案优点:能够有效的进行消息实体之间消息通信,降低了实体之间的耦合性
方案缺点:不能够检测到实体是否已经具备接受消息的能力。
问题3:
游戏的效率,是游戏是否值得玩的重要指标。为了能够实现,同时处理创建多个对象的CPU消耗问题,使用内存池和工厂方法组合的方式进行。通过工厂方法封装对象的创建过程,便于在工厂方法中添加对配置文件的支持。使用内存池的方式,将在先前已经创建过的对象缓存在系统中,而在下次需要使用的时候,将这个对象重新初始化,然后继续使用,避免重复创建对象的开销,从而达到重复利用对象的目的。实现这样的功能主要通过GameObjectManager中的以下几个接口:
........... public: /* * brief : Accquire the bullet */ ID accquireBullet(int type, float x, float y, float vx, float vy);
/* * brief : Accquire the Enemy */ ID accquireEnemey(int type, float x, float y, float vx, float vy);
/* * brief : Accquire the item */ ID accquireItem(int type,float x, float y, float vx, float vy);
/* * brief : Release the bullet */ bool releaseBullet(ID);
/* * brief : Release the enemy */ bool releaseEnemy(ID);
/* * brief : Release the item */ bool releaseItem(ID); ........................................
|
方案优点:通过这样的方式,有效的提高了系统在运行时的效率,不至于出现卡帧的情况。
方案缺点:系统总是维持峰值的对象数目,无法做到适时释放一些内存,使得内存占用过大。
问题4:
通过抽象一个名为Team(队)的类,来统一的创建,更新多个敌方士兵。通过这样的方式,就能够实现有组织的控制多个士兵进行移动,攻击,或者组成阵型来对玩家进行打击。
以下是抽象Team的定义:
//File:Team.h /* * brief : Define the class */ class Team : public GameObject { public: Team(); virtual ~Team();
public: virtual bool init() = 0 ; virtual void update(float dt) = 0 ; virtual bool handleMessage(MESSAGE msg) = 0 ; virtual void reset() = 0;
public: //setter void setFrame(int frame); void setEnemyType(int type);
//getter int getFrame() const ; int getEnemyType() const ;
protected: std::vector<ID> m_SoldierList ; int m_nCurFrame ; int m_nEnemyType ; };
|
方案优点:
能够实现有组织的控制敌方士兵进行攻击,让玩家觉得敌人更加的智能话,同时便于系统进行统一的创建管理。
方案缺点:
使用Team对敌方士兵进行了又一次的封装,使得对单体士兵的操控变得更加困难。
问题5:
让实体在不同的状态下进行不同的工作,可以使用状态设计模式和模板方法结合的方法来实现这样的功能。
以下是实现状态的模板方法的定义:
//定义FSM状态机 template<class element_type_ptr> class FSM { public: FSM() :m_Owner(NULL), m_CurrentState(NULL), m_PreviousState(NULL), m_GlobleState(NULL) {
} ~FSM() { }
//状态机的操作函数 public: void Update() //进行状态机的更新 { //先判断是否有需要进行全局状态的操作 if(GetGlobleState()) GetGlobleState()->Excute(GetOwner());
//运行当前状态 if(GetCurrentState()) GetCurrentState()->Excute(GetOwner()); }
void ResetPreviousState() //恢复到先前状态 { ChangeCurrentState(GetCurrentState()); }
void ChangeCurrentState(State<element_type_ptr>* state) //改变当前状态 { //运行退出状态的函数 GetCurrentState()->Exit(GetOwner());
//将当前状态赋给先前状态 SetPreviousState(GetCurrentState());
//设置新的状态 SetCurrentState(state);
//运行进入状态时的函数 GetCurrentState()->Enter(GetOwner()); }
bool IsState(State<element_type_ptr>* state) //判断是否为给定的状态类型 { if(m_CurrentState == state) return true ; else return false ; }
bool HandleMessage(const MESSAGE & msg) //处理传递过来的消息 { //首先交由当前状态来处理 if(m_CurrentState && (m_CurrentState->onMessage(m_Owner, msg))) { return true ; }
//当前状态不处理交由全局状态处理 if(m_GlobleState && m_GlobleState->onMessage(m_Owner, msg)) { return true ; }
return false ; } //操作成员属性的方法 public: void SetCurrentState(State<element_type_ptr>* state) //设置当前状态 { m_CurrentState = state ; }
State<element_type_ptr>* GetCurrentState() const //获取当前状态 { return m_CurrentState ; }
void SetPreviousState(State<element_type_ptr>*state) //设置先前状态 { m_PreviousState = state ; }
State<element_type_ptr>* GetPreviousState() const //获取先前状态 { return m_PreviousState ; }
void SetGlobleState(State<element_type_ptr>* state) //设置全局状Á态 { m_GlobleState = state ; }
State<element_type_ptr>* GetGlobleState() const //获取全局状态 { return m_GlobleState ; }
void SetOwner(element_type_ptr owner) //设置拥有者 { m_Owner = owner ; }
element_type_ptr GetOwner() const //获取拥有者 { return m_Owner ; }
private: element_type_ptr m_Owner; //拥有状态机的对象 State<element_type_ptr>* m_CurrentState ; //当前状态 State<element_type_ptr>* m_PreviousState; //先前状Á态 State<element_type_ptr>* m_GlobleState ; //全局状态 };
|
以下是状态的继承类图:
方案优点:
能够方便的进行不同状态之间的转化,并且在不同状态下完成不同的任务。
方案缺点:
每多一个状态,就需要多一个类,这样会造成类爆炸的情况出现。
好了,项目总结就到这里,希望以后能开发出更加酷炫的游戏出来!期待ing....