AndEngine 安卓游戏开发秘籍(三)
原文:
zh.annas-archive.org/md5/DC9ACC22F79E7DA8DE93ED0AD588BA9A译者:飞龙
第六章:物理学的应用
基于物理的游戏为玩家提供了许多其他类型无法体验的独特体验。本章介绍了 AndEngine 的 Box2D 物理扩展 的使用。我们的食谱包括:
-
Box2D 物理扩展简介
-
理解不同的物体类型
-
创建分类过滤的物体
-
创建多固定装置物体
-
通过指定顶点创建独特的物体
-
使用力、速度和扭矩
-
对特定物体应用反重力
-
与关节一起工作
-
创建布娃娃
-
创建绳子
-
与碰撞工作
-
使用 preSolve 和 postSolve
-
创建可破坏的物体
-
射线投射
Box2D 物理扩展简介
基于物理的游戏是移动设备上最受欢迎的游戏类型之一。AndEngine 允许使用 Box2D 扩展来创建基于物理的游戏。通过这个扩展,我们可以构建任何类型的物理现实的 2D 环境,从小的简单模拟到复杂游戏。在本食谱中,我们将创建一个演示简单设置的活动,以利用 Box2D 物理引擎扩展。此外,我们将在本章的剩余食谱中使用此活动。
准备就绪...
首先,创建一个名为 PhysicsApplication 的新活动类,该类扩展了 BaseGameActivity 并实现了 IAccelerationListener 和 IOnSceneTouchListener。
如何操作...
按照以下步骤构建我们的 PhysicsApplication 活动类:
-
在类中创建以下变量:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public FixedStepPhysicsWorld mPhysicsWorld; public Body groundWallBody; public Body roofWallBody; public Body leftWallBody; public Body rightWallBody; -
我们需要建立活动的基础。为此,首先在类中放置这四个常见的重写方法,以设置引擎、资源和主场景:
@Override public Engine onCreateEngine(final EngineOptions pEngineOptions) { return new FixedStepEngine(pEngineOptions, 60); } @Override public EngineOptions onCreateEngineOptions() { EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)); engineOptions.getRenderOptions().setDithering(true); engineOptions.getRenderOptions(). getConfigChooserOptions() .setRequestedMultiSampling(true); engineOptions.setWakeLockOptions( WakeLockOptions.SCREEN_ON); return engineOptions; } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback. onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } -
继续设置活动,通过添加以下重写方法,该方法将用于填充我们的场景:
@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { } -
接下来,我们将用以下代码填充前一个方法,以创建我们的
PhysicsWorld对象和Scene对象:mPhysicsWorld = new FixedStepPhysicsWorld(60, new Vector2(0f,-SensorManager.GRAVITY_EARTH*2), false, 8, 3); mScene.registerUpdateHandler(mPhysicsWorld); final FixtureDef WALL_FIXTURE_DEF = PhysicsFactory.createFixtureDef(0, 0.1f, 0.5f); final Rectangle ground = new Rectangle(cameraWidth / 2f, 6f, cameraWidth - 4f, 8f, this.getVertexBufferObjectManager()); final Rectangle roof = new Rectangle(cameraWidth / 2f, cameraHeight – 6f, cameraWidth - 4f, 8f, this.getVertexBufferObjectManager()); final Rectangle left = new Rectangle(6f, cameraHeight / 2f, 8f, cameraHeight - 4f, this.getVertexBufferObjectManager()); final Rectangle right = new Rectangle(cameraWidth - 6f, cameraHeight / 2f, 8f, cameraHeight - 4f, this.getVertexBufferObjectManager()); ground.setColor(0f, 0f, 0f); roof.setColor(0f, 0f, 0f); left.setColor(0f, 0f, 0f); right.setColor(0f, 0f, 0f); groundWallBody = PhysicsFactory.createBoxBody( this.mPhysicsWorld, ground, BodyType.StaticBody, WALL_FIXTURE_DEF); roofWallBody = PhysicsFactory.createBoxBody( this.mPhysicsWorld, roof, BodyType.StaticBody, WALL_FIXTURE_DEF); leftWallBody = PhysicsFactory.createBoxBody( this.mPhysicsWorld, left, BodyType.StaticBody, WALL_FIXTURE_DEF); rightWallBody = PhysicsFactory.createBoxBody( this.mPhysicsWorld, right, BodyType.StaticBody, WALL_FIXTURE_DEF); this.mScene.attachChild(ground); this.mScene.attachChild(roof); this.mScene.attachChild(left); this.mScene.attachChild(right); // Further recipes in this chapter will require us to place code here. mScene.setOnSceneTouchListener(this); pOnPopulateSceneCallback.onPopulateSceneFinished(); -
以下重写活动处理场景触摸事件、加速度计输入以及两个引擎生命周期事件—
onResumeGame和onPauseGame。将它们放在类的末尾以完成此食谱:@Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { // Further recipes in this chapter will require us to place code here. return true; } @Override public void onAccelerationAccuracyChanged( AccelerationData pAccelerationData) {} @Override public void onAccelerationChanged( AccelerationData pAccelerationData) { final Vector2 gravity = Vector2Pool.obtain( pAccelerationData.getX(), pAccelerationData.getY()); this.mPhysicsWorld.setGravity(gravity); Vector2Pool.recycle(gravity); } @Override public void onResumeGame() { super.onResumeGame(); this.enableAccelerationSensor(this); } @Override public void onPauseGame() { super.onPauseGame(); this.disableAccelerationSensor(); }
工作原理...
我们首先要定义一个相机的宽度和高度。然后,我们定义一个 Scene 对象和一个 FixedStepPhysicsWorld 对象,在其中进行物理模拟。最后一组变量定义了作为我们基于物理的场景边界的对象。
在第二步中,我们重写了onCreateEngine()方法,以返回一个每秒处理60次更新的FixedStepEngine对象。我们这样做的同时还使用了一个FixedStepPhysicsWorld对象,是为了创建一个在所有设备上都能保持一致的模拟,而不管设备处理物理模拟的效率如何。然后我们使用标准偏好创建EngineOptions对象,仅用一个简单的回调创建onCreateResources()方法,并将主场景设置为浅灰色背景。
在onPopulateScene()方法中,我们创建了一个FixedStepPhysicsWorld对象,其重力是地球的两倍,通过(x,y)坐标的Vector2对象传递,并且每秒更新60次。重力可以被设置为其他值以使模拟更加真实,或者设置为0以创建零重力模拟。重力设置为0对于太空模拟或者使用俯视摄像机视角而不是侧视视角的游戏很有用。布尔参数false设置了PhysicsWorld对象的AllowSleep属性,告诉PhysicsWorld在停止后不要让任何实体自行停用。FixedStepPhysicsWorld对象的最后两个参数告诉物理引擎计算速度和位置移动的次数。更高的迭代次数将创建更准确的模拟,但也可能因为处理器负载增加而导致延迟或抖动。在创建FixedStepPhysicsWorld对象之后,我们将其注册为主场景的更新处理器。未经注册,物理世界不会运行模拟。
变量WALL_FIXTURE_DEF是一个固定装置定义。固定装置定义包含了将在物理世界中作为固定装置创建的实体的形状和材质属性。固定装置的形状可以是圆形或多边形的。固定装置的材质通过其密度、弹性和摩擦系数来定义,这些都是在创建固定装置定义时需要提供的。在创建WALL_FIXTURE_DEF变量之后,我们创建了四个矩形,它们将代表墙壁实体的位置。在 Box2D 物理世界中,一个实体是由固定装置组成的。虽然只需要一个固定装置来创建一个实体,但多个固定装置可以创建具有不同属性的复杂实体。
在onPopulateScene()方法的后续部分,我们创建了将作为物理世界中的墙壁的盒子实体。之前创建的矩形被传递给这些实体以定义它们的位置和形状。然后我们将这些实体定义为静态的,这意味着它们在物理模拟中不会对任何力产生反应。最后,我们将墙壁固定装置定义传递给实体以完成它们的创建。
创建刚体后,我们将矩形附加到主场景,并将场景的触摸监听器设置为我们活动,该活动将通过 onSceneTouchEvent() 方法访问。onPopulateScene() 方法中的最后一行告诉引擎场景已准备好显示。
重写的 onSceneTouchEvent() 方法将处理我们场景的所有触摸交互。onAccelerationAccuracyChanged() 和 onAccelerationChanged() 方法继承自 IAccelerationListener 接口,允许我们在设备倾斜、旋转或平移时改变物理世界的重力。我们重写 onResumeGame() 和 onPauseGame() 方法,以防止游戏活动不在前台时加速计使用不必要的电池电量。
还有更多...
在重写的 onAccelerationChanged() 方法中,我们两次调用了 Vector2Pool 类。Vector2Pool 类只是为我们提供了一种复用 Vector2 对象的方法,否则这些对象可能需要系统进行垃圾回收。在较新的设备上,Android 垃圾收集器已经得到优化,以减少明显的卡顿,但较旧的设备可能会根据被垃圾回收的变量占用的内存量仍然出现延迟。
注意
访问 www.box2d.org/manual.html 查看完整的Box2D 用户手册。AndEngine Box2D 扩展基于官方 Box2D C++ 物理引擎的 Java 移植版本,因此在程序上存在一些差异,但总体概念仍然适用。
另请参阅
- 了解本章中的不同身体类型。
了解不同的身体类型
Box2D 物理世界为我们提供了创建不同身体类型的方法,使我们能够控制物理模拟。我们可以生成动态刚体,它们会对力和其他刚体做出反应;静态刚体,它们不会移动;以及运动刚体,它们会移动但不受力或其他刚体的影响。选择每个刚体的类型对于产生准确的物理模拟至关重要。在本教程中,我们将看到三种不同身体类型的刚体在碰撞期间如何相互反应。
准备工作...
按照本章开始部分给出的 Box2D 物理扩展介绍 部分的教程,创建一个新的活动,以便创建具有不同身体类型的刚体。
如何操作...
完成以下步骤,了解为刚体指定不同的身体类型如何影响它们:
-
首先,在
onPopulateScene()方法中插入以下固定定义:FixtureDef BoxBodyFixtureDef = PhysicsFactory.createFixtureDef(20f, 0f, 0.5f); -
接下来,在上一步的固定定义之后放置以下代码,创建三个矩形及其对应的刚体:
Rectangle staticRectangle = new Rectangle(cameraWidth / 2f,75f,400f,40f,this.getVertexBufferObjectManager()); staticRectangle.setColor(0.8f, 0f, 0f); mScene.attachChild(staticRectangle); PhysicsFactory.createBoxBody(mPhysicsWorld, staticRectangle, BodyType.StaticBody, BoxBodyFixtureDef); Rectangle dynamicRectangle = new Rectangle(400f, 120f, 40f, 40f, this.getVertexBufferObjectManager()); dynamicRectangle.setColor(0f, 0.8f, 0f); mScene.attachChild(dynamicRectangle); Body dynamicBody = PhysicsFactory.createBoxBody(mPhysicsWorld, dynamicRectangle, BodyType.DynamicBody, BoxBodyFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( dynamicRectangle, dynamicBody); Rectangle kinematicRectangle = new Rectangle(600f, 100f, 40f, 40f, this.getVertexBufferObjectManager()); kinematicRectangle.setColor(0.8f, 0.8f, 0f); mScene.attachChild(kinematicRectangle); Body kinematicBody = PhysicsFactory.createBoxBody(mPhysicsWorld, kinematicRectangle, BodyType.KinematicBody, BoxBodyFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( kinematicRectangle, kinematicBody); -
最后,在上一步定义之后添加以下代码,为我们的运动刚体设置线性和角速度:
kinematicBody.setLinearVelocity(-2f, 0f); kinematicBody.setAngularVelocity((float) (-Math.PI));
工作原理...
在第一步中,我们创建了BoxBodyFixtureDef夹具定义,我们将在第二步创建刚体时使用它。有关夹具定义的更多信息,请参阅本章中的Box2D 物理扩展介绍食谱。
在第二步中,我们首先通过调用Rectangle构造函数来定义staticRectangle矩形。我们将staticRectangle放置在场景的下方中央位置,坐标为cameraWidth / 2f, 75f,并设置矩形的宽度为400f,高度为40f,使其成为一条长条形平板。然后,我们通过调用staticRectangle.setColor(0.8f, 0f, 0f)将staticRectangle矩形的颜色设置为红色。最后,对于staticRectangle矩形,我们通过调用mScene.attachChild()方法,并将staticRectangle作为参数,将其附加到场景中。接下来,我们在物理世界中创建一个与staticRectangle相匹配的刚体。为此,我们调用PhysicsFactory.createBoxBody()方法,参数包括mPhysicsWorld(我们的物理世界)、staticRectangle(告诉箱子以与staticRectangle矩形相同的位置和大小创建)、BodyType.StaticBody(将刚体定义为静态)以及我们的BoxBodyFixtureDef夹具定义。
我们下一个矩形dynamicRectangle在位置400f和120f处创建,位于场景的中央,略高于staticRectangle矩形。我们的dynamicRectangle矩形的宽度和高度设置为40f,使其成为一个小的正方形。然后,我们通过调用dynamicRectangle.setColor(0f, 0.8f, 0f)将其颜色设置为绿色,并通过mScene.attachChild(dynamicRectangle)将其附加到我们的场景中。接下来,我们使用与staticRectangle矩形相同的方式,通过PhysicsFactory.createBoxBody()方法创建dynamicBody变量。注意,我们将dynamicBody变量的BodyType设置为DynamicBody。这会将刚体设置为动态的。现在,我们注册PhysicsConnector与物理世界,将dynamicRectangle和dynamicBody连接起来。PhysicsConnecter类将场景中的实体与物理世界中的刚体相连接,表示刚体在场景中的实时位置和旋转。
我们最后的矩形kinematicRectangle在位置600f和100f处创建,这将其放置在我们场景右侧的staticRectangle矩形上方。它被设置为具有40f的高度和宽度,使其成为像我们的dynamicRectangle矩形那样的小正方形。然后我们将kinematicRectangle矩形的颜色设置为黄色并附加到我们的场景中。与我们之前创建的两个物体类似,我们调用PhysicsFactory.createBoxBody()方法来创建我们的kinematicBody变量。请注意,我们使用BodyType类型为KinematicBody的参数来创建kinematicBody变量。这将其设置为运动学物体,因此只能通过设置其速度来进行移动。最后,我们在kinematicRectangle矩形和kinematicBody物体类型之间注册一个PhysicsConnector类。
在最后一步中,我们通过调用setLinearVelocity()方法并设置 x 轴上的-2f向量来设置kinematicBody物体的线性速度,使其向左移动。最后,我们通过调用kinematicBody.setAngularVelocity((float) (-Math.PI))将kinematicBody物体的角速度设置为负的π。有关设置物体速度的更多信息,请参见本章节中的使用力、速度和扭矩食谱。
还有更多内容...
静态物体不能通过施加或设定的力来移动,但可以使用setTransform()方法进行重新定位。然而,在模拟运行时,我们应避免使用setTransform()方法,因为它会使模拟变得不稳定,并可能导致一些奇怪的行为。相反,如果我们想要改变静态物体的位置,可以在创建模拟时进行,如果需要在运行时改变位置,只需检查新的位置是否会导致静态物体与现有的动态物体或运动学物体重叠。
运动学物体不能施加力,但我们可以通过setLinearVelocity()和setAngularVelocity()方法设置它们的速度。
另请参阅
-
本章节中的Box2D 物理扩展介绍。
-
本章节中的使用力、速度和扭矩。
创建分类筛选的物体
根据我们想要实现的物理模拟类型,控制哪些物体能够发生碰撞可能非常有用。在 Box2D 中,我们可以为夹具分配一个类别和类别筛选器,以控制哪些夹具可以互动。本食谱将介绍两个定义了类别筛选的夹具,这些夹具将通过触摸场景创建的物体来演示类别筛选。
准备好了...
按照本章开始部分给出的Box2D 物理扩展介绍部分的步骤创建一个活动。这个活动将促进本节中使用的分类筛选物体的创建。
如何操作...
按照以下步骤构建我们的分类筛选演示活动:
-
在活动中定义以下类级别变量:
private int mBodyCount = 0; public static final short CATEGORYBIT_DEFAULT = 1; public static final short CATEGORYBIT_RED_BOX = 2; public static final short CATEGORYBIT_GREEN_BOX = 4; public static final short MASKBITS_RED_BOX = CATEGORYBIT_DEFAULT + CATEGORYBIT_RED_BOX; public static final short MASKBITS_GREEN_BOX = CATEGORYBIT_DEFAULT + CATEGORYBIT_GREEN_BOX; public static final FixtureDef RED_BOX_FIXTURE_DEF = PhysicsFactory.createFixtureDef(1, 0.5f, 0.5f, false, CATEGORYBIT_RED_BOX, MASKBITS_RED_BOX, (short)0); public static final FixtureDef GREEN_BOX_FIXTURE_DEF = PhysicsFactory.createFixtureDef(1, 0.5f, 0.5f, false, CATEGORYBIT_GREEN_BOX, MASKBITS_GREEN_BOX, (short)0); -
接下来,在类中创建此方法,以在给定位置生成新的类别筛选刚体:
private void addBody(final float pX, final float pY) { this.mBodyCount++; final Rectangle rectangle = new Rectangle(pX, pY, 50f, 50f, this.getVertexBufferObjectManager()); rectangle.setAlpha(0.5f); final Body body; if(this.mBodyCount % 2 == 0) { rectangle.setColor(1f, 0f, 0f); body = PhysicsFactory.createBoxBody(this.mPhysicsWorld, rectangle, BodyType.DynamicBody, RED_FIXTURE_DEF); } else { rectangle.setColor(0f, 1f, 0f); body = PhysicsFactory.createBoxBody(this.mPhysicsWorld, rectangle, BodyType.DynamicBody, GREEN_FIXTURE_DEF); } this.mScene.attachChild(rectangle); this.mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( rectangle, body, true, true)); } -
最后,用以下代码填充
onSceneTouchEvent()方法的主体,该代码通过传递触摸位置来调用addBody()方法:if(this.mPhysicsWorld != null) if(pSceneTouchEvent.isActionDown()) this.addBody(pSceneTouchEvent.getX(), pSceneTouchEvent.getY());
它的工作原理是...
在第一步中,我们创建了一个整数mBodyCount,用于计算我们向物理世界添加了多少个刚体。mBodyCount整数在第二步中用于确定应将哪种颜色(从而确定哪个类别)分配给新的刚体。
我们通过定义具有唯一幂次方的短整数来创建CATEGORYBIT_DEFAULT、CATEGORYBIT_RED_BOX和CATEGORYBIT_GREEN_BOX类别位,以及通过将相关的类别位相加来定义MASKBITS_RED_BOX和MASKBITS_GREEN_BOX掩码位。类别位用于为固定装置分配一个类别,而掩码位结合不同的类别位以确定固定装置可以与哪些类别发生碰撞。然后,我们将类别位和掩码位传递给固定装置定义以创建具有类别碰撞规则的固定装置。
第二步是一个简单的方法,用于创建一个矩形及其对应的刚体。该方法采用我们想要用来创建新刚体的X和Y位置参数,并将它们传递给Rectangle对象的构造函数,同时我们还传递了一个高度和宽度为50f以及活动的VertexBufferObjectManager。然后,我们使用rectangle.setAlpha()方法将矩形设置为 50%透明。之后,我们定义一个刚体,并通过将mBodyCount变量与2取模来确定每个创建的刚体的颜色和装置。确定颜色和装置后,我们通过设置矩形的颜色和创建一个刚体来分配它们,传递我们的mPhysicsWorld物理世界、矩形、动态刚体类型以及先前确定的装置。最后,我们将矩形附加到我们的场景中,并注册一个PhysicsConnector类以将矩形连接到我们的刚体。
第三步仅当已创建物理世界且场景的TouchEvent为ActionDown时,才从第二步调用addBody()方法。传递的参数pSceneTouchEvent.getX()和pSceneTouchEvent.getY()表示在场景上接收触摸输入的位置,这也是我们想要创建新的类别筛选刚体的位置。
还有更多...
所有固定装置的默认类别值为一。在为特定固定装置创建掩码位时,请记住,包含默认类别的任何组合都会导致该固定装置与所有未设置为避免与之碰撞的其他固定装置发生碰撞。
另请参阅
-
本章介绍Box2D 物理扩展。
-
本章中了解不同的刚体类型。
创建具有多个装置的刚体。
有时我们需要一个具有不同物理属性的身体部位。例如,带保险杠的车如果撞墙应该与没有保险杠的车有不同的反应。在 Box2D 中创建这样的多固定装置身体是相当简单直接的。在本节中,我们将了解如何通过创建两个固定装置并将它们添加到空身体中来创建多固定装置身体。
准备工作...
按照本章开始部分Box2D 物理扩展介绍一节中的步骤创建一个新的活动,以便促进我们多固定装置身体的创建。
如何操作...
按照以下步骤,了解如何创建多固定装置身体:
-
在
onPopulateScene()方法中放置以下代码,以创建两个具有修改过的AnchorCenter值的矩形,这允许在连接到身体时进行精确放置:Rectangle nonbouncyBoxRect = new Rectangle(0f, 0f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()); nonbouncyBoxRect.setColor(0f, 0f, 0f); nonbouncyBoxRect.setAnchorCenter(((nonbouncyBoxRect.getWidth() / 2) - nonbouncyBoxRect.getX()) / nonbouncyBoxRect.getWidth(), ((nonbouncyBoxRect.getHeight() / 2) – nonbouncyBoxRect.getY()) / nonbouncyBoxRect.getHeight()); mScene.attachChild(nonbouncyBoxRect); Rectangle bouncyBoxRect = new Rectangle(0f, -55f, 90f, 10f, this.getEngine().getVertexBufferObjectManager()); bouncyBoxRect.setColor(0f, 0.75f, 0f); bouncyBoxRect.setAnchorCenter(((bouncyBoxRect.getWidth() / 2) – bouncyBoxRect.getX()) / bouncyBoxRect.getWidth(), ((bouncyBoxRect.getHeight() / 2) – bouncyBoxRect.getY()) / bouncyBoxRect.getHeight()); mScene.attachChild(bouncyBoxRect); -
以下代码创建了一个
Body对象和两个固定装置,一个完全弹性,另一个完全非弹性。在前面步骤中创建矩形后添加它:Body multiFixtureBody = mPhysicsWorld.createBody(new BodyDef()); multiFixtureBody.setType(BodyType.DynamicBody); FixtureDef nonbouncyBoxFixtureDef = PhysicsFactory.createFixtureDef(20, 0.0f, 0.5f); final PolygonShape nonbouncyBoxShape = new PolygonShape(); nonbouncyBoxShape.setAsBox((nonbouncyBoxRect.getWidth() / 2f) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, (nonbouncyBoxRect.getHeight() / 2f) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, new Vector2(nonbouncyBoxRect.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, nonbouncyBoxRect.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT), 0f); nonbouncyBoxFixtureDef.shape = nonbouncyBoxShape; multiFixtureBody.createFixture(nonbouncyBoxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( nonbouncyBoxRect, multiFixtureBody)); FixtureDef bouncyBoxFixtureDef = PhysicsFactory.createFixtureDef(20, 1f, 0.5f); final PolygonShape bouncyBoxShape = new PolygonShape(); bouncyBoxShape.setAsBox((bouncyBoxRect.getWidth() / 2f) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, (bouncyBoxRect.getHeight() / 2f) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, new Vector2(bouncyBoxRect.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, bouncyBoxRect.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT), 0f); bouncyBoxFixtureDef.shape = bouncyBoxShape; multiFixtureBody.createFixture(bouncyBoxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( bouncyBoxRect, multiFixtureBody)); -
最后,我们需要设置多固定装置身体的位置,既然它已经被创建了。在前面步骤中创建身体后,放置以下对
setTransform()的调用:multiFixtureBody.setTransform(400f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, 240f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, 0f);
工作原理...
我们首先使用Rectangle构造函数定义一个矩形,它将代表一个非弹性的固定装置,在 x 轴和 y 轴上传递0f,表示世界原点。然后我们传递一个高度和宽度为100f,这使得矩形成为一个大正方形,以及活动的VertexBufferObjectManager。
接着,我们将非弹跳矩形的颜色设置为黑色 0f, 0f, 0f,并使用 nonbouncyBoxRect.setAnchorCenter() 方法设置其锚点中心,以表示在第二步创建的物体上,非弹跳矩形将被附着的地点的位置。锚点中心的位置 (((nonbouncyBoxRect.getWidth() / 2) - nonbouncyBoxRect.getX()) / nonbouncyBoxRect.getWidth(), ((nonbouncyBoxRect.getHeight() / 2) – nonbouncyBoxRect.getY()) / nonbouncyBoxRect.getHeight() 将矩形的定位和大小转换为相对于原点的位置。在我们的非弹跳矩形的情况下,锚点中心保持在默认的 0.5f, 0.5f,但对于任何从非原点中心矩形创建的固定装置,这个公式是必要的。接下来,我们将非弹跳矩形附加到场景中。然后,我们使用与非弹跳矩形相同的方法创建一个将表示弹跳组件的矩形,但是我们将矩形在 y 轴上放置在 -55f 的位置,使其直接位于非弹跳矩形的下方。我们还将矩形的宽度设置为 90f,使其比之前的矩形略小,并将高度设置为 10f,使其成为一个细长的条,作为非弹跳矩形下方的弹跳部分。使用与非弹跳矩形相同的公式设置弹跳矩形的锚点中心后,我们将其附加到场景中。请注意,我们已经修改了每个矩形的 AnchorCenter 值,这样在第二步中注册的 PhysicsConnectors 类可以在运行模拟时将矩形放置在正确的位置。还要注意,我们在世界原点创建我们的矩形和多固定装置物体,以简化计算并提高速度。在物体创建之后,我们将其移动到模拟中应有的位置,如第三步所示,当我们调用 multiFixtureBody.setTransform() 方法时,使用参数 400f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT 和 240f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT,这代表了物理世界中的屏幕中心,以及 0f,这表示物体将具有的零旋转。
在第二步中,我们通过调用 mPhysicsWorld.createBody(new BodyDef()) 创建一个空的物体 multiFixtureBody,并通过调用其 setType() 方法并传入参数 BodyType.DynamicBody 来设置它为动态的。然后,我们定义一个用于非弹跳组件的固定装置定义 nonbouncyBoxFixtureDef。
接下来,我们创建一个名为 nonbouncyBoxShape 的 PolygonShape 形状,并通过调用 nonbouncyBoxShape 形状的 setAsBox() 方法,将第一个两个参数设置为 nonbouncyBoxRect.getWidth() / 2f 和 nonbouncyBoxRect.getHeight() / 2f,使其模仿我们的 nonbouncyBoxRect 矩形,从而将其设置为与 nonbouncyBoxRect 矩形具有相同的宽度和高度。这两个参数都除以 PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT 以将值缩放到物理世界。此外,setAsBox() 方法的头两个参数是半尺寸。这意味着正常的宽度 10f 将作为 5f 传递给 setAsBox() 方法。setAsBox() 方法的下一个参数是一个 Vector2 参数,用于标识我们的 nonbouncyBoxShape 形状在物理世界中的位置。我们将其设置为我们的 nonbouncyBoxRect 矩形的当前位置,并通过使用 PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT 变量进行缩放,将位置转换为物理世界坐标。setAsBox() 方法的最后一个参数是 nonbouncyBoxShape 应具有的旋转。因为我们的 nonbouncyBoxRect 矩形没有旋转,所以我们使用 0f。
然后,我们将 nonbouncyBoxFixtureDef 夹具定义的 shape 属性设置为 nonbouncyBoxShape,将形状应用到我们的夹具定义中。接下来,通过调用物体的 createFixture() 方法,并将 nonbouncyBoxFixtureDef 夹具定义作为参数,将夹具附加到我们的 multifixture 物体上。然后,我们注册一个 PhysicsConnector 类,将场景中的 nonbouncyBoxRect 矩形与物理世界中的 multiFixtureBody 物体连接起来。最后,我们按照创建非弹跳夹具时使用的相同程序来创建我们的弹跳夹具。结果应该是一个带有绿色弹跳边的黑色正方形。
注意
通过将夹具定义中的 isSensor 属性设置为 true,可以创建一个传感器夹具,使其能够与其他夹具接触而不发生物理交互。关于传感器的更多信息,请参见 Box2D 手册中的夹具部分,链接为 www.box2d.org/manual.html。
另请参阅
-
本章介绍 Box2D 物理扩展*。
-
本章中了解不同的物体类型。
通过指定顶点创建独特的物体。
我们物理模拟中的所有东西不必都是由矩形或圆形制成的。我们还可以通过创建多边形点的列表来创建多边形物体。这种方法对于创建特定类型的地形、车辆和角色很有用。在本教程中,我们将演示如何从顶点列表中创建一个独特的物体。
准备就绪...
按照本章开始部分给出的Box2D 物理扩展介绍一节中的步骤创建一个活动。这个活动将轻松允许创建一个具有顶点的独特构造的物体。
如何操作...
完成以下步骤以定义和创建我们独特的多边形主体:
-
我们独特主体的顶点将由
Vector2对象列表定义。将以下列表添加到onPopulateScene()方法中:List<Vector2> UniqueBodyVertices = new ArrayList<Vector2>(); UniqueBodyVertices.addAll((List<Vector2>) ListUtils.toList( new Vector2[] { new Vector2(-53f,-75f), new Vector2(-107f,-14f), new Vector2(-101f,41f), new Vector2(-71f,74f), new Vector2(69f,74f), new Vector2(98f,41f), new Vector2(104f,-14f), new Vector2(51f,-75f), new Vector2(79f,9f), new Vector2(43f,34f), new Vector2(-46f,34f), new Vector2(-80f,9f) })); -
要使用前面的顶点列表,我们必须通过
EarClippingTriangulator类处理它们,将顶点列表转换为物理引擎将用于创建多个固定装置并连接成一个单一主体的三角形列表。在初始Vector2列表创建后放置以下代码:List<Vector2> UniqueBodyVerticesTriangulated = new EarClippingTriangulator(). computeTriangles(UniqueBodyVertices); -
要创建表示我们独特主体的网格,以及调整三角化顶点以在物理世界中使用,请添加以下代码片段:
float[] MeshTriangles = new float[UniqueBodyVerticesTriangulated.size() * 3]; for(int i = 0; i < UniqueBodyVerticesTriangulated.size(); i++) { MeshTriangles[i*3] = UniqueBodyVerticesTriangulated.get(i).x; MeshTriangles[i*3+1] = UniqueBodyVerticesTriangulated.get(i).y; UniqueBodyVerticesTriangulated.get(i). mul(1/PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT); } Mesh UniqueBodyMesh = new Mesh(400f, 260f, MeshTriangles, UniqueBodyVerticesTriangulated.size(), DrawMode.TRIANGLES, this.getVertexBufferObjectManager()); UniqueBodyMesh.setColor(1f, 0f, 0f); mScene.attachChild(UniqueBodyMesh); -
既然我们已经调整了顶点以在物理世界中使用,我们可以创建主体:
FixtureDef uniqueBodyFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.5f, 0.5f); Body uniqueBody = PhysicsFactory.createTrianglulatedBody( mPhysicsWorld, UniqueBodyMesh, UniqueBodyVerticesTriangulated, BodyType.DynamicBody, uniqueBodyFixtureDef); mPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(UniqueBodyMesh, uniqueBody)); -
最后,我们希望独特主体有与之碰撞的物体。添加以下主体定义以创建两个静态主体,它们将在我们的物理世界中充当小钉子:
FixtureDef BoxBodyFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.6f, 0.5f); Rectangle Box1 = new Rectangle(340f, 160f, 20f, 20f, this.getVertexBufferObjectManager()); mScene.attachChild(Box1); PhysicsFactory.createBoxBody(mPhysicsWorld, Box1, BodyType.StaticBody, BoxBodyFixtureDef); Rectangle Box2 = new Rectangle(600f, 160f, 20f, 20f, this.getVertexBufferObjectManager()); mScene.attachChild(Box2); PhysicsFactory.createBoxBody(mPhysicsWorld, Box2, BodyType.StaticBody, BoxBodyFixtureDef);
工作原理...
我们最初创建的顶点列表表示我们独特主体的形状,相对于主体的中心。在第二步中,我们使用EarClippingTriangulator类创建另一个顶点列表。从EarClippingTriangulator类的computeTriangles()方法返回的列表包含了构成我们独特主体的所有三角形点。以下图展示了在通过EarClippingTriangulator类处理顶点之前和之后我们的多边形主体的样子。请注意,我们的主体将由表示原始形状的几个三角形形状组成:
在第三步中,将每个顶点添加到MeshTriangles数组以用于创建表示我们主体的网格后,我们每个顶点都乘以1/PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT,这相当于将顶点的坐标除以默认的像素到米比率。这种除法过程是用于将场景坐标转换为物理世界坐标的常见做法。物理世界以米为单位测量距离,因此需要从像素进行转换。任何一致且合理的值都可以用作转换常数,但默认的像素到米比率是每米 32 像素,几乎在每一个模拟中都已被证明是有效的。
第四步通过调用PhysicsFactory.createTrianglulatedBody创建独特的主体。需要注意的是,尽管可以从非三角化的顶点列表创建多边形主体,但只有当我们的列表顶点少于七个时,这样做才会有所好处。即使列表很小,三角化主体对模拟的影响也没有明显的负面影响。
注意
有几个物理主体编辑器可以简化主体的创建。以下都是与 AndEngine 兼容的:
-
Physics Body Editor(免费):
code.google.com/p/box2d-editor -
PhysicsEditor(付费):
www.codeandweb.com/physicseditor -
Inkscape(免费,需要插件):
inkscape.org/
另请参阅
-
本章介绍Box2D 物理扩展。
-
本章了解不同的物体类型。
使用力、速度和扭矩
无论我们创建何种类型的模拟,我们很可能会至少想要控制一个物体。在 Box2D 中移动物体,我们可以施加线性或角力,设置线性或角速度,并以扭矩的形式施加角力。在本食谱中,我们将看到如何对多个物体施加这些力和速度。
准备就绪...
按照本章开始部分Box2D 物理扩展一节的步骤创建一个新活动,以便创建能够对力、速度和扭矩做出反应的物体。然后,更新活动,包括来自代码捆绑包中ForcesVelocitiesTorqueActivity类的附加代码。
如何操作...
参考补充的ForcesVelocitiesTorqueActivity类,以获取本食谱的完整示例。我们将在本节中仅介绍食谱的基础知识:
-
我们首先会使用处理物体线性运动的方法。在
LinearForceRect矩形重写的onAreaTouched()方法中放置以下代码片段:LinearForceBody.applyForce(0f, 2000f, LinearForceBody.getWorldCenter().x, LinearForceBody.getWorldCenter().y); -
接下来,将此代码插入到
LinearImpulseRect矩形的onAreaTouched()方法中:LinearImpulseBody.applyLinearImpulse(0f, 200f, LinearImpulseBody.getWorldCenter().x, LinearImpulseBody.getWorldCenter().y); -
然后,将此代码添加到
LinearVelocityRect矩形的onAreaTouched()方法中:LinearVelocityBody.setLinearVelocity(0f, 20f); -
现在,我们将使用影响物体角运动的
Body方法。将此代码放在AngularTorqueRect矩形的onAreaTouched()方法中:AngularTorqueBody.applyTorque(2000f); -
在
AngularImpulseRect矩形的onAreaTouched()方法中插入以下代码:AngularImpulseBody.applyAngularImpulse(20f); -
最后,将此代码添加到
AngularVelocityRect矩形的onAreaTouched()方法中:AngularVelocityBody.setAngularVelocity(10f);
工作原理...
在第一步中,我们通过调用LinearForceBody的applyForce()方法,并在 x 轴上使用0f,在 y 轴上使用2000f的力参数,在其世界坐标中心LinearForceBody.getWorldCenter().x和LinearForceBody.getWorldCenter().y处施加一个强大的、正的垂直力。
第二步通过LinearImpulseBody.applyLinearImpulse()方法在LinearImpulseBody物体上应用一个线性冲量。applyLinearImpulse()方法的前两个参数是相对于世界坐标轴的冲量量。我们使用值0f和200f来应用一个指向正上方的适度冲量。applyLinearImpulse()方法的剩余两个参数是冲量在世界坐标中应用到的物体的 x 和 y 位置。我们传递LinearImpulseBody.getWorldCenter().x和LinearImpulseBody.getWorldCenter().y,以在LinearImpulseBody物体的中心应用冲量。
在第三步中,我们通过调用LinearVelocityBody.setLinearVelocity()方法并传入参数0f和20f来设置LinearVelocityBody的线性速度。参数0f表示物体在 x 轴上不会移动,而参数20f则立即将 y 轴上的运动速度设置为每秒 20 米。使用setLinearVelocity()方法时,速度会自动设置在物体的质心上。
第四步给AngularTorqueBody应用一个扭矩。我们调用AngularTorqueBody.applyTorque()方法并传入值2000f,以在物体的质心上给AngularTorqueBody施加一个非常大的扭矩。
在第五步中,我们通过调用AngularImpulseBody.applyAngularImpulse()方法并传入值20f,给AngularImpulseBody物体应用一个角冲量。这个小的角冲量将被应用到AngularImpulseBody物体的质心上。
在最后一步中,我们设置AngularVelocityBody物体的角速度。我们调用AngularVelocityBody.setAngularVelocity()方法并传入值10f,使物体立即以每秒 10 弧度的速度旋转。
还有更多...
冲量与力的不同之处在于,它们独立于时间步长起作用。实际上,冲量等于力乘以时间。同样,力等于冲量除以时间。
设置物体的速度和应用冲量相似,但有一个重要的区别——直接应用冲量会增加或减少速度,而设置速度并不会逐渐增加或减少速度。
另请参阅
-
本章中的《Box2D 物理扩展简介》。
-
本章中了解不同的物体类型。
对特定物体应用反重力
在上一个食谱中,我们了解了力如何影响物体。使用与重力相对抗的恒定力,我们可以使物体从物理世界的重力中释放出来。如果与重力相对抗的力足够大,物体甚至会飘走!在这个食谱中,我们将创建一个与重力相抵消的物体。
准备好了...
按照本章开始部分《Box2D 物理扩展简介》一节中的步骤创建一个活动。这个活动将有助于创建一个受到恒定力作用的物体,该力与重力相对抗。
如何操作...
对于此教程,按照以下步骤创建一个反对重力的刚体:
-
在活动中放置以下定义:
Body gravityBody; Body antigravityBody; final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(2f, 0.5f, 0.9f); -
接下来,创建一个矩形和刚体,以演示重力对刚体的正常影响。在
onPopulateScene()方法中放置以下代码片段:Rectangle GravityRect = new Rectangle(300f, 240f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()); GravityRect.setColor(0f, 0.7f, 0f); mScene.attachChild(GravityRect); mScene.registerTouchArea(GravityRect); gravityBody = PhysicsFactory.createBoxBody(mPhysicsWorld, GravityRect, BodyType.DynamicBody, boxFixtureDef); gravityBody.setLinearDamping(0.4f); gravityBody.setAngularDamping(0.6f); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( GravityRect, gravityBody)); -
最后,创建一个矩形和刚体,以展示如何通过在每次更新时施加反重力力使刚体忽略重力:
Rectangle AntiGravityRect = new Rectangle(500f, 240f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()) { @Override protected void onManagedUpdate(final float pSecondsElapsed) { super.onManagedUpdate(pSecondsElapsed); antigravityBody.applyForce( -mPhysicsWorld.getGravity().x * antigravityBody.getMass(), -mPhysicsWorld.getGravity().y * antigravityBody.getMass(), antigravityBody.getWorldCenter().x, antigravityBody.getWorldCenter().y); } }; AntiGravityRect.setColor(0f, 0f, 0.7f); mScene.attachChild(AntiGravityRect); mScene.registerTouchArea(AntiGravityRect); antigravityBody = PhysicsFactory.createBoxBody(mPhysicsWorld, AntiGravityRect, BodyType.DynamicBody, boxFixtureDef); antigravityBody.setLinearDamping(0.4f); antigravityBody.setAngularDamping(0.6f); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( AntiGravityRect, antigravityBody));
它是如何工作的...
我们采取的第一步是定义一个受重力影响的刚体、一个反对重力的刚体,以及创建刚体时使用的夹具定义。
接下来,我们创建一个受重力影响的矩形及其对应的刚体。有关创建矩形的更多信息,请参阅第二章中的将基元应用于层教程,使用实体,或有关创建刚体的更多信息,请参阅本章中的了解不同的刚体类型教程。
然后,我们创建反重力刚体及其连接的矩形。通过重写反重力矩形的onManagedUpdate()方法,我们可以在其中放置代码,这些代码将在每次引擎更新后运行。在AntiGravityRect矩形的情况下,我们用antigravityBody.applyForce()方法填充onManagedUpdate()方法,传递负的mPhysicsWorld.getGravity()方法的x和y值乘以antigravityBody的质量,并最终设置力在世界中心的antigravityBody上施加。通过在onManagedUpdate()方法中使用与物理世界的重力完全相反的力,每次更新后,反重力刚体都能对抗物理世界的重力。此外,我们施加的力必须乘以刚体的质量,以完全抵消重力的效果。参考以下图表以更好地了解反重力刚体的功能:
另请参阅
-
本章中的 Box2D 物理扩展介绍。
-
在本章中使用力、速度和扭矩。
使用关节
在 Box2d 中,关节用于连接两个刚体,使每个刚体以某种方式附着在另一个上。各种类型的关节使我们能够定制角色、车辆和世界。此外,关节可以在模拟过程中创建和销毁,这为我们的游戏提供了无限的可能性。在本教程中,我们将创建一个线关节,以演示如何在物理世界中设置和使用关节。
准备工作...
按照本章开始部分给出的Box2D 物理扩展介绍部分的步骤创建一个活动。这个活动将有助于创建两个刚体和一个连接线关节,我们将在此教程中使用它们。参考补充代码中的JointsActivity类,了解更多类型的关节示例。
如何操作...
按照以下步骤创建一个线关节:
-
在我们的活动中定义以下变量:
Body LineJointBodyA; Body LineJointBodyB; final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.2f, 0.9f); -
在
onPopulateScene()方法中添加以下代码,以创建两个矩形及其相关联的物体:Rectangle LineJointRectA = new Rectangle(228f, 240f, 30f, 30f, this.getEngine().getVertexBufferObjectManager()); LineJointRectA.setColor(0.5f, 0.25f, 0f); mScene.attachChild(LineJointRectA); LineJointBodyA = PhysicsFactory.createBoxBody(mPhysicsWorld, LineJointRectA, BodyType.KinematicBody, boxFixtureDef); Rectangle LineJointRectB = new Rectangle(228f, 200f, 30f, 30f, this.getEngine().getVertexBufferObjectManager()) { @Override protected void onManagedUpdate(final float pSecondsElapsed) { super.onManagedUpdate(pSecondsElapsed); LineJointBodyB.applyTorque(1000f); LineJointBodyB.setAngularVelocity( Math.min( LineJointBodyB.getAngularVelocity(),0.2f)); } }; LineJointRectB.setColor(0.75f, 0.375f, 0f); mScene.attachChild(LineJointRectB); LineJointBodyB = PhysicsFactory.createBoxBody(mPhysicsWorld, LineJointRectB, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( LineJointRectB, LineJointBodyB)); -
在前一步显示的代码之后放置以下代码,以创建一个连接前一步物体的线关节:
final LineJointDef lineJointDef = new LineJointDef(); lineJointDef.initialize(LineJointBodyA, LineJointBodyB, LineJointBodyB.getWorldCenter(), new Vector2(0f,1f)); lineJointDef.collideConnected = true; lineJointDef.enableLimit = true; lineJointDef.lowerTranslation = -220f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT; lineJointDef.upperTranslation = 0f; lineJointDef.enableMotor = true; lineJointDef.motorSpeed = -200f; lineJointDef.maxMotorForce = 420f; mPhysicsWorld.createJoint(lineJointDef);
工作原理...
我们首先定义两个物体LineJointBodyA和LineJointBodyB,它们将连接到我们的线关节,以及将应用于这些物体的boxFixtureDef夹具定义。关于创建夹具定义的更多信息,请参考本章开始部分提供的Box2D 物理扩展介绍食谱。
在第二步中,我们使用Rectangle()构造函数创建LineJointRectA矩形,其位置为228f和240f,使其位于场景左半部分的中间位置,高度和宽度设置为30f以使其成为一个小正方形。然后,我们通过调用LineJointRectA.setColor()方法并传入参数0.5f、0.25f和0f将其颜色设置为深橙色。接下来,我们通过调用PhysicsFactory.createBoxBody()构造函数创建与LineJointRectA矩形相关联的LineJointBodyA物体,传入参数mPhysicsWorld(即我们的物理世界)、LineJointRectA(用于定义物体的形状和位置)、BodyType为BodyType.KinematicBody以及boxFixtureDef夹具定义。
接下来,我们以创建LineJointRectA和LineJointBodyA相同的方式处理LineJointRectB和LineJointBodyB的创建,但在创建LineJointRectB时增加了重写的onManagedUpdate()方法,并添加了一个PhysicsConnector类以连接LineJointRectB和LineJointBodyB。LineJointRectB的onManagedUpdate()方法通过调用LineJointBodyB.applyTorque()方法并传入值1000f,对LineJointBodyB施加大的扭矩。施加扭矩后,我们确保LineJointBodyB物体的角速度不超过0.2f,通过将Math.min(LineJointBodyB.getAngularVelocity(), 0.2f)传递给LineJointBodyB.setAngularVelocity()方法。最后,在第二步末尾创建并注册的PhysicsConnector类将我们场景中的LineJointRectB与物理世界中的LineJointBodyB连接起来。
在第三步中,我们创建线性关节。为了初始化线性关节,我们使用lineJointDef.initialize()方法,并传入相关联的刚体LineJointBodyA和LineJointBodyB。然后,我们将LineJointBodyB的世界中心作为关节的锚点,并传入包含关节的世界单位轴的Vector2。我们关节的世界轴设置为0f和1f,这意味着在 x 轴上没有移动,在 y 轴上以1f的比例移动。然后,我们通过将lineJointDef.collideConnected变量设置为true来告诉关节允许两个刚体之间的碰撞,并通过将lineJointDef.enableLimit变量设置为true来启用关节的限制,这限制了LineJointBodyB与第一个刚体的距离。为了设置关节的下限距离,即LineJointBodyB可以在负方向上移动多远,我们将lineJointDef.lowerTranslation变量设置为-220f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT。对于上限距离,我们将lineJointDef.upperTranslation变量设置为0f,以防止LineJointBodyB被推到LineJointBodyA之上。接下来,我们通过将lineJointDef.enableMotor变量设置为true来启用关节的电机,这将根据电机的速度将LineJointBodyB向LineJointBodyA拉或推。最后,我们通过将lineJointDef.motorSpeed变量设置为-200f来给关节的电机一个快速的负速度,使LineJointBodyB向lowerTranslation限制移动,并通过将lineJointDef.maxMotorForce变量设置为420f来给电机一个强大的最大力。
线性关节的作用类似于汽车的悬挂和车轮部分。它允许在一个轴上进行约束运动,通常是车辆的垂直方向,并允许第二个刚体旋转或必要时作为动力轮。下图说明了线性关节的各个组成部分:
还有更多...
所有关节都有两个刚体,并为我们提供了允许连接刚体之间碰撞的选项。我们可以在需要时启用碰撞,但每个关节的collideConnected变量的默认值是false。此外,所有关节的第二个刚体应该是具有BodyType.DynamicBody类型的BodyType。
对于具有频率的任何关节,该频率决定了关节的弹性行为,切勿将频率设置为超过物理世界时间步长的一半。如果物理世界的时间步长为 40,我们应分配给关节频率的最大值应为20f。
如果在关节处于活动状态时,与关节连接的任一刚体被销毁,则关节也会被销毁。这意味着当我们处理物理世界时,只要我们销毁所有刚体,就不需要销毁其中的关节。
更多关节类型
线性关节只是我们可以在物理模拟中使用的几种关节类型之一。其他类型的关节包括距离关节、鼠标关节、棱柱关节、滑轮关节、旋转关节和焊接关节。继续阅读以了解每种类型的更多信息。参考补充的JointsActivity类,以获得每种关节类型的更深入示例。
距离关节
距离关节只是试图保持其连接的刚体之间的特定距离。如果我们不设置距离关节的长度,它会假定长度为其刚体之间的初始距离。以下代码创建了一个距离关节:
final DistanceJointDef distanceJointDef = new DistanceJointDef();
distanceJointDef.initialize(DistanceJointBodyA,
DistanceJointBodyB, DistanceJointBodyA.getWorldCenter(),
DistanceJointBodyB.getWorldCenter());
distanceJointDef.length = 3.0f;
distanceJointDef.frequencyHz = 1f;
distanceJointDef.dampingRatio = 0.001f;
请注意,我们通过传递两个要连接的刚体DistanceJointBodyA和DistanceJointBodyB以及刚体的中心点DistanceJointBodyA.getWorldCenter()和DistanceJointBodyB.getWorldCenter()作为关节的锚点来初始化距离关节。接下来,我们通过设置distanceJointDef.length变量为3.0f来设置关节的长度,这告诉关节在物理世界中两个刚体应该相隔 3 米。最后,我们将distanceJointDef.frequencyHz变量设置为1f以强制关节弹簧具有小的频率,并将distanceJointDef.dampingRatio变量设置为0.001f以产生连接刚体的非常小的阻尼效果。为了更容易理解距离关节的外观,请参考前面的图表。
鼠标关节
鼠标关节试图使用设定的最大力将一个刚体拉到特定位置,通常是触摸的位置。它是一个很好的测试用关节,但对于大多数游戏的发布版本,我们应选择使用适当的代码将动力刚体移动到触摸注册的位置。要了解鼠标关节的作用,请参考前面的图表。以下代码定义了一个鼠标关节:
final MouseJointDef mouseJointDef = new MouseJointDef();
mouseJointDef.bodyA = MouseJointBodyA;
mouseJointDef.bodyB = MouseJointBodyB;
mouseJointDef.dampingRatio = 0.0f;
mouseJointDef.frequencyHz = 1f;
mouseJointDef.maxForce = (100.0f * MouseJointBodyB.getMass());
与其他关节不同,鼠标关节没有initialize()方法来帮助设置关节。我们首先创建mouseJointDef鼠标关节定义,并将mouseJointDef.bodyA变量设置为MouseJointBodyA,将mouseJointDef.bodyB变量设置为MouseJointBodyB,以告诉关节它将连接哪些刚体。在我们所有的模拟中,MouseJointBodyA应该是一个不动的刚体,在鼠标关节激活时不会移动。
接下来,我们将mouseJointDef.dampingRatio变量设置为0.0f,使关节完全没有阻尼。然后,我们将mouseJointDef.frequencyHz变量设置为1f,以在MouseJointBodyB达到鼠标关节的目标时强制产生轻微的频率响应,我们可以在下面的代码中看到这一点。最后,我们将mouseJointDef的maxForce变量设置为(100.0f * MouseJointBodyB.getMass())方法。强大的力100.0f乘以MouseJointBodyB的质量,以考虑MouseJointBodyB质量的变化。
在这段代码中,我们初始化了鼠标关节,但它只应在模拟开始后激活。要在模拟运行时从类的onSceneTouchEvent()方法内部激活鼠标关节,请参阅以下代码。请注意,mouseJoint变量是一个鼠标关节,在类级别创建:
if(pSceneTouchEvent.isActionDown()) {
mouseJointDef.target.set(MouseJointBodyB.getWorldCenter());
mouseJoint = (MouseJoint)mPhysicsWorld.createJoint(
mouseJointDef);
final Vector2 vec = Vector2Pool.obtain(
pSceneTouchEvent.getX() /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT,
pSceneTouchEvent.getY() /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT);
mouseJoint.setTarget(vec);
Vector2Pool.recycle(vec);
} else if(pSceneTouchEvent.isActionMove()) {
final Vector2 vec = Vector2Pool.obtain(
pSceneTouchEvent.getX() /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT,
pSceneTouchEvent.getY() /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT);
mouseJoint.setTarget(vec);
Vector2Pool.recycle(vec);
} else if(pSceneTouchEvent.isActionCancel() ||
pSceneTouchEvent.isActionOutside() ||
pSceneTouchEvent.isActionUp()) {
mPhysicsWorld.destroyJoint(mouseJoint);
}
当屏幕首次被触摸时,通过检查pSceneTouchEvent.isActionDown()确定,我们使用mouseJointDef.target.set()方法将初始鼠标关节目标设置为MouseJointBodyB的世界中心,通过MouseJointBodyB.getWorldCenter()方法获取。然后,我们通过在物理世界中使用MouseJoint关节转换的mPhysicsWorld.createJoint()方法以及mouseJointDef变量作为参数创建鼠标关节定义,来设置mouseJoint变量。关节创建后,我们从Vector2Pool创建Vector2,保存场景触摸位置pSceneTouchEvent.getX()和pSceneTouchEvent.getY(),通过除以PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT转换为物理世界的坐标。
然后,我们将mouseJoint关节的目标变量更改为先前创建的Vector2,并将Vector2回收至Vector2Pool。当触摸仍然有效时,通过检查pSceneTouchEvent.isActionMove()确定,我们使用在物理世界中创建鼠标关节后立即使用的相同过程来更新鼠标关节的目标。我们从Vector2Pool获取Vector2,将其设置为转换后的物理世界触摸位置,将鼠标关节的目标设置为该Vector2,然后回收Vector2。一旦触摸释放,通过检查pSceneTouchEvent.isActionCancel(),pSceneTouchEvent.isActionOutside(),或pSceneTouchEvent.isActionUp()确定,我们通过调用mPhysicsWorld.destroyJoint()方法并传入我们的mouseJoint变量作为参数,在世界中销毁鼠标关节。
斜轴关节(prismatic joint)
斜轴关节允许其连接的刚体沿单一轴滑动分离或靠拢,必要时可以由电机驱动。刚体具有锁定的旋转,因此在使用斜轴关节设计模拟时我们必须牢记这一点。考虑前面的图表来理解这个关节是如何工作的。以下代码创建了一个斜轴关节:
final PrismaticJointDef prismaticJointDef =
new PrismaticJointDef();
prismaticJointDef.initialize(PrismaticJointBodyA,
PrismaticJointBodyB, PrismaticJointBodyA.getWorldCenter(),
new Vector2(0f,1f));
prismaticJointDef.collideConnected = false;
prismaticJointDef.enableLimit = true;
prismaticJointDef.lowerTranslation = -80f /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT;
prismaticJointDef.upperTranslation = 80f /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT;
prismaticJointDef.enableMotor = true;
prismaticJointDef.maxMotorForce = 400f;
prismaticJointDef.motorSpeed = 500f;
mPhysicsWorld.createJoint(prismaticJointDef);
定义了prismaticJointDef变量之后,我们使用prismaticJointDef.initialize()方法对其进行初始化,并传递我们的连接刚体PrismaticJointBodyA和PrismaticJointBodyB,锚点被声明为PrismaticJointBodyA在世界坐标系中的中心点,以及关节的世界单位向量轴,以Vector2对象Vector2(0f,1f)表示。我们通过将prismaticJointDef.collideConnected变量设置为false来禁用两个刚体之间的碰撞,并通过将prismaticJointDef.enableLimit变量设置为true来启用关节滑动范围的限制。
为了设置关节的限制,我们将lowerTranslation和upperTranslation属性分别设置为-80f和80f像素,然后除以PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT,将像素限制转换为物理世界中的米。最后,我们通过将prismaticJointDef.enableMotor属性设置为true来启用马达,通过prismaticJointDef.maxMotorForce属性将其最大力设置为400f,并通过prismaticJointDef.motorSpeed属性将其速度设置为正值500f,以驱动PrismaticJointBodyB向关节的上限移动。
滑轮关节
滑轮关节的作用非常类似于真实的滑轮——当一侧下降时,另一侧上升。滑轮关节的长度在初始化时确定,并且在创建后不应更改。参考前图以了解滑轮关节的外观。以下代码创建了一个滑轮关节:
final PulleyJointDef pulleyJointDef = new PulleyJointDef();
pulleyJointDef.initialize(
PulleyJointBodyA,
PulleyJointBodyB,
PulleyJointBodyA.getWorldPoint(
new Vector2(0f, 2.5f)),
PulleyJointBodyB.getWorldPoint(
new Vector2(0f, 2.5f)),
PulleyJointBodyA.getWorldCenter(),
PulleyJointBodyB.getWorldCenter(),
1f);
mPhysicsWorld.createJoint(pulleyJointDef);
创建了pulleyJointDef变量之后,我们通过pulleyJointDef.initialize()方法对其进行初始化。pulleyJointDef.initialize()方法的前两个参数是两个连接的刚体,分别是PulleyJointBodyA和PulleyJointBodyB。接下来的两个参数是滑轮的地面锚点,在这种情况下,它们分别位于每个刚体上方2.5f米处。为了获取每个刚体上方世界坐标系中的相对点,我们使用每个刚体的getWorldPoint()方法,x 参数为0,y 参数为每个刚体上方2.5米。pulleyJointDef.initialize()方法的第五和第六个参数是每个刚体在世界坐标系中的锚点。在这个模拟中,我们使用中心点,因此传递每个连接刚体的getWorldCenter()方法。
方法的最后一个参数是滑轮的比例,在这种情况下是1f。比例为2将导致PulleyJointBodyA相对于其地面锚点的移动距离是PulleyJointBodyB的每段距离变化的两倍。此外,因为PulleyJointBodyA相对于其地面锚点移动所需的工作量是PulleyJointBodyB所需工作量的一半,所以PulleyJointBodyA比PulleyJointBodyB有更大的杠杆作用,导致在正常模拟中PulleyJointBodyA更容易受到重力的影响,从而起到提升PulleyJointBodyB的作用。创建滑轮接头的最后一步是调用mPhysicsWorld.createJoint()方法,并将我们的pulleyJointDef变量传递给它。
转动关节
转动关节是 Box2D 模拟中最受欢迎的关节类型。它本质上是两个连接体之间的一个支点,具有可选的电机和限制。查看前一个图表可以帮助更清楚地了解转动关节的工作原理。以下代码创建了一个转动关节:
final RevoluteJointDef revoluteJointDef = new RevoluteJointDef();
revoluteJointDef.initialize(
RevoluteJointBodyA,
RevoluteJointBodyB,
RevoluteJointBodyA.getWorldCenter());
revoluteJointDef.enableMotor = true;
revoluteJointDef.maxMotorTorque = 5000f;
revoluteJointDef.motorSpeed = -1f;
mPhysicsWorld.createJoint(revoluteJointDef);
我们首先将revoluteJointDef定义为新创建的RevoluteJointDef()方法。然后,我们使用revoluteJointDef.initialize()方法初始化它,参数为RevoluteJointBodyA和RevoluteJointBodyB以连接两个刚体,以及RevoluteJointBodyA的getWorldCenter()方法来定义关节旋转的位置。接着,我们通过将revoluteJointDef.enableMotor属性设置为true来启用我们的转动关节的电机。然后,我们将maxMotorTorque属性设置为5000f以使电机非常强大,并将motorSpeed属性设置为-1f以使电机以非常慢的速度顺时针旋转。最后,我们通过调用mPhysicsWorld.createJoint(revoluteJointDef)在物理世界中创建转动关节,使物理世界使用我们的revoluteJointDef变量创建一个转动关节。
焊接接头
焊接关节将两个刚体连接在一起并禁用它们之间的旋转。它对于可破坏物体非常有用,但是较大的可破坏物体可能会由于 Box2D 的迭代位置求解器产生的抖动而偶尔失败。在这种情况下,我们会从多个夹具创建物体,并在物体分离时以新刚体的形式重新创建每个部分。参考前一个焊接关节的图表,可以更好地理解它是如何工作的。以下代码创建了一个焊接关节:
final WeldJointDef weldJointDef = new WeldJointDef();
weldJointDef.initialize(WeldJointBodyA, WeldJointBodyB,
WeldJointBodyA.getWorldCenter());
mPhysicsWorld.createJoint(weldJointDef);
要创建我们的焊接关节,我们首先创建一个名为weldJointDef的WeldJointDef定义。然后,通过调用weldJointDef.initialize()方法并传入WeldJointBodyA和WeldJointBodyB的身体参数以及关节在世界坐标系中WeldJointBodyA身体的中心作为锚点来初始化它。焊接关节的锚点似乎可以放在任何地方,但由于 Box2D 在碰撞处理焊接关节的锚点时的方式,我们希望将其放置在连接身体之一的中心位置。否则,在与具有大质量的身体碰撞时,可能会导致关节剪切或位移。
另请参阅
-
本章中的Box2D 物理扩展介绍。
-
本章中的了解不同的身体类型。
创建布娃娃
物理模拟中最受欢迎的角色描绘之一是布娃娃。这类角色的视觉外观根据细节而有所不同,但底层系统始终相同——我们只是通过关节将几个物理身体附着到更大的物理身体上。在本食谱中,我们将创建一个布娃娃。
准备工作...
复习本章中Box2D 物理扩展介绍食谱中物理活动创建,了解不同的身体类型食谱中身体创建,以及处理关节食谱中旋转关节和鼠标关节的使用。
如何操作...
请参考补充的RagdollActivity类,这是我们在此食谱中使用的代码。
工作原理...
第一步是定义代表布娃娃多个身体的变量。我们的身体包括代表头部的headBody,代表躯干的torsoBody,代表左臂的leftUpperarmBody和leftForearmBody,代表右臂的rightUpperarmBody和rightForearmBody,代表左腿的leftThighBody和leftCalfBody,以及最后代表右腿的rightThighBody和rightCalfBody。以下图表显示了如何使用旋转关节将我们所有的身体连接在一起:
接下来,我们定义了当屏幕被触摸时用来抛掷布娃娃的鼠标关节所需的变量,即Vector2 localMouseJointTarget鼠标关节的目标,mouseJointDef鼠标关节定义,mouseJoint关节,以及鼠标关节的地面身体MouseJointGround。然后,我们创建了将应用于布娃娃各个部分的固定装置定义——头部的headFixtureDef,躯干的torsoFixtureDef,手臂的armsFixtureDef以及腿部的legsFixtureDef。有关创建固定装置定义的更多信息,请参考本章中的Box2D 物理扩展介绍食谱。
然后,在onPopulateScene()方法中,我们为布娃娃的每个身体部位创建单独的矩形和它们关联的物体,这些在活动中定义。每个矩形与其对应身体部位的位置和大小完全匹配。在我们创建要链接到矩形的物体时,我们通过PhysicsFactory.createBoxBody()方法的最后一个参数分配活动中定义的适当的固定定义。最后,对于每个矩形身体组,我们向物理世界注册一个PhysicsConnector对象。有关创建物体和PhysicsConnector对象的更多信息,请参考本章中的了解不同的身体类型食谱。
接下来,我们将创建许多旋转关节,用以连接布娃娃的身体部位。每个关节的锚点位置是在世界坐标系中我们希望该身体部位旋转的地方,通过每个关节定义的initialize()方法的最后一个参数传递。我们确保每个关节连接的物体不会相互碰撞,通过将关节的collideConnected属性设置为false。这不会阻止物体与其他布娃娃部分发生碰撞,但确实允许关节的物体在旋转时重叠。接下来,注意我们给关节定义应用了限制,以防止身体部位移动超出一定的运动范围,这很像人类移动四肢时的限制。如果不为关节设置限制,将会创建一个允许其四肢完全旋转的布娃娃,这种表示虽然不真实,但对于某些模拟是必要的。有关旋转关节的更多信息,请参考本章中的使用关节食谱。
创建表示布娃娃关节的旋转关节之后,我们创建了mouseJointDef鼠标关节定义,这将允许我们拖动布娃娃在场景中飞来飞去。我们将布娃娃的headBody作为鼠标关节的第二个物体,但根据模拟的需要,可以使用连接到布娃娃的任何物体。创建布娃娃的最后一个步骤是设置鼠标关节,以便在运行时通过onSceneTouchEvent()方法的触摸交互使用。有关使用鼠标关节的更多信息,请参考本章中的使用关节食谱。
另请参阅
-
本章中的Box2D 物理扩展介绍。
-
本章中的了解不同的身体类型。
-
本章中的使用关节。
创建一根绳子
尽管使用 Box2D 模拟真实的绳子是性能密集型的,但一个简单的绳子不仅快速,而且非常可定制。从构建的角度来看,绳子类似于布娃娃,可以为游戏增加额外的可玩层次。如果一个物理模拟看起来过于平淡,无法吸引玩家,那么添加绳子将肯定给玩家另一个喜欢游戏的原因。在本食谱中,我们将创建一个用于我们模拟的物理启用的绳子。
准备好...
回顾本章中的Box2D 物理扩展介绍食谱中基于物理的活动创建,了解不同的身体类型食谱中的身体创建,以及使用关节食谱中的旋转关节和鼠标关节的使用。
如何操作...
请参考补充的Rope和RopeActivity类,了解我们在此食谱中使用的代码。
它是如何工作的...
在 Box2D 中创建的绳子可以被视为由关节连接在一起的相似身体的链条。我们可以使用矩形或圆形身体来定义绳子的每个部分,但圆形身体在与其他身体碰撞时抓住并拉伸的可能性较小。查看以下图表,了解我们如何为物理模拟设计绳子:
首先,参考Rope类,这将使我们更容易创建多条绳子,并一次性为我们的模拟调整所有绳子。Rope类中的初始代码是一组反映每条绳子特定属性的变量。numRopeSegments变量保存我们的绳子将拥有的段数。ropeSegmentsLength和ropeSegmentsWidth变量保存每个绳子段的长度和宽度。接下来,ropeSegmentsOverlap变量表示每个绳子段与上一个绳子段重叠多少,这可以防止在轻微拉伸时出现间隙。RopeSegments数组和RopeSegmentsBodies数组为我们的绳子的每个部分定义矩形和身体。最后,RopeSegmentFixtureDef固定定义将保存我们将应用于绳子每个部分的固定装置数据。
接下来,我们创建一个名为Rope的构造函数,以处理绳子的放置、细节、长度、宽度、重量以及绳子的总体创建。然后,我们为上一步创建的变量赋值。注意,RopeSegmentFixtureDef固定定义从最大密度开始。由于绳子的每个部分都是通过构造函数后面的for循环创建的,因此固定装置的密度(从而质量)会递减到最小密度。这通过给最高身体部分最大的强度来防止拉伸,以保持较低身体部分。
在Rope构造函数的for循环开始处,我们为每个绳索段定义了旋转关节。关于旋转关节的更多信息,请参见本章中的使用关节部分。然后,我们创建了表示该段的矩形RopeSegments[i],并检查确保当i小于1时,第一个段根据构造函数中传递的pAttachTo铰链放置,而其余的段相对于它们的前一段RopeSegments[i-1]放置。创建矩形时包括了一个重叠值ropeSegmentsOverlap,以消除由 Box2D 的迭代过程造成的绳索中的间隔。
在我们创建了段的矩形并通过调用RopeSegments[i].setColor(0.97f, 0.75f, 0.54f)将其颜色设置为棕色之后,我们对RopeSegmentFixtureDef固定定义应用了密度计算,并使用PhysicsFactory.createCircleBody()方法基于段的矩形创建了一个圆形体。关于创建体的更多信息,请参考本章中的了解不同的体类型部分。然后,我们通过setAngularDamping(4f)方法为每个绳索段体设置适中的角阻尼,并通过setLinearDamping(0.5f)方法设置轻微的线性阻尼,以消除绳索行为中的不可预测性。
之后,我们通过将RopeSegmentsBodies[i].setBullet属性设置为true,使绳索段能够作为子弹行动,这减少了我们的段穿过碰撞体的机会。最后,我们为当前绳索段相对于前一段或如果当前段是绳索中的第一段则相对于铰链创建旋转关节。关于旋转关节的更多信息,请参见本章中的使用关节部分。
对于我们的活动类,我们首先创建了用于鼠标关节的必要变量,该关节将绳索的铰链体移动到触摸位置,并定义了我们的RopeHingeBody体,该体将作为绳索的锚点。然后在onPopulateScene()方法中,我们创建了RopeHingeBody体,随后是我们的rope对象,将绳索铰链体作为第一个参数传递给Rope构造函数。关于创建体的更多信息,请参考本章中的了解不同的体类型部分。Rope构造函数的下一个参数告诉我们的绳索要有10个段长,每个段长25f像素,宽10f像素,重叠2f像素,具有最小密度5f和最大密度50f,以及我们附加绳索段矩形的mScene场景。Rope构造函数的最后两个参数告诉绳索在我们的mPhysicsWorld物理世界中创建段体,并将每个段的矩形设置为活动类的VertexBufferObjectManager管理。
接下来,我们定义并设置用于我们的鼠标关节的变量。请注意,我们将RopeHingeBody设置为鼠标关节的第二个物体。最后,我们设置onSceneTouchEvent()方法来处理我们的鼠标关节。有关鼠标关节的更多信息,请参考本章中的处理关节部分。
另请参阅
-
本章中的Box2D 物理扩展介绍。
-
本章中的了解不同的物体类型。
-
本章中的处理关节。
处理碰撞
在基于物理模拟的游戏中,使物体之间的碰撞产生某种效果,无论是播放声音还是处理物体,通常都是必要的部分。处理碰撞一开始看起来可能是一项艰巨的任务,但当我们了解了ContactListener接口的每个部分如何工作之后,它就会变得很自然。在本教程中,我们将演示如何处理固定装置之间的碰撞。
准备好了...
按照本章开始部分Box2D 物理扩展介绍中的步骤创建一个新活动,这将有助于创建我们的模拟,其中我们将控制碰撞行为。
如何操作...
按照以下步骤演示我们对碰撞的控制:
-
在活动类的开始处放置以下定义:
public Rectangle dynamicRect; public Rectangle staticRect; public Body dynamicBody; public Body staticBody; public boolean setFullAlphaForDynamicBody = false; public boolean setHalfAlphaForDynamicBody = false; public boolean setFullAlphaForStaticBody = false; public boolean setHalfAlphaForStaticBody = false; final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(2f, 0f, 0.9f); -
要在
ContactListener接口中确定特定物体是否被接触,请在活动中插入以下方法:public boolean isBodyContacted(Body pBody, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody) || pContact.getFixtureB().getBody().equals(pBody)) return true; return false; } -
下面的方法与之前的方法相似,但除了第一个物体外,还测试了另一个物体。在之前的 方法后,将以下代码添加到类中:
public boolean areBodiesContacted(Body pBody1, Body pBody2, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody1) || pContact.getFixtureB().getBody().equals(pBody1)) if(pContact.getFixtureA().getBody().equals(pBody2) || pContact.getFixtureB().getBody().equals(pBody2)) return true; return false; } -
接下来,我们将创建一个动态物体和一个静态物体来测试碰撞。在
onPopulateScene()方法中放置以下内容:dynamicRect = new Rectangle(300f, 240f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()); dynamicRect.setColor(0f, 0.7f, 0f); dynamicRect.setAlpha(0.5f); mScene.attachChild(dynamicRect); dynamicBody = PhysicsFactory.createBoxBody(mPhysicsWorld, dynamicRect, BodyType.DynamicBody, boxFixtureDef); dynamicBody.setLinearDamping(0.4f); dynamicBody.setAngularDamping(0.6f); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( dynamicRect, dynamicBody)); staticRect = new Rectangle(500f, 240f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()); staticRect.setColor(0f, 0f, 0.7f); staticRect.setAlpha(0.5f); mScene.attachChild(staticRect); staticBody = PhysicsFactory.createBoxBody(mPhysicsWorld, staticRect, BodyType.StaticBody, boxFixtureDef); -
现在,我们需要设置物理世界的
ContactListener属性。在onPopulateScene()方法中添加以下内容:mPhysicsWorld.setContactListener(new ContactListener(){ @Override public void beginContact(Contact contact) { if(contact.isTouching()) if(areBodiesContacted(staticBody,dynamicBody,contact)) setFullAlphaForStaticBody = true; if(isBodyContacted(dynamicBody,contact)) setFullAlphaForDynamicBody = true; } @Override public void endContact(Contact contact) { if(areBodiesContacted(staticBody,dynamicBody,contact)) setHalfAlphaForStaticBody = true; if(isBodyContacted(dynamicBody,contact)) setHalfAlphaForDynamicBody = true; } @Override public void preSolve(Contact contact, Manifold oldManifold) {} @Override public void postSolve(Contact contact, ContactImpulse impulse) {} }); -
由于物理世界可能会每次接触多次调用
ContactListener接口,我们希望将所有逻辑从ContactListener接口移动到一个每次引擎更新只调用一次的更新处理程序中。在onPopulateScene()方法中放置以下内容,以完成我们的活动:mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { if(setFullAlphaForDynamicBody) { dynamicRect.setAlpha(1f); setFullAlphaForDynamicBody = false; } else if(setHalfAlphaForDynamicBody) { dynamicRect.setAlpha(0.5f); setHalfAlphaForDynamicBody = false; } if(setFullAlphaForStaticBody) { staticRect.setAlpha(1f); setFullAlphaForStaticBody = false; } else if(setHalfAlphaForStaticBody) { staticRect.setAlpha(0.5f); setHalfAlphaForStaticBody = false; } } @Override public void reset() {} });
它是如何工作的...
首先,我们定义了用于可视化碰撞的矩形和物体。我们还定义了几个布尔变量,这些变量将根据ContactListener接口的结果进行更改。最后的变量是用于创建具有碰撞功能的物体的固定装置定义。
在第二步和第三步中,我们创建了两个便捷方法isBodyContacted()和areBodiesContacted(),这将使在ContactListener接口中确定物体的存在变得更加容易。注意,每个方法中的if语句检查了每个物体与两个固定装置的碰撞情况。由于接触监听器传递Contact对象的方式,我们无法确定哪个固定装置将与特定物体相关联,因此我们必须检查两者。
第四步创建了本模拟中使用的矩形和物体——一个静态的和一个动态的。我们使用它们的setAlpha()方法将矩形的透明度设置为0.5f,以演示当前没有发生接触。在碰撞时,矩形的透明度恢复为不透明,并在碰撞结束后重新设置为透明。
在第五步中,我们通过重写继承的方法来设置物理世界的接触监听器。第一个方法,beginContact(),在物理世界中发生碰撞时被调用。在这个方法中,我们首先通过检查contact参数的isTouching()属性来测试碰撞是否真正涉及到两个物体的接触。Box2D 认为,当两个物体的AABB(边界框)重叠时,碰撞就开始了,而不是实际物体接触时。参考下一张图来了解碰撞和接触的区别。之后,我们会检查我们的物体是否都参与了碰撞,或者只有其中一个。如果是,我们将我们的完全不透明布尔变量设置为true。下一个方法,endContact(),在物体不再碰撞时被调用。如果我们的物体参与了正在结束的碰撞,我们会将半透明布尔变量设置为true。接触监听器中的其余方法在碰撞纠正计算发生之前或之后被调用。因为我们只想测试哪些物体发生了碰撞,所以不需要使用这两个方法。
在第六步中,我们创建了一个更新处理器,以从ContactListener接口中移除有效代码。它只是检查ContactListener接口内设置的布尔值,以确定在每次引擎更新后需要采取哪些操作。在采取了正确的操作后,我们重置布尔变量。我们需要从接触监听器中移除有效代码的原因是,接触监听器可能会被多次调用,而且通常在每次碰撞时都会被调用多次。如果在接触监听器内部改变游戏的得分,得分通常会比我们预期的变化大得多。我们可以有一个变量来检查是否已经处理了接触,但这样的代码流程会变得混乱,最终会适得其反。
另请参阅
-
本章节的Box2D 物理扩展介绍。
-
本章节讲述理解不同的身体类型。
-
本章节介绍
preSolve和postSolve。
使用preSolve和postSolve
在接触监听器的presolve方法中使用碰撞的可用数据,该方法在 Box2D 迭代器引起反应之前被调用,这让我们能独特地控制碰撞发生的方式。preSolve()方法通常用于创建角色可以从下面跳过但仍然可以从上面走过去的“单向”平台。在反应已经启动后被调用的postSolve()方法,为我们提供了碰撞的纠正数据,也称为冲击力。这些数据可以用来销毁或分解物体。在本教程中,我们将演示如何正确使用ContactListener接口的preSolve()和postSolve()方法。
准备就绪...
按照本章开始部分提供的Box2D 物理扩展介绍部分中的步骤创建一个新活动。这个新活动将方便我们使用接触监听器内调用的preSolve()和postSolve()方法。
如何操作...
按以下步骤完成演示这些方法使用的活动:
-
在活动的开始处放置以下定义:
Body dynamicBody; Body staticBody; FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.5f, 0.9f); Vector2 localMouseJointTarget = new Vector2(); MouseJointDef mouseJointDef; MouseJoint mouseJoint; Body groundBody; -
为了确定哪个或哪些刚体被接触,将这些方法插入到类中:
public boolean isBodyContacted(Body pBody, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody) || pContact.getFixtureB().getBody().equals(pBody)) return true; return false; } public boolean areBodiesContacted(Body pBody1, Body pBody2, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody1) || pContact.getFixtureB().getBody().equals(pBody1)) if(pContact.getFixtureA().getBody().equals(pBody2) || pContact.getFixtureB().getBody().equals(pBody2)) return true; return false; } -
我们将测试一个小型动态刚体和一个大型静态刚体之间的碰撞。在
onPopulateScene()方法中放置以下代码以创建这样的刚体:Rectangle dynamicRect = new Rectangle(400f, 60f, 40f, 40f, this.getEngine().getVertexBufferObjectManager()); dynamicRect.setColor(0f, 0.6f, 0f); mScene.attachChild(dynamicRect); dynamicBody = PhysicsFactory.createBoxBody(mPhysicsWorld, dynamicRect, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( dynamicRect, dynamicBody)); Rectangle staticRect = new Rectangle(400f, 240f, 200f, 10f, this.getEngine().getVertexBufferObjectManager()); staticRect.setColor(0f, 0f, 0f); mScene.attachChild(staticRect); staticBody = PhysicsFactory.createBoxBody(mPhysicsWorld, staticRect, BodyType.StaticBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( staticRect, staticBody)); -
接下来,我们需要为物理世界设置接触监听器。在
onPopulateScene()方法中插入以下内容:mPhysicsWorld.setContactListener(new ContactListener(){ float maxImpulse; @Override public void beginContact(Contact contact) {} @Override public void endContact(Contact contact) {} @Override public void preSolve(Contact contact, Manifold oldManifold) { if(areBodiesContacted(dynamicBody, staticBody, contact)) if(dynamicBody.getWorldCenter().y < staticBody.getWorldCenter().y) contact.setEnabled(false); } @Override public void postSolve(Contact contact, ContactImpulse impulse) { if(areBodiesContacted(dynamicBody, staticBody, contact)) { maxImpulse = impulse.getNormalImpulses()[0]; for(int i = 1; i < impulse.getNormalImpulses().length; i++) maxImpulse = Math.max( impulse.getNormalImpulses()[i], maxImpulse); if(maxImpulse>400f) dynamicBody.setAngularVelocity(30f); } } }); -
我们希望可以通过触摸想要移动到的位置来移动较小的刚体。添加以下代码以设置一个鼠标关节,使我们能够这样做:
groundBody = mPhysicsWorld.createBody(new BodyDef()); mouseJointDef = new MouseJointDef(); mouseJointDef.bodyA = groundBody; mouseJointDef.bodyB = dynamicBody; mouseJointDef.dampingRatio = 0.5f; mouseJointDef.frequencyHz = 1f; mouseJointDef.maxForce = (40.0f * dynamicBody.getMass()); mouseJointDef.collideConnected = false; -
最后,在
onSceneTouchEvent()方法中插入以下内容,以控制上一步创建的鼠标关节:if(pSceneTouchEvent.isActionDown()) { mouseJointDef.target.set(dynamicBody.getWorldCenter()); mouseJoint = (MouseJoint)mPhysicsWorld.createJoint(mouseJointDef); final Vector2 vec = Vector2Pool.obtain(pSceneTouchEvent.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, pSceneTouchEvent.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT); mouseJoint.setTarget(vec); Vector2Pool.recycle(vec); } else if(pSceneTouchEvent.isActionMove()) { final Vector2 vec = Vector2Pool.obtain(pSceneTouchEvent.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, pSceneTouchEvent.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT); mouseJoint.setTarget(vec); Vector2Pool.recycle(vec); return true; } else if(pSceneTouchEvent.isActionCancel() || pSceneTouchEvent.isActionOutside() || pSceneTouchEvent.isActionUp()) { mPhysicsWorld.destroyJoint(mouseJoint); }
它是如何工作的...
我们首先定义一个静态刚体、一个动态刚体以及一个将用于创建这两个刚体的夹具定义。然后,我们创建两个使使用接触监听器管理碰撞变得更容易的方法。接下来,我们使用相关联的矩形创建刚体。
在第四步中,我们设置了物理世界的接触监听器。注意,我们在接触监听器开始处创建了一个变量maxImpulse,以在接触监听器的末尾的postSolve()方法中使用。对于这个模拟,我们不需要beginContact()和endContact()方法,因此我们让它们保持为空。在preSolve()方法中,我们首先测试以确定接触是否发生在我们的两个身体之间,dynamicBody和staticBody。如果是,我们测试dynamicBody是否在我们的staticBody下方,通过检查dynamicBody.getWorldCenter().y属性是否小于staticBody.getWorldCenter().y属性,如果是,我们取消碰撞。这使得动态身体可以从下方穿过静态身体,同时仍然从上方与静态身体发生碰撞。
在postSolve()方法中,我们测试以确保只处理我们先前定义的动态和静态身体。如果是这样,我们将maxImpulse变量设置为impulse.getNormalImpulses()数组中的第一个冲量。这个列表保存了两个碰撞夹具之间所有接触点的纠正冲量。接下来,我们遍历冲量列表,并将maxImpulse变量设置为当前maxImpulse值或列表中的当前冲量值,以较大者为准。这为我们提供了碰撞中的最大纠正冲量,然后我们使用它来旋转动态身体,如果冲力足够大,在这个模拟中是400f的冲量。
第五步初始化用于在屏幕上拖动动态身体的鼠标关节,第六步使用onSceneTouchEvent()方法控制鼠标关节。有关鼠标关节的更多信息,请参考处理关节。
另请参阅
-
本章节中介绍了Box2D 物理扩展入门。
-
本章节中了解不同的身体类型。
-
本章节中处理关节。
-
本章节中处理碰撞。
创建可破坏的物体
使用物理世界接触监听器中的postSolve()方法提供的冲量数据,我们可以得到每次碰撞的冲击力。将这个数据扩展到使多体物体破碎,只需确定哪个身体发生碰撞,以及冲击力是否足够大以至于能将身体从多体物体中分离。在本教程中,我们将演示如何创建由多个身体组成的可破坏物体。
准备就绪...
按照本章开始部分Box2D 物理扩展入门一节中的步骤创建一个活动。这个活动将促进我们在此节中使用的可破坏身体组的创建。
如何操作...
按照以下步骤创建一个在受到大力碰撞时可以破碎的物体:
-
向活动类中添加以下定义:
public Body box1Body; public Body box2Body; public Body box3Body; public boolean breakOffBox1 = false; public boolean breakOffBox2 = false; public boolean breakOffBox3 = false; public Joint box1And2Joint; public Joint box2And3Joint; public Joint box3And1Joint; public boolean box1And2JointActive = true; public boolean box2And3JointActive = true; public boolean box3And1JointActive = true; public final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.0f, 0.9f); -
为了更容易确定哪个身体被接触,请在类中插入此方法:
public boolean isBodyContacted(Body pBody, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody) || pContact.getFixtureB().getBody().equals(pBody)) return true; return false; } -
我们将要创建一个由三个盒子组成的物理对象,这些盒子通过焊接关节保持在一起。在
onPopulateScene()方法中定义以下盒子:Rectangle box1Rect = new Rectangle(400f, 260f, 40f, 40f, this.getEngine().getVertexBufferObjectManager()); box1Rect.setColor(0.75f, 0f, 0f); mScene.attachChild(box1Rect); box1Body = PhysicsFactory.createBoxBody(mPhysicsWorld, box1Rect, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( box1Rect, box1Body)); Rectangle box2Rect = new Rectangle(380f, 220f, 40f, 40f, this.getEngine().getVertexBufferObjectManager()); box2Rect.setColor(0f, 0.75f, 0f); mScene.attachChild(box2Rect); box2Body = PhysicsFactory.createBoxBody(mPhysicsWorld, box2Rect, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( box2Rect, box2Body)); Rectangle box3Rect = new Rectangle(420f, 220f, 40f, 40f, this.getEngine().getVertexBufferObjectManager()); box3Rect.setColor(0f, 0f, 0.75f); mScene.attachChild(box3Rect); box3Body = PhysicsFactory.createBoxBody(mPhysicsWorld, box3Rect, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( box3Rect, box3Body)); -
接下来,在上一步中定义的盒子定义之后,在
onPopulateScene()方法中放置以下焊接关节定义:final WeldJointDef box1and2JointDef = new WeldJointDef(); box1and2JointDef.initialize(box1Body, box2Body, box1Body.getWorldCenter()); box1And2Joint = mPhysicsWorld.createJoint(box1and2JointDef); final WeldJointDef box2and3JointDef = new WeldJointDef(); box2and3JointDef.initialize(box2Body, box3Body, box2Body.getWorldCenter()); box2And3Joint = mPhysicsWorld.createJoint(box2and3JointDef); final WeldJointDef box3and1JointDef = new WeldJointDef(); box3and1JointDef.initialize(box3Body, box1Body, box3Body.getWorldCenter()); box3And1Joint = mPhysicsWorld.createJoint(box3and1JointDef); -
现在我们需要设置物理世界的接触监听器。将以下代码添加到
onPopulateScene()方法中:mPhysicsWorld.setContactListener(new ContactListener(){ float maxImpulse; @Override public void beginContact(Contact contact) {} @Override public void endContact(Contact contact) {} @Override public void preSolve(Contact contact, Manifold oldManifold) {} @Override public void postSolve(Contact contact, ContactImpulse impulse) { maxImpulse = impulse.getNormalImpulses()[0]; for(int i = 1; i < impulse.getNormalImpulses().length; i++) { maxImpulse = Math.max(impulse.getNormalImpulses()[i], maxImpulse); } if(maxImpulse>800f) { if(isBodyContacted(box1Body,contact)) breakOffBox1 = true; else if(isBodyContacted(box2Body,contact)) breakOffBox2 = true; else if(isBodyContacted(box3Body,contact)) breakOffBox3 = true; } } }); -
最后,为了从接触监听器中移除逻辑,请在
onPopulateScene()方法中放置以下更新处理程序:mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { if(breakOffBox1) { if(box1And2JointActive) mPhysicsWorld.destroyJoint(box1And2Joint); if(box3And1JointActive) mPhysicsWorld.destroyJoint(box3And1Joint); box1And2JointActive = false; box3And1JointActive = false; breakOffBox1 = false; } if(breakOffBox2) { if(box1And2JointActive) mPhysicsWorld.destroyJoint(box1And2Joint); if(box2And3JointActive) mPhysicsWorld.destroyJoint(box2And3Joint); box1And2JointActive = false; box2And3JointActive = false; breakOffBox1 = false; } if(breakOffBox3) { if(box2And3JointActive) mPhysicsWorld.destroyJoint(box2And3Joint); if(box3And1JointActive) mPhysicsWorld.destroyJoint(box3And1Joint); box2And3JointActive = false; box3And1JointActive = false; breakOffBox1 = false; } } @Override public void reset() {} });
工作原理...
第一步初步定义了三个我们将通过焊接关节连接在一起的身体。接下来,我们定义了三个布尔变量,表示如果有,哪个身体应该从身体组中释放。然后,我们定义了三个保持我们的身体在一起的焊接关节以及表示关节是否存在的相应布尔值。最后,我们定义了一个固定装置定义,我们将根据它创建三个盒状身体。
第二步创建了一个方法,该方法允许我们确定一个特定的身体是否参与了碰撞,这与处理碰撞的教程中看到的内容类似。第三步创建我们的身体,第四步创建连接它们的焊接关节。有关创建身体的更多信息,请参考理解不同的物体类型的教程,或者有关使用关节的更多信息,请参考使用关节的教程。
在第五步中,我们设置了物理世界的接触监听器,只创建了maxImpulse变量,并只填充了postSolve()方法。在postSolve()方法中,我们确定碰撞冲量的力是否足以破坏与一个物体相连的关节。如果是,我们会确定应该从组中分离哪个物体,并为此物体设置相关的布尔值。设置ContactListener接口后,我们注册了一个更新处理程序,根据哪些物体被标记为需要分离来销毁相应的关节。由于三个物体中的每一个都与另外两个物体相连,因此对于组中的每个物体都有两个关节需要销毁。当我们销毁关节时,我们会将每个被销毁的关节标记为非活动状态,这样我们就不会尝试销毁已经销毁的关节。
另请参阅
-
本章节介绍Box2D 物理扩展的入门。
-
本章节讲解如何理解不同的物体类型。
-
本章节讲解如何使用关节。
-
本章节介绍如何使用
preSolve和postSolve。
光线投射
通过物理世界进行光线投射是一种从一个点向另一个点发射假想线,并返回距离、遇到的每个固定装置以及每个被撞击表面的法线向量的计算。光线投射可用于任何从激光和视野锥到确定一个假想子弹击中什么的一切。在本教程中,我们将演示如何在我们的物理世界中执行光线投射。
准备工作...
按照本章开始部分Box2D 物理扩展介绍中的步骤创建一个新活动,以便我们在物理世界中使用光线投射。
如何操作...
按照以下步骤创建一个光线投射演示:
-
在活动的开始处放置以下定义:
Body BoxBody; Line RayCastLine; Line RayCastLineHitNormal; Line RayCastLineHitBounce; float[] RayCastStart = {cameraWidth/2f,15f}; float RayCastAngle = 0f; float RayCastNormalAngle = 0f; float RayCastBounceAngle = 0f; float RaycastBounceLineLength = 200f; final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(1f, 0.5f, 0.9f); -
当我们告诉物理世界执行光线投射(raycast)时,它会使用一个提供的
callback接口,让我们可以利用光线投射收集到的信息。在活动中放置以下RayCastCallback定义:RayCastCallback rayCastCallBack = new RayCastCallback() { @Override public float reportRayFixture(Fixture fixture, Vector2 point, Vector2 normal, float fraction) { float[] linePos = { point.x * PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, point.y * PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, (point.x + (normal.x)) * PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, (point.y + (normal.y)) * PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT}; RayCastLineHitNormal.setPosition( linePos[0],linePos[1], linePos[2],linePos[3]); RayCastLineHitNormal.setVisible(true); RayCastNormalAngle = MathUtils.radToDeg( (float) Math.atan2( linePos[3]-linePos[1], linePos[2]-linePos[0])); RayCastBounceAngle = (2*RayCastNormalAngle)-RayCastAngle; RayCastLineHitBounce.setPosition( linePos[0], linePos[1], (linePos[0] + FloatMath.cos((RayCastBounceAngle + 180f) * MathConstants.DEG_TO_RAD) * RaycastBounceLineLength), (linePos[1] + FloatMath.sin((RayCastBounceAngle + 180f) * MathConstants.DEG_TO_RAD)*RaycastBounceLineLength)); RayCastLineHitBounce.setVisible(true); return 0f; } }; -
为了让光线投射有撞击的对象,我们将在物理世界中创建一个盒子。在
onPopulateScene()方法中插入以下代码片段:Rectangle Box1 = new Rectangle(400f, 350f, 200f, 200f, this.getEngine().getVertexBufferObjectManager()); Box1.setColor(0.3f, 0.3f, 0.3f); BoxBody = PhysicsFactory.createBoxBody(mPhysicsWorld, Box1, BodyType.StaticBody, boxFixtureDef); BoxBody.setTransform(BoxBody.getWorldCenter(), MathUtils.random(0.349f, 1.222f)); mScene.attachChild(Box1); mPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(Box1, BoxBody)); -
接下来,我们将定义一个
Line对象,它表示从光线投射中收集到的一些信息。在onPopulateScene()方法中添加以下内容:RayCastLine = new Line(0f, 0f, 0f, 0f, mEngine.getVertexBufferObjectManager()); RayCastLine.setColor(0f, 1f, 0f); RayCastLine.setLineWidth(8f); mScene.attachChild(RayCastLine); RayCastLineHitNormal = new Line(0f, 0f, 0f, 0f, mEngine.getVertexBufferObjectManager()); RayCastLineHitNormal.setColor(1f, 0f, 0f); RayCastLineHitNormal.setLineWidth(8f); mScene.attachChild(RayCastLineHitNormal); RayCastLineHitBounce = new Line(0f, 0f, 0f, 0f, mEngine.getVertexBufferObjectManager()); RayCastLineHitBounce.setColor(0f, 0f, 1f); RayCastLineHitBounce.setLineWidth(8f); mScene.attachChild(RayCastLineHitBounce); -
最后,我们希望光线投射在触摸场景的任何地方发生。在
onSceneTouchEvent()方法中放置以下内容:if(pSceneTouchEvent.isActionMove()||pSceneTouchEvent.isActionDown()){ RayCastAngle = MathUtils.radToDeg((float) Math.atan2(pSceneTouchEvent.getY() - RayCastStart[1], pSceneTouchEvent.getX() - RayCastStart[0])); RayCastLine.setPosition( RayCastStart[0], RayCastStart[1], pSceneTouchEvent.getX(), pSceneTouchEvent.getY()); RayCastLine.setVisible(true); RayCastLineHitNormal.setVisible(false); RayCastLineHitBounce.setVisible(false); mPhysicsWorld.rayCast(rayCastCallBack, new Vector2( RayCastStart[0] / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, RayCastStart[1] / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT), new Vector2( pSceneTouchEvent.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, pSceneTouchEvent.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT)); } if(pSceneTouchEvent.isActionUp() || pSceneTouchEvent.isActionOutside() || pSceneTouchEvent.isActionCancel()) { RayCastLine.setVisible(false); }
工作原理...
我们首先定义了一个刚体BoxBody,我们将使用光线投射与之交互。然后,我们定义了几条视觉上表示光线投射的线条。最后,我们定义了一系列变量,帮助我们确定光线投射的位置和结果。
在第二步中,我们定义了一个RayCastCallback接口,当我们请求物理世界计算光线投射时,将传递这个接口。在回调中,我们使用重写的reportRayFixture()方法。每次请求的光线投射遇到新的固定装置时,都会调用这个方法。在方法中,我们使用光线投射返回的点和平面变量来修改表示报告固定装置撞击表面的法线位置。设置法线可见后,我们确定法线角度,然后是反弹角度。接着我们定位反弹线以表示光线投射的反弹,并设置反弹线可见。最后,我们返回0,告诉光线投射在撞击第一个固定装置后终止。为了更好地理解光线投射回调中返回的各种参数,请参考以下图表:
第三步创建了第一步中定义的刚体,并通过调用BoxBody.setTransform()方法设置了半随机的旋转,最后一个参数为MathUtils.random(0.349f, 1.222f),这使得刚体的旋转在0.349弧度和1.222弧度之间。第四步创建了表示光线投射各个部分的视觉线条。关于创建刚体的更多信息,请参阅本章中的了解不同的刚体类型菜谱;关于线条的更多信息,请参阅第二章,使用实体。
在第五步中,我们将onSceneTouchEvent()方法分配给处理射线投射(raycasting)。当触摸发生时,我们首先设置RayCastAngle变量以供射线投射的回调函数使用。然后,我们定位主射线线,并将其设置为可见,同时将与其他射线相关的线设置为不可见。最后,我们通过传递我们的回调函数、射线投射的起始位置和结束位置,从物理世界中请求射线投射。当触摸事件结束时,我们将主射线线设置为不可见。
另请参阅。
- 本章介绍Box2D 物理扩展。
第七章:使用更新处理器
更新处理器让我们能够每次引擎更新时运行特定的代码片段。一些游戏引擎内置了一个作为主循环的更新处理器,但使用 AndEngine,我们可以轻松地创建尽可能多的这些循环。本章将介绍以下内容:
-
开始使用更新处理器
-
将更新处理器附加到实体
-
使用条件更新处理器
-
处理从游戏中移除实体的操作
-
添加游戏计时器
-
根据经过的时间设置实体属性
开始使用更新处理器
更新处理器本质上是我们在每次引擎更新场景时注册到实体或引擎的代码片段。在大多数情况下,这种更新每次绘制帧时都会发生,无论实体或场景是否已更改。更新处理器可以是运行游戏的一种强大方式,但过度使用它们或在它们中执行繁重的计算将导致性能下降。本节将介绍向活动添加简单更新处理器的基础知识。
准备好了...
创建一个名为UpdateHandlersActivity的新类,该类继承自BaseGameActivity。我们将使用这个类来创建一个基本的更新处理器。
如何操作...
按照以下步骤创建一个显示已发生多少次更新的更新处理器:
-
在我们的
UpdateHandlersActivity类中放置以下定义:public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene;public Font fontDefault32Bold; public Text countingText; public int countingInt = 0; -
接下来,向类中添加以下重写的方法。
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { fontDefault32Bold = FontFactory.create( mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32f, true, Color.BLACK_ARGB_PACKED_INT); fontDefault32Bold.load(); pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } -
最后,将这个最后的方法插入我们的类中:
@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { countingText = new Text(400f, 240f, fontDefault32Bold, "0", 10, this.getVertexBufferObjectManager()); mScene.attachChild(countingText); mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { countingInt++; countingText.setText( String.valueOf(countingInt)); } @Override public void reset() {} }); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
工作原理...
第一步和第二步涵盖了创建一个简单的BaseGameActivity类的过程。关于创建BaseGameActivity类的更多信息,请参见第一章中的Know the life cycle部分,AndEngine 游戏结构。注意,我们在onCreateResources()方法中创建并加载了一个Font对象。关于字体和使用它们的Text实体的更多信息,请参见第二章中的Applying text to a layer部分,使用实体。
在第三步中,我们通过将onCreateResources()方法中创建的fontDefault32Bold字体传递给Text构造函数,以及屏幕居中和最大字符串长度参数10个字符,创建了一个Text实体countingText。将countingText实体附加到场景后,我们注册了更新处理器。在我们的更新处理器的onUpdate()方法中,我们增加countingInt整数,并将countingText实体的文本设置为该整数。这让我们可以直接在游戏中以文本形式显示已经发生了多少次更新,从而绘制了多少帧。
另请参阅
-
第一章中的Know the life cycle,AndEngine 游戏结构。
-
在第二章,使用实体中Applying text to a layer。
将更新处理器附加到实体上
除了能够将更新处理器与Scene对象注册之外,我们还可以与特定实体注册更新处理器。通过将更新处理器与实体注册,只有当实体被附加到引擎的场景中时,处理器才会被调用。这个示例通过创建一个更新处理器,它与一个最初未附加的实体注册,来递增屏幕上的文本,演示了这一过程。
准备中...
创建一个名为AttachUpdateHandlerToEntityActivity的新类,该类继承自BaseGameActivity并实现IOnSceneTouchListener接口。我们将使用这个类将更新处理器附加到一个Entity对象上,等到场景被触摸时再将其附加到场景中。
如何操作...
按照以下步骤创建一个活动,演示更新处理器如何依赖于其父实体来运行:
-
在我们新的活动类中插入以下定义:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Font fontDefault32Bold; public Text countingText; public int countingInt = 0; public Entity blankEntity; -
然后,在类中放置以下重写的方法:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { fontDefault32Bold = FontFactory.create( mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32f, true, Color.BLACK_ARGB_PACKED_INT); fontDefault32Bold.load(); pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } -
接下来,将以下重写的
onPopulateScene()方法添加到我们的活动类中:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { countingText = new Text(400f, 240f, fontDefault32Bold, "0", 10, this.getVertexBufferObjectManager()); mScene.attachChild(countingText); blankEntity = new Entity(); blankEntity.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { countingInt++; countingText.setText( String.valueOf(countingInt)); } @Override public void reset() {} }); mScene.setOnSceneTouchListener(this); pOnPopulateSceneCallback.onPopulateSceneFinished(); } -
最后,在我们的
AttachUpdateHandlerToEntityActivity类中插入以下重写的方法以完成它:@Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { if(pSceneTouchEvent.isActionDown() && !blankEntity.hasParent()) mScene.attachChild(blankEntity); return true; }
工作原理...
第一步和第二步涵盖了创建一个简单的BaseGameActivity类。有关创建 BaseGameActivity 类的更多信息,请参见第一章中的Know the life cycle一节,AndEngine 游戏结构。然而请注意,我们在onCreateResources()方法中创建并加载了一个Font对象。关于字体和使用它们的Text实体的更多信息,请参见第二章中的Applying text to a layer一节,使用实体。
第三步创建了一个文本实体countingText,并将其附加到我们场景的中心。然后,通过调用Entity()构造函数创建我们的空白实体blankEntity,并且将更新处理器与其注册。注意,在第四步中的onSceneTouchEvent()方法检测到触摸事件之前,空白实体并不会被附加到场景中。更新处理器的onUpdate()方法仅仅是将countingText实体的文本递增,以显示更新处理器正在运行。
第四步创建了onSceneTouchEvent()方法,该方法在场景被触摸时被调用。我们检查以确保触摸事件是一个按下动作,并且我们的空白实体还没有父实体,然后将blankEntity附加到场景中。
还有更多...
运行此示例时,我们可以看到,在空白实体附加到场景之前,更新处理程序不会被调用。这种效果与覆盖实体的onManagedUpdate()方法类似。将更新处理程序注册到实体可以用于创建具有自身逻辑的敌人,或者是在显示之前不应该动画化的场景部分。注册到Scene对象中另一个Entity对象的子Entity对象的更新处理程序仍然有效。此外,实体的可见性并不影响其注册的更新处理程序是否运行。
另请参阅
-
在本章中开始使用更新处理程序。
-
在第一章中了解AndEngine 游戏结构的生命周期。
-
在第二章中了解AndEngine 实体,使用实体。
-
在第二章中,将文本应用到图层,使用实体。
-
在第二章中覆盖 onManagedUpdate,使用实体。
结合条件使用更新处理程序。
为了减少运行具有繁重计算更新处理程序的性能成本,我们可以包含一个条件语句,告诉更新处理程序在另一个更新周期内运行一组特定的指令。例如,如果我们有敌人检查玩家是否在他们的视线范围内,我们可以选择让视野计算每三次更新只运行一次。在本示例中,我们将演示一个简单的条件语句,通过触摸屏幕在性能密集型计算和非常简单的计算之间进行切换。
准备工作...
创建一个名为UpdateHandlersAndConditionalsActivity的新类,该类继承自BaseGameActivity并实现IOnSceneTouchListener接口。我们将使用这个类来演示如何在使用更新处理程序时使用条件语句。
如何操作...
按照以下步骤创建一个使用条件块来确定要运行哪段代码的更新处理程序:
-
在新类中放置以下定义:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Font fontDefault32Bold; public Text countingText; public int countingInt = 0; public boolean performanceIntensiveLoop = true; public double performanceHeavyVariable; -
然后,添加以下重写的方法:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { fontDefault32Bold = FontFactory.create( mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32f, true, Color.BLACK_ARGB_PACKED_INT); fontDefault32Bold.load(); pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } -
接下来,插入以下重写的
onPopulateScene()方法:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { countingText = new Text(400f, 240f, fontDefault32Bold, "0", 10, this.getVertexBufferObjectManager()); mScene.attachChild(countingText); mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { if(performanceIntensiveLoop) { countingInt++; for(int i = 3; i < 1000000; i++) performanceHeavyVariable = Math.sqrt(i); } else { countingInt--; } countingText.setText( String.valueOf(countingInt)); } @Override public void reset() {} }); mScene.setOnSceneTouchListener(this); pOnPopulateSceneCallback.onPopulateSceneFinished(); } -
最后,创建此
onSceneTouchEvent()方法以完成我们的活动:@Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { if(pSceneTouchEvent.isActionDown()) performanceIntensiveLoop = !performanceIntensiveLoop; return true; }
工作原理...
在第一步中,我们定义了测试床共有的变量以及一个布尔型变量performanceIntensiveLoop,它告诉我们的更新处理程序采取哪个动作,以及一个双精度变量performanceHeavyVariable,我们将在性能密集型计算中使用它。第二步为我们的活动创建标准方法。有关创建BaseGameActivity类的更多信息,请参见第一章中的了解生命周期示例。
在第三步中,我们在注册更新处理器到场景之前创建了countingText。在每次更新时,它会检查performanceIntensiveLoop布尔变量,以确定它应该执行繁重任务(几乎调用一百万次Math类的sqrt()方法),还是执行简单任务(递减countingInt变量的文本)。
第四步是onSceneTouchEvent()方法,每次触摸屏幕时都会切换performanceIntensiveLoop布尔变量。
另请参阅
-
本章节中的开始使用更新处理器。
-
第一章中的Know the life cycle,AndEngine 游戏结构。
-
第二章中的将文本应用到层上,使用实体。
处理从游戏中移除实体的操作
在更新处理器中分离实体有时可能会抛出IndexOutOfBoundsException异常,因为实体是在引擎更新过程中被移除的。为了避免该异常,我们创建了一个Runnable参数,在所有其他更新完成后,在更新线程上最后运行。在本教程中,我们将通过使用BaseGameActivity类的runOnUpdateThread()方法,从游戏中安全地移除实体。
准备就绪...
创建一个名为HandlingRemovalOfEntityActivity的新类,该类继承自BaseGameActivity。我们将使用这个类来学习如何安全地从更新处理器中移除实体。
如何操作...
按照以下步骤,我们可以在不抛出异常的情况下从一个父实体中移除一个实体:
-
在
HandlingRemovalOfEntityActivity类中插入以下定义:public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Rectangle spinningRect; public float totalElapsedSeconds = 0f; -
接下来,将这些重写的方法添加到类中:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } -
最后,在活动中放置以下
onPopulateScene()方法以完成它:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { spinningRect = new Rectangle(400f, 240f, 100f, 20f, this.getVertexBufferObjectManager()); spinningRect.setColor(Color.BLACK); spinningRect.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { spinningRect.setRotation( spinningRect.getRotation()+0.4f); totalElapsedSeconds += pSecondsElapsed; if(totalElapsedSeconds > 5f) { runOnUpdateThread(new Runnable() { @Override public void run() { spinningRect.detachSelf(); }}); } } @Override public void reset() {} }); mScene.attachChild(spinningRect); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
工作原理...
在第一步中,我们定义了常规的BaseGameActivity变量以及一个正方形Rectangle对象,spinningRect,它将在原地旋转,还有一个浮点变量totalElapsedSeconds,用于跟踪自更新处理程序开始以来已经过去了多少秒。第二步创建了标准的BaseGameActivity方法。有关创建 AndEngine 活动的更多信息,请参见第一章中的Know the life cycle部分,AndEngine 游戏结构。
在第三步中,我们通过调用Rectangle构造函数并设置屏幕中心位置来创建第一步中定义的spinningRect矩形。然后通过setColor()方法将Rectangle对象设置为黑色。接下来,它注册了我们的更新处理器,记录经过的时间,如果自活动开始以来已经超过5秒,则从屏幕上移除矩形。请注意,我们从场景中分离矩形的方式是调用runOnUpdateThread()。此方法将Runnable参数传递给引擎,以便在更新周期完成后运行。
另请参阅
-
在本章中,开始使用更新处理器。
-
在第一章 AndEngine 游戏结构 中,了解生命周期。
-
在第二章 使用实体 中,给图层应用图元。
添加游戏计时器
许多游戏会倒计时并挑战玩家在给定的时间内完成任务。这样的挑战对玩家来说是有益的,并且常常增加了游戏的重复玩价值。在之前的教程中,我们跟踪了总经过时间。在本教程中,我们将从一个时间开始,并从中减去更新处理器提供的时间。
准备就绪...
创建一个名为 GameTimerActivity 的新类,该类继承自 BaseGameActivity。我们将使用这个类从更新处理器创建一个游戏计时器。
如何操作...
按照以下步骤使用更新处理器创建游戏计时器:
-
在我们新活动类中放置以下变量定义:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Font fontDefault32Bold; public Text countingText; public float EndingTimer = 10f; -
接下来,插入以下标准覆盖方法:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { fontDefault32Bold = FontFactory.create( mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32f, true, Color.BLACK_ARGB_PACKED_INT); fontDefault32Bold.load(); pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } -
最后,将这个覆盖的
onPopulateScene()方法添加到GameTimerActivity类中:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { countingText = new Text(400f, 240f, fontDefault32Bold, "10", 10, this.getVertexBufferObjectManager()); mScene.attachChild(countingText); mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { EndingTimer-=pSecondsElapsed; if(EndingTimer<=0) { // The timer has ended countingText.setText("0"); mScene.unregisterUpdateHandler(this); } else { countingText.setText(String.valueOf( Math.round(EndingTimer))); } } @Override public void reset() {} }); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
工作原理...
在第一步中,我们将常见的 BaseGameActivity 变量以及一个设置为 10 秒的 EndingTimer 浮点变量定义。第二步为我们的活动创建常见方法。有关创建 BaseGameActivity 类的更多信息,请参见第一章 AndEngine 游戏结构 中的了解生命周期教程。
在第三步中,我们创建 countingText 实体,并使用我们的场景注册一个更新处理器,该处理器通过 pSecondsElapsed 变量倒计时 EndingTimer 变量,直到它达到 0。当它达到 0 时,我们只需通过调用场景的 unregisterUpdateHandler() 方法,从场景中注销更新处理器。在实际游戏中,计时器结束可能意味着结束一个关卡,甚至召唤下一波敌人攻击玩家。
另请参阅
-
在本章中,开始使用更新处理器。
-
在第一章 AndEngine 游戏结构 中,了解生命周期。
-
在第二章 使用实体 中,给图层应用文本。
根据经过的时间设置实体属性
在移动游戏开发中,设备之间的连贯性是更为重要的方面之一。玩家期望游戏能够适当地缩放以适应他们设备的屏幕,但游戏开发中另一个重要且经常被忽视的方面是基于时间而不是引擎更新来设置移动和动画。在本教程中,我们将使用更新处理器来设置实体的属性。
准备就绪...
创建一个名为 SettingEntityPropertiesBasedOnTimePassedActivity 的新类,该类继承自 BaseGameActivity。我们将使用这个类来演示如何使用更新处理器来随时间设置实体属性。
如何操作...
按照以下步骤,我们可以根据已经过去的时间设置实体的属性:
-
在活动中定义以下变量:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Rectangle spinningRect; -
然后,在这些重写的方法中放入以下类:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } -
最后,在活动末尾插入此
onPopulateScene()方法以完成它:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { spinningRect = new Rectangle(400f, 240f, 100f, 20f, this.getVertexBufferObjectManager()); spinningRect.setColor(Color.BLACK); spinningRect.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { spinningRect.setRotation( spinningRect.getRotation() + ((pSecondsElapsed*360f)/2f)); } @Override public void reset() {} }); mScene.attachChild(spinningRect); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
它是如何工作的...
与本章中的其他方法一样,我们首先创建常见的BaseGameActivity变量。对于这个方法,我们还定义了一个Rectangle对象spinningRect,它将以每秒特定的圈数旋转。有关创建 AndEngine 活动的更多信息,请参见第一章 AndEngine 游戏结构中的了解生命周期方法。
在第三步中,我们通过首先创建我们的spinningRect矩形来填充onPopulateScene()方法,然后我们使用它来注册我们的更新处理程序。在更新处理程序的onUpdate()方法内部,我们将矩形的旋转设置为等于其当前的旋转,通过getRotation()方法,加上一个计算,将pSecondsElapsed变量调整为每秒设定的圈数。下图展示了我们游戏中的更新并不具有相等的持续时间,因此必须利用pSecondsElapsed参数而不是一个恒定值:
还有更多...
我们在更新处理程序的onUpdate()方法中使用的计算使Rectangle对象以每秒半圈的速度旋转。如果我们把计算中的(pSecondsElapsed*360f)部分乘以4,矩形将以每秒 4 圈的速度旋转。对于基于时间的线性移动,只需将所需的每秒像素数与pSecondsElapsed变量相乘。
另请参阅
-
本章节将开始介绍更新处理程序。
-
了解生命周期在第 第一章 AndEngine 游戏结构中。