65.9K
CodeProject 正在变化。 阅读更多。
Home

Androng,一款适用于 Android 的 Pong 克隆游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (43投票s)

2011 年 5 月 1 日

CPOL

16分钟阅读

viewsIcon

162491

downloadIcon

11997

本文介绍了我是如何开发 Androng 的,这是一款适用于 Android 的 Pong 克隆游戏。

Screenshot of Androng, the game Screenshot of Androng, the game

目录

引言

本文介绍了我是如何开发并发布 Androng 的,这是一款经典的 Pong 游戏在 Android 上的克隆版本。我使用 Java 开发了这款游戏,它支持单人和双人模式。我想分享我是如何从头到尾开发这款游戏的。所谓的“从头到尾”是指真正地将游戏发布到 Android 市场。上面是两个游戏中的截图。

Screenshot of the original version of Pong by Atari

Pong

Pong 是一款最初作为乒乓球电子版开发的游戏。Atari 于 1972 年最初创作了这款游戏。游戏目标是通过获得更高的分数来击败对手。你需要保持球在游戏中,并希望对手失误。在我的实现中,谁先得到 10 分就赢得游戏。

Android Logo

Android

Android 是基于 Linux 2.6 内核的移动设备操作系统。Android 公司开发了 Android,Google 于 2005 年收购了 Android。尽管它是一个移动设备操作系统,但操作系统本身非常庞大。它包含超过 1200 万行代码。Android 支持多种不同的移动设备,并且有一个特殊的 Android 版本(3.0 Honeycomb)可用于平板设备。

您可以使用 C++.NET 开发 Android 应用,但大多数 Android 开发都使用 Java。我使用 Java 的原因是它与 C# 相似,这是我在日常工作中使用的语言。要开发 Android 的 Java 应用程序,您需要 Android SDKJava SDK 以及集成开发环境 (IDE),例如 EclipseIntelliJ

我使用的是 IntelliJ 的社区版,它是免费的。这是因为我在日常工作中使用了 Visual Studio 和 Resharper,它们与 IntelliJ 有许多相似之处。Resharper 和 IntelliJ 共享许多键盘快捷键,并且它们都由 JetBrains 公司开发。请注意,虽然 Java 可用于开发,但并非所有 Java 库都可用,只有 Android 运行时支持的库才可用。

Android 游戏开发

在 Android 上开始开发游戏时,您有三种可能的图形实现方式。您可以使用 drawable 包并使用 `view` 或 `canvas`。另一方面,您可以使用 OpenGL ES API,这是 OpenGL 规范的特殊嵌入式设备实现。OpenGL ES 包括对 3D 图形的支持。对于 Androng,我决定使用 graphics 包并使用 `canvas` 进行绘图。Canvas 包足以支持这款游戏所需的动画和移动。

Activity

Android 应用程序围绕 Activity 和 View 构建。Activity 是应用程序中提供交互界面的部分。一个应用程序可以包含多个 Activity。Android 会启动应用程序清单中指定的 Activity。这个清单很重要,它是一个配置文件,包含系统启动您的应用程序之前所需的信息。例如,应用程序运行所需的权限。启动时,Android 会调用指定 Activity 的 `onCreate` 方法。下面的源代码显示了 Androng 的主 Activity。所有 Activity 都应继承自 `Activity` 类。

public class AndrongActivity extends Activity
{
  private AndrongSurfaceView pongSurfaceView;

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    pongSurfaceView = (AndrongSurfaceView) findViewById(R.id.androng);
    pongSurfaceView.setTextView((TextView) findViewById(R.id.text));
  }

  ....
}

View

View 创建 Activity 的用户界面,并继承自 `View` 类。一个总体的 View 可以包含 View 组和 View。View 组用于组合 View。它们遵循 Composite 设计模式。应用程序的用户界面可以由多个 View 组成,即所谓的 View 层次结构。您可以使用代码或资源文件来定义 View 的布局。通过使用资源布局文件,您的用户界面的灵活性会增加。维护变得更容易,并且可以包含本地化支持。布局资源文件使用 XML 实现。下面的资源文件显示了 Androng 的布局。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <net.semantic.games.AndrongSurfaceView
      android:id="@+id/androng"
      android:layout_width="match_parent"
      android:layout_height="match_parent"/>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <TextView
          android:id="@+id/text"
          android:text="Androng"
          android:visibility="visible"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_centerInParent="true"
          android:layout_alignParentBottom="true"
          android:layout_marginBottom="10px"
          android:gravity="center_horizontal"
          android:textColor="#99FFFFFF"
          android:textSize="16sp"/>
     </RelativeLayout>
</FrameLayout>

所有布局类型都是 ViewGroups,用于创建分层模型。Androng 使用 `FrameLayout`,这是最简单的布局对象。`FrameLayout` 是屏幕上一个空白的预留区域,可以填充一个对象。所有子对象都固定在左上角。无法为子对象指定位置;后续的子对象绘制在之前的对象之上,可能会遮挡之前的对象。

Visual representation of the Androng layout

在 Androng 的情况下,`AndrongSurfaceView` 和 `TextView` 会绘制在彼此之上。在 Activity 的 `onCreate` 方法中,通过 **`setContentView(R.layout.main);`** 来加载用户界面中的 XML,其中 `R.layout.main` 标识了资源中的 XML 文件。

还有其他的布局类型。有 `LineairLayout`、`RelativeLayout`、`TableLayout` 和 `AbsoluteLayout`。这些布局各有其优点。不建议在应用程序中使用 `AbsoluteLayout`,因为它在您的设备上可能看起来不错,但在另一台设备上可能有所不同。

现在我们有了用户界面,让我们看看如何将资源绘制到屏幕上。

使用 Canvas 绘图

在将资源绘制到 Canvas 之前,您需要有一个要绘制的资源并获取一个 Canvas 来进行绘制。`Canvas` 是通过 `SurfaceView` 获取的,这是一个位于活动视图窗口下方的可绘制表面。对于 Androng,我实现了一个名为 `AndrongSurfaceView` 的类,它扩展了 `SurfaceView`,这与布局文件中提到的 View 相同。下面的源代码显示了 `AndrongSurfaceView` 类的一部分。

public class AndrongSurfaceView extends SurfaceView
       implements SurfaceHolder.Callback
{
  private AndrongThread androngThread;
  private TextView statusText;
  private SurfaceHolder holder;
  private Context context;
  
  public AndrongSurfaceView(Context context, AttributeSet attrs)
  {
    super(context, attrs);
    this.context = context;
    this.holder = getHolder();
    holder.addCallback(this);
    setFocusable(true);
  }
  
  ......
  
}

通过调用 `SurfaceHolder` 上的 `lockCanvas` 方法,我们可以获得 `Canvas` 类的实例,该实例可用于操作表面的像素。`SurfaceHolder` 可以通过 `AndrongSurfaceView` 类中的 `getHolder()` 方法检索。

Canvas canvas = surfaceHolder.lockCanvas(null);

资源可以通过应用程序的 Context 获取。例如,以下源代码在每一帧中绘制游戏的背景。

Bitmap backgroundImage = BitmapFactory.decodeResource(resources, R.drawable.background2);
canvas.drawBitmap(backgroundImage, 0, 0, null);

动画

在 Androng 中,球是动画的。动画包含 12 帧,每帧旋转球 30 度。当球击中挡板或边缘时,动画会反转。这在游戏中创造了漂亮的球体动画效果。

Animation frames of the Anrong ball

Android 内置了对动画的支持。它有动画属性,您可以在其中设置对象属性的开始和结束值。还有使用 `AnimationDrawable` 的帧动画。帧动画按定义的顺序显示一系列图像。`AnimationDrawable` 的动画必须在 XML 中定义。

这个帧动画的问题是,您可以在 XML 文件中设置动画的持续时间,但不能设置动画的速度。在 Androng 中,我想在球碰到东西时反转动画。因此,我决定自己实现动画。

public class Sprite
{
   protected DrawableResourceCollection drawableResourceCollection;
   private int currentFrame;

   public void draw(Canvas canvas)
   {
      drawableResourceCollection
         .get(currentFrame)
         .setBounds((int) xPosition, 
                    (int) yPosition, 
                    (int) xPosition + getWidth(), 
                    (int) yPosition + getHeight());
      drawableResourceCollection.get(currentFrame).draw(canvas);
      currentFrame = GetNewFrame();
   }
   
   ...
}

`Sprite` 是所有可动画对象(如球和挡板)的基础类。每个 Sprite 都有一个 `DrawableResourceCollection`,它由可绘制资源列表组成。

public class DrawableResourceCollection extends LinkedList<drawable>

当调用 `Sprite` 的 draw 方法时,会从 `DrawableResourceCollection` 中检索一个 `Drawable` 资源并将其绘制到 Canvas 上。`GetNewFrame()` 确定资源中下一帧的索引。`GetNewFrame()` 方法使用一个布尔值 `animationForward`,顾名思义,它决定动画是向前还是向后移动。这样,我们就可以通过将此布尔值从 `true` 更改为 `false`(反之亦然)来轻松反转动画方向。

帧速无关的动画

Android 运行在许多设备上,每台设备都有自己的硬件规格。这意味着运行您应用程序的设备的处理速度将不同。这反过来意味着 Sprite(例如 Androng 游戏中的球)的动画速度因设备而异。这种情况是不希望发生的,游戏玩法可能会因设备而异。因此,我们希望动画独立于设备的处理能力。我们通过将时间纳入应用程序,并以像素移动或每时间单位的动画速度来指定动画速度来实现这一点。

从 Android 获取高分辨率定时

有三种不同的方法可以从 Android OS 获取时间:

  1. currentTimeMillis()
  2. upTimeMillis()
  3. elapsedRealtime()

第一个 `System.currentTimeMillis()` 表示自纪元以来的毫秒数。Unix 类系统上的纪元是 1970 年 1 月 1 日。这显然取决于设备的当前时间;当时间由于手机网络同步或用户操作而改变时,这个数字会向前或向后跳。

第二个 `System.upTimeMillis()` 是设备启动以来的毫秒数。当设备进入睡眠模式时,此计时器会停止,但不受时间变化的影响。

第三个也是最后一个选项是 `elapsedRealtime()`,它也是设备启动以来的毫秒数。与第二个选项的区别在于,当设备进入睡眠模式时,它会继续运行。

对于我们的帧速无关动画,我选择了 `upTimeMillis()`,因为第一个选项可能会向前或向后跳,这不利于计算帧速,而第三个选项在游戏暂停时会继续运行,这也可能导致帧速计算出现问题。以下代码使用第二个选项计算每秒帧数。

while (isRunning)
{
  currentTimeInMillis = System.upTimeMillis();
  double timeNeededToDrawFrame = 
        (currentTimeInMillis - previousTimeInMillis) / 1000;
  previousTimeInMillis = currentTimeInMillis;
  DrawFrame(time);
  UpdatePhysics(timeNeededToDrawFrame);
}

结果变量 `timeNeededToDrawFrame` 被发送到构成游戏屏幕的所有对象。例如,球接收到这个值,其速度是每秒两个像素的水平移动。通过将其乘以绘制此帧所需的时间,我们得到球应该移动的像素数。垂直速度也进行相同的处理。这实现了帧速无关的动画。

碰撞检测

大多数(如果不是全部)游戏都需要碰撞检测。检测两个游戏对象是否发生碰撞有多种方法。Androng 结合了边界框和像素方法。

边界框碰撞检测

边界框方法可以通过下图轻松说明。

Bounding box method

以下算法检测每个 Sprite 周围的虚拟框是否重叠。

if (bottom1 < top2)
  return false;
if (top1 > bottom2)
  return false;
if (right1 < left2)
  return false;
if (left1 > right2)
  return false;

//bounding box do overlap

边界框碰撞检测算法是一种快速检测碰撞的方法,但如果形状不是矩形(如球),我们可能会得到假阳性。例如,在下图所示的情况下,边界框检测会检测到碰撞,但实际上并没有。

False detection when using bounding box method

像素完美检测

我们可以通过为球使用边界圆来解决这个问题,但我希望通过更通用的方法来解决,即使用 Sprite 的像素。因此,当边界框算法检测到碰撞时,我们会扫描两个 Sprite 中的重叠区域的像素。如果两个 Sprite 中相同的位置都包含一个像素(颜色 != 0),则发生碰撞。

Collision detection using pixel scan

该算法确定重叠框的宽度和高度,以及该框在每个 Sprite 中的位置。该算法扫描框中的每个像素以检测碰撞。可以通过调用 Drawable 上的 `getBitmap()` 方法来读取位图中的像素。有关完整的碰撞检测例程,请参阅源代码中的 Sprite 类的 `collideswith` 方法。

声音管理

当球与挡板、墙壁碰撞或玩家得分时,Androng 会播放声音。使用 Android 播放声音很简单。MediaPlayer 或 Sound Pool 类都可以播放声音。我使用了 Sound Pool 类,因为它们提供了更多的灵活性。使用 Sound Pool 类播放声音涉及 AudioManager;AudioManager是所谓的 Android 系统服务。下面的源代码显示了如何获取 AudioManager 系统服务并播放名为“hit”的媒体文件。“hit”在球击中挡板或边缘时播放。

AudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
SoundPool mSoundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
mSoundPool.play(R.raw.hit, streamVolume, streamVolume, 1, 0, 1);

游戏中的所有声音都预先加载到 `SoundPoolMap` 中,并从此 `SoundPoolMap` 中播放。所有声音管理方法都分组到单个类 `SoundManager` 中;请参阅该类的源代码。该类基于 Stephen Flockton(他撰写了关于 Android 开发的博客)提供的示例。

输入管理

游戏可以使用 Android 设备的触摸屏进行控制,这称为触摸模式。当您用手指触摸屏幕上的按钮时,触摸模式会被激活。处理触摸模式事件就像重写 SurfaceView 的 `onTouchEvent` 一样简单。下面显示了 `onTouchEvent` 的签名。

public boolean onTouchEvent(MotionEvent event)

当您用一个或多个手指触摸屏幕时,会触发此事件。`MotionEvent` 参数有一个名为 `getPointerCount()` 的方法,该方法返回放在屏幕上的手指数量。尽管这实际上取决于设备的功能和 Android 的版本。Androng 具有双人模式,两名玩家可以在同一设备上使用手指进行对战。在此模式下,使用 `getPointerCount()` 方法。如果有两个手指触摸设备屏幕,`index = 0` 代表第一个手指,而 `index = 1` 代表第二个手指。使用 `event` 上的 `getX()` 或 `getY()` 方法中的索引,可以确定手指的位置。

float xPosition1 = event.getX(pointerIndex);
float yPosition1 = event.getY(pointerIndex);

`xPosition1` 和 `yPosition1` 用于在屏幕上放置挡板。

通知

Androng 使用 Toast 通知来告知用户某些事件,例如如何开始游戏以及哪个玩家赢得了游戏。Toast 通知是一种出现在窗口表面的消息。消息会自动淡入淡出,并在屏幕上停留预设的时间。以下代码在 Androng 游戏屏幕上显示了一个 Toast 通知。

Toast toast = Toast.makeText(context, "Select Menu for a new game.", Toast.LENGTH_LONG);
toast.show();

Showing a Toast notification

根据 Android 文档,常量 `Toast.LENGTH_LONG` 会告诉 Android 该文本通知应显示较长时间。默认值 `Toast.LENGHT_LONG` 对应于 3.5 秒。`Show()` 会实际在屏幕上显示文本。

应用生命周期管理

由于 Android 是移动设备操作系统,它需要特别关注管理此类设备的稀缺资源。每个 Android 应用程序都在自己的进程中运行,并且能够执行特定任务。一个任务可以包含多个 Activity。每个 Android 应用程序都应管理应用程序的生命周期。例如,当 Android OS 需要额外资源时,它可能会决定暂停或销毁您的应用程序。因此,您的应用程序应该能够在需要时保存和恢复其状态。

Activity 生命周期

如前所述,Android 应用程序由 Activity 组成;在这些 Activity 中,您应该管理您应用程序的生命周期。启动或重启您的应用程序有三种可能的情况。

全新启动 全新重启 从暂停状态重启
onCreate onRestart  
onStart onStart  
onResume onResume onResume

所有这些“on*”方法都是 Activity 的一部分。全新启动的情况发生在您正常启动应用程序时。全新重启的情况发生在 Android 停止您的 Activity 之后,就在它再次启动之前。最后一种情况,从暂停状态重启,发生在系统即将恢复之前的 Activity 时。当另一个应用程序进入前台时,您的 Activity 会被暂停。 此处显示了应用程序生命周期的完整图形概览。

请注意,当您更改 Android 设备的屏幕方向时,您的应用程序会重新启动。

对于 Androng 的第一个版本,我决定不在游戏被 Android 操作系统销毁时保存游戏状态。当游戏暂停并重新启动时,我直接重新启动 Androng 游戏线程。

重启线程

游戏屏幕的绘制和物理计算在一个单独的线程上运行。这个 `AndrongThread` 继承自 `Thread`。

public class AndrongThread extends Thread
{  
  @Override
  public void run()
  {
    long startTime = SystemClock.uptimeMillis();
    while (isRunning)
    {    
      ...
    }
  }
  
  ...
}

线程在一个由 `boolean isRunning` 布尔值控制的循环中连续运行。当程序停止时,`isRunning` 布尔值被设置为 `false`,线程停止运行。在 Android OS 调用 `surfaceDestroyed` 方法时,我使用 `Join()` 语句进行等待。

public void surfaceDestroyed(SurfaceHolder surfaceHolder)
{
  androngThread.setRunning(false);
  boolean retry = true;
  while (retry)
  {
    try
    {
      androngThread.join();
      retry = false;
    }
    catch (InterruptedException e)
    {
    }
  }
}

该方法使用 `setRunning` 方法将布尔值 `isRunning` 设置为 false,这会阻止线程运行。接下来,代码调用 `androngThread.join()`,根据文档,这会阻塞当前线程直到接收者完成其执行并**终止**。

这正是我想要的行为。然而,在应用程序重启或恢复期间,我尝试(重新)启动线程时遇到了 **“Thread already started”(线程已启动)** 错误。看来 `join` 语句成功了,但没有停止线程。Thread 类还有其他方法,如 `stop()` 和 `destroy()`,但根据文档,它们都已弃用,不应使用。我决定在创建线程时解决这个问题。

下面的代码显示了我的解决方案;它并不优雅,但有效。

public void surfaceCreated(SurfaceHolder surfaceHolder)
{
  androngThread.setRunning(true);
  try
  {
    androngThread.start();
  }
  catch (Exception error)
  {
    androngThread = CreateNewAndrongThread();
    androngThread.start();
    androngThread.setRunning(true);
  }
}

该方法尝试启动线程;如果失败,则异常处理程序会创建一个新线程并启动新创建的线程。

Android 市场

我想在 Android Market 上发布 Androng。Android Market 是 Android 应用程序的开放分发平台。开放意味着您的应用程序不受监管,也没有审批流程。您的应用程序的可见性取决于您从客户那里获得的评分。

Android Market 并不是唯一的 Android 应用程序分发平台;另一个发布渠道是 Amazon。目前,Amazon 市场仅对美国客户开放。

在您可以在 Android Market 上发布游戏之前,需要一次性支付 25 美元的注册费。除此之外,Android Market 上售出的每款应用,Google 会收取 30% 的费用。我认为这与其他分发渠道相比是合理的。下图显示了 Androng 的发布者屏幕。Androng 是免费的,可以从 Android Market 下载。

Publish your application on Android market

IntelliJ 的社区版为您提供了打包应用程序的机会。在发布应用程序之前,您必须使用公钥/私钥组合签名应用程序。应用程序的更新必须使用相同的密钥进行签名。这个带有 `.apk` 扩展名的包可以发布到 Android Market。您必须在发布前填写一些字段,例如描述和一些截图、Logo 等。如果您有 Android 手机,可以通过 此处通过 Android 市场下载游戏。

Android Market 的好处是您可以深入了解您的应用程序用户。它会显示用户是否遇到任何错误、他们使用的 Android 版本以及他们拥有的设备类型。例如,下图显示了下载过 Androng 的设备类型。

Which phones run your application

源代码

游戏的源代码在此处;如果您使用 IntelliJ 社区版,可以打开项目文件。否则,您可以打开单独的源文件或 Java 文件。

下一版本

对于 Androng 的下一版本,我计划了以下功能,顺序不分先后。

  • 使用真实的物理引擎,如 Box2dAndEngine
  • 高分榜,带有 Web 上的中央存储
  • 使用 Google-guice 进行依赖注入
  • 增加挡板的移动自由度
  • 支持 Android 1.6 版本
  • 创建宣传视频

历史

  • 2011 年 5 月 1 日
    • 初始发布。
© . All rights reserved.