Cocos2d-x之初级物理引擎

c/c++

浏览数:323

2019-3-28

我连什么是重力都不懂,却让我开始用物理引擎。

一切故事发生的背景

同济大学软件学院每个学期会要求学生独立或者组队完成一个大项目,于是2016年的大项目是用cocos2d-x这款引擎制作一个自己的游戏(我最终做的网游,请勿模仿)。

之前写了一篇cocos2d-x关于键盘按住事件的教程,不足之处还请大家多多指出。

在自己做练习的过程中,逐渐接触到了碰撞,行走,降落一类的行为。我发现,如果全部由自己来实现,不仅实现起来复杂,而且执行效率也不一定见得高,所以我决心开始学习cocos2d-x Physics 2D。

物理引擎的基础

两种物理引擎

根据官方文档的介绍,目前有两大重要的物理引擎, Box2D 和 Chipmunk,并且cocos2d-x已经集成了它们,在3.x版本中可基于Chipmunk的核心API的物理引擎使用。

一句话: cocos2d-x 3.x版本中使用Chipmunk物理引擎更加方便了,大家都升级成3.x吧=。=

为什么要用物理引擎

我为什么要用物理引擎?举个开发中遇到的问题,在上一期的博文中,我们成功创建了一个hero sprite(对,就是那个可爱的僵尸)。现在我们要再创建一个坚果墙 sprite,叫做 wall sprite,它的作用是:hero会被wall而无法继续前进。

大家都知道,如果什么都不设置,我们控制hero的时候,是会直接穿过wall sprite的。这肯定不是我们想要的,机智的霄同学就想到了一个办法:获取hero要走的下一步位置(仔细看看上一期,就会发现这并不难),然后再判断这个位置(Point)是不是存在一个精灵,如果是,就强制取消移动命令。

好办法啊,思路清晰,我简直要为他鼓掌了。

方法虽好,可惜我不会。之前提到过,我是一个呆呆呆呆的初学者,我只想用最方便的方式来实现我要的效我甚至不惜牺牲程序的效率来追求减少代码量,一想到要再次封装一大堆,我就烫烫烫。

cocos2d-x physics 2D就可以完美解决这个问题,而且方法十分简单。

存在物理规律的世界?

我们这个世界受着来自物理规律的支配,那么物理引擎创建出来的scene也同样要存在某种物理规律,身为造物主的你可以自由定义这些规律。

首先,我们在Demo.cpp中创建一个物理世界。

Scene* Demo::createScene()
{
    auto scene = Scene::createWithPhysics();
    auto layer = HelloWorld::create();
    scene->addChild(layer);
    return scene;
}

很简单对吧?只需要将Scene::create改成Scene::createWithPhysics,在这里scene中的物理世界就算创建成功啦。

重新创建sprites

上一次教程中,我们创建了一个hero sprite, 创建的方法是这样的:

auto hero = Sprite::create("hero.png");

太愚蠢了是不是?高贵的物理世界怎么能这样呢?…

很遗憾,这样的创建方式是没错的,我们依然沿袭这个方法来创建sprite。

如果此时,你开启调试,就会发现sprite没有开始自由落体运动。那到底是哪里出现了问题呢?

这样创建出来的sprite只是一个空壳而已,它没有任何灵魂和信仰的力量(Physics body),我们此时需要给这只可怜的小家伙+1s信仰。

auto heroBody = PhysicsBody::createBox(hero->getContentSize());
hero->setPhysicsBody(bodyHero);

这样,它拥有了一个Physics body。现在再调试,你就发现我们可爱的hero已经在自由落体了。

可是,你不想听听createBox到底是什么意思吗?body存在一个边界,里面的空间表示sprite的实体。而这个边界有几种存在的形态:矩形、圆形和多边形。

在刚刚的例子中,我们创建了一个矩形的边界,规定了边界范围是hero sprite的大小(png图片)。再说说之后要创建的坚果墙吧,它的形状基本趋近一个圆形,那么则可以使用createCircle的方法来创建它的body.

setPhysicsBody顾名思义,就是将我们的灵魂(body)赋给hero sprite.

掉…掉下去了

看着我们的hero能够实现自由落体,我也很开心啊。可是…不一会儿,它就掉到屏幕外面去了,怎么办?

恍然大悟,我们的背景图片(它也是一个sprite,这不能忘啊),没有被添加body。但是我们又发现一个问题,body是进不去的,所以,物理引擎专门提供了一个方法createEdgeBox,只创建边界。

# Demo.cpp

auto map = Sprite::create("background.png");
auto mapFrame = PhysicsBody::createEdgeBox(map->getContentSize());

map->setPhysicsBody(mapFrame);

再次调试,hero稳稳地落在了地面上。

支配我的世界

虽然我不知道什么是G = mg,但是我知道世界上一定是有重力的,嗯。所以我们创建出来的scene中的物体也需要受到重力的作用。

看API文档了解到我们需要传入一个Vec2类型的重力参数,第一个和第二个数值是什么意思呢?我通过xcode进行调试发现:

第二个数值为默认的重力,98。那么我们就可以通过setGravity方法来设置属于我们自己的重力了。

# Demo.cpp

auto scene = Scene::createWithPhysics();
scene->getPhysicsWorld()->setGravity(Vec2(0.0f, -500.0f));
...

先使用物理scene中的getPhysicsWorld方法来获取我们的物理世界,然后再设置重力,经过调试就可以看见hero sprite和wall sprite飞快地加速下降了。

还有很多好玩的功能强劲的API可以供大家使用,比如getAllBodies,都等着我们去探索。

关系到具体body的属性

那么我想为hero sprite和wall sprite添加一些属于他们自己的物理属性,怎么做到呢?

就像现实世界中有人质量大,有人质量小一样,我要给坚果墙设置一个极大的质量以至于不可动摇,而僵尸(our hero)就可以自由行动(如何自由行动上一期已经说过啦~)。

就像API中所提到的,可以在创建Physics body的时候,就传入一个physics material进去。

之前我们只是传入一个sprite content进去(第一个参数),现在要传入更多的参数,使hero sprite的physics body达到我们预期的效果。

# Demo.cpp

auto hero = Sprite::create("hero.png");
auto heroBody = PhysicsBody::createBox(hero->getContentSize(), PhysicsMaterial(1.0f, 1.0f, 20.0f));

什么是Physics Material呢?

根据API可知,我们可以调用这个类的构造函数来创建一个physics material,使得physics body获取一定量的材质。密度,还原力和摩擦力。对于我们的需求来说,只要设置必要的摩擦力就够了。

第三个参数offset为偏移量,想要physics body和sprite的位置错开的话,可以填写这个参数。

heroBody->setDynamic(true);      //设置为静态的刚体,不受重力影响  
heroBody->setMass(999999);  //设置刚体不可动  
heroBody->setRotationEnable(false);      //设置刚体不可转动  
heroBody->getShape(0)->setRestitution(1.0f);

这些都是可以在API文档中找到设置physics body的方法,学会之后就可以随心所欲地创建属于自己的物理场景了。

没有重力的世界

不是所有游戏都是2D横版闯关的,比如上帝视角。
重力在这个场景中存在吗?存在,但是它不是明目张胆地表现出来。就比如一个个小棋子,定格在棋盘上,此时我们不能为这个场景添加重力,于是:

# Demo1.cpp

auto scene = Scene::createWithPhysics();
scene->getPhysicsWorld()->setGravity(Vec2(0.0f, 0.0f));
...

并且还要为sprite设置不受重力影响的效果。

# Demo1.cpp
auto sprite = Sprite::create("sprite.png");
auto spriteBody = PhysicsBody::createBox(sprite->getContentSize());
spriteBody->setGravityEnable(false);

这样,我们的小棋子就定格在棋盘上了。

动动动动起来

既然没有了重力,我们如何让它们在存在一个作用力的情况下,让它们停下来呢?

首先,你需要一个作用力。applyForce和applyImpulse这两个方法能够很好地帮助我们创建给物体施加的力。

# Demo1.cpp

spriteBody->applyForce(Vec2(100.0f, 100.0f));
// spriteBody->applyImpulse(Vec2(100.0f, 100.0f));

我们将物体发射到点100.0, 100.0的位置方向去。我还没来得及解释这两个方法是什么意思的时候,心急的朋友就马上开始调试了,结果发现sprite并没有按照预期的那样动起来。这是为什么呢?

因为力不够大啊孩子,很神奇的是,我们似乎只规定了力的反向而力的大小并没有被规定,但是又如何衡量一个力的大小呢?这是一个很令人纠结的问题。

且先来看文档, 参数只要求填入一个Vec2类型的数值,而且注释是force …

会不会是默认了添加1N的力呢?于是我将代码改成下面这样:

# Demo1.cpp

spriteBody->applyForce(Vec2(100.0f, 100.0f) * 1000);
// spriteBody->applyImpulse(Vec2(100.0f, 100.0f) * 1000);

果然,精灵动了起来。但是问题又来了,不一会,我就发现精灵根本没有停下来的意思,它在不停地运动。

造成这种问题,主要有两个原因:1、没有摩擦力;2、添加力的方式存在问题。

趁热打铁,我们先来解决力的问题。applyForce有什么问题吗?我们仔细看看API文档就会发现,这是添加了一个持续的力,这个力会不停地添加在sprite上,直到你手动地将其停下来。

那么我们需要一个瞬间的力,就类似弹弓一样。applyImpulse这个时候就出场啦。这个方法能为物体添加一个瞬时的力。

# Demo1.cpp

spriteBody->applyImpulse(Vec2(100.0f, 100.0f) * 1000);
// spriteBody->applyForce(Vec2(100.0f, 100.0f) * 1000);

快快快快快停下

可是我们发现精灵还是不能很好地停下,但是至少它不会像之前那么飞奔了。

刚刚说了,摩擦力存在问题。我们不是已经设置过摩擦力了吗?它会有什么问题呢?

摩擦力不代表空气阻力,在cocos2d-x的物理引擎创建的世界中,是默认不存在空气阻力这个高大上的属性的。

完蛋,物理引擎都没提供的功能,让我如何是好啊。很幸运的是,physics body提供了一个叫做setLinearDamping(设置线性阻尼)的方法。

这个方法可以很好地使在无重力状态下的物体停下来。

# Demo1.cpp

...
spriteBody->setLinearDamping(5.0f);

我将sprite body的阻尼设置为5.0f,其所产生的具体效果,肯定要在调试中才能看出。
很好,sprite在飞一段时间后,能很好地停下来了。

烂尾

当然物理引擎的魅力到这里还并没有被完全探索出来,只是给大家一个系统地学习方案而已。

还是那句老话,我希望将所学的一切用来解决实际的问题并将其转化为生产力,以上的全部都是我在学习中了解到的,每一章节都包含了很多小坑坑,不断地填补,以至刻骨铭心。

以上。