九、AndEngine 扩展概述

在这一章中,我们将介绍一些 AndEngine 最流行的扩展的用途和用法。本章包含以下主题:

  • 创建实时壁纸
  • 与多人扩展联网
  • 使用可缩放矢量图形 ( SVG )创建高分辨率图形
  • 带有 SVG 纹理区域的颜色映射

简介

在扩展概述一章中,我们将开始使用许多没有与 AndEngine 打包在一起的类。有许多扩展可以被编写来为任何默认和嵌入式游戏添加各种改进或额外的功能。在这一章中,我们将使用三个主要的扩展,允许我们使用 AndEngine 创建实时壁纸,创建允许多个设备直接相互连接或连接到专用服务器的在线游戏,最后,将 SVG 文件作为纹理区域合并到我们的游戏中,从而在我们的游戏中实现高分辨率和可伸缩的图形。

AndEngine 包括一个相对较长的扩展列表,可以包含在我们的项目中,以使某些任务更容易完成。不幸的是,由于扩展的数量和其中一些的当前状态,我们在扩展的数量上受到限制,这可以包含在本章中。然而,大多数 AndEngine 扩展相对容易使用,包括可以从 Nicolas Gramlich 的公共 GitHub 资源库–https://github.com/nicolasgramlich获得的示例项目。以下是附加和工程扩展的列表,以及描述其用途的简要说明:

  • AndEngineCocosBuilderExtension : 这个扩展允许开发者通过使用所见即所得来创建游戏,你看到的就是你得到的概念。这种方法允许开发人员使用桌面计算机的 CocosBuilder 软件在基于图形用户界面的拖放环境中构建应用。这个扩展可以帮助菜单和关卡设计变得简单,就像在屏幕上放置对象并将设置导出到一个可以通过AndEngineCocosBuilderExtension扩展读入的文件一样。
  • AndEngineAugmentedRealityExtension:增强现实扩展允许开发人员轻松地将原本普通的 AndEngine 活动转换为增强现实活动,这将在屏幕上显示设备的物理摄像头视图。然后,我们能够将实体附加到屏幕上显示的摄像机视图的顶部。
  • AndEngineTexturePackerExtension : 该扩展允许开发人员导入通过使用桌面计算机的 TexturePacker 程序创建的精灵表。这个程序通过允许我们将我们的图像拖放到程序中,将完成的精灵表导出为一个易于理解的格式,并简单地将其加载到我们的项目中,扩展为AndEngineTexturePackerExtension,使得创建精灵表变得非常容易。
  • AndEngineTMXTiledMapExtensions : 这个扩展基于游戏玩法的平铺地图风格,可以大大提高游戏的生产力。使用 TMX 切片地图编辑器,开发人员可以简单地将子画面/切片拖放到基于网格的级别编辑器上,以创建级别。一旦在编辑器中创建了级别,只需将其导出为.tmx文件格式,然后使用AndEngineTMXTiledMapExtension将级别加载到我们的项目中。

制作直播壁纸

实时壁纸扩展是对现有安卓开发资源的一个很好的补充。有了这个扩展,我们可以通过使用我们在游戏开发中习惯使用的所有普通类和工程类来轻松创建壁纸。在本主题中,我们将创建一个包含简单粒子系统的实时壁纸,该系统在屏幕顶部生成粒子。壁纸设置将包括允许用户增加粒子移动速度的值。

这个食谱假设你至少对安卓软件开发工具包的Activity类有一个基本的了解,并且对安卓视图对象有一个大致的了解,比如SeekBarsTextViews

做好准备

直播壁纸不是你典型的安卓活动。相反,它们是一种服务,在建立项目方面需要稍微不同的方法。在访问代码之前,让我们继续为实时壁纸创建必要的文件夹和文件。

参考代码包中名为LiveWallpaperExtensionExample的项目。

我们将在下一节介绍驻留在每个文件中的代码:

  1. res/layout文件夹中创建或覆盖当前main.xml文件,将其命名为settings_main.xml。该布局文件将用于创建设置活动布局,其中壁纸的属性由用户调整。
  2. res文件夹中创建新文件夹xml。在这个文件夹中,创建一个新的xml文件并命名为wallpaper.xml。该文件将用作壁纸图标、描述的参考,以及用于修改壁纸属性的设置活动的参考。

怎么做…

我们将从填充所有的 XML 文件开始,以适应实时壁纸服务。这些文件包括settings_main.xmlwallpaper.xml,最后是AndroidManifest.xml

  1. Create the settings_main.xml layout file:

    第一步包括将settings_main.xml文件定义为壁纸设置活动的布局。没有规则限制开发者使用特定的布局风格,但是对于实时壁纸来说,最常见的方法是使用简单的TextView和相应的Spinner来修改实时壁纸的可调值。

  2. 打开res/xml/文件夹中的wallpaper.xml文件。将以下代码导入wallpaper.xml :

    java <?xml version="1.0" encoding="utf-8"?> <wallpaper xmlns:android="http://schemas.android.com/apk/res/android" android:settingsActivity="com.Live.Wallpaper.Extension.Example.LiveWallpaperSettings" android:thumbnail="@drawable/ic_launcher"/>

  3. Modify AndroidManifest.xml to suit the wallpaper service needs:

    第三步,我们必须修改AndroidManifest.xml才能让我们的项目作为壁纸服务运行。在项目的AndroidManifest.xml文件中,用以下内容替换<manifest>标签中的所有代码:

    ```java

        <meta-data
            android:name="android.service.wallpaper"
            android:resource="@xml/wallpaper" />
    </service>
    
    <activity
        android:name=".LiveWallpaperSettings"
        android:exported="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/live_wallpaper_settings"
        android:theme="@android:style/Theme.Black" >
    </activity>
    

    ```

一旦这三个 xml 文件得到处理,我们就可以创建实时壁纸所需的类。我们将使用三个类来处理实时壁纸的执行。这些类是LiveWallpaperExtensionService.javaLiveWallpaperSettings.javaLiveWallpaperPreferences.java,将在以下步骤中介绍:

  1. Create the live wallpaper preferences class:

    LiveWallpaperPreferences.java类类似于我们在第 1 章保存和加载游戏数据部分中介绍的偏好类。在这种情况下,首选项类的主要目的是处理衍生粒子的速度值。以下方法用于保存和加载粒子的速度值。请注意,当我们希望粒子向屏幕底部移动时,我们否定了mParticleSpeed值:

    ```java // Return the saved value for the mParticleSpeed variable public int getParticleSpeed(){ return -mParticleSpeed; }

    // Save the mParticleSpeed value to the wallpaper's preference file public void setParticleSpeed(int pParticleSpeed){ this.mParticleSpeed = pParticleSpeed; this.mSharedPreferencesEditor.putInt(PARTICLE_SPEED_KEY, mParticleSpeed); this.mSharedPreferencesEditor.commit(); } ```

  2. Create the live wallpaper settings activity:

    实时壁纸的设置活动扩展了安卓 SDK 的Activity类,使用settings_main.xml文件作为活动的布局。本活动的目的是根据SeekBar对象的进度获取mParticleSpeed变量的值。退出设置活动后,mParticleSpeed值将保存到我们的首选项中。

  3. Create the live wallpaper service:

    设置实时壁纸的最后一步是创建LiveWallpaperExtensionService.java类,包含实时壁纸服务的代码。为了指定我们希望该类使用实时壁纸扩展类,我们只需将extends BaseLiveWallpaperService添加到LiveWallpaperExtensionService.java声明中。一旦这样做了,我们可以看到建立一个BaseLiveWallpaperService类与从现在开始建立一个BaseGameActivity类非常相似,允许我们加载资源,应用精灵,或者任何其他我们已经习惯的普通和工程任务。

它是如何工作的…

如果我们看一下整个项目,这个食谱是一个相当大的食谱,但幸运的是,大多数与类文件相关的代码已经在前面的章节中讨论过了,所以不要担心!为了简洁起见,我们将省略前面章节中已经讨论过的类。看看中提到的话题,了解更多...如有需要,分段进行复习。

第一步,我们所做的就是创建一个最小的安卓xml布局,用于设置活动。很有可能跳过这一步,使用 AndEngine 的BaseGameActivity进行设置活动,但是为了简单起见,我们使用了非常基本的TextView/SeekBar方法。为了方便起见,这使得开发人员和用户都很方便。尽量保持这个屏幕的整洁,因为它是一个简单的屏幕,目的很简单。

在第二步中,我们将创建wallpaper.xml文件,作为AndroidManifest.xml文件中实时壁纸服务所需的一些规范的参考。这个文件只是用来存储服务的属性,包括包和类名,或者“链接”到通过按设置启动的设置活动...按钮在壁纸预览期间。wallpaper.xml还包括对壁纸选择窗口中使用的图标的引用。

在第三步中,我们正在修改AndroidManifest.xml文件,以允许我们运行实时壁纸服务作为这个项目的主要组件,而不是启动一个活动。在<service>标签中,我们包括了壁纸服务的nameiconlabel属性。这些属性具有与活动相同的目的。另外两个属性是android:enabled="true",意思是我们希望默认启用壁纸服务,还有android:permission="android.permission.BIND_WALLPAPER"属性,意思是只有安卓操作系统可以绑定到服务。活动的属性是相似的,除了我们包括了exportedtheme属性,排除了enabledpermission属性。android:exported="true"属性表示活动可以通过外部进程启动,而主题属性将改变设置活动界面的外观。

第四步包括创建首选项类,我们将使用它来存储可供用户调整的值。在这个配方中,我们在 preferences 类中包含了一个名为mParticleSpeed的值,以及相应的 getter 和 setter 方法。在一个更复杂的实时壁纸中,我们可以在这个类的基础上构建,允许我们轻松地为我们的壁纸添加或移除任意多的可定制属性的变量。

在第五步中,我们正在创建当用户按下设置时显示的Activity类...按钮在直播壁纸预览画面上。在这个特殊的Activity中,我们获得了settings_main.xml文件作为我们的布局,包含两个TextView视图类型用于显示标签和相应的值,一个SeekBar允许操纵壁纸的可调值。这个Activity最重要的任务是一旦用户选择了他们的理想速度,就能够保存到偏好文件中。这是通过在SeekBar意识到用户已经移动了SeekBar滑块时调整mParticleSpeed变量来实现的:

// OnProgressChanged represents a movement on the slider
  @Override
  public void onProgressChanged(SeekBar seekBar, int progress,
      boolean fromUser) {
    // Set the mParticleSpeed depending on the SeekBar's position(progress)
    mParticleSpeed = progress;

除了更新mParticleSpeed值,关联TextView也在此事件中更新。但是,在用户离开设置活动之前,该值实际上不会保存到首选项文件中,以避免对首选项文件进行不必要的覆盖。为了将新值保存到偏好文件中,我们可以在最小化Activity类的过程中从LiveWallpaperPreferences单例调用setParticleSpeed(mParticleSpeed):

@Override
protected void onPause() {
  // onPause(), we save the current value of mParticleSpeed to the preference file.
  // Anytime the wallpaper's lifecycle is executed, the mParticleSpeed value is loaded
  LiveWallpaperPreferences.getInstance().setParticleSpeed(mParticleSpeed);
  super.onPause();
}

在第六步,也是最后一步,我们终于可以开始为我们的实时壁纸的视觉方面编码了。在这张特殊的壁纸中,我们在视觉吸引力方面保持简单,但我们确实涵盖了开发壁纸的所有必要信息。如果我们看一下LiveWallpaperExtensionService类,需要注意的几个关键变量,包括以下几个:

  private int mParticleSpeed;

  // These ratio variables will be used to keep proper scaling of entities
  // regardless of the screen orientation
  private float mRatioX;
  private float mRatioY;

虽然我们已经在其他类的解释中讨论了mParticleSpeed变量,但很明显,我们将使用这个变量来最终确定粒子的速度,因为这是处理ParticleSystem对象的类。上面声明的另外两个“比例”变量是为了帮助我们为我们的实体保持一个适当的比例。如果用户将其设备从横向倾斜到纵向,或者从纵向倾斜到横向,则需要这些变量,因此我们可以根据表面视图的宽度和高度来计算粒子的比例。这是为了防止我们的实体在方向改变时被拉伸或扭曲。跳到这个类的底部被覆盖的方法,下面的代码确定了mRatioXmRatioY的值:

@Override
public void onSurfaceChanged(GLState pGLState, int pWidth, int pHeight) {

  if(pWidth > pHeight){
      mRatioX = 1;
      mRatioY = 1;
    } else {
      mRatioX = ((float)pHeight) / pWidth;
      mRatioY = ((float)pWidth) / pHeight;
    }

    super.onSurfaceChanged(pGLState, pWidth, pHeight);
  }

我们可以在这里看到if语句正在检查设备是处于横向模式还是纵向模式。如果pWidth大于pHeight,则表示方向当前处于横向模式,将 x 和 y 比例设置为默认值 1。另一方面,如果设备设置为纵向模式,那么我们必须重新计算粒子实体的比例。

一旦onSurfaceChanged()方法被处理,让我们继续到剩余的关键点,下一个是偏好管理。照顾偏好是一项相当琐碎的任务。首先,我们应该初始化首选项文件,以防这是第一次启动壁纸。我们通过从onCreateEngineOptions()中的LiveWallpaperPreferences实例调用initPreferences(this)方法来实现这一点。我们还需要覆盖onResume()方法,以便通过从LiveWallpaperPreferences实例调用getParticleSpeed()方法来加载mParticleSpeed变量,该变量具有存储在首选项文件中的值。

最后,我们来到了为实时壁纸设置的剩余步骤,这是设置粒子系统。这个特殊的粒子系统并不太花哨,但它确实包括一个ParticleModifier对象,其中包括一些需要注意的点。因为我们在粒子系统中增加了一个IParticleModifier接口,所以每次更新每个粒子时,我们都可以访问系统产生的单个粒子。在onUpdateParticle()方法中,我们将基于从首选项文件加载的mParticleSpeed变量来设置粒子的速度:

  // speed set by the preferences...
  if(currentVelocityY != mParticleSpeed){
    // Adjust the particle's velocity to the proper value
    particlePhysicsHandler.setVelocityY(mParticleSpeed);
  }

如果粒子的比例不等于mRatioX/mRatioY值,我们还必须调整粒子的比例,以补偿设备方向:

  // If the particle's scale is not equal to the current ratio
  if(entity.getScaleX() != mRatioX){
    // Re-scale the particle to better suit the current screen ratio
    entity.setScale(mRatioX, mRatioY);
  }

这就是用 AndEngine 设置实时壁纸所需要的全部内容!试着玩一下粒子系统,给设置添加新的可定制值,看看你能想到什么。有了这个扩展,您将可以立即开始运行,创建新的实时壁纸!

另请参见…

  • 第一章和【引擎游戏结构】中的保存和加载游戏数据部分。
  • 第二章中的使用粒子系统部分使用实体

与多人扩展联网

在这里,我们来到了毫无疑问最受欢迎的游戏设计方面。这当然是多人游戏。在这个项目配方中,我们将与 AndEngine 的多人扩展合作,以便直接在移动设备上创建一个功能齐全的客户端和服务器。一旦我们涵盖了这个扩展所包含的使网络编程变得更容易的一系列类和特性,您将能够将您的在线游戏想法变成现实!

做好准备

为了满足项目的可读性,创建一个多人游戏需要相当多的组件。

参考代码包中名为MultiplayerExtensionExample的项目。

为此,我们将把这些不同的组件分成五类。

创建一个新的安卓项目,命名为MultiplayerExtensionExample。项目准备就绪后,用以下名称创建四个新的类文件:

  • MultiplayerExtensionExample.java:配方的BaseGameActivity
  • MultiplayerServer.java:包含主服务器组件的类
  • MultiplayerClient.java:包含主客户端组件的类
  • ServerMessages.java:包含要从服务器发送到客户端的消息的类
  • ClientMessages.java:包含要从客户端发送到服务器的消息的类

打开项目的AndroidManifest.xml文件,添加以下两个<uses-permission>属性:

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>

怎么做...

为了在整个食谱中保持事物的相对性,我们将按照在准备部分中提到的顺序,从MultiplayerExtensionExample类开始与每个类一起工作。

  1. 声明并注册mMessagePool :

    java this.mMessagePool.registerMessage(ServerMessages.SERVER_MESSAGE_ADD_POINT, AddPointServerMessage.class); this.mMessagePool.registerMessage(ClientMessages.CLIENT_MESSAGE_ADD_POINT, AddPointClientMessage.class);

    的服务器/客户端消息 2. 配置场景触摸监听器,允许向服务器发送消息和从服务器接收消息:

    ```java if (pSceneTouchEvent.getAction() == TouchEvent.ACTION_MOVE) { if (mServer != null) {

    if(mClient != null){
      // Obtain a ServerMessage object from the mMessagePool
      AddPointServerMessage message = (AddPointServerMessage) MultiplayerExtensionExample.this.mMessagePool.obtainMessage(ServerMessages.SERVER_MESSAGE_ADD_POINT);
      // Set up the message with the device's ID, touch coordinates and draw color
      message.set(SERVER_ID, pSceneTouchEvent.getX(), pSceneTouchEvent.getY(), mClient.getDrawColor());
      // Send the client/server's draw message to all clients
      mServer.sendMessage(message);
      // Recycle the message back into the message pool
      MultiplayerExtensionExample.this.mMessagePool.recycleMessage(message);
    return true;
    }
    // If device is running as a client...
    

    } else if(mClient != null){ / Similar to the message sending code above, except * in this case, the client is not running as a server. * This means we have to first send the message to the server * via a ClientMessage rather than ServerMessage / AddPointClientMessage message = (AddPointClientMessage) MultiplayerExtensionExample.this.mMessagePool.obtainMessage(ClientMessages.CLIENT_MESSAGE_ADD_POINT); message.set(CLIENT_ID, pSceneTouchEvent.getX(), pSceneTouchEvent.getY(), mClient.getDrawColor()); mClient.sendMessage(message); MultiplayerExtensionExample.this.mMessagePool.recycleMessage(message);

    return true;
    

    }
    } ```

  2. 创建对话框切换语句,提示用户选择充当服务器还是客户端。如果选择了服务器或客户端组件,我们将初始化两个组件之一:

    ```java mServer = new MultiplayerServer(SERVER_PORT); mServer.initServer();

    // or...

    mClient = new MultiplayerClient(mServerIP,SERVER_PORT, mEngine, mScene); mClient.initClient(); ```

  3. Override the activity's onDestroy() method to terminate both the server and client components when the activity is destroyed:

    ```java @Override protected void onDestroy() { // Terminate the client and server socket connections // when the application is destroyed if (this.mClient != null) this.mClient.terminate();

    if (this.mServer != null) this.mServer.terminate(); super.onDestroy(); } ```

    一旦所有主要活动的功能到位,我们就可以继续编写服务器端代码了。

  4. 创建服务器的初始化方法——创建SocketServer对象,该对象处理与服务器客户端的连接:

    ```java // Create the SocketServer, specifying a port, client listener and // a server state listener (listeners are implemented in this class) MultiplayerServer.this.mSocketServer = new SocketServer( MultiplayerServer.this.mServerPort, MultiplayerServer.this, MultiplayerServer.this) {

      // Handle client connection here...
    

    }; ```

  5. 处理客户端与服务器的连接。这包括注册客户端消息并定义如何处理它们:

    ```java // Called when a new client connects to the server... @Override protected SocketConnectionClientConnector newClientConnector( SocketConnection pSocketConnection) throws IOException { // Create a new client connector from the socket connection final SocketConnectionClientConnector clientConnector = new SocketConnectionClientConnector(pSocketConnection);

    // Register the client message to the new client
    

    clientConnector.registerClientMessage(ClientMessages.CLIENT_MESSAGE_ADD_POINT, AddPointClientMessage.class, new IClientMessageHandler(){

    // Handle message received by the server...
    @Override
    public void onHandleMessage(
        ClientConnector<SocketConnection> pClientConnector,
        IClientMessage pClientMessage)
        throws IOException {
      // Obtain the client message
      AddPointClientMessage incomingMessage = (AddPointClientMessage) pClientMessage;
    
      // Create a new server message containing the contents of the message received
      // from a client
      AddPointServerMessage outgoingMessage = new AddPointServerMessage(incomingMessage.getID(), incomingMessage.getX(), incomingMessage.getY(), incomingMessage.getColorId());
    
      // Reroute message received from client to all other clients
      sendMessage(outgoingMessage);
    }
    

    });

    // Return the new client connector return clientConnector; } ```

  6. 一旦SocketServer对象被声明和初始化,我们需要调用它的start()方法:

    java // Start the server once it's initialized MultiplayerServer.this.mSocketServer.start();

  7. 创建sendMessage()服务器广播方式:

    java // Send broadcast server message to all clients public void sendMessage(ServerMessage pServerMessage){ try { this.mSocketServer.sendBroadcastServerMessage(pServerMessage); } catch (IOException e) { e.printStackTrace(); } }

  8. Create the terminate() method to shut down the connection:

    java // Terminate the server socket and stop the server thread public void terminate(){ if(this.mSocketServer != null) this.mSocketServer.terminate(); }

    除去服务器端代码,我们将继续在MultiplayerClient类中实现客户端代码。该类与MultiplayerServer类非常相似,因此我们将从这里省略不必要的客户端步骤。

  9. 创建Socket``SocketConnection,最后创建ServerConnector与服务器建立连接:

    java // Create the socket with the specified Server IP and port Socket socket = new Socket(MultiplayerClient.this.mServerIP, MultiplayerClient.this.mServerPort); // Create the socket connection, establishing the input/output stream SocketConnection socketConnection = new SocketConnection(socket); // Create the server connector with the specified socket connection // and client connection listener MultiplayerClient.this.mServerConnector = new SocketConnectionServerConnector(socketConnection, MultiplayerClient.this);

  10. 处理从服务器接收的消息:

    ```java // obtain the class casted server message AddPointServerMessage message = (AddPointServerMessage) pServerMessage;

    // Create a new Rectangle (point), based on values obtained via the server // message received Rectangle point = new Rectangle(message.getX(), message.getY(), 3, 3, mEngine.getVertexBufferObjectManager());

    // Obtain the color id from the message final int colorId = message.getColorId(); ```

  11. Creating client and server messages:

    ClientMessageServerMessage旨在充当能够发送到服务器和从服务器接收以及发送到客户端和从客户端接收的数据包。在这个方法中,我们为客户端和服务器创建了一个消息来处理发送关于在客户端设备上何处绘制点的信息。这些消息中存储的变量包括:

    java // Member variables to be read in from the server and sent to clients private int mID; private float mX; private float mY; private int mColorId;

    虽然读写数据进行通信很简单,如下所示:

    ```java // Apply the read data to the message's member variables @Override protected void onReadTransmissionData(DataInputStream pDataInputStream) throws IOException { this.mID = pDataInputStream.readInt(); this.mX = pDataInputStream.readFloat(); this.mY = pDataInputStream. readFloat(); this.mColorId = pDataInputStream.readInt(); }

    // Write the message's member variables to the output stream @Override protected void onWriteTransmissionData( DataOutputStream pDataOutputStream) throws IOException { pDataOutputStream.writeInt(this.mID); pDataOutputStream.writeFloat(this.mX); pDataOutputStream.writeFloat(this.mY); pDataOutputStream.writeInt(mColorId); } ```

它是如何工作的...

在服务器/客户端通信的这个方法的实现中,我们正在构建一个应用,允许服务器直接部署在移动设备上。从这里,其他移动设备能够充当客户端并连接到前述移动服务器。一旦与至少一个客户端建立了服务器,如果任何客户端创建了触摸事件,服务器将开始向所有客户端转发消息,在所有连接的客户端的屏幕上绘制点。如果这听起来有点混乱,不要害怕。很快一切都会好起来的!

前五步,我们写BaseGameActivity课。这个类只是服务器和客户端的入口点,也是为客户端在屏幕上绘图提供触摸事件功能的一种手段。

第一步,我们将必要的ServerMessageClientMessage对象注册到我们的mMessagePool中。mMessagePool对象是 AndEngine 中MultiPool类的扩展。有关如何使用MessagePool类回收通过网络发送和接收的消息,请参见第 8 章最大化性能中的创建精灵池部分。

在第二步中,我们使用场景触摸监听器接口设置场景,其目的是通过网络发送消息。在 touch listener 中,我们可以使用简单的条件语句来检查设备是作为客户端运行还是作为服务器运行,如果设备作为服务器运行,则返回 true。另外,我们可以调用if(mClient != null)来检查设备是否作为客户端运行。当设备同时作为客户端和服务器运行时,服务器检查中的嵌套客户端检查将返回 true。如果设备作为客户端运行,发送消息就像从mMessagePool获取新消息一样简单,对所述消息调用set(device_id, touchX, touchY, colorId)方法,然后调用mClient.sendMessage(message)。一旦消息被发送,我们应该总是将其回收到池中,以免浪费内存。在继续之前,还有最后一点要提。在嵌套的客户端条件中,我们发送的是服务器消息,而不是客户端消息。这是因为在这种情况下,客户端也是服务器。这意味着我们可以跳过向服务器发送客户端消息,因为服务器已经包含了触摸事件数据。

对于大多数开发人员来说,第三步很可能不是一个理想的情况,因为我们使用对话框来选择设备是作为服务器还是客户端。这个场景只是用来显示如何初始化组件,所以对话框不一定很重要。选择用户是否应该能够托管游戏在很大程度上取决于游戏类型和开发者的想法,但这个食谱至少涵盖了如果需要如何设置服务器。请记住,在初始化服务器时,我们只需要知道端口号 。另一方面,客户端需要知道有效的服务器 IP 和服务器端口,以便建立连接。一旦用这些参数构造了MultiplayerServer和/或MultiplayerClient类,我们就可以初始化组件了。初始化的目的将很快介绍。

BaseGameActivity类的第四步也是最后一步是在活动调用onDestroy()的情况下,允许活动终止MultiplayerServerMultiplayerClient连接。这将在应用被销毁之前关闭通信线程和套接字。

转到MultiplayerServer代码,让我们看看第五步中服务器的初始化。当创建一个服务器用来监听新客户端连接的SocketServer对象时,我们必须输入服务器的端口号,以及一个ClientConnectorListener和一个SocketServerListenerMultiplayerServer类实现了这两个监听器,在服务器启动、停止、客户端连接到服务器以及客户端断开连接时记录日志。

在第六步中,我们将实现处理服务器如何响应传入连接以及如何处理客户端接收到的消息的系统。以下各点涵盖了实施顺序中涉及的流程:

  • 当新客户端连接到服务器时,调用protected SocketConnectionClientConnector newClientConnector(...)
  • 创建一个新的SocketConnectionClientConnector是供客户端用作新客户端和服务器之间的通信手段。
  • 通过registerClientMessage(flag, message.class, messageHandlerInterface)注册您希望服务器识别的ClientMessages
  • messageHandlerInterface接口的onHandleMessage()方法中,我们处理从网络上收到的任何消息。在这种情况下,服务器只是将客户端的消息转发回所有连接的客户端。
  • 返回新的clientConnector对象。

这些要点概述了服务器/客户端通信的主要功能。在这个方法中,我们使用单个消息来在客户端设备上绘制点,但是对于更广泛的消息,我们可以继续调用registerClientMessage()方法,只要标志参数与我们在onHandleMessage()界面中获得的消息类型匹配。一旦注册了所有适当的消息,并且我们完成了客户端处理代码,我们就可以继续执行第七步,并在mSocketServer对象上调用start()

第八步,我们为服务器创建sendMessage(message)方法。服务器的变体sendMessage(message)通过简单地循环客户端连接器列表,向所有客户端发送广播消息,向每个连接器调用sendServerMessage(message)。如果我们希望向单个客户端发送服务器消息,我们可以简单地在单个客户端连接器上调用sendServerMessage(message)。在另一端,我们有客户的变体sendMessage(message)。客户端的sendMessage()方法实际上并没有向其他客户端发送消息;事实上,客户端根本不与其他客户端通信。客户端的工作是与服务器通信,然后服务器再与其他客户端通信。为了更好地理解我们的网络交流是如何工作的,请参见下图:

How it works...

在上图中,流程由数字概括。首先,客户端向服务器发送消息。一旦服务器收到消息,它将循环遍历其客户端列表中的每个ClientConnector对象,向所有客户端发送广播。

创建MultiplayerServer组件的最后一步是创建终止mSocketServer的方法。这个方法在我们的主要活动中被onDestroy()调用,目的是在我们结束它的时候破坏通信线程。

有了所有的服务器端代码,我们就可以继续编写客户端了。MultiplayerClient代码有点类似于服务器的代码,只有一些不同。当与服务器建立连接时,我们必须比服务器初始化时更具体一点。首先,我们必须创建一个新的套接字,它有一个指定的 IP 地址和一个服务器端口号。然后我们将Socket传递给一个新的SocketConnection对象,用于在套接字上建立输入/输出流。一旦完成,我们就可以创建我们的ServerConnector,其目的是在客户端和服务器之间建立最终连接。

我们即将完成一个完整的客户端/服务器通信项目!第十一步是真正的奇迹发生的地方——客户端接收服务器消息。为了接收服务器消息,类似于接收消息的服务器实现,我们简单地调用mServerConnector.registerServerMessage(...),然后给我们机会为onHandleMessage(serverConnector, serverMessage)填写一个界面。同样,类似于服务器端实现,我们可以将serverMessage对象类转换为AddPointServerMessage类,从而允许我们获取存储在消息中的自定义值。

现在,随着所有的服务器和客户端代码的消失,我们来到了最后一步。当然,这是在创建将用于MessagePool的消息,以及我们在各地发送和接收的对象。我们需要注意两种不同类型的消息对象。第一种类型是ServerMessage,它由从客户端发送并由服务器接收/读取的消息组成。另一种类型的消息是ClientMessage,意思是从服务器发送并由客户端接收/读取的。通过创建我们自己的消息类,我们可以轻松地将由原始数据类型表示的数据块打包在一起,并通过网络发送它们。原始数据类型包括intfloatlongboolean等。**

在这个食谱中使用的消息中,我们存储了一个标识,它的意思是告诉我们消息是从客户端还是服务器发送的,每个客户端触摸事件的 x 和 y 坐标,以及当前选择的用于绘图的颜色标识。每个值都应该有自己对应的 get 方法,这样我们就可以在收到消息时获取消息的详细信息。此外,通过覆盖客户端或服务器消息,我们必须实现onReadTransmissionData(DataInputStream)方法,该方法允许我们从输入流中获取数据类型,并将它们复制到我们的成员变量中。我们还必须实现onWriteTransmissionData(DataOutputStream),用于将成员变量写入数据流并通过网络发送。在创建服务器和客户端消息时,我们需要注意的一件事是,读入我们的接收成员变量的数据是按照它们被发送的相同顺序获得的。查看我们的服务器消息读写方法的顺序:

  // write method
  pDataOutputStream.writeInt(this.mID);
  pDataOutputStream.writeFloat(this.mX);
  pDataOutputStream.writeFloat(this.mY);
  pDataOutputStream.writeInt(this.mColorId);

  // read method
  this.mID = pDataInputStream.readInt();
  this.mX = pDataInputStream.readFloat();
  this.mY = pDataInputStream. readFloat();
  this.mColorId = pDataInputStream.readInt();

记住前面的代码,我们可以确定,如果我们将包含intfloatintbooleanfloat的消息写入输出流,任何接收消息的设备都会分别读入intfloatintbooleanfloat

用 SVG 创建高分辨率图形

可扩展矢量图形 ( SVG ) 融入我们的移动游戏的能力对开发来说是一个巨大的好处,在使用安卓系统时更是如此。最大的好处,也是我们将在本主题中介绍的好处,是 SVG 可以进行扩展,以适应运行我们应用的设备。不再需要为更大的显示器创建多个 PNG 集,更重要的是,不再需要在大屏幕设备上处理可怕的像素化图形!在本主题中,我们将使用AndEngineSVGTextureRegionExtension扩展来为我们的精灵创建高分辨率纹理区域。请参见下面的屏幕截图,左侧是缩放的标准分辨率图像,右侧是 SVG 图像:

Creating high-resolution graphics with SVG

虽然当涉及到跨多个屏幕尺寸创建高分辨率图形时,SVG 资产可能非常令人信服,但在SVG扩展的当前状态下,它们也有一些缺点。SVG扩展不会呈现所有可用的元素,例如文本和三维形状。但是,大多数必要的元素都是可用的,并且会在运行时正确加载,例如路径、渐变、填充颜色和一些形状。在静止无功发生器浮动期间,未能加载的元素将通过日志显示。T3】

从 SVG 文件中删除SVG扩展名不支持的元素是一个明智的选择,因为它们会影响加载时间,这是使用SVG扩展名的另一个负面影响。SVG纹理的加载时间比 PNG 文件要长得多,因为它们在加载到内存之前必须先转换成 PNG。根据每个 SVG 中包含的元素数量,看到SVG纹理比同等 PNG 图像花费的时间长两到三倍的情况并不少见。最常见的解决方法是在应用首次启动时将SVG纹理以 PNG 格式保存到设备中。随后每次启动都会加载 PNG 图像,以减少加载时间,同时保持设备特定的图像分辨率。

做好准备

参考代码包中名为WorkingWithSVG的项目。

怎么做...

创建一个SVG纹理区域是一个很容易完成的任务,而且效果很好。

  1. 类似于你的平均水平TextureRegion,首先我们需要一个BuildableBitmapTextureAtlas :

    java // Create a new buildable bitmap texture atlas to build and contain texture regions BuildableBitmapTextureAtlas bitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 1024, 1024, TextureOptions.BILINEAR);

  2. 现在我们已经有了纹理图谱设置,我们可以通过使用SVGBitmapTextureAtlasTextureRegionFactory单例:

    ```java // Create a low-res (32x32) texture region of svg_image.svg mLowResTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "svg_image.svg", 32,32);

    // Create a med-res (128x128) texture region of svg_image.svg mMedResTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "svg_image.svg", 128, 128);

    // Create a high-res (256x256) texture region of svg_image.svg mHiResTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "svg_image.svg", 256,256);
    ```

    来创建SVG纹理区域

它是如何工作的…

正如我们所看到的,创建一个SVG纹理区域与你的平均TextureRegion没有太大区别。两者在实例化方面唯一真正的区别是,我们必须输入一个widthheight值作为最后两个参数。这是因为,不像您的平均光栅图像格式,由于固定的像素位置,其宽度和高度或多或少是硬编码的,SVG像素位置可以放大或缩小到我们想要的任何大小。如果我们缩放SVG纹理区域,向量的坐标将简单地自我调整,以便继续产生清晰、精确的图像。一旦SVG纹理区域被建立,我们可以像应用于任何其他纹理区域一样将其应用于精灵。

这一切都很好,知道如何创建SVG纹理区域,但还有更多。毕竟,能够在我们的游戏中使用 SVG 图像的好处是能够根据设备的显示大小缩放图像。通过这种方式,我们可以避免为了容纳平板电脑而不得不为较小的屏幕设备加载较大的图像,并且我们不必为了节省内存而创建较小的纹理区域,从而让平板电脑用户受苦。SVG扩展实际上让我们很容易处理根据显示器尺寸进行缩放的想法。下面的代码向我们展示了如何对所有创建的SVG纹理区域实施质量缩放因子。这将允许我们避免手动创建不同大小的纹理区域,这取决于显示大小:

float mScaleFactor = 1;

// Obtain the device display metrics (dpi)
DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();

int deviceDpi = displayMetrics.densityDpi;

switch(deviceDpi){
case DisplayMetrics.DENSITY_LOW:
  // Scale factor already set to 1
  break;

case DisplayMetrics.DENSITY_MEDIUM:
  // Increase scale to a suitable value for mid-size displays
  mScaleFactor = 1.5f;
  break;

case DisplayMetrics.DENSITY_HIGH:
  // Increase scale to a suitable value for larger displays
  mScaleFactor = 2;
  break;

case DisplayMetrics.DENSITY_XHIGH:
  // Increase scale to suitable value for largest displays
  mScaleFactor = 2.5f;
  break;

default:
  // Scale factor already set to 1
  break;
}

SVGBitmapTextureAtlasTextureRegionFactory.setScaleFactor(mScaleFactor);

前面的代码可以复制粘贴到一个活动的onCreateEngineOptions()方法中。所有需要做的就是根据设备大小决定您希望应用于 SVG 的比例因子!从这一点开始,我们可以创建一个单独的SVG纹理区域,根据显示大小,纹理区域将相应地缩放。例如,我们可以加载一个纹理区域,如下所示:

  mLowResTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "svg_image.svg", 32,32);

我们可以将纹理区域的宽度和高度值定义为32,但是通过调整工厂类中的比例因子,纹理区域将通过将指定值乘以DENSITY_XHIGH显示的比例因子来构建为80x80。在处理带有自动缩放因子的纹理区域时要小心。该秤还会增加它们在BuildableBitmapTextureAtlas物体内消耗的空间,如果超出,可能会导致错误,就像其他任何TextureRegion一样。

另请参见…

  • 第一章和【引擎游戏结构】中的不同类型的纹理部分。

使用 SVG 纹理区域的颜色映射

SVG纹理区域的一个有用的方面是我们能够容易地映射纹理的颜色。这种技术在游戏中很常见,允许用户为他们的玩家角色选择自定义颜色,无论是服装和配饰颜色、头发颜色、皮肤颜色、地形主题等等。在本主题中,我们将在构建SVG纹理区域时使用ISVGColorMapper界面,以便为我们的精灵创建定制的颜色集。

做好准备

在我们进入颜色映射的编码端之前,我们需要创建一个带有预设颜色的 SVG 图像。我们可以把这些预设的颜色想象成我们的地图。许多开发人员最喜欢的 SVG 编辑器之一叫做 Inkscape ,这是一个免费的、非常容易使用的、功能齐全的编辑器。Inkscape 可以从以下链接下载,http://inkscape.org/download/,或者随意使用您选择的另一个 SVG 编辑器。

怎么做...

颜色映射听起来可能是一项乏味的工作,但实际上它非常容易完成。我们只需要在SVG图像和代码之间保持一点点一致性。记住这一点,创建多色,单一来源纹理的想法可以是一个非常快速的任务。下面的步骤包括从绘制SVG图像开始的过程,以便轻松进行颜色映射,以及编写代码将颜色映射到我们应用中的SVG图像的特定区域。

  • Drawing our SVG image:

    为了在运行时轻松地将颜色映射到SVG纹理区域,我们需要在我们选择的编辑器中绘制一个SVG图像。这包括对我们图像的不同部分进行颜色编码,以便在我们的ISVGColorMapper界面中容易识别。下图描述了显示在图左侧的具有已定义颜色值的形状。

    How to do it...

  • Implementing the ISVGColorMapper interface:

    就在通过SVGBitmapTextureAtlasTextureRegionFactory创建SVG纹理区域之前,我们将定义与SVG图像相关的ISVGColorMapper界面。如果我们查看以下代码中的条件句,我们可以看到我们正在检查上图中的相同颜色值:

    ```java ISVGColorMapper svgColorMapper = new ISVGColorMapper(){ @Override public Integer mapColor(final Integer pColor) { // If the path contains no color channels, return null if(pColor == null) { return null; }

    // Obtain color values from 0-255
    int alpha = Color.alpha(pColor);
    int red = Color.red(pColor);
    int green = Color.green(pColor);
    int blue = Color.blue(pColor);
    
    // If the SVG image's color values equal red, or ARGB{0,255,0,0}
    if(red == 255 && green == 0 && blue == 0){
      // Return a pure blue color
      return Color.argb(0, 0, 0, 255);
    
    // If the SVG image's color values equal green, or ARGB{0,0,255,0}
    } else if(red == 0 && green == 255 && blue == 0){
      // Return a pure white
      return Color.argb(0, 255, 255, 255);
    
    // If the SVG image's color values equal blue, or ARGB{0,0,0,255}
    } else if(red == 0 && green == 0 && blue == 255){
      // Return a pure blue color
      return Color.argb(0, 0, 0, blue);
    
    // If the SVG image's color values are white, or ARGB{0,254,254,254}
    } else if(red == 254 && blue == 254 && green == 254){
      // Return a pure red color
      return Color.argb(0, 255, 0, 0);
    
    // If our "custom color" conditionals do not apply...
    } else {
    
      // Return the SVG image's default color values
      return Color.argb(alpha, red, green, blue);
    }
    

    } };

    // Create an SVG texture region mSVGTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "color_mapping.svg", 256,256, svgColorMapper); ```

  • 最后,一旦定义了界面,我们就可以在创建纹理区域时将其作为最终参数传入。一旦这样做了,用SVG纹理区域创建一个新的精灵将产生在颜色映射器中定义的颜色值。

它是如何工作的…

在我们开始之前,先简单讲一下颜色;如果你正在看这个食谱的代码,并且对为我们的条件和颜色结果选择的随机值感到困惑,这很简单。每个颜色分量,红色、绿色和蓝色,可以提供 0 到 255 之间的任何颜色值。将值 0 传递给颜色分量将不会导致该颜色的贡献,而传递 255 将被认为是颜色贡献。考虑到这一点,我们知道,如果所有的颜色分量都返回值 0,我们将把黑色传递给我们的纹理区域的路径。如果我们给红色部分传递一个值 255,同时给绿色和蓝色传递 0,我们知道纹理区域的路径将是一个明亮的红色。

如果我们回头看看中的图该怎么做...部分,我们可以看到阿尔法、红色、绿色和蓝色 ( ARGB )颜色值,箭头指向它们所代表的圆上的区域。这些不会直接影响我们纹理区域颜色的最终结果;它们只是在适当的位置,这样我们就可以在我们的颜色映射器界面中获得对圆的每个部分的引用。请注意,第一个圆的最外侧部分是亮红色,值为 255。考虑到这一点,请查看我们的颜色映射器中的以下条件:

    // If the SVG image's color values equal red, or ARGB{0,255,0,0}
    } else if(red == 255 && green == 0 && blue == 0){
      // Return a pure blue color
      return Color.argb(0, 0, 0, 255);

    // If the SVG image's color values equal green, or ARGB{0,0,255,0}
    }

前面代码中的条件语句将检查SVG图像的任何路径,该路径包含纯红色值,没有绿色或蓝色的贡献,而是返回纯蓝色。这就是颜色交换是如何发生的,这就是我们如何将颜色映射到我们的图像上!知道了这一点,完全可以为我们的SVG图像创建许多不同的颜色集,但是对于每个颜色集,我们必须提供一个单独的纹理区域。

需要注意的一个重要的关键点是,我们应该包含一个返回值,它将在我们的条件都不满足的情况下返回默认路径的颜色值。这允许我们省略较小细节的条件,例如SVG图像的轮廓或其他颜色,而是当它们出现在图像中时填充它们,如果我们要在我们最喜欢的SVG编辑器中打开它的话。这应该作为最终的else声明包含在颜色映射器中:

    // If our "custom color" conditionals do not apply...
    } else {
      // Return the SVG image's default color values
      return Color.argb(alpha, red, green, blue);
    }

还有更多…

中它是如何工作的...部分,我们介绍了如何改变静态SVG图像路径的颜色。不要过多考虑如上所述的创建颜色主题的想法,这听起来可能是创建更多对象、地形、角色等的最终目的。事实是,在这个时代,许多游戏需要变化才能创造出有吸引力的资产。通过方差,我们当然是指梯度。如果我们回想一下上面写的条件句,我们会在返回自定义颜色之前检查绝对颜色值。

谢天谢地,使用渐变并不太困难,因为我们可以调整渐变的停止颜色,颜色之间的插值将自动为我们处理!我们可以将停止视为渐变的颜色定义点,随着距离的增加,渐变会在其他停止之间进行插值。这是造成渐变混合效果的原因,也是通过使用本食谱中描述的相同方法轻松创建颜色主题的原因。从纯红色RGB{255, 0, 0},到纯绿色RGB{0, 255, 0},最后到蓝色RGB{0, 0, 255}的渐变,见下图截图:

There's more…

如果我们在SVG图像中使用上述渐变,我们可以通过简单地修改每个色标位置的特定颜色,轻松地在色标之间应用适当插值的颜色映射。以下代码将渐变更改为红色、绿色和黄色,而不是将蓝色作为第三个颜色停止点:

    } else if(red == 0 && green == 0 && blue == 255){
      // Return a pure blue color
      return Color.argb(0, 255, 255, 0);
       }

另请参见…

  • 用 SVG 创建高分辨率图形部分。