十、从 AndEngine 中获得更多

这一章将涵盖比前几章有更具体应用的其他食谱。这些食谱包括:

  • 从文件夹加载所有纹理
  • 使用纹理网格
  • 应用基于精灵的阴影
  • 创建基于物理的移动平台
  • 创建一个基于物理的绳索桥

从文件夹加载所有纹理

当创建一个有大量纹理的游戏时,单独加载每个纹理会变得乏味。在这样的游戏中创建一个加载和检索纹理的方法不仅可以节省开发时间,还可以减少运行时的整体加载时间。在这个食谱中,我们将创建一种方法,只使用一行代码加载大量纹理。

做好准备...

首先,创建一个名为TextureFolderLoadingActivity的新活动类,扩展BaseGameActivity类。接下来,在assets/gfx/文件夹中创建一个名为FolderToLoad的文件夹。最后,在assets/gfx/FolderToLoad/文件夹中放置五张图片,名称分别为:Coin1Coin5Coin10Coin50Coin100

怎么做...

按照以下步骤填写我们的TextureFolderLoadingActivity活动课:

  1. 在我们的活动中放置以下简单的代码,使其发挥作用:

    java @Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480)) .setWakeLockOptions(WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pOnPopulateSceneCallback.onPopulateSceneFinished(); }

  2. 接下来,将这个ArrayList变量和ManagedStandardTexture类放入活动中:

    java public final ArrayList<ManagedStandardTexture> loadedTextures = new ArrayList<ManagedStandardTexture>(); public class ManagedStandardTexture { public ITextureRegion textureRegion; public String name; public ManagedStandardTexture(String pName, final ITextureRegion pTextureRegion) { name = pName; textureRegion = pTextureRegion; } public void removeFromMemory() { loadedTextures.remove(this); textureRegion.getTexture().unload(); textureRegion = null; name = null; } }

  3. 然后,将接下来的两个方法添加到活动类中,允许我们仅通过传递TextureOptions参数和文件名:

    java public ITextureRegion getTextureRegion(TextureOptions pTextureOptions, String pFilename) { loadAndManageTextureRegion(pTextureOptions,pFilename); return loadedTextures.get( loadedTextures.size()-1).textureRegion; } public void loadAndManageTextureRegion(TextureOptions pTextureOptions, String pFilename) { AssetBitmapTextureAtlasSource cSource = AssetBitmapTextureAtlasSource.create( this.getAssets(), pFilename); BitmapTextureAtlas TextureToLoad = new BitmapTextureAtlas(mEngine.getTextureManager(), cSource.getTextureWidth(), cSource.getTextureHeight(), pTextureOptions); TextureRegion TextureRegionToLoad = BitmapTextureAtlasTextureRegionFactory. createFromAsset(TextureToLoad, this, pFilename, 0, 0); TextureToLoad.load(); loadedTextures.add(new ManagedStandardTexture( pFilename.substring( pFilename.lastIndexOf("/")+1, pFilename.lastIndexOf(".")), TextureRegionToLoad)); }

    来加载纹理 4. 现在插入以下方法,允许我们加载一个文件夹或多个文件夹中的所有纹理:

    java public void loadAllTextureRegionsInFolders(TextureOptions pTextureOptions, String... pFolderPaths) { String[] listFileNames; String curFilePath; String curFileExtension; for (int i = 0; i < pFolderPaths.length; i++) try { listFileNames = this.getAssets(). list(pFolderPaths[i].substring(0, pFolderPaths[i].lastIndexOf("/"))); for (String fileName : listFileNames) { curFilePath = pFolderPaths[i].concat(fileName); curFileExtension = curFilePath.substring( curFilePath.lastIndexOf(".")); if(curFileExtension. equalsIgnoreCase(".png") || curFileExtension. equalsIgnoreCase(".bmp") || curFileExtension. equalsIgnoreCase(".jpg")) loadAndManageTextureRegion( pTextureOptions, curFilePath); } } catch (IOException e) { System.out.print("Failed to load textures from folder!"); e.printStackTrace(); return; } }

  4. 接下来,将以下方法放入活动中,让我们卸载所有ManagedStandardTexture类或通过其短文件名检索纹理:

    ```java public void unloadAllTextures() { for(ManagedStandardTexture curTex : loadedTextures) { curTex.removeFromMemory(); curTex=null; loadedTextures.remove(curTex); } System.gc(); }

    public ITextureRegion getLoadedTextureRegion(String pName) { for(ManagedStandardTexture curTex : loadedTextures) if(curTex.name.equalsIgnoreCase(pName)) return curTex.textureRegion; return null; } ```

  5. 现在我们已经在活动中拥有了所有的方法,在onCreateResources()方法中放置下面一行代码:

    java this.loadAllTextureRegionsInFolders(TextureOptions.BILINEAR, "gfx/FolderToLoad/");

  6. 最后,在onPopulateScene()方法中添加以下代码,展示我们如何通过名称检索加载的纹理:

    java pScene.attachChild(new Sprite(144f, 240f, getLoadedTextureRegion("Coin1"), this.getVertexBufferObjectManager())); pScene.attachChild(new Sprite(272f, 240f, getLoadedTextureRegion("Coin5"), this.getVertexBufferObjectManager())); pScene.attachChild(new Sprite(400f, 240f, getLoadedTextureRegion("Coin10"), this.getVertexBufferObjectManager())); pScene.attachChild(new Sprite(528f, 240f, getLoadedTextureRegion("Coin50"), this.getVertexBufferObjectManager())); pScene.attachChild(new Sprite(656f, 240f, getLoadedTextureRegion("Coin100"), this.getVertexBufferObjectManager()));

它是如何工作的...

在第一步中,我们通过实现标准的BaseGameActivity方法来建立我们的TextureFolderLoadingActivity活动类,这种方法是大多数安卓游戏都使用的。有关设置与和引擎一起使用的活动的更多信息,请参见第 1 章和引擎游戏结构中的了解生命周期食谱。

在第二步中,我们创建一个由ManagedStandardTexture对象组成的ArrayList变量,该变量是根据ArrayList变量的定义直接定义的。ManagedStandardTextures是简单的容器,包含指向ITextureRegion区域的指针和表示ITextureRegion对象名称的字符串变量。ManagedStandardTexture类还包括一个卸载ITextureRegion的方法,并准备在下一次垃圾收集时从内存中移除变量。

第三步包括两种方法,getTextureRegion()loadAndManageTextureRegion():

  • getTextureRegion()方法调用loadAndManageTextureRegion()方法,并从步骤二中定义的名为loadedTexturesArrayList变量中返回最近加载的纹理。
  • loadAndManageTextureRegion()方法创建名为cSourceAssetBitmapTextureAtlasSource源,该源仅用于传递BitmapTextureAtlas对象TextureToLoad的以下定义中纹理的宽度和高度。

TextureRegion对象TextureRegionToLoad是通过调用BitmapTextureAtlasTextureRegionFactory对象的createFromAsset()方法创建的。然后加载TextureToLoad,通过创建新的ManagedStandardTexture类将TextureRegionToLoad对象添加到loadedTextures ArrayList变量中。有关纹理的更多信息,请参见第 1 章和《工程游戏结构》中的不同类型的纹理配方。

在第四步中,我们创建一个方法,解析在pFolderPaths数组中传递的每个文件夹中的文件列表,并将图像文件加载为纹理,其中TextureOptions参数应用于每个图像。listFileNames字符串数组保存每个pFolderPaths文件夹中的文件列表,curFilePathcurFileExtension变量用于存储文件路径和它们的相对扩展名,以确定哪些文件是和工程支持的图像。第一个for循环只是为每个给定的文件夹路径运行解析和加载过程。getAssets().list()方法抛出IOException异常,因此需要包含在try-catch块中。它用于检索传递的String参数内所有文件的列表。第二个for循环将curFilePath设置为当前i值的文件夹路径,该路径与来自listFileNames数组的当前文件名相连接。接下来,curFileExtension字符串变量被设置为curFilePath变量的最后一个索引“.”,使用substring()方法返回扩展名。然后,我们检查当前文件的扩展名是否等于 AndEngine 支持的扩展名,如果是true,则调用loadAndManageTextureRegion()方法。最后,我们通过向日志发送消息并打印来自IOException异常的StackTrace消息来捕获IOException异常。

第五步包括两个方法,unloadAllTextures()getLoadedTextureRegion(),它们帮助我们管理由我们之前的方法加载的纹理:

  • unloadAllTextures()方法贯穿loadedTextures ArrayList对象中的所有ManagedStandardTextures,并使用removeFromMemory()方法卸载它们,然后将它们从loadedTextures中移除并请求从系统中进行垃圾收集
  • getLoadedTextureRegion()方法对照pName字符串参数检查loadedTextures变量中的每个ManagedStandardTexture,如果名称相等,则返回当前的ManagedStandardTexture类‘ITextureRegion区域,如果不匹配,则返回null

第六步通过传递一个BILINEAR TextureOption参数和我们的FolderToLoad文件夹的资产文件夹路径,从onCreateResources()活动方法内部调用loadAllTextureRegionsInFolders()方法。有关TextureOptions的更多信息,请参见第 1 章和工程游戏结构中的将选项应用于我们的纹理配方。

在最后一步中,我们在onPopulateScene()活动方法中为场景添加了五个精灵。每个精灵构造函数调用getLoadedTextureRegion() 方法,并传递精灵图像文件各自的短名称。每个精灵的位置将它们放置在屏幕上的一条水平线中。一次加载所有纹理的精灵的显示应该类似于下图。有关创建精灵的更多信息,请参见第 2 章、使用实体中的将精灵添加到图层配方。

How it works...

另见

  • 了解第一章中的生命周期
  • 不同类型的纹理第一章和引擎游戏结构中的
  • 第一章中对我们的纹理应用选项。
  • 第二章中添加精灵到图层中使用实体

使用纹理网格

纹理网格、是简单的应用了纹理的三角多边形,在移动游戏中变得越来越流行,因为它们允许创建和操纵非矩形形状。拥有处理纹理网格的能力通常会创建一个额外的游戏机制层,这在以前是非常昂贵的。在这个食谱中,我们将学习如何从一组预定的三角形创建一个纹理网格。

做好准备...

首先,创建一个名为TexturedMeshActivity的新活动类,扩展BaseGameActivity。接下来,在我们项目的assets/gfx/文件夹中放置一个名为dirt.png的无缝平铺纹理,尺寸为 512 x 128。最后,将TexturedMesh.java类从代码包导入到我们的项目中。

怎么做...

按照以下步骤构建我们的TexturedMeshActivity活动课:

  1. 将以下代码放入我们的活动中,为我们提供一个标准的工程活动:

    java @Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480)) .setWakeLockOptions(WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pOnPopulateSceneCallback.onPopulateSceneFinished(); }

  2. 将以下代码片段添加到onPopulateScene()方法中:

    java BitmapTextureAtlas texturedMeshT = new BitmapTextureAtlas( this.getTextureManager(), 512, 128, TextureOptions.REPEATING_BILINEAR); ITextureRegion texturedMeshTR = BitmapTextureAtlasTextureRegionFactory. createFromAsset(texturedMeshT, this, "gfx/dirt.png", 0, 0); texturedMeshT.load(); float[] meshTriangleVertices = { 24.633111f,37.7835047f,-0.00898f,113.0324447f, -24.610162f,37.7835047f,0.00387f,-37.7900953f, -103.56176f,37.7901047f,103.56176f,37.7795047f, 0.00387f,-37.7900953f,-39.814736f,-8.7311953f, -64.007044f,-83.9561953f,64.00771f,-83.9621953f, 39.862562f,-8.7038953f,0.00387f,-37.7900953f}; float[] meshBufferData = new float[TexturedMesh.VERTEX_SIZE * (meshTriangleVertices.length/2)]; for( int i = 0; i < meshTriangleVertices.length/2; i++) { meshBufferData[(i * TexturedMesh.VERTEX_SIZE) + TexturedMesh.VERTEX_INDEX_X] = meshTriangleVertices[i*2]; meshBufferData[(i * TexturedMesh.VERTEX_SIZE) + TexturedMesh.VERTEX_INDEX_Y] = meshTriangleVertices[i*2+1]; } TexturedMesh starTexturedMesh = new TexturedMesh(400f, 225f, meshBufferData, 12, DrawMode.TRIANGLES, texturedMeshTR, this.getVertexBufferObjectManager()); pScene.attachChild(starTexturedMesh);

它是如何工作的...

在第一步中,我们通过在我们的TexturedMeshActivity类中插入标准的、被覆盖的BaseGameActivity方法来准备我们的TexturedMeshActivity类,这是大多数安卓游戏都使用的方法。有关设置与和引擎一起使用的活动的更多信息,请参见第 1 章和引擎游戏结构中的了解生命周期食谱。

在第二步中,我们首先定义texturedMeshT,一个BitmapTextureAtlas对象,构造器的最终参数是REPEATING_BILINEAR TextureOption参数,以创建一个纹理,该纹理将无缝地平铺在构成我们的纹理网格的三角形内。有关TextureOptions的更多信息,请参见第 1 章和工程游戏结构中的将选项应用于我们的纹理配方。

在创建了texturedMeshTR ITextureRegion对象并加载了我们的texturedMeshT对象之后,我们定义了一个浮动变量数组,该数组指定了构成我们的纹理网格的每个三角形的每个顶点的相对和连续的 x 和 y 位置。查看下图,更好地了解如何在纹理网格中使用三角形的顶点:

How it works...

接下来,我们创建meshBufferData浮动数组,并将其大小设置为TexturedMesh类的顶点大小乘以meshTriangleVertices数组中的顶点数——一个顶点占据数组中的两个索引,XY,因此我们必须将长度除以2。然后,对于meshTriangleVertices数组中的每个顶点,我们将顶点的位置应用于meshBufferData数组。最后,我们创建TexturedMesh对象,命名为starTexturedMeshTexturedMesh构造器的参数如下:

  • 构造函数的前两个参数是400f225f的 x 和 y 位置
  • 接下来的两个参数是meshBufferData缓冲区数据和顶点数量12,我们将其放入meshBufferData数组中
  • TexturedMesh构造器的最后三个参数是TrianglesDrawMode、【网格】的ITextureRegion,以及我们的VertexBufferObjectManager对象。

有关创建TexturedMesh类的更多信息,请参见第 2 章使用实体中的将图元应用于图层方法。

另见

  • 了解第一章中的生命周期
  • 第一章中对我们的纹理应用选项。
  • 第 2 章中对图层应用图元处理实体

应用基于精灵的阴影

给游戏增加阴影可以增加视觉深度,让游戏有更吸引人的外观。简单地在对象下面放置一个带有阴影纹理的精灵是处理阴影创建的快速有效的方法。在本章中,我们将学习如何在保持阴影与其父对象正确对齐的同时做到这一点。

做好准备...

首先,创建一个名为SpriteShadowActivity的新活动类,扩展BaseGameActivity并实现IOnSceneTouchListener。接下来,将一个大小为 256 x 128 并命名为shadow.png的阴影图像放入assets/gfx/文件夹中。最后,将一个大小为 128 x 256 并命名为character.png的字符图像放入assets/gfx/文件夹中。

怎么做...

按照以下步骤构建我们的SpriteShadowActivity活动课:

  1. 在我们的活动类中放置以下标准和工程活动代码:

    java @Override public EngineOptions onCreateEngineOptions() { EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480)) .setWakeLockOptions(WakeLockOptions.SCREEN_ON); engineOptions.getRenderOptions().setDithering(true); return engineOptions; } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.8f,0.8f,0.8f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pScene.setOnSceneTouchListener(this); pOnPopulateSceneCallback.onPopulateSceneFinished(); } @Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { return true; }

  2. 接下来,将这些变量放入我们的活动中,以给予我们对阴影的特定控制:

    java Static final float CHARACTER_START_X = 400f; static final float CHARACTER_START_Y = 128f; static final float SHADOW_OFFSET_X = 0f; static final float SHADOW_OFFSET_Y = -64f; static final float SHADOW_MAX_ALPHA = 0.75f; static final float SHADOW_MIN_ALPHA = 0.1f; static final float SHADOW_MAX_ALPHA_HEIGHT = 200f; static final float SHADOW_MIN_ALPHA_HEIGHT = 0f; static final float SHADOW_START_X = CHARACTER_START_X + SHADOW_OFFSET_X; static final float SHADOW_START_Y = CHARACTER_START_Y + SHADOW_OFFSET_Y; static final float CHARACTER_SHADOW_Y_DIFFERENCE = CHARACTER_START_Y - SHADOW_START_Y; static final float SHADOW_ALPHA_HEIGHT_DIFFERENCE = SHADOW_MAX_ALPHA_HEIGHT - SHADOW_MIN_ALPHA_HEIGHT; static final float SHADOW_ALPHA_DIFFERENCE = SHADOW_MAX_ALPHA - SHADOW_MIN_ALPHA; Sprite shadowSprite; Sprite characterSprite;

  3. 现在在我们的活动中放置以下方法,使阴影的 alpha 与角色离阴影的距离成反比:

    java public void updateShadowAlpha() { shadowSprite.setAlpha(MathUtils.bringToBounds( SHADOW_MIN_ALPHA, SHADOW_MAX_ALPHA, SHADOW_MAX_ALPHA - ((((characterSprite.getY()- CHARACTER_SHADOW_Y_DIFFERENCE)-SHADOW_START_Y) / SHADOW_ALPHA_HEIGHT_DIFFERENCE) * SHADOW_ALPHA_DIFFERENCE))); }

  4. 将以下代码片段插入onSceneTouchEvent()方法 :

    java if(pSceneTouchEvent.isActionDown() || pSceneTouchEvent.isActionMove()) { characterSprite.setPosition( pSceneTouchEvent.getX(), Math.max(pSceneTouchEvent.getY(), CHARACTER_START_Y)); }

  5. 最后,用以下片段填充onPopulateScene()方法:

    java BitmapTextureAtlas characterTexture = new BitmapTextureAtlas(this.getTextureManager(), 128, 256, TextureOptions.BILINEAR); TextureRegion characterTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset( characterTexture, this, "gfx/character.png", 0, 0); characterTexture.load(); BitmapTextureAtlas shadowTexture = new BitmapTextureAtlas(this.getTextureManager(), 256, 128, TextureOptions.BILINEAR); TextureRegion shadowTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset( shadowTexture, this, "gfx/shadow.png", 0, 0); shadowTexture.load(); shadowSprite = new Sprite(SHADOW_START_X, SHADOW_START_Y, shadowTextureRegion,this.getVertexBufferObjectManager()); characterSprite = new Sprite(CHARACTER_START_X, CHARACTER_START_Y, characterTextureRegion,this.getVertexBufferObjectManager()) { @Override public void setPosition(final float pX, final float pY) { super.setPosition(pX, pY); shadowSprite.setPosition( pX + SHADOW_OFFSET_X, shadowSprite.getY()); updateShadowAlpha(); } }; pScene.attachChild(shadowSprite); pScene.attachChild(characterSprite); updateShadowAlpha();

它是如何工作的...

在第一步中,我们通过实现标准的BaseGameActivity方法来建立我们的SpriteShadowActivity活动类,这种方法是大多数安卓游戏都使用的。有关设置与和引擎一起使用的活动的更多信息,请参见第 1 章和引擎游戏结构中的了解生命周期食谱。

下图显示了这个配方如何将我们的阴影精灵与角色精灵相关联:

How it works...

在第二步中,我们定义了几个常量来控制阴影精灵shadowSprite如何与角色精灵characterSprite : 对齐

  • 前两个常数CHARACTER_START_XCHARACTER_START_Y设置characterSprite的初始位置
  • 接下来的两个常数SHADOW_OFFSET_XSHADOW_OFFSET_Y控制 x 轴和 y 轴上的距离,即阴影相对于角色精灵的初始位置
  • 当移动角色精灵时,SHADOW_OFFSET_X常数也用于更新阴影精灵的位置

接下来的四个常量控制shadowSprite精灵的 alpha 将如何控制,以及控制到什么级别:

  • SHADOW_MAX_ALPHASHADOW_MIN_ALPHA设置绝对最小和最大 alpha,根据角色在 y 轴上距离阴影的距离变化。距离越远,shadowSprite精灵的阿尔法值越低,直到达到最低等级。
  • SHADOW_MAX_ALPHA_HEIGHT常数表示在默认为SHADOW_MIN_ALPHA之前,shadowSprite精灵的 alpha 将受到影响的角色到阴影的最大距离。
  • SHADOW_MIN_ALPHA_HEIGHT常数表示角色到阴影的最小距离,阴影的 alpha 应该改变。如果SHADOW_MIN_ALPHA_HEIGHT大于0,当角色与阴影的距离低于SHADOW_MIN_ALPHA_HEIGHT时,阴影的 alpha 将达到最大值。

剩余的常数是根据前一组自动计算的。SHADOW_START_XSHADOW_START_Y代表shadowSprite精灵的起始位置。它们是通过将阴影的偏移值添加到角色的起始位置来计算的。CHARACTER_SHADOW_Y_DIFFERENCE常数代表 y 轴上字符和阴影之间的初始起始距离。SHADOW_ALPHA_HEIGHT_DIFFERENCE常数代表最小和最大高度之间的差值,用于在运行时调整阴影的 alpha。最终常数SHADOW_ALPHA_DIFFERENCE代表shadowSprite精灵的最小和最大阿尔法等级之间的差异。与SHADOW_ALPHA_HEIGHT_DIFFERENCE常量类似,它在运行时用于确定阴影的 alpha 级别。

第二步最后两个变量shadowSpritecharacterSprite代表我们场景中的阴影和人物。

第三步,我们创建一个更新阴影阿尔法的方法。我们称shadowSprite.setAlpha()方法为MathUtils.bringToBounds()方法为参数。MathUtils.bringToBounds()方法取最小值和最大值,并确保第三个值在该范围内。我们将SHADOW_MIN_ALPHASHADOW_MAX_ALPHA常数作为bringToBounds()方法的前两个参数。

第三个参数是根据characterSprite子画面到shadowSprite子画面的距离确定阴影阿尔法的算法。该算法从字符在 y 轴上的位置减去CHARACTER_SHADOW_Y_DIFFERENCE常数开始。这给了我们影响阴影阿尔法的 y 值的当前上限。接下来,我们减去阴影在 y 轴上的起始位置,得到角色与阴影的当前理想距离。接下来,我们将该距离除以SHADOW_ALPHA_HEIGHT_DIFFERENCE以获得约束距离与α的单位比率,并将该比率乘以SHADOW_ALPHA_DIFFERENCE常数以获得约束距离与约束α的单位比率。目前,我们的比例是颠倒的,将随着距离增加阿尔法,这与我们的目标是随着角色进一步移动而减少阿尔法相反,所以我们从SHADOW_MAX_ALPHA常数中减去它,以给我们一个适当的比例,随着距离的增加而减少阿尔法。完成算法后,我们再用bringToBounds()方法保证算法产生的 alpha 值被约束在SHADOW_MIN_ALPHASHADOW_MAX_ALPHA的范围内。

第四步通过检查触摸事件的isActionDown()isActionMove()属性,设置首次触摸屏幕时characterSprite精灵的位置,或者如果触摸被移动。在这种情况下,setPosition()方法只需将 x 值设置为触摸的 x 值,将 x 值设置为触摸的 y 值或字符的起始 y 值,以较大者为准。

在最后一步中,我们为角色和阴影加载TextureRegionscharacterTextureRegionshadowTextureRegion对象。关于TextureRegions的更多信息,请参见第一章和【引擎游戏结构】中的不同类型纹理配方。然后,我们使用它们的起始常数作为构造函数中的位置来创建shadowSpritecharacterSprite精灵。对于characterSprite,我们覆盖 setPosition()方法来设置shadowSprite精灵的位置,并应用 x 偏移,然后调用updateShadowAlpha()方法来设置角色移动后阴影的适当 alpha。最后,我们将shadowSpritecharacterSprite精灵附加到我们的场景中,并调用updateShadowAlpha()方法来设置阴影的初始 alpha。下图显示了阴影的 alpha 级别如何随着与角色的距离而变化:

How it works...

另见

  • 了解第一章中的生命周期
  • 不同类型的纹理第一章和引擎游戏结构中的

创建基于物理的移动平台

大多数平台风格的游戏都有某种移动平台,这要求玩家在准确的时间落地。从开发人员的角度来看,平台只是一个物理实体,从一个位置移动到另一个位置。在这个食谱中,我们将看到如何创建一个水平移动的平台。

做好准备...

创建一个名为MovingPhysicsPlatformActivity的新活动类,扩展BaseGameActivity

怎么做...

按照以下步骤构建我们的MovingPhysicsPlatformActivity活动课:

  1. 将以下代码片段插入到我们的活动中,使其发挥作用:

    java @Override public Engine onCreateEngine(final EngineOptions pEngineOptions) { return new FixedStepEngine(pEngineOptions, 60); } @Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480) ).setWakeLockOptions(WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pOnPopulateSceneCallback.onPopulateSceneFinished(); }

  2. 将以下代码片段添加到onPopulateScene()方法中:

    java FixedStepPhysicsWorld mPhysicsWorld = new FixedStepPhysicsWorld(60, new Vector2(0,-SensorManager.GRAVITY_EARTH*2f), false, 8, 3); pScene.registerUpdateHandler(mPhysicsWorld); Rectangle platformRect = new Rectangle(400f, 200f, 250f, 20f, this.getVertexBufferObjectManager()); platformRect.setColor(0f, 0f, 0f); final FixtureDef platformFixtureDef = PhysicsFactory.createFixtureDef(20f, 0f, 1f); final Body platformBody = PhysicsFactory.createBoxBody( mPhysicsWorld, platformRect, BodyType.KinematicBody, platformFixtureDef); mPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(platformRect, platformBody)); pScene.attachChild(platformRect); float platformRelativeMinX = -200f; float platformRelativeMaxX = 200f; final float platformVelocity = 3f; final float platformMinXWorldCoords = (platformRect.getX() + platformRelativeMinX) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT; final float platformMaxXWorldCoords = (platformRect.getX() + platformRelativeMaxX) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT; platformBody.setLinearVelocity(platformVelocity, 0f);

  3. onPopulateScene()方法中,在前面代码的正下方插入以下代码:

    java pScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { if(platformBody.getWorldCenter().x > platformMaxXWorldCoords) { platformBody.setTransform( platformMaxXWorldCoords, platformBody.getWorldCenter().y, platformBody.getAngle()); platformBody.setLinearVelocity( -platformVelocity, 0f); } else if(platformBody.getWorldCenter().x < platformMinXWorldCoords) { platformBody.setTransform( platformMinXWorldCoords, platformBody.getWorldCenter().y, platformBody.getAngle()); platformBody.setLinearVelocity( platformVelocity, 0f); } } @Override public void reset() {} });

  4. 完成onPopulateScene()方法和我们的活动,将下面的代码放在前面的代码之后,创建一个物理激活的盒子,放在平台上:

    java Rectangle boxRect = new Rectangle(400f, 240f, 60f, 60f, this.getVertexBufferObjectManager()); boxRect.setColor(0.2f, 0.2f, 0.2f); FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(200f, 0f, 1f); mPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(boxRect, PhysicsFactory.createBoxBody( mPhysicsWorld, boxRect, BodyType.DynamicBody, boxFixtureDef))); pScene.attachChild(boxRect);

它是如何工作的...

在第一步中,我们准备我们的MovingPhysicsPlatformActivity类,在其中插入标准的,被大多数安卓游戏使用的BaseGameActivity方法覆盖。有关设置与和引擎一起使用的活动的更多信息,请参见第 1 章和引擎游戏结构中的了解生命周期食谱。下图显示了我们的平台如何在单个轴上移动,在本例中是向右移动,同时将盒子放在上面:

How it works...

在第二步中,我们首先创建一个FixedStepPhysicsWorld对象,并将其注册为场景的更新处理器。然后,我们创建一个名为platformRectRectangle对象,它将代表我们的移动平台,并将其放置在屏幕中心附近。接下来,我们使用setColor()方法将platformRect矩形的颜色设置为黑色,红色、绿色和蓝色浮动参数的值为0f。然后,我们为平台创建一个夹具定义。请注意,摩擦力被设置为1f以防止其上的物体在移动时滑动过多。

接下来,我们为平台创建名为platformBodyBody对象。然后,我们注册一个PhysicsConnector类,将platformRect矩形连接到platformBody主体。将platformRect附加到我们的场景后,我们声明并设置控制移动平台的变量:

  • platformRelativeMinXplatformRelativeMaxX变量表示平台将从其起始位置向左和向右移动多远(以场景为单位)。
  • platformVelocity变量代表我们物理平台体的速度,单位为米每秒。
  • 接下来的两个变量platformMinXWorldCoordsplatformMaxXWorldCoords代表platformRelativeMinXplatformRelativeMaxX变量的绝对位置,并且是根据默认PIXEL_TO_METER_RATIO_DEFAULT缩放的平台初始 x 位置计算的。
  • 最后,我们将platformBody身体的初始速度设置为platformVelocity变量,以便在第一次绘制场景时让身体主动移动。有关创建物理模拟的更多信息,请参见第 6 章、物理应用对 Box2D 物理扩展的介绍和了解不同的身体类型食谱。

第三步向我们的场景注册一个新的IUpdateHandler处理器。在onUpdate()方法中,我们测试平台的位置是否超出了我们之前定义的绝对边界platformMinXWorldCoordsplatformMaxXWorldCoords。根据到达的绝对边界,我们将platformBody物体的位置设置为到达的边界,并设置其离开边界的速度。有关条件更新处理器的更多信息,请参见第 7 章使用更新处理器中的更新处理器和条件配方。

在第四步中,我们创建并附加一个箱体来放置在平台上。有关创建物理启用盒的更多信息,请参见第 6 章物理应用中的了解不同身体类型食谱。

另见

  • 了解第一章中的生命周期
  • 第 6 章物理学应用中的 Box2D 物理学扩展介绍。
  • 了解不同体型第六章物理学应用
  • 更新处理器和条件句第 7 章使用更新处理器

创建基于物理的绳索桥

使用 Box2D 物理扩展,创建复杂的物理使能元素很简单。这种复杂元素的一个例子是对碰撞做出反应的绳索桥。在本食谱中,我们将看到如何实现一种方法,根据控制桥梁大小和物理属性的特定参数创建一个绳索桥。

做好准备...

创建一个名为PhysicsBridgeActivity的新活动类,扩展BaseGameActivity

怎么做...

按照以下步骤构建我们的PhysicsBridgeActivity活动课:

  1. 将以下代码放入我们的活动中,为我们提供一个标准的工程活动:

    java @Override public Engine onCreateEngine(final EngineOptions pEngineOptions) { return new FixedStepEngine(pEngineOptions, 60); } @Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480)) .setWakeLockOptions(WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pOnPopulateSceneCallback.onPopulateSceneFinished(); }

  2. 接下来,在我们的活动中放置以下不完整的方法。这种方法将有助于创建我们的桥梁:

    ```java public void createBridge(Body pGroundBody, final float[] pLeftHingeAnchorPoint, final float pRightHingeAnchorPointX, final int pNumSegments, final float pSegmentsWidth, final float pSegmentsHeight, final float pSegmentDensity, final float pSegmentElasticity, final float pSegmentFriction, IEntity pScene, PhysicsWorld pPhysicsWorld, VertexBufferObjectManager pVertexBufferObjectManager) { final Rectangle[] BridgeSegments = new Rectangle[pNumSegments]; final Body[] BridgeSegmentsBodies = new Body[pNumSegments]; final FixtureDef BridgeSegmentFixtureDef = PhysicsFactory.createFixtureDef( pSegmentDensity, pSegmentElasticity, pSegmentFriction); final float BridgeWidthConstant = pRightHingeAnchorPointX – pLeftHingeAnchorPoint[0] + pSegmentsWidth; final float BridgeSegmentSpacing = ( BridgeWidthConstant / (pNumSegments+1) – pSegmentsWidth/2f); for(int i = 0; i < pNumSegments; i++) {

    } } ```

  3. 在上述createBridge()方法的for循环内插入以下代码:

    java BridgeSegments[i] = new Rectangle( ((BridgeWidthConstant / (pNumSegments+1))*i) + pLeftHingeAnchorPoint[0] + BridgeSegmentSpacing, pLeftHingeAnchorPoint[1]-pSegmentsHeight/2f, pSegmentsWidth, pSegmentsHeight, pVertexBufferObjectManager); BridgeSegments[i].setColor(0.97f, 0.75f, 0.54f); pScene.attachChild(BridgeSegments[i]); BridgeSegmentsBodies[i] = PhysicsFactory.createBoxBody( pPhysicsWorld, BridgeSegments[i], BodyType.DynamicBody, BridgeSegmentFixtureDef); BridgeSegmentsBodies[i].setLinearDamping(1f); pPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(BridgeSegments[i], BridgeSegmentsBodies[i])); final RevoluteJointDef revoluteJointDef = new RevoluteJointDef(); if(i==0) { Vector2 anchorPoint = new Vector2( BridgeSegmentsBodies[i].getWorldCenter().x – (BridgeSegmentSpacing/2 + pSegmentsWidth/2)/ PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, BridgeSegmentsBodies[i].getWorldCenter().y); revoluteJointDef.initialize(pGroundBody, BridgeSegmentsBodies[i], anchorPoint); } else { Vector2 anchorPoint = new Vector2( (BridgeSegmentsBodies[i].getWorldCenter().x + BridgeSegmentsBodies[i-1] .getWorldCenter().x)/2, BridgeSegmentsBodies[i].getWorldCenter().y); revoluteJointDef.initialize(BridgeSegmentsBodies[i-1], BridgeSegmentsBodies[i], anchorPoint); } pPhysicsWorld.createJoint(revoluteJointDef); if(i==pNumSegments-1) { Vector2 anchorPoint = new Vector2(BridgeSegmentsBodies[i].getWorldCenter().x + (BridgeSegmentSpacing/2 + pSegmentsWidth/2)/PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, BridgeSegmentsBodies[i].getWorldCenter().y); revoluteJointDef.initialize(pGroundBody, BridgeSegmentsBodies[i], anchorPoint); pPhysicsWorld.createJoint(revoluteJointDef); }

  4. 最后,在我们的onPopulateScene()方法中添加以下代码:

    ```java final FixedStepPhysicsWorld mPhysicsWorld = new FixedStepPhysicsWorld(60, new Vector2(0,-SensorManager.GRAVITY_EARTH), false, 8, 3); pScene.registerUpdateHandler(mPhysicsWorld);

    FixtureDef groundFixtureDef = PhysicsFactory.createFixtureDef(0f, 0f, 0f); Body groundBody = PhysicsFactory.createBoxBody(mPhysicsWorld, 0f, 0f, 0f, 0f, BodyType.StaticBody, groundFixtureDef);

    createBridge(groundBody, new float[] {0f,240f}, 800f, 16, 40f, 10f, 4f, 0.1f, 0.5f, pScene, mPhysicsWorld, this.getVertexBufferObjectManager());

    Rectangle boxRect = new Rectangle(100f,400f,50f,50f,this.getVertexBufferObjectManager()); FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(25f, 0.5f, 0.5f); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector(boxRect, PhysicsFactory.createBoxBody(mPhysicsWorld, boxRect, BodyType.DynamicBody, boxFixtureDef))); pScene.attachChild(boxRect); ```

它是如何工作的...

在第一步中,我们通过实现标准的BaseGameActivity方法来建立我们的PhysicsBridgeActivity活动类,这种方法是大多数安卓游戏都使用的。有关设置与和引擎一起使用的活动的更多信息,请参见第 1 章和引擎游戏结构中的了解生命周期食谱。下图显示了我们的物理启用桥,上面有一个物理启用的正方形:

How it works...

在第二步中,我们实现一个名为createBridge()的方法的开始,它将创建我们的物理使能桥。第一个参数pGroundBody是桥梁将附着的地面Body物体。第二个参数pLeftHingeAnchorPoint表示桥左上角的 x 和 y 位置。第三个参数pRightHingeAnchorPointX表示桥梁右侧的 x 位置。接下来的三个参数pNumSegmentspSegmentsWidthpSegmentsHeight表示桥梁将由多少节段组成,以及每个节段的宽度和高度。pSegmentDensitypSegmentElasticitypSegmentFriction参数将直接传递给夹具定义,夹具定义将应用于桥梁的各个部分。有关夹具定义的更多信息,请参见第 6 章物理应用中的Box2D 物理扩展配方介绍。接下来的两个参数pScenepPhysicsWorld告诉我们的方法桥段矩形和桥段体应该附着什么。最后一个参数是我们的VertexBufferObjectManager对象,将被传递给代表我们的桥的每个片段的矩形。

createBridge()方法中定义的前两个变量BridgeSegmentsBridgeSegmentsBodies是保存分段矩形和分段主体的数组。它们被定义为具有由pNumSegments参数传递的长度。下一个变量BridgeSegmentFixtureDef,是桥梁每个部分的固定装置定义。BridgeWidthConstant变量表示桥梁的宽度,通过计算左锚和右锚加到桥梁单个部分宽度上的差值来计算。最后一个变量BridgeSegmentSpacing表示每个线段之间应该有多少空间,它是通过将桥的宽度除以线段数的 1 并从中减去线段宽度的一半来确定的。然后我们创建一个for循环,该循环将创建并定位在pNumSegments参数中传递的线段数。

第三步,我们填充之前创建的for循环。首先,我们创建当前线段的矩形BridgeSegments[i],它将作为线段的视觉表示。我们使用BridgeWidthConstant变量将它放在 x 轴上,除以比线段数多一的数,再乘以当前线段数,然后加上左铰链的 x 位置pLeftHingeAnchorPoint[0]和线段之间的间距BridgeSegmentSpacing。对于当前线段矩形的 y 轴位置,我们将其放置在左铰链的 y 位置减去线段高度除以2f使其与铰链位置齐平。

接下来,我们将每个片段的颜色设置为浅橙色、0.97f红色、0.75f绿色和0.54f蓝色。将Rectangle对象附加到传递的场景后,我们通过将线段的矩形和DynamicBodyType值传递到标准的PhysicsFactory.CreateBoxBody()方法来创建当前线段的主体。然后我们将线性阻尼设置为1f来平滑碰撞引起的节奏运动。接下来,我们注册一个PhysicsConnector类,将当前线段的矩形连接到当前线段的主体。

现在我们已经建立了一个位置,并为每个片段创建了相应的矩形和主体,我们创建了一个RevoluteJointDef对象,revoluteJointDef,通过旋转关节将每个片段连接到桥上。我们测试当前线段是否是第一个线段,如果是,则将该线段连接到地面Body对象,而不是前一个线段。对于第一个桥段,Vector2 anchorPoint的定义将RevoluteJointDef定义的锚点放置在该段 x 值的 x 位置,BridgeSegmentsBodies[i].getWorldCenter().x,减去段间距,BridgeSegmentSpacing,除以2,加上段宽度,pSegmentsWidth,除以2,并缩放至PIXEL_TO_METER_RATIO_DEFAULT默认值。第一个线段锚点的 y 位置就是当前线段的 y 值BridgeSegmentsBodies[i].getWorldCenter().y。对于剩余的线段,通过将当前线段的 x 位置与前一线段的 x 位置进行平均来计算锚点的 x 位置。

然后,使用initialize()方法初始化revoluteJointDef,如果当前片段是第一个,则将第一个身体设置为地面身体,pGroundBody,如果当前片段不是第一个,则设置为前一个片段的身体,BridgeSegmentsBodies[i-1]revoluteJointDef的第二个主体设置为当前线段的主体,退出if语句后,使用pPhysicsWorld对象的createJoint()方法创建关节。然后,我们测试当前线段是否是最后创建的线段,如果是,则创建另一个旋转关节,使用与第一个线段类似的锚点 x 位置公式,将该线段附着到该线段右侧的地面体。有关物理模拟的更多信息,请参见第 6 章物理应用中的Box2D 物理扩展介绍和了解不同体型食谱。

在最后一步中,我们首先在onPopulateScene()方法内部创建一个FixedStepPhysicsWorld对象,并将其注册为场景的更新处理器。然后,我们创建一个地面体,我们的桥将连接到其上。接下来,我们通过调用 createBridge()方法来创建我们的桥。我们通过groundBody作为第一个参数,0f,240f的一个位置代表屏幕的左中间作为左锚点,x 位置代表屏幕的右手边作为右锚点。然后我们传递一个整数16作为要创建的线段数,以及一个线段宽度和高度40f10f。接下来,我们传递4f的线段密度、0.1f的线段弹性、0.5f的线段摩擦力、我们的线段矩形将要附着的场景、我们的物理世界以及我们的VertexBufferObjectManager对象。现在我们的桥梁已经建立,我们创建一个简单的箱体来显示桥梁对碰撞的正确反应。

另见

  • 了解第一章中的生命周期
  • 第 6 章物理学应用中的 Box2D 物理学扩展介绍。
  • 了解不同体型第六章物理学应用