cocos2dx3.2开发 RPG《Flighting》(五)只能行走的战斗场景

时间:2022-06-08 12:07:57

一、前言

前面几节好像与我们一开始说的游戏不太相关,现在我们正式进入战斗场景的开发。

不过凡事不要心急,要循序渐进,我们先搭建一个能够让角色在上面行走的战斗场景吧。


二、正文

首先精简一下Role类,让他能够实现移动功能就好了。

顾名思义,role就是角色,在战斗场景出现的一个一个人都是一个role,废话不多说,直接贴上经过精简过得Role头文件

class Role : public Node{
public:
	Role();
/*create 之前,请先确认已经将文件添加到ArmatureManager*/
	static Role* create(const std::string& name,FlightLayer* layer);
	virtual bool init(const std::string& name,FlightLayer* layer);
	void setControlable(bool b);
	virtual Rect getBoundingBox();
	virtual inline void setArmOffsetX(int x){m_arm_offsetX = x;}
	virtual inline void setArmOffsetY(int y){m_arm_offsetY = y;}

protected:
/*update相关的*/
	virtual void update(float delta);
	virtual void update_pos();

/*从RoleProtocol中继承下来的方法*/
public:

	virtual bool onTouchBegan(Touch* touch,Event* event);
	virtual void onTouchMoved(Touch* touch,Event* event);
	virtual void onTouchEnded(Touch* touch,Event* event);


protected:
	/*与显示相关的*/
	bool m_controlable;
	Armature* m_arm;
	int m_arm_offsetX;
	int m_arm_offsetY;

	bool m_armFaceTo;	//朝向,默认为true,向左


public:
	/*外部调用接口*/
	virtual inline void setDesPoint(const Point& p){m_desPoint = p;}

	//ID
	virtual inline int getId(){return m_id;}
	
	
protected:
	/*与战斗相关的数据*/
	int m_id;	//id

	Point m_desPoint;//目标位置


	int m_speed;	//移动速度
	int m_initSpeed;

};
#endif
经过精简后,Role类就剩下这些函数和变量了,现在的这个Role类只能简单的移动。不要心急,那我们就先实现role的移动吧。


好的,先撇开我们精简过得Role类,先看我们的GameScene

GameScene.h

class GameScene : public Scene{
public:
	CREATE_FUNC(GameScene);
	void setHeroTeam(const HeroMessage& h1,const HeroMessage& h2,const HeroMessage& h3);
	void setMonsterDeq(deque<MonsterMessage> deq);
private:
	virtual bool init();
	FlightLayer* layer;
};

GameScene的两个接口setHeroTeam和setMonsterDeq在上一节的选人界面已经使用过了。

void GameScene::setHeroTeam(const HeroMessage& h1,const HeroMessage& h2,const HeroMessage& h3){
	layer->initTeam(h1,h2,h3);
}

void GameScene::setMonsterDeq(deque<MonsterMessage> deq){
	layer->initMonsterDeq(deq);
}

GameScene也只是简单地将信息传递给layer层罢了。


下面主要研究FlightLayer

class Role;

typedef Role** Role_Ptr;
class FlightLayer : public Layer{
friend class Role;
/*外部提供接口*/
public:
	virtual bool init();
	CREATE_FUNC(FlightLayer);
	void addRole(Role* r);
	std::list<Role_Ptr> getRolesArray(){return m_rolesArray;}

	void initTeam(const HeroMessage& h1,const HeroMessage& h2,const HeroMessage& h3);
	void initMonsterDeq(deque<MonsterMessage> deq);

/*内部使用函数*/
private:
	virtual void update(float delta);
	void initListener();
	bool comparePosY(Role_Ptr a,Role_Ptr b);
	/*更新layer中role的叠放次序*/
	void refreshLocalZOrder();
	/*保持m_cur_control的正确性*/
	void updateMyControl();

	virtual bool onTouchBegan(Touch* touch,Event* event);
	virtual void onTouchMoved(Touch* touch,Event* event);
	virtual void onTouchEnded(Touch* touch,Event* event);

/*类成员变量*/
private:
	std::list<Role_Ptr> m_rolesArray;
	Role* m_cur_control;
	Role_Ptr m_cur_controlPtr;
};
#endif

当然啦,这个FlightLayer也是被我精简过得。

这里需要注意的是,我们将Role**这个二级指针typedef为Role_Ptr,并不是一个新的类啊,至于为啥要用二级指针(其实改为用智能指针也行),等下我会好好说的。


好的开始好好讲FlightLayer的实现

bool FlightLayer::init(){
	this->scheduleUpdate();
	initListener();
	m_cur_controlPtr = nullptr;
	m_cur_control = nullptr;
	Size visibleSize = Director::getInstance()->getVisibleSize();
	Sprite* BG = Sprite::create("flightBG.jpg");
	BG->setAnchorPoint(Point(0.5f,0.5f));
	BG->setPosition(visibleSize.width/2,visibleSize.height/2);
	this->addChild(BG);
	return true;
}
init函数除了启动update和调用initListener函数之外没有其他特别的。

void FlightLayer::initListener(){
	EventListenerTouchOneByOne* touchListener = EventListenerTouchOneByOne::create();
	touchListener->onTouchBegan = CC_CALLBACK_2(FlightLayer::onTouchBegan,this);
	touchListener->onTouchMoved = CC_CALLBACK_2(FlightLayer::onTouchMoved,this);
	touchListener->onTouchEnded = CC_CALLBACK_2(FlightLayer::onTouchEnded,this);
	Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(touchListener,this);
}
initListener函数也只是为layer添加触摸事件监听器而已。

bool FlightLayer::onTouchBegan(Touch* touch,Event* event){
	if(m_rolesArray.size() < 1){
		return false;
	}
	for(auto it = m_rolesArray.begin();it!=m_rolesArray.end();++it){
		if((**it)->onTouchBegan(touch,event)){
			m_cur_controlPtr = *it;
			m_cur_control = **it;
			return true;
		}
	}
	m_cur_controlPtr = nullptr;
	return false;
}

大致的意思是:当有点击事件发生时,onTouchBegan方法被触发,遍历m_rolesArray,看哪个角色被选中(能够接受触摸事件),如果有角色被选中,就把触摸事件交给那个角色处理。

m_roleArray是一个Role_Ptr(Role**)的list,用于保存现在存活的role

那么为什么要用二级指针呢?

考虑以下问题:假设现在有两个角色,如果用一级指针的话,定义如下:

Role* A = Role::create();

Role* B = Role::create();

好了,现在A要攻击,攻击目标设置为B(所谓攻击目标就是一直向目标走去,要时刻检测目标的当前位置)

如何设置攻击目标?一级指针的话不就是在Role类里面加一个Role*类型的成员变量(Role* attackTarget)代表攻击目标就可以了吗。 

好了,前提条件说完了,现在出现这样一种情况,A的攻击目标设置为B(A->attackTarget = B),这时候A会时刻检测自己的attackTarget的位置,并且不断地走过去。

但是,假如这个时刻B被C打死了。。那么这个时候B从场景中清除掉(调用removeFromParent)。那么A怎么知道B已经死了,被清除掉了呢?

聪明的同学可能会想:你判断一下A->attackTarget 是不是等于null不就可以了吗?但是很抱歉,cocos2dx里面,调用玩removeFromParent之后,原来那块中间在这一帧结束之前,并不不会置为null。也就是说B 进行remove后,A->attackTarget不等于null,那么有人又问了,你这里的m_rolesArray数组检测每一个role*(假设m_roleArray里面放入的是Role*而不是Role**),如果死了(Hp==0)就把它赋值为空,不可以吗?这个就是很经典的C语言关于值传递和地址传递的问题。你把m_rolesArray里面的临时变量置为null有什么用?m_rolesArray和A里面的attackTarget指针指向都是同一块空间(B的实际空间),但是两个是完全不同的变量,并不能说你把m_rolesArray的指针置为null,A的attackTarget也会变成null,二者是互不影响的。

好的,讲了那么多,大家可能会不懂。因为我这里也绕了很久。不过最后还是觉得很有必要理清楚,不懂的同学可以看一下《Effective C++》里面介绍的RAII思想。

最后,补充一点是,最好用智能指针代替二级指针,这里我懒得修改了,不好意思哈。


貌似绕开了话题了,我们回到FlightLayer吧。

刚刚讲到有一个m_rolesArray的List负责保存在场景中还存活的角色。那么这个这些角色是什么时候放进去的呢?

void FlightLayer::initTeam(const HeroMessage& h1,const HeroMessage& h2,const HeroMessage& h3){

	Hero* hero1 = Hero::create(h1.r_name,this);
	hero1->setPosition(-100,380);
	hero1->setDesPoint(Point(200,380));
	hero1->initWithMessage(h1);
	this->addRole(hero1);

	Hero* hero2 = Hero::create(h2.r_name,this);
	hero2->setPosition(-100,260);
	hero2->setDesPoint(Point(400,260));
	hero2->initWithMessage(h2);
	this->addRole(hero2);

	Hero* hero3 = Hero::create(h3.r_name,this);
	hero3->setPosition(-100,140);
	hero3->setDesPoint(Point(200,140));
	hero3->initWithMessage(h3);
	this->addRole(hero3);

}


void FlightLayer::initMonsterDeq(deque<MonsterMessage> deq){
	this->m_monsterDeq = deq;
}


void FlightLayer::addRole(Role* r){
	//r->setDesPoint(r->getPosition());
	this->addChild(r);
	Role_Ptr temp = (Role_Ptr)malloc(sizeof(Role*));
	*temp = r;
	m_rolesArray.push_back(temp);
}

对的,就是initTeam和initMonsterDeq这两个接口。这里的addRole是一个基本操作,负责在堆里面开一个区域存放Role的实体。

再回过头来看我们的三个触摸事件触发的回调函数。

bool FlightLayer::onTouchBegan(Touch* touch,Event* event){
	if(m_rolesArray.size() < 1){
		return false;
	}
	for(auto it = m_rolesArray.begin();it!=m_rolesArray.end();++it){
		if((**it)->onTouchBegan(touch,event)){
			m_cur_controlPtr = *it;
			m_cur_control = **it;
			return true;
		}
	}
	m_cur_controlPtr = nullptr;
	return false;
}

void FlightLayer::onTouchMoved(Touch* touch,Event* event){
	if(m_cur_control){
		m_cur_control->onTouchMoved(touch,event);
	}else{
		return;
	}
}

void FlightLayer::onTouchEnded(Touch* touch,Event* event){
	if(m_cur_control){
		m_cur_control->onTouchEnded(touch,event);
		Point tp = Director::getInstance()->convertToGL(touch->getLocationInView());
		if(!m_cur_control->getBoundingBox().containsPoint(tp)){
			m_cur_control->setDesPoint(m_cur_control->getEndPoint());
		}
	}
}

这三个简化后的函数应该不难看懂。大概流程是这样的

1.onTouchBegan接受到触摸事件之后,检测m_rolesArray,看是否有Role被点中了,如果有m_cur_controlPtr 和 m_cur_control就设置好,然后把触摸事件交给m_cur_control(Role)处理。

2.onTouchMoved,没什么,也是依赖于m_cur_control的处理。

3.onTouchEnded,就是用来设置m_cur_control的终点的。


是不是兜兜转转又回到去了Role类,关于精简过的Role类我们还是标上注释吧。

class Role : public Node{
public:
	Role();
/*create 之前,请先确认已经将文件添加到ArmatureManager*/
	static Role* create(const std::string& name,FlightLayer* layer);
	virtual bool init(const std::string& name,FlightLayer* layer);
	void setControlable(bool b);<span style="white-space:pre">	</span>//设置是不是可以控制的
	virtual Rect getBoundingBox();<span style="white-space:pre">	</span>//获取范围

	virtual inline void setArmOffsetX(int x){m_arm_offsetX = x;}<span style="white-space:pre">	</span>//设置x,y的偏移量
	virtual inline void setArmOffsetY(int y){m_arm_offsetY = y;}

protected:
/*update相关的*/
	virtual void update(float delta);<span style="white-space:pre">	</span>//update函数
	virtual void update_pos();


public:

	virtual bool onTouchBegan(Touch* touch,Event* event);
	virtual void onTouchMoved(Touch* touch,Event* event);
	virtual void onTouchEnded(Touch* touch,Event* event);


protected:
	/*与显示相关的*/
	bool m_controlable;<span style="white-space:pre">	</span>//是否可触摸
	Armature* m_arm;<span style="white-space:pre">	</span>//骨骼动画
	int m_arm_offsetX;<span style="white-space:pre">	</span>//xy偏移量
	int m_arm_offsetY;

	bool m_armFaceTo;	//朝向,默认为true,向左


public:
	/*外部调用接口*/
	virtual inline void setDesPoint(const Point& p){m_desPoint = p;} //设置终点位置

	//ID
	virtual inline int getId(){return m_id;}
	
	
protected:
	/*与战斗相关的数据*/
	int m_id;	//id

	Point m_desPoint;//目标位置


	int m_speed;	//移动速度
	int m_initSpeed;

};
#endif


其实先跟大家说,Role的行走主要还是依赖于update函数

再解释上面可能大家不太清楚的几个名词

1、xy的偏移量,因为我们用cocostudio做出来的骨骼动画可能范围和描点都不是和我们现在这个Role类(Node)完全重合的。所以可能要或多或少地调整一下骨骼动画的位置。

2、朝向。就是我们的角色是面向哪里的,有左右之分(true/false)


三个触摸回调函数实现在没有讲控制效果之前也就没什么的。除了onTouchBegan要根据传过来的触摸点判断是不是被点中。如果是,就返回true不是就返回false。


负责角色移动的还是要看update函数

</pre>update调用update_pos<pre name="code" class="cpp">void Role::update_pos(){
	
		if(m_desPoint.x > this->getPosition().x && m_armFaceTo == true){	
			m_armFaceTo = false;
		}
		if(m_desPoint.x < this->getPosition().x && m_armFaceTo == false){
			m_armFaceTo = true;
		}
		if(m_armFaceTo){
			m_arm->setVisible(false);
			m_arm->setPosition(m_arm_offsetX,m_arm_offsetY);
			m_arm->setScaleX(1.0f);
			m_arm->setVisible(true);
		}else{
			m_arm->setVisible(false);
			m_arm->setScaleX(-1.0f);
			m_arm->setPosition(-m_arm_offsetX,m_arm_offsetY);
			m_arm->setVisible(true);
		}
		if(!Rect(m_desPoint.x-m_speed/2,m_desPoint.y-m_speed/2,m_speed,m_speed).containsPoint(getPosition())){
			this->move();
			float distance = ccpDistance(getPosition(),m_desPoint);
			float t = distance / m_speed;
			float speed_x = (m_desPoint.x - getPositionX()) / t;
			float speed_y = (m_desPoint.y - getPositionY()) / t;
			setPositionX(getPositionX() + speed_x);
			setPositionY(getPositionY() + speed_y);
		}else{
			this->stand();
		}
	
	
}

讲一讲逻辑就可以了,就是不断更新Role的x和y,直到接近m_desPoint,要转身的时候就转身。


有两个函数 move()和stand() 其实是用来控制骨骼动画的。move负责播放跑步的动画,stand负责播放待机的动画


对了,好像Role的init函数还没有讲 

bool Role::init(const std::string& name,FlightLayer* layer){
	m_layer = layer;
	if(!m_arm){
		m_arm = Armature::create(name);
		m_arm->setPosition(m_arm_offsetX,m_arm_offsetY);
		setContentSize(m_arm->getContentSize());
		this->addChild(m_arm,1);
		
		m_armFaceTo = true;

		scheduleUpdate();
		return true;
	}else{
		return false;
	}
}

很简单吧。。也没什么。


好吧,这节到此结束。

我的csdn地址:http://blog.csdn.net/hezijian22

邮箱地址:578690286@qq.com

如有问题或指教,欢迎与我交流,谢谢。