在这一章中,我们将从下一个项目开始,一个看起来真实的经典蛇游戏的克隆。
也是时候让我们更好地理解安卓机罩下正在发生的事情了。我们经常提到“参考文献”,但是参考文献到底是什么,它如何影响我们构建游戏的方式?
我们将在本章中讨论以下主题:
- 使用栈、堆和垃圾收集器管理内存
- 蛇游戏介绍
- 蛇游戏项目入门
让我们从理论部分开始。
在 第 4 章用 Java 方法构造代码中,我们学习了一些关于引用的知识。这里简单回顾一下。
引用引用内存中变量存储开始的地方,但是引用类型本身并不定义使用的内存的具体数量。原因很简单。
在程序执行之前,我们并不总是知道需要在内存中存储多少数据。
我们可以把字符串和其他引用类型看作是不断扩展和收缩的存储盒。那么,这些字符串引用类型中的一个最终不会碰到另一个变量吗?
如果你把设备的内存想象成一个巨大的仓库,里面堆满了货架上贴着标签的储物盒,那么你可以把 ART 系统想象成一个超级高效的叉车司机,把不同类型的储物盒放在最合适的地方。
如果有必要,ART 会在几分之一秒内快速移动东西,以避免碰撞。此外,在适当的时候,ART——叉车司机——甚至会扔掉(删除)不需要的储物箱。这与不断卸载所有类型的新存储盒并将其放置在该类型变量的最佳位置同时发生。
我们之前了解到数组和对象也是引用,但是当时我们对数组和对象的细节一无所知。现在我们做到了,我们可以再深入一点。
ART 将仓库中不同部分的引用变量保存到原始变量中。
我们知道引用是一个内存位置,但我们需要更多地理解这一点。
你可能还记得在第一个游戏项目中,我们在onCreate
中声明了一些变量,在类声明的下面声明了其他变量。当我们在类声明的正下方声明它们时,我们使它们成为成员或实例变量,并且它们对整个类是可见的。
由于所有的事情都发生在同一个类中,我们可以访问所有的变量。但是当它们在onCreate
方法中被声明时,我们为什么不能这样做呢?我们了解到这种现象被称为作用域,并且该作用域依赖于变量访问说明符(对于成员)或者在哪个方法中声明它们(对于局部变量)。
但是为什么呢?这是人工构造吗?事实上,这是处理器和内存工作和交互方式的症状。我们现在不会深入讨论这些事情,但是进一步解释何时以及如何访问不同的变量可能会有帮助。
每个安卓设备内部的 ART 负责为我们的游戏分配内存。此外,它在不同的地方存储不同类型的变量。
我们在方法中声明和初始化的变量存储在称为栈的内存区域中。我们可以在谈论栈时坚持我们的仓库类比。我们已经知道如何操纵栈。
我们来谈谈堆和那里储存的东西。所有引用类型的对象,包括对象(类的)、数组和字符串,都存储在堆的上。把堆想象成同一个仓库的一个独立区域。堆中有大量的地板空间用于放置形状奇怪的物体,有许多架子用于放置较小的物体,有许多长的排,有较小尺寸的小孔用于放置阵列,等等。这是我们存放物品的地方。问题是,我们没有直接访问堆。让我们再来看看什么是引用变量。
参考变量是我们引用并通过参考使用的变量。引用可以被松散但有效地定义为地址或位置。对象的引用(地址或位置)在栈上。因此,当我们使用点运算符时,我们要求 ART 在引用中存储的特定位置执行任务。
重要说明
引用变量就是——引用。它们是访问和操作对象(变量、方法)的一种方式,但它们本身并不是实际的变量。一个例子可能是原语就在那里(在栈上),但是引用是一个地址,我们说在这个地址做什么。在这个例子中,所有地址都在堆上。
为什么我们会想要这样的系统?已经把我的东西给我了!原因如下。
这整个栈和堆的事情是计算机架构强加给我们的,但是 ART 充当中间人,基本上帮助我们管理这个内存。
正如我们刚刚学到的,ART 为我们跟踪所有的对象,并将它们存储在我们仓库的一个称为堆的特殊区域中。ART 会定期扫描栈(我们仓库的常规货架)并匹配对象的引用。它找到的任何对象(在堆上)如果没有匹配的引用(在栈上),它就会销毁。或者用 Java 术语来说,就是垃圾收集。
想象一辆非常精确和高科技的垃圾车行驶在我们的堆中间,扫描物体以匹配参考。没有参考,你现在就是垃圾。毕竟,如果一个对象没有引用变量,我们无论如何也不可能用它做任何事情。这个垃圾收集系统通过释放未使用的内存来帮助我们的游戏更高效地运行。
然而,也有不利的一面;垃圾收集器占用了处理时间,它不知道我们游戏的时间关键部分,比如主游戏循环。因此,它也有可能使我们的游戏运行不佳。
既然我们已经意识到了这一点,我们可以强调不要在游戏的性能关键部分编写可能触发垃圾收集器的代码。那么,到底是什么代码触发了垃圾收集器呢?记得我说过:
ART 会定期扫描栈(我们仓库的常规货架)并匹配对对象的引用。如果它找到的任何对象(在堆上)没有匹配的引用(在栈上),它就会销毁。或者用 Java 术语来说,就是垃圾收集。
假设我们这样调用代码:
someVariable = new SomeClass()
然后,someVariable
超出范围,被破坏;这很可能会触发垃圾收集器。即使不是一下子发生,也会发生,我们无法决定什么时候。当我们继续进行剩下的项目时,我们会记住这一点。
因此,方法中声明的变量在栈上是局部的,并且只在它们被声明的方法中可见。成员变量在堆中,可以从引用它的任何地方引用,并且访问规范(public
、private
、protected
)允许它。
目前我们只需要知道这些。事实上,您可能在不知道这些的情况下完成了这本书,但是从 Java 学习路径的早期就理解了内存的不同区域和垃圾收集器的不可预测性,这将帮助您理解您编写的代码中发生了什么。
现在我们可以进行下一场比赛了。
蛇游戏的历史可以追溯到 20 世纪 70 年代。然而,这是在 20 世纪 80 年代,当游戏呈现出我们将使用的外观。它以许多名字和许多平台出售,但当它在 20 世纪 90 年代末作为标准在诺基亚手机上发布时,可能获得了广泛的认可。
游戏包括控制一个街区或蛇头,只需向左或向右旋转 90 度,直到你成功吃到一个苹果。当你拿到苹果时,蛇会长出一个额外的块或身体部分。
如果,或者更确切地说,当蛇撞到屏幕边缘或者不小心吃了自己,游戏就结束了。蛇吃的苹果越多,分数越高。
重要说明
你可以在这里了解更多关于蛇的历史:https://en . Wikipedia . org/wiki/Snake _(video _ game _ gentle)。
游戏开始时会显示一条消息,让游戏开始:
图 14.1–蛇游戏屏幕
当玩家轻敲屏幕时,游戏开始,他们必须引导小蛇获得第一个苹果:
图 14.2–蛇游戏的开始
注意蛇总是朝着它面对的方向移动。它从未停止。
如果玩家技术好,运气好一点,那么可以获得巨大的蛇和分数。
与其他项目相比,我们这个项目代码的一个变化是游戏对象(T0 和 T1 类)将自己绘制。我们将通过传递Paint
和Canvas
对相关类的引用来实现这一点。
这有助于封装并且是合乎逻辑的,因为苹果和蛇是完全不同的东西,需要以不同的方式描绘它们自己。虽然在SnakeGame
类中绘制所有的图形是完全可能的,但是我们拥有的游戏对象越多,代码就会变得越混乱。所以,我认为这个项目是开始用这种方式做事的好时机。
此外,在这个项目中,我不会添加任何代码来打印调试文本,所以如果您觉得有必要或者您得到了意想不到的结果,请随时添加您自己的代码。
我们将学习的另一个新的 Java 主题是ArrayList
类。这个类是 Java Collections 框架的一部分,是一系列数据存储和操作类的一部分。ArrayList
类与我们已经了解的数组有很多相似之处,但也有一些优势。我们也将从 Java Collections 中学习HashMap
类,在最后的项目中,我们将学习存储图形的更智能的方法。
现在我们知道我们要建造什么了,我们可以开始了。
首先,用空活动模板创建一个名为Snake
的新项目。
正如我们之前所做的,我们将编辑安卓清单,但是首先,我们将把MainActivity
类重构为更合适的东西。
和之前的项目一样,MainActivity
有点模糊,所以我们把MainActivity
重构为SnakeActivity
。
在项目面板中,右键单击MainActivity
文件,选择重构 | 重命名。在弹出窗口中,将主活动更改为SnakeActivity
。将所有其他选项保留为默认值,然后左键单击重构按钮。
请注意,项目面板中的文件名已按预期更改,但在AndroidManifest.xml
文件中多次出现的MainActivity
已更改为SnakeActivity
,在SnakeActivity.java
文件中也有一个实例。
让我们将设备置于横向。
与之前的项目一样,我们希望使用设备提供的每一个像素,因此我们将对AndroidManifest.xml
文件进行更改,允许我们为应用使用一种样式,从用户界面隐藏所有默认菜单和标题。
确保AndroidManifest.xml
文件在编辑器窗口中打开。
在AndroidManifest.xml
文件中,找到下面一行代码:android:name=".SnakeActivity">
。
将光标放在前面显示的关闭>
之前。点击输入键几次,将>
移动到前面显示的线的其余部分下方几行。
紧接在".SnakeActivity"
下方,但在新定位的>
之前,键入或复制并粘贴下一行代码,使游戏在没有任何用户界面的情况下运行:
android:theme=
"@android:style/Theme.Holo.Light.NoActionBar.Fullscreen"
您的代码应该如下所示:
…
<activity android:name=".SnakeActivity"
android:theme= "@android:style/Theme.Holo.Light.
NoActionBar.Fullscreen"
>
<intent-filter>
<action android:name="android.intent.action.
MAIN" />
<category android:name= "android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
…
现在,我们的游戏将使用该设备提供的所有屏幕空间,而无需任何额外的菜单。
让我们为每个游戏对象添加一个空类。
正如我们在之前的项目中所做的一样,您可以通过选择文件 | 新建 | Java 类来创建一个新类。创建三个名为Snake
、Apple
和SnakeGame
的空类。
现在我们已经习惯了面向对象编程,我们将保存几行代码并将point
引用直接传递给SnakeGame
构造函数,而不是像在以前的项目中那样将其分解成单独的水平和垂直int
变量。编辑SnakeActivty
以匹配以下所有代码。编辑内容包括将Activity
类的类型从AppCompatActivity
更改为Activity
以及附带的import
指令。这就像我们在以前的项目中所做的一样。
以下是整个SnakeActivity
代码:
import android.app.Activity;
import android.graphics.Point;
import android.os.Bundle;
import android.view.Display;
import android.view.Window;
public class SnakeActivity extends Activity {
// Declare an instance of SnakeGame
SnakeGame mSnakeGame;
// Set the game up
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Get the pixel dimensions of the screen
Display display = getWindowManager()
.getDefaultDisplay();
// Initialize the result into a Point object
Point size = new Point();
display.getSize(size);
// Create a new instance of the SnakeGame class
mSnakeGame = new SnakeGame(this, size);
// Make snakeGame the view of the Activity
setContentView(mSnakeGame);
}
// Start the thread in snakeGame
@Override
protected void onResume() {
super.onResume();
mSnakeGame.resume();
}
// Stop the thread in snakeGame
@Override
protected void onPause() {
super.onPause();
mSnakeGame.pause();
}
}
前面的代码应该看起来很熟悉。因为我们需要对SnakeGame
类进行编码,所以通篇有很多错误。
如前所述,我们不用费心从size
读取 x 和 y 值;我们只是直接将其传递给构造器。稍后,我们将看到SnakeGame
构造器已经从上一个项目更新为Context
实例和这个Point
实例。
SnakeActivity
类的其余代码在功能上与之前的项目相同。显然,我们正在使用一个新的变量名称,SnakeGame
类型的mSnakeGame
,而不是PongGame
或BulletHellGame
。
为本项目抓取声音文件;它们在 GitHub repo 上的Chapter 14
文件夹中。复制assets
文件夹,然后使用操作系统的文件浏览器导航到Snake/app/src/main
,并粘贴assets
文件夹及其所有内容。声音文件现在可用于该项目。
接下来,我们将对游戏引擎进行编码。
让我们从这个项目中最重要的一类开始:SnakeGame
。这将是蛇游戏的游戏引擎。
在您之前创建的SnakeGame
类中,添加以下import
语句以及接下来显示的所有成员变量。添加变量时,请研究它们的名称和类型,因为它们可以让我们很好地了解本课要编码的内容:
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
class SnakeGame extends SurfaceView implements Runnable{
// Objects for the game loop/thread
private Thread mThread = null;
// Control pausing between updates
private long mNextFrameTime;
// Is the game currently playing and or paused?
private volatile boolean mPlaying = false;
private volatile boolean mPaused = true;
// for playing sound effects
private SoundPool mSP;
private int mEat_ID = -1;
private int mCrashID = -1;
// The size in segments of the playable area
private final int NUM_BLOCKS_WIDE = 40;
private int mNumBlocksHigh;
// How many points does the player have
private int mScore;
// Objects for drawing
private Canvas mCanvas;
private SurfaceHolder mSurfaceHolder;
private Paint mPaint;
// A snake ssss
private Snake mSnake;
// And an apple
private Apple mApple;
}
重要说明
前面的代码中会有错误,但是随着我们添加到代码中,这些错误会逐渐消失。
让我们通过这些变量来运行。他们中的许多人会很熟悉。我们有mThread
,这是我们的Thread
对象,但是我们也有一个新的long
变量叫做mNextFrameTime
。我们将使用这个变量来跟踪我们何时想要调用update
方法。这与之前的项目有点不同,因为之前我们只是以最快的速度绕过update
和draw
,并根据画面时长相应更新游戏对象。
我们在这个游戏中要做的只是在特定的时间间隔内调用update
让蛇一次移动一个街区,而不是像我们到目前为止创建的所有移动游戏对象一样平滑地滑行。这将很快变得清晰。
我们有两个boolean
变量,mPlaying
和mPaused
,它们将用于控制线程,当我们调用update
方法时,我们可以开始和停止游戏。
接下来,我们有一个SoundPool
实例和几个int
变量用于相关的音效。
接下来,我们有一个名为NUM_BLOCKS_WIDE
的final int
变量(在执行过程中不能更改)。这个变量被赋予了值40
。我们将使用这个变量和其他变量(最显著的是屏幕分辨率)来绘制我们将在上面绘制游戏对象的网格。注意NUM_BLOCKS_WIDE
之后是mNumBlocksHigh
,在构造函数中会动态赋值。
mScore
成员变量是一个int
变量,它将记录玩家的当前得分。
接下来的三个变量mCanvas
、mSurfaceHolder
和mPaint
的用途与之前的项目完全相同,它们是安卓应用编程接口的类,使我们能够进行绘图。不同的是,如前所述,我们将把这些的引用传递给代表游戏对象的类,这样他们就可以自己绘制,而不是在这个(SnakeGame
)类的draw
方法中这样做。
最后,我们声明一个名为mSnake
的Snake
和名为mApple
的Apple
的实例。显然,我们还没有对这些类进行编码,但是我们确实创建了空类,以避免这段代码在这个阶段显示错误。
像往常一样,我们将使用构造函数方法来设置游戏引擎。后面的许多代码对您来说都很熟悉,例如签名允许传入Context
对象和屏幕分辨率。同样熟悉的是我们设置SoundPool
实例并加载所有音效的方式。此外,我们将像以前一样初始化我们的Paint
和SurfaceHolder
实例。然而,在构造函数方法的开始有一些新的代码。添加代码时,请务必阅读注释并检查代码。
将构造函数添加到SnakeGame
类中,然后我们将检查两行新代码:
// This is the constructor method that gets called
// from SnakeActivity
public SnakeGame(Context context, Point size) {
super(context);
// Work out how many pixels each block is
int blockSize = size.x / NUM_BLOCKS_WIDE;
// How many blocks of the same size will fit into the
height
mNumBlocksHigh = size.y / blockSize;
// Initialize the SoundPool
if (Build.VERSION.SDK_INT>=
Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes audioAttributes =
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes
.CONTENT_TYPE_SONIFICATION)
.build();
mSP = new SoundPool.Builder()
.setMaxStreams(5)
.setAudioAttributes(audioAttributes)
.build();
} else {
mSP = new SoundPool(5, AudioManager.STREAM
_MUSIC, 0);
}
try {
AssetManager assetManager = context.getAssets();
AssetFileDescriptor descriptor;
// Prepare the sounds in memory
descriptor = assetManager.openFd(
"get_apple.ogg");
mEat_ID = mSP.load(descriptor, 0);
descriptor = assetManager.openFd(
"snake_death.ogg");
mCrashID = mSP.load(descriptor, 0);
} catch (IOException e) {
// Error
}
// Initialize the drawing objects
mSurfaceHolder = getHolder();
mPaint = new Paint();
// Call the constructors of our two game objects
}
为了您的方便,下面再次介绍这两条新线路:
// Work out how many pixels each block is
int blockSize = size.x / NUM_BLOCKS_WIDE;
// How many blocks of the same size will fit into the height
mNumBlocksHigh = size.y / blockSize;
一个名为blockSize
的新的局部int
变量被声明,然后通过将屏幕宽度除以NUM_BLOCKS_WIDE
进行初始化。blockSize
变量现在表示我们用来绘制游戏的网格的一个位置(块)的像素数。例如,将使用该值缩放蛇线段和苹果。
现在我们有了一个块的大小,我们可以通过将垂直方向的像素数除以我们刚刚初始化的变量来初始化mNumBlocksHigh
。如果不在一行代码中使用blockSize
,就可以初始化mNumBlocksHigh
,但是这样做可以让我们的意图和由块组成的网格的概念更加清晰。
这个方法目前只有两行代码,但是随着项目的进行,我们会增加更多。将newGame
方法添加到SnakeGame
类中:
// Called to start a new game
public void newGame() {
// reset the snake
// Get the apple ready for dinner
// Reset the mScore
mScore = 0;
// Setup mNextFrameTime so an update can triggered
mNextFrameTime = System.currentTimeMillis();
}
顾名思义,每次玩家开始新游戏时都会调用这个方法。目前,所发生的只是分数被设置为0
,mNextFrameTime
变量被设置为当前时间。接下来,我们将看看如何使用mNextFrameTime
来创建块状/抖动更新,这款游戏需要看起来真实。事实上,通过将mNextFrameTime
设置为当前时间,我们正在设置立即触发更新。
此外,在newGame
方法中,您可以看到一些注释,这些注释暗示了我们将在项目稍后添加的更多代码。
这个方法和我们在之前的项目中处理run
方法的方式有些不同。添加方法并检查代码,然后我们将讨论它:
// Handles the game loop
@Override
public void run() {
while (mPlaying) {
if(!mPaused) {
// Update 10 times a second
if (updateRequired()) {
update();
}
}
draw();
}
}
在线程运行时安卓反复调用的run
方法内部,我们首先检查mPlaying
是否为true
。如果是,我们接下来检查以确保游戏没有暂停。最后,嵌套在这两个检查中,我们称之为if(updateRequired())
。如果这个方法返回true
,那么才会调用update
方法。
注意调用draw
方法的位置。这个位置意味着它会一直被称为mPlaying
是true
。
updateRequired
方法使实际的update
方法每秒只执行 10 次并创建蛇的块状运动。添加updateRequired
方法:
// Check to see if it is time for an update
public boolean updateRequired() {
// Run at 10 frames per second
final long TARGET_FPS = 10;
// There are 1000 milliseconds in a second
final long MILLIS_PER_SECOND = 1000;
// Are we due to update the frame
if(mNextFrameTime <= System.currentTimeMillis()){
// Tenth of a second has passed
// Setup when the next update will be triggered
mNextFrameTime = System.currentTimeMillis()
+ MILLIS_PER_SECOND / TARGET_FPS;
// Return true so that the update and draw
// methods are executed
return true;
}
return false;
}
updateRequired
方法声明一个名为TARGET_FPS
的新final
变量,并将其初始化为10
。这是我们的目标帧率。下一行代码是为了清晰起见而创建的变量。MILLIS_PER_SECOND
变量被初始化为1000
,因为一秒钟有 1000 毫秒。
接下来的if
语句是方法完成工作的地方。检查mNextFrameTime
是否小于或等于当前时间。如果是,则执行if
语句中的代码。在if
语句中,mNextFrameTime
通过在当前时间上添加MILLIS_PER_SECOND
除以TARGET_FPS
进行更新。
接下来,mNextFrameTime
被设置为比当前时间提前十分之一秒,准备触发下一次更新。最后,在if
语句中,return true
将触发run
方法中的代码调用update
方法。
请注意,如果没有执行if
语句,那么mNextFrameTime
将保持其原始值,而return false
将意味着run
方法不会调用update
方法。
对空update
方法进行编码,看看评论,看看我们很快会在这个方法中编码什么:
// Update all the game objects
public void update() {
// Move the snake
// Did the head of the snake eat the apple?
// Did the snake die?
}
update
方法是空的,但是注释给出了关于我们在项目后期将做什么的提示。请注意,只有当线程正在运行,游戏正在玩,没有暂停,并且updateRequired
方法返回true
时,才会调用它。
编码并检查draw
方法。请记住,每当线程正在运行且游戏正在进行时,即使update
没有被调用,也会调用draw
方法:
// Do all the drawing
public void draw() {
// Get a lock on the mCanvas
if (mSurfaceHolder.getSurface().isValid()) {
mCanvas = mSurfaceHolder.lockCanvas();
// Fill the screen with a color
mCanvas.drawColor(Color.argb(255, 26, 128, 182));
// Set the size and color of the mPaint for the
text
mPaint.setColor(Color.argb(255, 255, 255, 255));
mPaint.setTextSize(120);
// Draw the score
mCanvas.drawText("" + mScore, 20, 120, mPaint);
// Draw the apple and the snake
// Draw some text while paused
if(mPaused){
// Set the size and color of mPaint for the
text
mPaint.setColor(Color.argb(255, 255, 255,
255));
mPaint.setTextSize(250);
// Draw the message
// We will give this an international
upgrade soon
mCanvas.drawText("Tap To Play!", 200, 700,
mPaint);
}
// Unlock the Canvas to show graphics for this
frame
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
draw
方法是大部分正如我们所预料的那样。这就是它的作用:
- 检查
Surface
是否有效 - 锁定
Canvas
- 用颜色填充屏幕
- 这幅画
- 解锁
Canvas
并展示我们辉煌的图画
在前面列表中提到的做图阶段,我们用setTextSize
方法缩放文本大小,然后在屏幕左上角画出分数。接下来,在这个阶段,我们检查游戏是否暂停,如果是,我们会向屏幕中心Tap To Play!
绘制一条消息。我们几乎可以运行游戏。只是一些更简短的方法。
接下来我们要做的是onTouchEvent
方法,每次玩家与屏幕交互时,安卓都会调用这个方法。随着我们的进展,我们将在这里添加更多的代码。现在添加以下代码,如果mPaused
是true
,则将mPaused
设置为false
,调用newGame
方法:
@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
switch (motionEvent.getAction()
&MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_UP:
if (mPaused) {
mPaused = false;
newGame();
// Don't want to process snake
// direction for this tap
return true;
}
// Let the Snake class handle the input
break;
default:
break;
}
return true;
}
前面的代码具有随着每次屏幕交互在暂停和未暂停之间切换游戏的效果。
加上熟悉的pause
和resume
方法。请记住,如果螺纹未启动,则不会发生任何情况。当我们的游戏由玩家运行时,SnakeActivity
类会调用这个resume
方法并启动线程。当玩家退出游戏时,SnakeActivity
类会调用pause
,停止线程:
// Stop the thread
public void pause() {
mPlaying = false;
try {
mThread.join();
} catch (InterruptedException e) {
// Error
}
}
// Start the thread
public void resume() {
mPlaying = true;
mThread = new Thread(this);
mThread.start();
}
我们现在可以测试我们的代码了。
运行游戏你会看到蓝屏显示当前分数,**点击玩!**消息:
图 14.3–运行游戏
轻按屏幕。文本消失,更新方法每秒被调用 10 次。
我们已经扩展了栈和堆的知识。我们知道局部变量在栈上,只有在作用域内才可以访问,类及其成员变量在堆上,只要当前执行的代码引用了所需的实例,就可以随时访问。我们还知道,如果一个对象在栈上没有引用,它将被垃圾收集。这很好,因为它释放了内存,但也有潜在的问题,因为它使用处理器时间,这可能会影响我们的游戏性能。
我们已经在 Snake 游戏中有了一个良好的开端,尽管我们编写的大部分代码与以前的项目相似。唯一的例外是,我们只有在上一次调用update
方法后十分之一秒才选择性地调用update
方法。
在下一章中,我们将做一些稍微不同的事情,看看我们如何本地化一个游戏(以 Snake 为例)来提供不同语言的文本。