cocos2d-x 游戏创作过程(三)
这几个月带来了越来越多的烦恼,但是掌握的也越来越多了。
- 碰撞问题
- 骨骼动画控制问题
- 更换骨骼动画里的骨骼
- 瓦片地图问题
- 多个线程问题
- 射线问题
- 落地与弹跳的姿势改变问题
- 炮弹问题
- 敌人问题
- 炮弹与敌人回收问题
- 梯子问题
- 数据问题
每一章都值得讲很久,血的教训,但是我觉得还是分几个段落讲吧。至少这几个月没有荒废吧。
碰撞问题
碰撞问题是我遇到的,最大的问题之一,直到上个星期,我才发现一个重要的问题。 就是瓦片地图是无法用 ContactListener 碰撞监听去检测 与地图的连续碰撞的!!!!!!!!!这个要记住。在别寄希望于瓦片地图了。
对于上边的提醒,更加醒目的定义, 那么首先就要从碰撞检测开始了。 首先Box2d的碰撞检测并不是像cocos2d一样内置在里边的,需要自己创建碰撞检测的类,再用Word去加载。
//创建碰撞检测
contactListener = new ContactListener();
//世界监听碰撞检测
world->SetContactListener(contactListener);
就是这么简单。 创建的检测类就是这样的
//继承了b2ContactListener的 虚类
class ContactListener : public b2ContactListener{
public:
virtual void BeginContact(b2Contact *contact);
virtual void PreSolve(b2Contact* contact, const b2Manifold* oldManifold);
virtual void PostSolve(b2Contact *contact, const b2ContactImpulse *impulse);
virtual void EndContact(b2Contact* contact);
bool ladder;
public:
std::vector<b2Body *> b2body;
std::vector<Sprite *> sprite;
};
最重要的是BeginContact 和EndContact 环节。 这里保证的是刚体碰撞,和碰撞以后的事件。
void ContactListener::BeginContact(b2Contact *contact){
// log("BeginContact AAAAAAAAAAAAAA");
b2Body *bodyA= contact->GetFixtureA()->GetBody();
b2Body *bodyB= contact->GetFixtureB()->GetBody();
auto spriteA = (Sprite *)bodyA->GetUserData();
auto spriteB = (Sprite *)bodyB->GetUserData();
if(spriteA!=nullptr && spriteB!=nullptr){
if(spriteA->getTag()==39 ){
if(spriteB->getName()=="zidan"){
log("EndContact 方块消除");
b2body.push_back(bodyA);
sprite.push_back(spriteA);
}
}
}
}
void ContactListener::EndContact(b2Contact* contact){
b2Body *bodyA= contact->GetFixtureA()->GetBody();
b2Body *bodyB= contact->GetFixtureB()->GetBody();
auto spriteA = (Sprite *)bodyA->GetUserData();
auto spriteB = (Sprite *)bodyB->GetUserData();
}
如果两个刚体发生了碰撞那么spriteA 和 spriteB 的标记或许就是你指定的那两个刚体的标记。
其实就是这么简单,碰撞检测在不断的刷新于检测 运动且相互碰撞的刚体。
但是我的提醒很重要,如果你要和一个瓦片地图进行连续的碰撞检测, 那么最后的答案就是否定的, 因为基于瓦片所创造的刚体在你主角运动的过程中,永远会出现 BeginContact 和 EndContact。 也就是说碰撞就是true 离开就是false 你无法得到一个持续true的 答案,除非你的地图刚体是一整块刚体。
这个就是瓦片地图制作游戏的现状。 瓦片地图还是有优势的但是我现在还不去说。所以与瓦片地图持续碰撞会用到射线,一会儿会讲。
骨骼动画控制问题
最开始用的是定格动画,虽然是从网上找的,图片,就像上上一章,但是问题是。画画功力有限,而且并不能控制人物的具体动作,射击,劈砍,和其他,动作虽然可以用定格动画来做,但是涉及到的具体关节动作,无法有效的控制,尤其是射击的时候根据敌人的具体位置,来改变枪的位置和手的位置,必须要对骨骼进行操作。。。。!!!!
走了很多歪路,但是发现都没有意义。因为底层的代码里边控制具体骨骼的是.h的C代码,所以 如果用C++来控制那么就需要用自带的转换器来转换
//-----------------------------------------引入的是下边两个c++翻译机
#include <spine/extension.h>
#include <algorithm>
extern "C"{
#include <spine/SkeletonData.h>
#include <spine/AnimationState.h>
//#include <spine/AnimationState.c>
}
上边的代码很重要, 走的歪路主要表现为, 不知道如何引用 C ,然后直接把C 转换成C++。 重现成C++,就不贴代码了,因为没有意义了。 所以引用了 spine 这种骨骼动画当中的插件 用 C 编写的 SkeletonData.h,和AnimationState.h以后 就可以有效的控制骨骼的具体动作了。
//主角刚体
b2Body *body=role->getb2Body();
//主角动画
spine::SkeletonAnimation *f = role->getSkeletonAnimation();
//寻找动画中的骨骼
spBone *bone1 =f->findBone("rear-upper-arm");
//修改骨骼的弧度数
bone1->rotation=bone1->rotation+1.0f;
spSkeleton *sk = bone1->skeleton;
//创建动画状态的数据
spAnimationStateData *s=spAnimationStateData_create(sk->data);
//创建骨骼动画的状态
spAnimationState *_state=spAnimationState_create (s);
//结束创建数据。
spAnimationStateData_dispose (s);
//结束修改状态
spAnimationState_dispose(_state);
以上就是修改骨骼具体旋转的位置的方式。 还可以修改骨骼的具体位置但是好像没必要吧。
更换骨骼动画里的骨骼
其实这是个很简单的工作。网上找了很多,在浩瀚的 .h 源码中 寻找 那几行修改,根本就没有任何意义。 其实只要在骨骼动画的插槽里多加几个图片就可以了。
亲身实验,别走歪路,其实替换很简单。
//骨骼动画的创建.json 位置,atlas动画,0.25是骨骼动画大小。
f=spine::SkeletonAnimation::createWithJsonFile("bodyname.json", "bodyname.atlas",0.25f);
//替换骨骼,左边是插槽,右边是图片,只要骨骼动画文件里有替换的图片,那么就可以通过文字随便替换。
f->setAttachment("gun","gun");
瓦片地图问题
跟我设想的一样,可以用不同的值 ,代表不同的精灵和刚体,然后刚体有不同的性质。 只要在Tiled上随便画画,就可以生成一个随便的地图。用橡皮擦,擦擦,也可以修改地图。 效率很高。
但是需要注意的是,已经创建好的图块层, 迭代一层一层的找瓦片的时候,瓦片不能为空(整张地图至少有一个瓦片),迭代也不能超过瓦片的数量,否则就会显示,一串代码。显示超过数量,或者没有。。。。这类的 很烦。
设置刚体的具体位置的时候需要注意的是一定要把刚体的锚点,设置为局中,或者把瓦片的位置加上一般刚体的大小达到刚体中间,因为刚体的位置设置的默认是刚体的左下角的位置,如果把瓦片的位置按上那么,整个地图刚体就会整体向,左下、偏移半个刚体。 这很重要。 我选择的是瓦片位置向又上偏移半个刚体,再确立刚体的位置。
具体的行为就是这样。
//通过地图块获取 瓦片的位置
Vec2 vec0=collidable2->getPositionAt(Vec2(i,j));
//瓦片的位置加上半个刚体大小
Vec2 vec=Vec2(vec0.x+PTM_RATIO/2, vec0.y+PTM_RATIO/2);
b2BodyDef bodyDef;
//静态刚体,多用于固定的地图
bodyDef.type = b2_staticBody;
//赋予刚体初始的位置
bodyDef.position.Set(vec.x/PTM_RATIO, (vec.y/PTM_RATIO));
//创建刚体
b2Body *body=world->CreateBody(&bodyDef);
多个线程问题
有些时候需要多个任务的调度问题。 就比如 在跑步的时候我需要让枪去移动, 上边讲过了,控制骨骼动画的精细方式,但是我们都是在 同一个线程 工作,如果按了A了,W键就无法工作,因为冲突了。 这个时候我们需要的是再加一个线程工作。
其实box2d 可以创建一些定时器 schedule_selector(GameLayer::asyncUpdate1) 也就相当于第二个线程 一样。虽然我不知道它是不是线程,但是敢肯定不会冲突。 我把它当作一个线程来看待。
只需要在一个类里创建一些 函数,
void asyncUpdate1(float t);
void asyncUpdate2(float t);
//当需要的时候开启这个特定的定时器。
this->schedule(schedule_selector(GameLayer::asyncUpdate1));
this->schedule(schedule_selector(GameLayer::asyncUpdate2));
//不需要的时候关闭这个定时器
this->unschedule(schedule_selector(GameLayer::asyncUpdate1));
this->unschedule(schedule_selector(GameLayer::asyncUpdate2));
然后像线程一样,开启多个线程不冲突,并且不用时候可以随时关闭。
射线问题
看过一些视频,和书,都没有对射线进行介绍,或许对于瓦片地图的刚体世界,没有人用过吧。 我用了1个月的时间研究了一下。很简单,非常适合瓦片的碰撞检测,和人物远距离检测。
这个是最重要的,在主角跳跃的半空中需要动画,而回到地面也需要返回原来的动画,那么最开始用的是刚体的碰撞检测,但是你知道的,一串true false,根本无法解决。因为瓦片地图的地面是n个小刚体组成的,所以就会出现无限个true false,无法进行长时间的触碰检测。 因为当人物走到下一个刚体的时候,上一个刚体,就会显示碰撞结束。而事实上你现在还是在地面。
而要长时间监听一个刚体的状态,那么刚体本来发出的射线的检测是至关重要的。因为刚体是在不断发射射线去与外界接触。
所以给刚体一个像下的射线,去检测刚体的是否离开地面是非常重要的,也有效的检测方式,由于刚体的检测是以射线发出的,那么甚至检测位置也可以随意操纵,射线可以从脚步发出也可以从头部发出,发出的角度也可以自定义,甚至可以全方位发出,而发出的射线检测到刚体还可以被感知到。 所以射线甚至可以感知不同的刚体,对不同的刚体产生不同的判断。
上边的方块是一个拥有一圈射线的的刚体,它可以根据射线的感知,来追随特定的刚体,也就是主角,但是射线的缺陷就是不能穿墙,如果主角躲在其他刚体旁边,方块就无法感知到主角, 图片上的主角踩着一个方块。
//这是图片里方块向四面发射射线的代码。
if(sprite->getTag()==39){
for(int i=0;i<18;i++){
auto a=i*100.0;
double x=cos(a* b2_pi/180);
double y=sin(a* b2_pi/180);
Vec2 v(x,y);
RaysCastCallback callback;
//世界里收到刚体的毁掉函数 callback,刚体本来的位置,和射线结束的位置
world->RayCast(&callback,b2Vec2(b->GetPosition().x,b->GetPosition().y)
,b2Vec2(b->GetPosition().x+v.x*27
, b->GetPosition().y+v.y*27));
Vec2 pos(b->GetPosition().x*PTM_RATIO,b->GetPosition().y*PTM_RATIO);
g->drawLine(pos,Vec2(callback.point.x * PT_RATIO,callback.point.y *PTM_RATIO),Color4F(1, 1, 1, 1));
//这里是射线检测到了刚体
if(callback.fixture!=nullptr){
b2Body *b2= callback.fixture->GetBody();
auto sprite=(Sprite *)b2->GetUserData();
// CCLOG("这个是碰撞的射线。。。。。。。。。。。%d",sprite->getTag());
// 这里是射线判断了刚体是哪个精灵的。
if(sprite!=nullptr){
//如果感知到主角
if(sprite->getTag()==29){
b2Vec2 bp(b2->GetPosition().x,b2->GetPosition().y);
b2Vec2 bz(b->GetPosition().x,b->GetPosition().y);
if(bp.x<bz.x){
//向左边走 ,可以调速度的
b->SetTransform(b2Vec2(b->GetPosition().x-0.0450,b->GetPosition().y),map->getRotation());
b->SetAwake(true);
}else{
// 向右边走,可以调速度的
b->SetTransform(b2Vec2(b->GetPosition().x+0.0450,b->GetPosition().y),map->getRotation());
b->SetAwake(true);
}
}
}
}
}
}
落地与弹跳的姿势改变问题
这个我想了一个月,也困扰了我很久,或许想通了,也就是用射线,和定时器所建立的线程,完成。 因为无法持续判断刚体的具体情况,所以落地与弹起的动画很难完成。
当然会用射线和定时器以后,制作不同状态的动画很容易就可以达到。 但是还有一个缺点就是,着是跳跃姿势,玩了很多动画以后,在人物跳跃的半空中,动画在结束后,应该停在结束时的状态,而不是一直重复着动画,虽然魂斗罗人物跳跃中是一个无限翻滚的状态,但是一般的游戏在半空中,跳跃就结束了。 所以跳跃的状态应该是一次性的动画,而不是可重复的动画。所以就出现了,问题。在box2d 中,可重复动画,并不能盖过不可重复的动画。
f->setAnimation(1,"run",false);
//如果同样的两个动画,那么动画会截止在上一个跳跃状态,而不是下一个普通状态。
f->setAnimation(0,"idle",true);
所以 需要一个过渡阶段,来缓解这中尴尬局面。 而是
//过渡动画来 来让跳跃变成静止状态。
void BaseFSM::changeToDefault1(){
b2Body *body=role->getb2Body();
spine::SkeletonAnimation *f = role->getSkeletonAnimation();
log("跳跃以后发生的动画。。。。。。。。。。。。。。。。。。。。。。。。。。。。。");
f->setAnimation(1,"idle",false);
body->SetAwake(true);
}
用定时器来完成,对于跳跃的检测。 按键跳跃的时候,主角发射 射线 对于地图的地板进行检测,如果没有检测返回false,这个是时候主角是不可以展示出其他类型的动画的,只有跳跃的动画,如果贴近地板,射线就会检测到,然后返回一个true,这时 静止动画就会启动,让主角处于静止时的动画。 然后关闭定时器。 关闭定时器的作用是大量的射线检测只有一个检测到那么就说明检测到了。也就是跳出检测循环。
if(f!=nullptr){
if(f->getName()=="role"){
g->clear();
float j=-0.3;
for(int i=0;i<=5;i++){
RaysCastCallback callback;
b2Vec2 v1(b->GetPosition().x+j,b->GetPosition().y);
b2Vec2 v2(b->GetPosition().x+j,b->GetPosition().y-0.2);
world->RayCast(&callback, v1, v2);
g->drawLine(Vec2((b->GetPosition().x+j) *PTM_RATIO,b->GetPosition().y*PTM_RATIO),Vec2((callback.point.x)* PTM_RATIO, callback.point.y*PTM_RATIO),Color4F(1,1,1,1));
j=j+0.3;
if(callback.fixture!=nullptr){
b2Body *b2= callback.fixture->GetBody();
auto sprite=(Sprite *)b2->GetUserData();
if(sprite->getTag()==11){
// CCLOG("这个是碰撞的射线。。。。。。。。。。。%d",sprite->getTag());
onJump = true;
baseFSM->initJump(onJump);
CCLOG("着地状态");
baseFSM->changeToDefault1();
this->unschedule(schedule_selector(GameLayer::asyncUpdate1));
}
if(sprite->getTag()==39){
// CCLOG("这个是碰撞的射线。。。。。。。。。。。%d",sprite->getTag());
onJump = true;
baseFSM->initJump(onJump);
CCLOG("着地状态");
baseFSM->changeToDefault1();
this->unschedule(schedule_selector(GameLayer::asyncUpdate1));
}
}else{
onJump = false;
baseFSM->initJump(onJump);
CCLOG("不着地状态");
}
}
}
}
还有很多问题请看下一个章,慢慢讲。 这是给我的记录,中途我把代码删除过,但是通过前边几个章,我可以在2天之内恢复之前的代码,那么我相信如果这些写完了,我也可以用三天恢复到现在状态。