四、使用相机

这一章将涵盖和工程师的各种相机对象和先进的相机控制。主题包括:

  • 介绍相机对象
  • 用绑定的摄像机限制摄像机区域
  • 用变焦相机仔细观察
  • 使用平滑相机创建平滑移动
  • 缩放相机功能
  • 将背景拼接在一起
  • 将平视显示器应用于摄像机
  • 将控制器连接到显示器
  • 座标变换
  • 创建分屏游戏

简介

AndEngine 包括三种类型的相机,不包括基础Camera对象,它允许我们控制(更具体地说)相机的行为方式。相机在游戏中可以扮演许多不同的角色,在某些情况下,我们可能会发现自己需要不止一个相机。本章将介绍我们可以使用 AndEngine 的Camera对象的一些不同目的和方式,以便在我们自己的游戏中应用更高级的相机功能。

介绍相机对象

在设计大型游戏时,相机可以有许多用途,但它的主要目标是在设备显示器上显示游戏世界的特定区域。本主题将介绍基本的Camera类,涵盖相机的一般方面,以便为未来的相机使用提供参考。

怎么做…

相机在游戏开发中很重要,因为它们控制着我们在设备上看到的东西。创建我们的相机就像下面的代码一样简单:

final int WIDTH = 800;
final int HEIGHT = 480;

// Create the camera
Camera mCamera = new Camera(0, 0, WIDTH, HEIGHT);

WIDTHHEIGHT值将定义游戏场景中显示在设备上的区域。

它是如何工作的…

了解相机的主要功能非常重要,以便在我们的项目中充分利用它。所有不同的摄像机都继承了本主题中的方法。让我们来看看 AndEngine 开发所需的一些最必要的相机方法:

定位摄像头:

Camera对象遵循与实体相同的坐标系。例如,将摄像机的坐标设置为(0,0),会将摄像机的中心点设置为定义的坐标。此外,增加 x 值会将相机移动到右侧,增加 y 值会将相机向上移动。降低数值会产生相反的效果。为了将摄像机重新定位到定义位置的中心,我们可以调用以下方法:

// We can position the camera anywhere in the game world
mCamera.setCenter(WIDTH / 2, HEIGHT / 2);

上述代码不会对默认摄像机位置产生任何影响(假设WIDTHHEIGHT值用于定义摄像机的宽度和高度)。这会将摄像机的中心设置为场景的“中心”,这自然等于创建Camera对象时摄像机WIDTHHEIGHT值的一半。前面的方法调用可以用于我们想要将相机重置回其初始位置的情况,这在游戏过程中相机移动的情况下很有用,但是当用户返回菜单时应该返回到其初始位置。

无需设置特定坐标即可移动摄像机,这可以通过offsetCenter(x,y)方法实现,其中xy值定义了摄像机在场景坐标中的偏移距离。此方法将指定的参数值添加到摄像机的当前位置:

// Move the camera up and to the right by 5 pixels
mCamera.offsetCenter(5, 5);
// Move the camera down and to the left by 5 pixels
mCamera.offsetCenter(-5, -5);

此外,我们可以通过使用以下方法获得摄像机的中心 x 和 y 坐标:

mCamera.getCenterX();
mCamera.getCenterY();

调节摄像头的宽度和高度:

相机的初始宽度和高度可以通过相机的set()方法进行调整。我们也可以通过调用setXMin() / setXMax()setYMin() / setYMax()等方法设置摄像头的最小/最大 x 和 y 值。以下代码将使摄像机宽度缩小一半,同时保持初始摄像机高度:

// Shrink the camera by half its width
mCamera.set(0, 0, mCamera.getWidth() / 2, mCamera.getHeight());

请记住,当缩小相机宽度时,我们会失去像素和定义区域之外的任何实体的可见性。此外,缩小或扩展相机的宽度和高度可能会导致实体看起来被拉伸或挤压。一般来说,修改相机的宽度和高度在典型游戏的开发中是不必要的。

Camera对象还允许我们通过调用getXMin() / getXMax()getYMin() / getYMax()来获取摄像机当前的最小/最大宽度和高度值。

能见度检查:

Camera类允许我们检查特定的Entity对象是否在摄像机的视野内可见。Entity对象子类型包括但不限于LineRectangle图元、SpriteText对象,以及它们的所有子类型,如TiledSpriteButtonSprite对象等。可以通过使用以下方法调用可见性检查:

// Check if entity is visible. true if so, false otherwise
mCamera.isEntityVisible(entityObject);

可见性检查对于许多游戏非常有用,可以重复使用可能会离开摄像机视野的物体。这可以允许我们限制在可能产生许多对象的情况下创建的对象的总数,这些对象最终会离开相机视图。相反,我们可以重复使用离开摄像机视野的物体。

追逐实体功能:

通常,游戏需要摄像机跟随一个在屏幕上移动的Entity物体,比如在侧滚轮中。我们可以通过调用一个简单的方法,轻松地设置我们的相机来跟踪实体在游戏世界中的任何移动位置:

mCamera.setChaseEntity(entityObject);

上述代码将在每次更新摄像机时,将摄像机位置应用于指定实体的位置。这确保了实体始终保持在摄像机的中心。

在本书的大多数食谱中,我们指定的相机宽度为 800 像素,相机高度为 480 像素。然而,这些价值完全取决于开发者,应该由游戏的需求来定义。这些特定的值被选择用于本书的食谱,因为它们相对来说既适合小屏幕设备,也适合大屏幕设备。

用绑定的摄像机限制摄像机区域

BoundCamera对象允许我们定义摄像机区域的特定边界,限制摄像机在 x 轴和 y 轴上的移动距离。这款相机在以下情况下非常有用:相机可能会跟随玩家,但如果用户靠近墙壁,相机仍不会超出水平界限。

怎么做...

创建BoundCamera对象需要与常规Camera对象相同的参数:

BoundCamera mCamera = new BoundCamera(0, 0, WIDTH, HEIGHT);

它是如何工作的...

BoundCamera对象扩展了普通的Camera对象,为我们提供了相机的所有原始功能,如本章中介绍相机对象配方中所述。事实上,除非我们在BoundCamera对象上配置一个有界区域,否则我们最好使用一个基本的Camera对象。

在我们的摄像机对其可用移动区域施加限制之前,我们必须定义摄像机可以自由移动的可用区域:

// WIDTH = 800;
// HEIGHT = 480;
// WIDTH and HEIGHT are equal to the camera's width and height
mCamera.setBounds(0, 0, WIDTH * 4, HEIGHT);

// We must call this method in order to apply camera bounds
mCamera.setBoundsEnabled(true);

前面的代码将设置从场景坐标中的位置(0,0)(3200,480)的摄像机边界,因为我们将摄像机的宽度乘以最大 x 区域的四倍,允许摄像机滚动摄像机宽度的四倍。当边界高度设置为与摄像机高度相同的值时,摄像机将不会对 y 轴上的变化做出响应。

另见

  • 介绍本章给出的相机对象

用变焦相机近距离观察

AndEngine 的BoundCameraCamera对象默认不支持放大和缩小。如果我们想允许缩放相机,我们可以创建一个扩展BoundCamera类的ZoomCamera对象。该对象包括其继承类的所有功能,包括创建摄像机边界。

怎么做...

BoundCamera类似,ZoomCamera对象在创建相机时不需要定义额外的参数:

ZoomCamera mCamera = new ZoomCamera(0, 0, WIDTH, HEIGHT);

它是如何工作的…

为了对相机应用缩放效果,我们可以调用setZoomFactor(factor)方法,其中factor是我们希望应用于我们的Scene对象的放大倍数。放大和缩小可以通过以下代码实现:

// Divide the camera width/height by 1.5x (Zoom in)
mCamera.setZoomFactor(1.5f);

// Divide the camera width and height by 0.5x (Zoom out)
mCamera.setZoomFactor(0.5f);

在处理相机的变焦因子时,我们必须知道1的因子等于Camera类的默认因子。大于1的缩放因子会将相机放大到场景中,而小于1的任何值都会将相机缩小。

处理缩放因子所涉及的数学是非常基本的。相机会简单地将变焦因子除以我们相机的WIDTHHEIGHT值,有效地使相机“变焦”。如果我们的相机的宽度是800,那么1.5f的缩放因子将向内缩放相机,最终将相机的宽度设置为533.3333,这将限制场景的显示面积。

在应用缩放因子(不等于 1)的情况下,ZoomCamera对象返回的getMinX()getMaxX()getMinY()getMaxY()getWidth()getHeight()值将自动被缩放因子除。

还有更多…

启用因子不等于 1 的变焦相机上的边界将对相机能够摇摄的总可用面积产生影响。假设边界的最小和最大 x 值设置为 0 到 800,如果相机宽度等于 800,则 x 轴上不允许有任何移动。如果我们放大相机,相机的宽度将会减小,从而允许相机的运动松弛。

如果定义了会导致摄像机的宽度或高度超出摄像机边界的缩放因子,缩放因子将应用于摄像机,但超出的轴上不允许移动。

另见

  • 介绍本章给出的相机对象
  • 用本章给出的绑定摄像头限制摄像头区域。

使用平滑相机创建平滑移动

SmoothCamera对象是四个摄像头中最高级的可供选择。这款相机支持所有不同类型的相机功能(边界、缩放等),并带有一个附加选项,用于在为相机设置新位置时,将定义的速度应用于相机的移动速度。结果可能看起来好像相机“轻松”地进出移动,允许相当微妙的相机移动。

怎么做…

这种摄像机类型是四种类型中唯一需要在构造函数中定义附加参数的类型。这些额外的参数包括相机可以行进的最大 x 和 y 速度,以及处理相机放大和缩小速度的最大缩放因子变化。让我们来看看这个相机作品会是什么样子:

// Camera movement speeds
final float maxVelocityX = 10;
final float maxVelocityY = 5;
// Camera zoom speed
final float maxZoomFactorChange = 5;

// Create smooth camera
mCamera = new SmoothCamera(0, 0, WIDTH, HEIGHT, maxVelocityX, maxVelocityY, maxZoomFactorChange);

它是如何工作的…

在这个配方中,我们正在创建一个相机,它对相机移动和缩放应用平滑的过渡效果。与其他三种相机类型不同,相机不是直接用setCenter(x,y)将相机中心设置到定义的位置,而是使用maxVelocityXmaxVelocityYmaxZoomFactorChange变量来定义相机从 A 点到 b 点的移动速度。增加速度将导致相机进行更快的移动。

对于SmoothCamera类,有两个选项,用于相机移动和相机缩放。我们可以通过调用这些任务的默认相机方法来允许相机平滑移动或缩放(camera.setCenter()camera.setZoomFactor())。另一方面,有时我们需要立即重新定位相机。这可以通过分别调用camera.setCenterDirect()camera.setZoomFactorDirect()方法来实现。这些方法最常用于重置平滑相机的位置。

另见

  • 介绍本章给出的相机对象
  • 用本章给出的绑定摄像头限制摄像头区域。
  • 用本章给出的变焦相机仔细观察。

收缩变焦相机功能

AndEngine 包括一个“检测器”类的小列表,可以与场景触摸事件结合使用。本主题将介绍PinchZoomDetector类的使用,以便通过按下显示器上的两个手指,将它们移动得更近或更远来调整缩放因子,从而允许缩放相机。

开始…

请参考代码包中名为ApplyingPinchToZoom的类。

怎么做…

按照以下步骤进行设置缩放功能的演练。

  1. 我们必须做的第一件事是在我们的类中实现适当的侦听器。由于我们将处理触摸事件,我们需要包括IOnSceneTouchListener界面。此外,我们需要实现IPinchZoomDetectorListener界面来处理相机缩放因子的变化,等待触摸事件:

    java public class ApplyingPinchToZoom extends BaseGameActivity implements IOnSceneTouchListener, IPinchZoomDetectorListener {

  2. BaseGameActivity类的onCreateScene()方法中,将Scene对象的触摸监听器设置为this活动,因为我们正在让BaseGameActivity类实现触摸监听器类。我们还将在此方法中创建并启用mPinchZoomDetector对象:

    ```java / Set the scene to listen for touch events using * this activity's listener / mScene.setOnSceneTouchListener(this);

    / Create and set the zoom detector to listen for * touch events using this activity's listener / mPinchZoomDetector = new PinchZoomDetector(this);

    // Enable the zoom detector mPinchZoomDetector.setEnabled(true); ```

  3. BaseGameActivity类实现的onSceneTouchEvent()方法中,我们必须将触摸事件传递给mPinchZoomDetector对象:

    java @Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { // Pass scene touch events to the pinch zoom detector mPinchZoomDetector.onTouchEvent(pSceneTouchEvent); return true; }

  4. 接下来,当mPinchZoomDetector对象记录到用户正在将两个手指应用于显示器时,我们将获得ZoomCamera对象的初始缩放因子。我们将使用onPinchZoomStarted()方法,通过IPinchZoomDetectorListener界面实现:

    java /* This method is fired when two fingers press down * on the display */ @Override public void onPinchZoomStarted(PinchZoomDetector pPinchZoomDetector, TouchEvent pSceneTouchEvent) { // On first detection of pinch zooming, obtain the initial zoom factor mInitialTouchZoomFactor = mCamera.getZoomFactor(); }

  5. 最后,如果在显示器上检测到挤压运动,我们将更改ZoomCamera对象的缩放因子。该代码将放置在onPinchZoom()onPinchZoomFinished()方法中:

    ```java @Override public void onPinchZoom(PinchZoomDetector pPinchZoomDetector, TouchEvent pTouchEvent, float pZoomFactor) {

    / On every sub-sequent touch event (after the initial touch) we offset * the initial camera zoom factor by the zoom factor calculated by * pinch-zooming / final float newZoomFactor = mInitialTouchZoomFactor * pZoomFactor;

    // If the camera is within zooming bounds if(newZoomFactor < MAX_ZOOM_FACTOR && newZoomFactor > MIN_ZOOM_FACTOR){ // Set the new zoom factor mCamera.setZoomFactor(newZoomFactor); } } ```

它是如何工作的…

在本食谱中,我们覆盖了场景中发生的场景触摸事件,将触摸事件传递给PinchZoomDetector对象,该对象将处理ZoomCamera对象的缩放功能。下面的步骤将指导我们完成收缩缩放的工作过程。因为我们在这个活动中使用了缩放因子,所以我们需要使用一个ZoomCamera类或者一个SmoothCamera类实现。

在这个食谱的前两个步骤中,我们实现了所需的监听器,并将它们注册到mScene对象和mPinchZoomDetector对象。由于ApplyingPinchToZoom活动正在实现监听器,因此我们可以将代表我们的BaseGameActivity类的this传递给作为触摸监听器的mScene对象。我们也可以将此活动作为夹点检测监听器传递。一旦创建了挤压检测器,我们可以通过调用setEnabled(pSetEnabled)方法来启用或禁用它。

在第三步中,我们将pSceneTouchEvent对象传递给夹点检测器的onTouchEvent()方法。这样做将允许挤压检测器获得特定的触摸坐标,这些坐标将在内部用于根据手指位置计算缩放因子。

按下屏幕上的两个手指,挤压检测器将触发第四步中显示的代码片段。我们必须在这一点获得相机的初始缩放因子,以便在触摸坐标改变时适当地偏移缩放因子。

最后一步包括计算偏移缩放因子,并将其应用于ZoomCamera对象。通过将初始缩放因子乘以PinchZoomDetector对象计算的缩放因子变化,我们可以成功地偏移相机的缩放因子。一旦我们计算出newZoomFactor物体的值,我们就调用setZoomFactor(newZoomFactor)来改变相机的变焦级别。

将缩放因子包含在特定范围内就像添加一个if语句一样简单,指定我们需要的最小和/或最大缩放因子。在这种情况下,我们的相机不能比0.5f缩小得更远,也不能比1.5f放大得更近。

另见

  • 用本章给出的变焦相机仔细观察。

将背景拼接在一起

虽然 AndEngine 的Scene对象允许我们为场景设置背景,但这并不总是我们项目的可行解决方案。为了允许背景的平移和缩放,我们可以将多个纹理区域缝合在一起,并将它们作为精灵直接应用到场景中。本主题将涵盖将两个 800 x 480 纹理区域缝合在一起,以创建更大的可平移和可缩放的背景。背景拼接背后的想法是允许场景的部分以更小的块显示。这使我们有机会创建更小的纹理尺寸,而不会超过大多数设备的 1024 x 1024 最大纹理尺寸。此外,为了提高性能,我们可以启用剔除功能,这样当场景片段没有显示在屏幕上时,就不会绘制它们。请看下图的结果:

Stitching a together

开始...

执行配方,缩放相机功能,本章将介绍缩放相机的工作原理。此外,我们必须准备两个单独的 800 x 480 图像,类似于本食谱介绍中的上图,以 PNG 格式,然后参考代码包中名为StitchedBackground的类。

怎么做…

背景拼接是一个简单的概念,它包括将两个或多个精灵直接放置在彼此旁边、彼此之上或彼此之下,以便看起来有一个单一的大精灵。在这个食谱中,我们将介绍如何做到这一点,以避免可怕的纹理出血效果。请遵循以下步骤:

  1. 首先,我们需要创建我们的BuildableBitmapTextureAtlasITextureRegion对象。非常重要的一点是,纹理图谱与我们的图像文件大小完全相同,以避免纹理出血。此外,在纹理图谱的构建过程中,我们不能包含任何填充或间距。以下代码将创建左侧纹理图谱和纹理区域,但是相同的代码将适用于右侧:

    ```java / Create the background left texture atlas / BuildableBitmapTextureAtlas backgroundTextureLeft = new BuildableBitmapTextureAtlas( mEngine.getTextureManager(), 800, 480);

    / Create the background left texture region / mBackgroundLeftTextureRegion = BitmapTextureAtlasTextureRegionFactory .createFromAsset(backgroundTextureLeft, getAssets(), "background_left.png");

    / Build and load the background left texture atlas / try { backgroundTextureLeft .build(new BlackPawnTextureAtlasBuilder( 0, 0, 0)); backgroundTextureLeft.load(); } catch (TextureAtlasBuilderException e) { e.printStackTrace(); } ```

  2. 一旦纹理资源到位,我们可以移动到活动的onPopulateScene()方法,在这里我们将创建精灵并将其应用到Scene对象:

    ```java final int halfTextureWidth = (int) (mBackgroundLeftTextureRegion.getWidth() * 0.5f); final int halfTextureHeight = (int) (mBackgroundLeftTextureRegion.getHeight() * 0.5f);

    // Create left background sprite mBackgroundLeftSprite = new Sprite(halfTextureWidth, halfTextureHeight, mBackgroundLeftTextureRegion, mEngine.getVertexBufferObjectManager()) ; // Attach left background sprite to the background scene mScene.attachChild(mBackgroundLeftSprite);

    // Create the right background sprite, positioned directly to the right of the first segment mBackgroundRightSprite = new Sprite(mBackgroundLeftSprite.getX() + mBackgroundLeftTextureRegion.getWidth(), halfTextureHeight, mBackgroundRightTextureRegion, mEngine.getVertexBufferObjectManager());

    // Attach right background sprite to the background scene mScene.attachChild(mBackgroundRightSprite); ```

它是如何工作的…

背景拼接可以用于许多不同的场景,以避免某些问题。这些问题包括导致某些设备不兼容的过大纹理尺寸、无法响应相机位置或缩放因子变化的静态背景以及性能问题等。在这个食谱中,我们通过并排拼接两个Sprite对象来创建一个大背景,每个对象代表一个不同的TextureRegion对象。结果是 1600 x 480 像素的大背景是相机宽度的两倍。

在大多数情况下,当处理允许场景滚动的拼接背景时,我们需要启用一些相机边界,以便在相机位置试图超出背景区域时停止更新相机位置。我们可以使用一个ZoomCamera对象来做到这一点,将边界设置为背景的预定大小。由于我们正在处理两幅 PNG 图像,每幅 800 x 480 像素并排拼接,可以肯定地说坐标(0,0)(1600 x 480)对于相机边界来说已经足够了。

如第一步所述,使用这种方法创建大规模背景时,我们必须遵循一些规则。图像尺寸必须与BuildableBitmapTextureAtlas纹理图谱尺寸完全相同!如果不遵守这个规则,很可能会导致精灵之间定期出现伪像,这对于玩家来说非常分散注意力。这也意味着我们不应该在一个用于背景拼接的BuildableBitmapTextureAtlas对象中包含一个以上的ITextureRegion对象。填充和间距也是我们在这种情况下应该避免的特性之一。但是,遵循这些规则,我们仍然能够将TextureOptions.BILINEAR纹理过滤应用于纹理图谱,并且不会引起问题。

在第二步中,我们继续创建Sprite对象。这里没什么特别的;我们只需在给定位置创建一个Sprite对象,然后直接在第一个旁边设置下一个精灵。对于非常大和多样化的背景,这种将纹理拼接在一起的方法可以帮助我们停止渲染不再可见的较小背景部分,从而大幅降低应用的性能成本。这个功能叫做剔除。有关如何实现这一点的更多信息,请参见第 8 章、中的使用实体剔除禁用渲染

另见

  • 用精灵第二章设计你的菜单中的将场景带入生活。
  • 用本章给出的变焦相机仔细观察。
  • 本章中的收缩变焦相机功能
  • 第 8 章最大化性能中禁用实体剔除渲染

将平视显示器应用于摄像机

一个平视显示器 ( 平视显示器 )可以是一个非常有用的组件,即使是最简单的游戏。平视显示器的目的是包含一组按钮、文本或任何其他Entity对象,以便为用户提供界面。平显有两个关键点;首先是无论摄像头是否改变位置,HUD 的孩子在屏幕上总是可见的。第二点是 HUD 的孩子总是会被展现在场景的孩子面前。在本章中,我们将在相机上应用平视显示器,以便在游戏过程中为用户提供一个界面。

怎么做...

将以下代码导入您选择的任何BaseGameActivityonCreateEngineOptions()方法中,如有必要,替换该代码片段中的摄像机类型:

@Override
public EngineOptions onCreateEngineOptions() {

  // Create the camera
  Camera mCamera = new Camera(0, 0, WIDTH, HEIGHT);

  // Create the HUD
  HUD mHud = new HUD();

  // Attach the HUD to the camera
  mCamera.setHUD(mHud);

  EngineOptions engineOptions = new EngineOptions(true,
      ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(),
      mCamera);

  return engineOptions;
}

它是如何工作的…

和一个HUD班级一起工作通常是一件非常容易的事情。一个HUD类的有用性可以根据正在创建的游戏类型有很大的不同,但是无论如何,在决定使用这个类之前,我们必须知道一些事情:

  • HUD实体在摄像机移动时不会改变位置。一旦它们的位置被定义,实体将保持在屏幕上的位置,除非通过setPosition()另有设置。
  • HUD实体将始终出现在任何Scene实体的顶部,无论 z 索引、应用顺序或任何其他情况如何。
  • 剔除应该而不是应用于在任何情况下都要附加到HUD类的实体。剔除影响HUD类上的Entity对象的方式与影响Scene对象上的Entity对象的方式相同,即使Entity对象看起来没有移出屏幕。这将导致看似随机消失的HUD实体。别这么做!

中找到的代码怎么做...部分,我们可以看到设置HUD类非常容易。创建HUD对象并将其应用于相机,只需下面两行代码即可完成:

  // Create the HUD
  HUD mHud = new HUD();

  // Attach the HUD to the camera
  mCamera.setHUD(mHud);

从这一点来看,我们可以把HUD对象当作我们游戏中的任何其他层来应用实体。

将控制器应用于显示器

根据我们正在创建的游戏类型,有许多可能的玩家互动解决方案。AndEngine 包括两个独立的类,其中一个模拟一个叫做DigitalOnScreenControl的方向控制面板,另一个模拟一个叫做AnalogOnScreenControl的操纵杆。本主题将介绍 AndEngine 的AnalogOnScreenControl类,但是使用这个类将为我们提供足够的信息来使用任何一个控制器。

开始...

这个配方需要两个独立的资产,作为控制器的基础和控制器的旋钮。在进入之前,如何操作...部分,请在您选择的项目中的assets/gfx文件夹中包含一个名为controller_base.pngcontroller_knob.png的图像。图像可能看起来像下图,底部为 128 x 128 像素,旋钮为 64 x 64 像素:

Getting started...

怎么做…

一旦我们为控制器准备好了两个必要的资产,我们就可以开始编码了。首先,我们可以从创建持有每个控制器资产的ITextureRegionBuildableBitmapTextureAtlas对象开始。控制器纹理图谱或纹理区域不需要特殊步骤;简单地创建它们,就像我们创建普通雪碧一样。像往常一样,在你选择的活动的onCreateResources()方法中这样做。

一旦ITextureRegion对象已经被编码并准备在活动中使用,我们就可以在活动对象的onCreateScene()方法中创建AnalogOnScreenControl类,如下所示:

// Position the controller in the bottom left corner of the screen
final float controllerX = mControllerBaseTextureRegion.getWidth();
final float controllerY = mControllerBaseTextureRegion.getHeight();

// Create the controller
mController = new AnalogOnScreenControl(controllerX, controllerY, mCamera, mControllerBaseTextureRegion, mControllerKnobTextureRegion, 0.1f, mEngine.getVertexBufferObjectManager(), new IAnalogOnScreenControlListener(){
  /* The following method is called every X amount of seconds,
  * where the seconds are determined by the pTimeBetweenUpdates
  * parameter in the controller's constructor  */
  @Override
  public void onControlChange(
      BaseOnScreenControl pBaseOnScreenControl, float pValueX,
      float pValueY) {
    mCamera.setCenter(mCamera.getCenterX() + (pValueX * 10), mCamera.getCenterY() + (pValueY * 10));
    Log.d("Camera", String.valueOf(mCamera.getCenterX()));
  }

  // Fired when the knob is simply pressed
  @Override
  public void onControlClick(
      AnalogOnScreenControl pAnalogOnScreenControl) {
    // Do nothing
  }

});

// Initialize the knob to its center position
mController.refreshControlKnobPosition();

// Set the controller as a child scene
mScene.setChildScene(mController);

它是如何工作的…

如我们所见,一些参数与我们在创建Sprite对象时定义的参数没有什么不同。前五个参数不言自明。第六个参数(0.1f)是“更新间隔时间”参数。该值控制onControlChange()方法中事件的触发频率。更多的 CPU 密集型代码可能会受益于更新间隔时间的增加,而不太复杂的代码可能不会有更新时间非常短的问题。

我们必须包含在控制器构造器中的最后一个参数是IanalogOnScreenControlListener,它根据控制器是被简单点击还是控制器被按下并保持在偏移位置来处理事件。

正如我们在onControlChange()事件中看到的,我们可以通过pValueXpValueY变量获得控制器旋钮的当前位置。这些值包含控制器的 x 和 y 偏移。在这个食谱中,我们使用旋钮的 x 和 y 偏移来移动相机的位置,这也让我们知道如何使用这些变量来移动其他实体,例如玩家的精灵。

坐标转换

坐标转换在Scene对象依赖多个实体作为游戏精灵的基础层的情况下非常有用。包含许多家长的游戏并不少见,每个家长都有自己的一组孩子,需要在某一点或另一点获得孩子相对于Scene对象的位置。在整个游戏中,每个层在场景中保持相同(0,0)坐标的情况下,这不是问题。另一方面,当我们的图层开始四处移动时,子位置将随父位置一起移动,但它们在图层上的坐标将保持不变。本主题将介绍如何将场景坐标转换为本地坐标,以允许嵌套实体在场景中正确定位。

怎么做…

将以下代码导入您选择的任何BaseGameActivityonCreateScene()方法。

  1. 该配方的第一步是创建一个Rectangle对象并将其应用于Scene对象。这个Rectangle对象将作为另一个Rectangle对象的父实体。我们将其颜色设置为蓝色,以便在重叠时区分两个矩形,因为父Rectangle对象将不断移动:

    ```java / Create a rectangle on the Scene that will act as a layer / final Rectangle rectangleLayer = new Rectangle(0, HEIGHT * 0.5f, 200, 200, mEngine.getVertexBufferObjectManager()){

    / Obtain the half width of this rectangle / int halfWidth = (int) (this.getWidth() * 0.5f);

    / Boolean value to determine whether to pan left or right / boolean incrementX = true;

    @Override protected void onManagedUpdate(float pSecondsElapsed) {

    float currentX = this.getX();
    
    /* Determine whether or not the layer should pan left or right */
    if(currentX + halfWidth > WIDTH){
      incrementX = false;
    }
    else if (currentX - halfWidth < 0){
      incrementX = true;
    }
    /* Increment or decrement the layer's position based on incrementX */
    if(incrementX){
      this.setX(currentX + 5f);
    } else {
      this.setX(currentX - 5f);
    }
    
    super.onManagedUpdate(pSecondsElapsed);
    

    } };

    rectangleLayer.setColor(0, 0, 1);

    // Attach the layer to the scene mScene.attachChild(rectangleLayer); ```

  2. 接下来,我们将把子Rectangle对象添加到我们创建的第一个Rectangle对象中。这个Rectangle物体不会动;相反,它将保持在屏幕的中心,而它的父对象继续四处移动。该Rectangle对象将利用坐标转换来保持其位置:

    ```java / Create a smaller, second rectangle and attach it to the first / Rectangle rectangle = new Rectangle(0, 0, 50, 50, mEngine.getVertexBufferObjectManager()){

    / Obtain the coordinates in the middle of the Scene that we will * convert to everytime the parent rectangle moves / final float convertToMidSceneX = WIDTH * 0.5f; final float convertToMidSceneY = HEIGHT * 0.5f;

    @Override protected void onManagedUpdate(float pSecondsElapsed) {

    /* Convert the specified x/y coordinates into Scene coordinates,
      * passing the resulting coordinates into the convertedCoordinates array */
    final float convertedCoordinates[] = rectangleLayer.convertSceneCoordinatesToLocalCoordinates(convertToMidSceneX, convertToMidSceneY);
    
    /* Since the parent is moving constantly, we must adjust this rectangle's
     * position on every update as well. This will keep in in the center of the 
     * display at all times */
    this.setPosition(convertedCoordinates[0], convertedCoordinates[1]);
    
    super.onManagedUpdate(pSecondsElapsed);
    

    }

    };

    / Attach the second rectangle to the first rectangle / rectangleLayer.attachChild(rectangle); ```

它是如何工作的…

上面的onCreateScene()方法创建了一个包含两个独立Rectangle实体的Scene对象。第一个Rectangle实体将直接附加到Scene对象。第二个Rectangle实体将附加到第一个Rectangle实体。名为rectangleLayer的第一个Rectangle实体将不断地从左向右和从右向左移动。通常,这将导致其子实体的位置遵循相同的移动模式,但在本食谱中,我们使用坐标转换来允许子实体Rectangle在其父实体移动时保持静止。

本食谱中的rectangle对象包括两个名为convertToMidSceneXconvertToMidSceneY的变量。这些变量只是保持在Scene坐标中的位置,我们希望将局部坐标转换为该位置。如我们所见,它们的坐标被定义到场景的中间。在rectangle对象的onManagedUpdate()方法中,我们使用rectangleLayer.convertSceneCoordinatesToLocalCoordinates(convertToMidSceneX, convertToMidSceneY)方法,将结果坐标传递给一个浮点数组。这主要是问rectangleLayer对象,“在你看来,x/y 在场景中的位置在哪里?”由于rectangleLayer对象直接附着在Scene对象上,它可以很容易地确定具体的Scene坐标在哪里,因为它依赖于原生的Scene坐标系。

当试图访问返回的坐标时,我们可以访问convertedCoordinates[0]获得转换后的 x 坐标,并使用convertedCoordinates[1]获得转换后的 y 坐标。

除了将Scene转换为本地Entity坐标之外,我们还可以将本地Entity转换为Scene坐标、触摸事件坐标、相机坐标以及一系列其他选项。然而,一旦我们对坐标转换有了基本的了解,从这个食谱开始,其余的转换方法将看起来非常相似。

创建分屏游戏

这个食谱将介绍DoubleSceneSplitScreenEngine类,最常用于允许多个玩家在显示器的每一半上玩他们自己的游戏实例的游戏。DoubleSceneSplitScreenEngine类允许我们为设备显示器的每一半提供自己的SceneCamera对象,让我们完全控制显示器的每一半将看到什么。

开始…

请参考代码包中名为SplitScreenExample的类。

怎么做…

设置我们的游戏以允许两个独立的Scene对象需要我们在最初设置BaseGameActivity类时采取稍微不同的方法。然而,一旦我们设置了单独的Scene对象,管理它们实际上非常类似于我们只处理一个场景,除了每个场景只有一半的原始显示空间。执行以下步骤,了解如何设置DoubleSceneSplitScreenEngine课程。

  1. 我们必须注意的第一件事是将WIDTH值减少一半,因为每个摄像头将需要设备显示的一半。尝试将 800 个像素的宽度放在每个相机上会导致每个场景中的对象出现明显的倾斜。在我们声明变量的同时,我们还将设置两个Scene对象和两个Camera对象,这是DoubleSceneSplitScreenEngine实现所需要的:

    ```java public static final int WIDTH = 400; public static final int HEIGHT = 480;

    / We'll need two Scene's for the DoubleSceneSplitScreenEngine / private Scene mSceneOne; private Scene mSceneTwo;

    / We'll also need two Camera's for the DoubleSceneSplitScreenEngine / private SmoothCamera mCameraOne; private SmoothCamera mCameraTwo; ```

  2. 接下来,我们将在BaseGameActivity类的onCreateEngineOptions()方法中创建两个单独的SmoothCamera对象。这些摄像机将用于显示显示器每一半的独立视图。在这个食谱中,我们应用了自动缩放以显示DoubleSceneSplitScreenEngine :

    ```java / Create the first camera (Left half of the display) / mCameraOne = new SmoothCamera(0, 0, WIDTH, HEIGHT, 0, 0, 0.4f){ / During each update to the camera, we will determine whether * or not to set a new zoom factor for this camera / @Override public void onUpdate(float pSecondsElapsed) { final float currentZoomFactor = this.getZoomFactor(); if(currentZoomFactor >= MAX_ZOOM_FACTOR){ this.setZoomFactor(MIN_ZOOM_FACTOR); } else if(currentZoomFactor <= MIN_ZOOM_FACTOR){ this.setZoomFactor(MAX_ZOOM_FACTOR); } super.onUpdate(pSecondsElapsed); } }; / Set the initial zoom factor for camera one/ mCameraOne.setZoomFactor(MAX_ZOOM_FACTOR);

    / Create the second camera (Right half of the display) / mCameraTwo = new SmoothCamera(0, 0, WIDTH, HEIGHT, 0, 0, 1.2f){ / During each update to the camera, we will determine whether * or not to set a new zoom factor for this camera / @Override public void onUpdate(float pSecondsElapsed) { final float currentZoomFactor = this.getZoomFactor(); if(currentZoomFactor >= MAX_ZOOM_FACTOR){ this.setZoomFactor(MIN_ZOOM_FACTOR); } else if(currentZoomFactor <= MIN_ZOOM_FACTOR){ this.setZoomFactor(MAX_ZOOM_FACTOR); } super.onUpdate(pSecondsElapsed); } }; / Set the initial zoom factor for camera two / mCameraTwo.setZoomFactor(MIN_ZOOM_FACTOR); ```

    的结果 3. 我们的BaseGameActivity类的onCreateEngineOptions()方法中还有一个任务需要处理,就是创建EngineOptions对象,传递mCameraOne对象作为主摄像机。此外,场景可能需要同时触摸事件,因此我们也将启用多点触摸:

    ```java / The first camera is set via the EngineOptions creation, as usual / EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(), mCameraOne);

    / If users should be able to control each have of the display * simultaneously with touch events, we'll need to enable * multi-touch in the engine options / engineOptions.getTouchOptions().setNeedsMultiTouch(true); ```

  3. 在第四步中,我们将覆盖BaseGameActivity类的onCreateEngine()方法,以便创建一个DoubleSceneSplitScreenEngine对象,而不是默认的Engine对象:

    ```java @Override public Engine onCreateEngine(EngineOptions pEngineOptions) {

    / Return the DoubleSceneSplitScreenEngine, passing the pEngineOptions * as well as the second camera object. Remember, the first camera has * already been applied to the engineOptions which in-turn applies the * camera to the engine. / return new DoubleSceneSplitScreenEngine(pEngineOptions, mCameraTwo); } ```

  4. 转到onCreateScene()方法,我们将创建两个Scene对象,按照我们的选择设置它们,最后将每个Scene对象设置为DoubleSceneSplitScreenEngine对象:

    ```java @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) {

    / Create and setup the first scene / mSceneOne = new Scene(); mSceneOne.setBackground(new Background(0.5f, 0, 0));

    / In order to keep our camera's and scenes organized, we can * set the Scene's user data to store its own camera / mSceneOne.setUserData(mCameraOne);

    / Create and setup the second scene / mSceneTwo = new Scene(); mSceneTwo.setBackground(new Background(0,0,0.5f));

    / Same as the first Scene, we set the second scene's user data * to hold its own camera / mSceneTwo.setUserData(mCameraTwo);

    / We must set the second scene within mEngine object manually. * This does NOT need to be done with the first scene as we will * be passing it to the onCreateSceneCallback, which passes it * to the Engine object for us at the end of onCreateScene()/ ((DoubleSceneSplitScreenEngine) mEngine).setSecondScene(mSceneTwo);

    / Pass the first Scene to the engine / pOnCreateSceneCallback.onCreateSceneFinished(mSceneOne); } ```

  5. 现在,我们的两个Camera对象都已设置,并且我们的两个Scene对象都已设置并连接到引擎,我们可以开始将Entity对象连接到我们认为合适的每个Scene对象,只需像往常一样指定要连接哪个Scene对象即可。该代码应放在BaseGameActivity类的onPopulateScene()方法中:

    ```java / Apply a rectangle to the center of the first scene / Rectangle rectangleOne = new Rectangle(WIDTH * 0.5f, HEIGHT * 0.5f, rectangleDimensions, rectangleDimensions, mEngine.getVertexBufferObjectManager()); rectangleOne.setColor(org.andengine.util.adt.color.Color.BLUE); mSceneOne.attachChild(rectangleOne);

    /* Apply a rectangle to the center of the second scene */
    Rectangle rectangleTwo = new Rectangle(WIDTH * 0.5f, HEIGHT * 0.5f, rectangleDimensions, rectangleDimensions, mEngine.getVertexBufferObjectManager());
    rectangleTwo.setColor(org.andengine.util.adt.color.Color.RED);
    mSceneTwo.attachChild(rectangleTwo);
    

    ```

它是如何工作的...

当使用DoubleSceneSplitScreenEngine类时,我们可以假设,如果我们正在为多人游戏进行设置,我们的项目将需要两样东西。更具体地说,我们需要两个Scene对象和两个Camera对象。由于我们正在将每个Camera对象的观看区域分成两半,我们将把相机的WIDTH值减少一半。在大多数情况下,宽度为 400 像素、高度为 480 像素的相机尺寸是合理的,这也允许我们对实体保持适当的视角。

在第二步中,我们将设置两个SmoothCamera对象,它们将自动放大和缩小各自的场景,以便为该配方提供视觉结果。但是,DoubleSceneSplitScreenEngine类可以使用Camera对象的任何变体,包括最基本的类型,而不会导致任何问题。

第三步,我们继续创建EngineOptions对象。我们在EngineOptions构造函数中提供mCameraOne对象作为pCamera参数,就像在任何普通情况下一样。此外,我们正在EngineOptions对象中启用多点触摸,以便允许同时为每个Scene对象注册触摸事件。忽略多点触摸设置将导致每个场景必须等到另一个场景没有被按下,才能注册触摸事件。

在第四步,我们创建DoubleSceneSplitScreenEngine对象,传入上一步创建的pEngineOptions参数以及第二个Camera对象— mCameraTwo。在代码的这一点上,我们现在已经将我们的两个摄像机注册到引擎上;第一个在EngineOptions对象中注册,第二个作为参数传递给DoubleSceneSplitScreenEngine类。

第五步包括BaseGameActivity类的onCreateScene()方法,在这里我们将创建和设置我们想要的两个Scene对象。在最基本的层面上,这包括创建Scene对象,启用和设置或禁用场景的背景,设置场景的用户数据以存储其各自的摄像机,最后将Scene对象传递给我们的mEngine对象。当第二个Scene对象要求我们在我们的mEngine对象上调用setSecondScene(mSceneTwo)方法时,mSceneOne对象像在任何BaseGameActivity中一样被传递给Engine对象;在pOnCreateSceneCallback.onCreateSceneFinished(mSceneOne)法中。

第六步,我们现在可以说是“走出了困境”。此时,我们已经完成了引擎、场景和相机的设置,现在我们可以按照自己的意愿开始填充每个场景。就我们目前所能做的而言,可能性相当广泛,包括将第二个场景用作迷你地图、多人游戏的视图、第一个场景的替代视角等等。此时选择要将Entity对象附加到哪个Scene对象,就像调用mSceneOne.attachChild(pEntity)mSceneTwo.attachChild(pEntity)一样简单。