安卓平板游戏编程入门指南-二-

189 阅读1小时+

安卓平板游戏编程入门指南(二)

原文:Beginning Android Tablet Games Programming

协议:CC BY-NC-SA 4.0

六、球和球拍的游戏

在第五章中,你构建了一个简单的游戏,玩家躲避移动的炸弹。这给了你一个借口来使用许多在 Android 平板电脑上创建游戏的核心功能和编程概念。在这一章中,你将构建一个更复杂的游戏。

你在这一章的主要任务是建立一个 pong 类型的游戏,在这个游戏中,玩家使用一个球拍来保持球的弹跳,同时他们试图用球撞击和破坏方块。我第一次体验手机游戏是在一台旧的黑莓手机上,当时唯一的产品就是这个简单的游戏。我不得不用笨拙的黑莓轨迹球来控制球拍,小屏幕和低分辨率使这种努力不太令人满意。令人惊讶的是,这个游戏是用功能强大的 Java 语言编写的,您可以用它来创建更吸引人、更有趣的游戏。

当您构建球拍游戏时,您将掌握新的技能,并将其添加到您的工具箱中。向资源文件中添加额外的图像。你用球拍和木块替换了第五章的角色和炸弹。为了让球保持运动,您需要管理精灵的交互并检测更多的碰撞。你必须在游戏中加入一些额外的物理元素,需要更多的即时计算。你也可以用声音和消失的方块更有效地奖励玩家。最后,您将学习用单个 XML 布局文件初始化多个块的技术。

我们开始吧。

开始使用

让我们从收集你将在游戏中使用的图片和其他资源开始,然后为你的工作打开一个新的项目。

收集游戏资源

因为 pong 风格的游戏使用相当普通的形状和物体,所以制作图形应该不会有太大的困难。当然,最重要的考虑是每个元素的相对比例和大小。球拍必须足够大,能够始终如一地击球,但又足够小,这对球员来说是一个挑战。你可以看到,如果你想让能量和奖金出现在屏幕上,可以添加其他图像。

图 6-1 显示了你在这个游戏中使用的图片和尺寸。注意,它们都是不同的.png文件。对于我的实现,我自己用 GIMP 画的,GIMP 是第二章提到的一个开源工具。

除了常规的图形和声音之外,第七章将结合使用一种新的资源来存储关卡的布局。不用在每个块的位置编码,而是用 XML 布局指定它。这是这个项目的棘手部分,所以我把它留到下一章。第一次演示仅使用了三个模块,没有任何额外的放置资源。

images

***图 6-1。*方块(上图)为 30 × 50 像素,球(中图)为 30 × 30 像素,球拍(下图)为 30 × 200 像素。

如果你担心使用黑球(因为背景传统上是黑色的),不要害怕。你可以很容易地改变背景的颜色。事实上,使用更浅的颜色会让游戏对玩家更有吸引力。

images 提示球拍和球的图像是半透明的。您可以通过在 GIMP 程序中选择白色透明来做到这一点。我强烈建议你也这样做,因为如果你不完全处理方块,这个游戏会显得更专业。当其他语言需要代码使元素透明时,您很幸运能够使用带有透明层的图像。

如果你在玩游戏的时候有一些好听的声音,这个游戏会更加令人身临其境。因为乒乓球游戏不会发出一系列不同的声音,所以你可以随心所欲地使用任何声音。我选择只使用一种声音:每当球与砖块碰撞时都会播放的一小段 MP3“twang”。代码不包括任何其他噪音或音乐,但你可以自由添加它们。当你开始一个新游戏时,它越简单,就越容易发现你代码中的错误和 bug。

创建新项目

因为你的游戏是完整的(也就是它有用户交互,有目标,有获胜的能力),所以你应该把它当作一个专业的 app,而不是一个练习。因此,最好对元素和代码使用特定的名称。所以,我们就把这个 app 命名为 TabletPaddle 吧。虽然没有创意,但这个名字描述了你对 pong 风格游戏的新看法。

要开始,请按照下列步骤操作:

  1. Make an Eclipse project with your name, and copy the code in the AllTogether project to your new project. Create a new folder in res and name it raw to store the new sounds you added.
  2. Upload your assets to its specific folder. Figure 6-2 shows what the project settings look like. images ?? * ?? 】 Figure 6-2. Correct setting of test pad items*
  3. If there is an error at the beginning of the project, it is because the graphics and sound files that the code is looking for are missing. When you use the application, you can solve this problem in the code.
  4. Open the SpriteObject.java and GameView.java files in the edit pane. You can leave other source files alone.

现在你已经收集了资源,并为 TabletPaddle 打开了一个新项目,你可以编写你需要的游戏元素了,准备好使用它们的界面,并调整游戏循环。

准备游戏环境

在处理的游戏循环之前,您必须启动所有这些新精灵——球拍、球和方块——每个精灵都有不同的属性和特性。你还必须准备好使用精灵的环境——游戏界面。让我们从更改您在上一节中打开的源文件开始,为您的新游戏做准备。

修改 SpriteObject.java

SpriteObject.java需要一个额外的函数来返回MoveXMoveY值,它们是存储精灵水平和垂直速度的变量。通过这种方式,您可以轻松地反转它们,使球改变方向。在其他类型的游戏中,你可能想要检查精灵的速度,以确保它不会太快。

请遵循以下步骤:

  1. Add the following two methods in SpriteObject.java: public double getMoveY(){         return y_move; } public double getMoveX(){         return x_move; }
  2. You can make another change to SpriteObject.java to make your programming more convenient. Instead of worrying about the adj_mov variable that keeps the game at a constant speed, let the game run as fast as possible. This avoids the trouble of dealing with very small moving values and adds unpredictability to normal games. To make this change, please go to update() function and change the code as follows: public void update(int adj_mov) {                 x += x_move;                 y += y_move; }

有了这些小的修正,你在游戏循环中的工作将会更加轻松。在接下来的几页里,你会看到这些碎片拼凑在一起。

修改 GameView.java

一旦你在GameView.java中制定出你的流程和更新,你的游戏就可以最终成型了。请记住,这是您存储改变游戏性能和功能的代码的地方。以下是步骤:

  1. Because this game didn't use the noise of the previous game, so from GameView.java: private SoundPool soundPool; private int sound_id; private int ID_robot_noise; private int ID_alien_noise; private int ID_human_noise;

  2. Remove these variable declarations in, carefully check your code, and remove any references to these elements, because they will generate errors. You also need to change the two elf objects used in your previous game. The bigger your game is, the more likely you are to use a group of elves. This game is no exception. Later, you will study how to fill the block array with XML documents. You can remove the elves from the last chapter, because you don't have a bomb in this game! Declare your new elf in GameView.java: private SpriteObject paddle;         private SpriteObject[] block;         private SpriteObject ball;

  3. 添加以下变量,当测试球是否接触到边缘时,你可以使用这些变量来访问屏幕大小:private int game_width; private int game_height;

    images 注意如果这一切删除和重新键入都很麻烦,你可以通过这本书的网站([code.google.com/p/android-tablet-games/](http://code.google.com/p/android-tablet-games/))下载一个空白的机器人项目。从那里,你可以从头开始创建游戏.必须彻底重做

  4. GameView的构造函数方法,才能让你的新 app 工作。清单 6-1 展示了新的构造函数方法,并附有简要说明。确保你的代码与清单 6-1 中的代码一致。

    清单 6-1。游戏视图构造器

    `public GameView(Context con) {         super(con);         context = con;         getHolder().addCallback(this);         paddle = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.paddle), 600, 600);

            block = new SpriteObject[3];

            block[0] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.block), 300, 200); block[1] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.block), 600, 200);

            block[2] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.block), 900, 200);

            ball = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.ball), 600, 300);

            mGameLogic = new GameLogic(getHolder(), this);         createInputObjectPool();                 setFocusable(true); }`

  5. If you look back at the last project, this should look familiar. The soundPool object was deleted from the code, and new coordinates were inserted for the wizard during the initial rendering. Sometimes this can be tricky, so I like to create a blank image in GIMP, whose size is the size of the screen (1280 × 1040). Then, you can collect the coordinates that look suitable for your game.

  6. 之前的游戏涉及到三个炸弹,这里你基本上用三块来代替。显然,您希望将来有更多的块,但是这样您就可以重用所有的for循环来遍历这些块。因为您现在已经熟悉了 sprite 对象,所以请注意,您唯一需要更改的是 sprite 的位置和要使用的图像资源。你需要让球动起来。下一个必须更改的功能是surfaceCreated(),只需对 ball 功能做一些更改就可以简化它。您还可以添加两行代码,将画布或屏幕的高度和宽度分配给更新函数中使用的变量。将清单 6-2 中所示的代码添加到项目中。

    ??清单 6-2 .surfaceCreated()功能覆盖

    `@Override public void surfaceCreated(SurfaceHolder holder) {         mGameLogic.setGameState(mGameLogic.RUNNING);         mGameLogic.start();         ball.setMoveY(-10);         ball.setMoveX(10);         Canvas c = holder.lockCanvas();         game_width = canvas.getWidth();         game_height = canvas.getHeight();         holder.unlockCanvasAndPost(c);

    }`

  7. This makes the ball start to move to the upper right, which should give the player enough time to track the movement of the ball and prepare for the reaction. If the starting speed you set here seems too fast or too slow, surfaceCreated() is where you come back to change it, because you will get the speed from the ball object later.

  8. You also need to change the function of onDraw(), but again it is not a very complicated change. The cycle of drawing all bricks is the same as the cycle you used to update the bomb before. Overwrite your onDraw() function, as shown in Listing 6-3 .

清单 6-3。 onDraw()功能覆盖

@Override public void onDraw(Canvas canvas) {         canvas.drawColor(Color.WHITE);         ball.draw(canvas);         paddle.draw(canvas);         for(int i = 0; i < 3; i++){                 block[i].draw(canvas);         } }

你已经解决了基本问题。现在,您将继续为之前的碰撞和事件工作添加一些附加功能。

增加碰撞检测和事件处理

可能没有什么比在一项编码任务上极其努力地工作,然后意识到这是不必要的更糟糕的了。为了避免这个问题,我花了大量的时间绘制图表,并弄清楚程序将如何工作,它看起来会是什么样子。图 6-3 是一个图表,显示了需要做什么以及游戏循环必须如何工作。

如果你和一个团队一起开发你的应用,每个人分享一个完成项目的愿景就更加重要了。这时,您可能想要创建概念艺术,以便每个人在处理代码或素材时都有东西可看。

images

***图 6-3。*游戏循环中你必须处理的事件。每个框代表从几行代码到处理变化的整个方法。

在你之前的作品中,你测试了碰撞,然后重置了游戏。TabletPaddle 增加了一层复杂性,因为您必须以多种方式响应碰撞。最重要的是,反应必须是即时的,以避免奇怪的行为,如球穿过球拍或离开屏幕。

好消息是,在这个游戏中,与墙壁、砖块和球拍的碰撞都会导致球反向运动。例如,当你把球扔向墙壁时,球会反弹回来。如果你把同一个球扔向桌子,它也会反弹。一旦你理解了这个概念,它很容易应用到所有的游戏元素中。

然而,并不是所有的反弹都是一样的。有时你需要翻转水平速度,而其他时候你需要翻转垂直速度。球的运动的交替是指球的方向。你的MoveXMoveY值实际上是向量,当它们放在一起时,代表球的速度和方向。改变其中一个值的符号(如果它是正的,就变成负的;如果它是负的,就变成正的),这与球的前进方向相反。

图 6-4 和 6-5 说明了这是如何工作的。诀窍是检测球何时需要改变其水平方向,何时需要改变其垂直运动。这就是您必须在update()函数中使用大量代码和if语句的原因。

images

***图 6-4。*如果球从右侧撞上了木块,那么球会偏向右侧。这里水平运动发生变化,而垂直运动保持不变。

images

***图 6-5。*在这种情况下,球从底部撞到木块,然后又弹回来。因为球还是向右运动,只有垂直运动发生了变化。

你能够计算出两个精灵碰撞的时间,但是你从来没有指定物体的哪一侧被另一个精灵击中。清单 6-4 中的代码通过测试球的 x、y、右侧和底部与球拍、墙和木块的对比来解决这个问题。请注意,您在方块被击中后将其设置为dead,但是您没有做任何事情将它们从游戏中移除。一旦你测试了你当前的工作,这个问题就会得到解决。

清单 6-4 显示了你用来修改update()冲突的代码。

清单 6-4。 Update()同碰撞物理学

`public void update(int adj_mov) {

        int ball_bottom = (int)(ball.getY() + ball.getBitmap().getHeight());         int ball_right = (int)(ball.getX() + ball.getBitmap().getWidth());         int ball_y = (int) ball.getY();         int ball_x = (int) ball.getX();

        //Bottom Collision         if(ball_bottom > game_height){                 ball.setMoveY(-ball.getMoveY()); //player loses         }

        //Top collision         if(ball_y < 0){                 ball.setMoveY(-ball.getMoveY());         }

        //Right-side collision         if(ball_right > game_width){                 ball.setMoveX(-ball.getMoveX());         }

        //Left-side collision         if(ball_x < 0){                 ball.setMoveX(-ball.getMoveX());         }                        

        //paddle collision         if(paddle.collide(ball)){                 if(ball_bottom > paddle.getY() && ball_bottom < paddle.getY() + 20){                         ball.setMoveY(-ball.getMoveY());                 }         }

        //check for block collisions         for(int i = 0; i < 3; i++){                 if(ball.collide(block[i])){                         block[i].setstate(block[i].DEAD);

                        int block_bottom = (int)(block[i].getY() + images block[i].getBitmap().getHeight());

                        int block_right =(int)(block[i].getX() + images block[i].getBitmap().getWidth());

                        //hits bottom of block                         if(ball_y > block_bottom - 10){                                 ball.setMoveY(ball.getMoveY());                         }                         //hits top of block                         else if(ball_bottom < block[i].getY() + 10){                                 ball.setMoveY(-ball.getMoveY());                         }                         //hits from right                         else if(ball_x > block_right - 10){                                 ball.setMoveX(ball.getMoveX());                         }                         //hits from left                         else if(ball_right < block[i].getX() + 10){                                 ball.setMoveX(-ball.getMoveX());                         }

                }         }

        //perform specific updates         for(int i = 0; i < 3; i++){                 block[i].update(adj_mov);         }                         paddle.update(adj_mov);         ball.update(adj_mov);

}`

在开始测试碰撞之前,您需要定义球的点。这样可以节省你每次需要使用球的宽度和位置时获取球的宽度和位置的时间。我建议你尽可能地这样做,因为这确实能清理你的代码,让其他人更容易阅读。

接下来的四个if语句完成了相当简单的任务,检查球是否击中了屏幕的一个边缘。你在SpriteObject中创建的方法getMoveX()getMoveY()被使用了几次,因为你想反转球之前的任何运动。与侧壁的碰撞显然会改变水平运动,而顶部和底部会导致球在垂直方向上的移动。

你可能已经敏锐地注意到,你只是把球从屏幕底部弹开,而不是因此惩罚球员。这使得编辑游戏变得更容易,因为你不必一直担心重新启动它。

images 提示通常,当我创建一个游戏时,我会给自己留些“遗漏”或作弊的信息,这样我就不必在整个游戏中测试一个单独的部分。例如,我不想为了测试最终的挑战而战斗通过一个游戏的 10 个级别;相反,我需要跳到这一部分。

检查球与球拍碰撞的代码看起来似乎很简单,因为你只想知道球是否碰到了球拍的顶部。虽然可以想象球会撞到球拍的侧面,但这只会改变球的水平运动,仍然会导致球撞到屏幕底部,从而结束游戏。为了避免不必要的处理,我们不要担心与侧面的碰撞。没有附加方面更容易看出概念。

球拍的代码确保球在球拍顶部 20 个像素以内。因为球一次只能向任何方向移动十个单位,它永远不会越过这个窗口。始终确保该区域超过精灵的最大移动量,这样你就不必处理卡在另一个精灵里面的球或其他物品。

与积木的碰撞是一个不同的故事。要处理那些必须能够从四面八方被击中的木块,你必须做更多的工作。主要的一点是,首先分配一些变量,以便更容易地访问块的位置和尺寸。然后,首先测试最有可能发生的顶部和底部碰撞。然后你测试左右两边的打击。请注意,您查看碰撞的顺序会影响球的整体行为。

一旦其中一个条件为真,游戏将停止搜索更多可能的碰撞。图 6-6 说明了这个概念。左右碰撞框相当小,因为你不想冒险搞砸顶部或底部碰撞。

images

***图 6-6。*球在哪里会与木块相撞

添加触摸、声音和奖励

现在,您已经准备好完成应用了。你需要让用户控制游戏手柄,并添加声音和一些回报来吸引玩家。

增加拨片的触摸控制

AllTogether 项目使用平板电脑屏幕的触摸和释放来推动角色前进。在 TabletPaddle 中,根据用户在屏幕上的拖动,面板会水平移动。为了测试碰撞,你让用户在屏幕上拖动整个球拍。当你完成游戏时,你可以通过不允许用户自由拖动球拍来锁定球拍的 y 位置。

请遵循以下步骤:

  1. Here is the new processMotionEvent(), which updates the position of the paddle according to the position of the last finger touch. Modify the project code accordingly: public void processMotionEvent(InputObject input){         paddle.setX(input.x);         paddle.setY(input.y); }
  2. The code also needs some minor cleaning. Do you remember playsound() function and processOrientationEvent code? You can safely comment these out.
  3. With the ability to control paddle, you are finally ready to try TabletPaddle. Run programs and play games as usual. This may not be very interesting, but it is an amazing functional game for very limited code. Figure 6-7 shows the expected results.

images

***图 6-7。*桌面板

添加声音

游戏可以玩,但远未完成。下一步是给游戏添加声音。您可以按照前面章节中的步骤来完成此操作。因为您只想要一种声音,所以您可以使用MediaPlayer类,而不是使用SoundPool s:

  1. Add this variable to the list of variables at the beginning of the program: Private MediaPlayer mp;
  2. Insert this code into the constructor of GameView.java: mp = MediaPlayer.create(context, R.raw.bounce);
  3. Listing 6-5 shows the part of the update() function where the sound instruction is placed. Recall that no matter which side of the building block the ball touches, you will make a sound.

清单 6-5。 Update()同声

`//check for brick collisions for(int i = 0; i < 3; i++){         if(ball.collide(block[i])){                 block[i].setstate(block[i].DEAD);

                mp.start();                 int block_bottom = (int)(block[i].getY() + block[i].getBitmap().getHeight());                 int block_right =(int)(block[i].getX() + block[i].getBitmap().getWidth());

                //hits bottom of block                 if(ball_y > block_bottom - 10){                         ball.setMoveY(ball.getMoveY());                 }                 //hits top of block                 else if(ball_bottom < block[i].getY() + 10){                         ball.setMoveY(-ball.getMoveY());                 }                 //hits from right                 else if(ball_x > block_right - 10){                         ball.setMoveX(ball.getMoveX());                 }                 //hits from left                 else if(ball_right < block[i].getX() + 10){                         ball.setMoveX(-ball.getMoveX());                 } } }`

实例化块

随着一些噪音的继续,你可以找到一种方法来添加更多的块,使游戏变得有趣。您可以将 x 和 y 位置放入一个 XML 文档中,而不必经历对每个块的位置进行硬编码的艰苦过程。在将数据存储到另一个 XML 文件时,Android 非常聪明。事实上,这种做法是非常值得鼓励的,因为它使代码更具可读性和可编辑性,性能上只有轻微的滞后,这通常是不明显的。

以下是步骤:

  1. Right-click the values folder in the res folder, select New, and then select "File" to create blockposition.xml. Type the name blockposition.xml. Here is the starting code for typing this new file. The goal is to keep the blocks in the same position, but allow you to add more as you think fit: `     3

             300     600     900     

             200     200     200     

    `

  2. All this code does is create an integer value of 3, which specifies how many blocks there will be. Then, the two arrays handle the X and Y positions of the block respectively. When adding more blocks, update the blocknumber value and add more positions for the blocks.

  3. To access the data stored in the XML file, at GameView.java: private Resources res; private int[] x_coords; private int[] y_coords; private int block_count;

  4. Declare these variables at the top of the. You are using the Resources class, so add the following line of code to your import set: import android.content.res.Resources;

  5. In the constructor of GameView.java, delete the lines of code that handle these blocks. You have to completely redo that part. The following is the improved new code, which extracts data from the XML document you created: res = getResources(); block_count = res.getInteger(R.integer.blocknumber); x_coords = res.getIntArray(R.array.x); y_coords = res.getIntArray(R.array.y); block = new SpriteObject[block_count]; for(int i = 0; i < block_count; i++){ block[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.block), x_coords[i], y_coords[i]); }

  6. res is basically a handler for you to call functions getInteger() and getIntArray() from XML files. The array and integer are stored, and then a for loop is executed to create each new block. You no longer need to specify the number of blocks in the code, so it is very easy to change the number of blocks.

  7. Unfortunately, the number of blocks you originally specified was 3. Now you need to replace the values in the onDraw() and update() functions. Find these points and insert block_count in the for ring where you see 3. The update() method must be changed in two places, because it calls the update() function of each wizard at the end and needs to check the collision between each block and the ball.

images 注意我喜欢在 XML 文件中存储块的布局和位置的原因之一是能够容易地比较每个块在哪里。例如,您首先使用的三个块的 y 值都是 200。这使得慢慢增加 x 值变得容易,因为您可以注意到垂直位置的趋势。因为块的高度为 30 像素,所以可以在垂直位置 y = 230 处制作下一组块。

移除死块

在认真对待这个游戏之前,必须解决一个主要问题:方块必须在被击中后消失。您已经将它们的状态设置为dead,但是您没有以任何方式对状态做出响应。为了解决这个问题,您需要在SpriteObject.java文件中做一些工作。

基本上,每个函数都必须有一个检查其状态的初始if语句。如果块是活动的,那么动作继续。如果不是,函数返回null,不担心死 sprite。

请遵循以下步骤:

  1. Add this statement to the SpriteObject constructor to ensure that all created wizards are active. It is useless to initialize a dead sprite: state = ALIVE;

  2. 看清单 6-6 中的代码为draw()update()collide()。一个简单的if语句只有在 sprite 活动时才继续。

    清单 6-6:draw()``update()``collide()

    `public void draw(Canvas canvas) {         if(state == ALIVE){                 canvas.drawBitmap(bitmap, (int)x - (bitmap.getWidth() / 2), (int)y - (bitmap.getHeight() / 2), null);         } }

    public void update(int adj_mov) {         if(state == ALIVE){                 x += x_move;                 y += y_move;         } }

    public boolean collide(SpriteObject entity){         if(state == ALIVE){                 double left, entity_left;                 double right, entity_right;                 double top, entity_top;                 double bottom, entity_bottom; left = x;                 entity_left = entity.getX();                 right = x + bitmap.getWidth();                 entity_right = entity.getX() + entity.getBitmap().getWidth();                 top = y;                 entity_top = entity.getY();                 bottom = y + bitmap.getHeight();                 entity_bottom = entity.getY() + entity.getBitmap().getHeight();

                    if (bottom < entity_top) {                         return false;                 }                 else if (top > entity_bottom){                         return false;                 }                 else if (right < entity_left) {                         return false;                 }                 else if (left > entity_right){                         return false;                 }                 else{                         return true;                 }         }                          else{                 return false;         }                  }`

唯一真正的技巧是,collide()函数需要在末尾有一个else语句,因为必须从该方法返回一个值。否则,你已经集成了一个非常简单的程序,使你的块消失,只要他们被击中。您仍然可以访问块的 x、y、位图和状态,但是没有必要这样做。

总结

你在这一章完成了很多。就目前情况来看,TabletPaddle 是一款不错的游戏,有很大的发展和改进空间。困难和核心功能是存在的,物理处理碰撞流畅,游戏反应迅速和正确。我已经整理了一个列表,列出了一些可能会引起你兴趣的游戏创意。它们都不涉及 Android touch 编程,但它们确实涉及逻辑和创造力:

  • Reset the game when the ball lands: At this time, the ball just keeps bouncing. What about the image with the word "Game over"?
  • Keep score: You can detect when a ball is hit, so why not track the number of hits? Users can see how well they are doing.
  • Add levels: This task may be quite laborious, but please remember that the only difference between levels in this game is the layout of the squares.

brickposition.xml文件中,您可以创建整数集和整数数组来存储每个级别中块的位置。读完这一章后,你就可以开发一些杀手级应用了。在处理球、球拍和木块之间的碰撞时,你学到了一些新的技能。您还学习了一些游戏逻辑,并开发了一种有趣的方法来处理复杂的冲突。在对用户输入做出反应这一至关重要的世界中,你提供声音并让方块消失以奖励用户的工作。

在未来,你增加了平板电脑操作的复杂性。具体来说,在下一章,处理器将有自己的思想。代码可以自己创建事件,并使玩家对不可预测的行为做出反应,而不是只处理玩家的动作。

七、构建双人游戏

你在 Android 平板电脑游戏上做了一些了不起的工作。现在,您将通过允许一个人与附近的其他人进行游戏来为工作添加另一个级别。这是制作拥有大量粉丝的游戏的关键一步。如果你看看现在众多流行的游戏,绝大多数主要是为了在用户自己的家里玩朋友和陌生人的游戏。

添加连接多个设备的功能变得相当复杂。幸运的是,Android 文档提供了一些示例,您可以对其进行修改以实现您的目标,因此您所要做的就是理解代码是如何工作的,然后将它整合到您的游戏中。

在这一章中,你将学习多人游戏的各个方面,包括不同的类型和实现。然后,你继续专注于 Android。在本章的结尾,你会明白如何创建和改编你自己的游戏来获得多人游戏的体验。在你开始之前,让我们先看看不同类型的多人游戏模式,以及它们通常是如何实现的。

images 注意如果你对这一章中的任何一部分代码感到困惑,请继续阅读,它们会一起出现。如果你还是不明白,可以上网查询解决方案,或者运行程序,只修改你需要的部分。Android 文档总是一个很好的起点:[developer.android.com/guide/index.html](http://developer.android.com/guide/index.html)。通常,只要你理解它是如何工作的,就没有必要编写所有的代码。

了解多人游戏

你曾经通过电子游戏机或者你的个人电脑和别人玩第一人称射击游戏吗?这些游戏每年为视频游戏公司带来数亿美元的收入,因为它们能够吸引其他玩家,而不仅仅是电脑创造的角色。

涉及整个世界的在线游戏也非常受欢迎(想想魔兽世界)。平板电脑和手机也抓住了这股越来越多的连接热潮。可能最新类型的多人游戏是社交游戏。Farmville、Mafia Wars 和其他各种产品连接到社交网站(最著名的是脸书),将您的进度信息传输到您的朋友正在玩的游戏中。

通过服务器进行多人游戏

刚才提到的所有游戏都涉及到通过服务器连接玩家。这意味着设备或玩家不是直接相互连接,而是通过另一个实体连接。事实上,互联网上的网站使用同样的方法:你(客户端)从网站(服务器)获取网页材质。

图 7-1 是一个简单的图表,展示了几个人为了玩多人游戏而连接到一个服务器。

images

***图 7-1。*一群来自不同地方的玩家登录一个中央服务器,然后就可以互相对战。

在您研究服务器类型的多人游戏的优缺点之前,能够将这种方法与其他方法进行比较是有帮助的。我们来看对等方法。

点对点多人游戏

当玩家彼此直接连接时,他们使用的是点对点(P2P)网络。对手在几英尺之内玩的 P2P 游戏通常使用蓝牙实现,蓝牙是大多数 Android 平板电脑上可用的局域网协议。这意味着没有实体控制所有的通信。如果你使用过 P2P 文件共享网络(例如,使用 torrents 从其他用户而不是单一服务器下载大文件),那么你已经连接到其他像你一样的计算机来下载文件;你不需要每个人都连接的大型服务器。许多大型游戏机视频游戏不使用点对点模式,因为一次只能有几个玩家玩。

要了解服务器-客户端游戏和点对点游戏的区别,请看一下图 7-2 。

images

***图 7-2。*为了在游戏中竞争,两个玩家直接相互连接。

显然,多人游戏的这两种策略非常不同,但你可能想知道哪种更好。没有正确的答案;相反,存在一个优于另一个的情况。

选择多人游戏方式

表 7-1 和表 7-2 列出了两种多人游戏方式的主要优缺点。这不是一个官方列表(有些人可能不同意某件事是积极的还是消极的),但它给了你一个如何选择解决方案的非常重要的把握。

images

images

如果您仔细查看了这些表,您应该已经注意到服务器-客户机方法的优点列是对等方法的缺点列,而对等方法的优点是服务器-客户机的缺点。然而,将每种类型的优缺点相加并不能得出正确的选择。相反,你必须为你想要创造的东西制定一个计划,然后选择最能让你实现目标的方法。

在本章的剩余部分,您将改编您在第六章中为两个玩家构建的平板划球拍游戏,每个玩家都可以控制平板电脑上显示的两个划球拍中的一个。因为多人游戏编程可能很复杂,这一章将介绍主要概念。这里有完整的代码供您使用:[code.google.com/p/android-tablet-games/](http://code.google.com/p/android-tablet-games/)

因为你一次只需要容纳两个玩家,而你又想用最高效的手段打造这样一个游戏,所以你使用了点对点的多人游戏模式。你可以使用大多数 Android 设备上的蓝牙网络直接连接玩家,而不是通过 3G 或 Wi-Fi 网络连接。通过选择这种方式,您可以节省大量的时间,而这些时间本来是用来设置服务器架构和确保设备能够正确连接的。

images 提示对于初学游戏编程的人来说,最好远离服务器-客户端多人游戏,因为它们几乎总是要复杂得多。不要因此而气馁;您可以通过蓝牙连接创建大量优秀的游戏。在这种情况下,玩家的额外兴奋是他们几乎总是在彼此附近,可以通过棘手的关卡互相交谈,或者进行一些有趣的垃圾交谈。

构建一个双人点对点游戏

作为一名开发人员,您可以合理地确信,大多数 Android 平板设备都支持蓝牙。几乎所有现代手机都支持蓝牙连接无线耳机,实现免提通话。这项技术在平板电脑中实现,以允许使用相同的耳机以及键盘和各种其他外围设备。

虽然有些人使用术语蓝牙来指代耳机和他们用来连接电话的设备,但实际上蓝牙是一种无线电广播系统,各种设备都用它来连接和共享照片、音乐、视频和几乎所有其他类型的数据。蓝牙最大的优点是速度快得令人难以置信。如果你可以用蓝牙耳机不间断地打电话,那么你可以确信它对大多数游戏来说足够快了。

在接下来的几节中,您将改编第六章中的平板球拍游戏,供两名玩家使用。首先添加代码,使用内置的蓝牙无线电连接两个 Android 平板电脑,然后添加第二个球拍和代码,允许球员争夺球的控制权。

我们开始吧。首先打开一个新的 Eclipse 项目,并将其命名为 TwoPlayerPaddleGame。

增加蓝牙连接

因为连接多个设备是一项复杂的任务,在 Android 平板电脑上支持这种交互的代码更难解释。本例中的片段摘自 Android samples 中一个更大的蓝牙项目:BluetoothChat。您在这里使用它们来探索主要概念。这些变量还没有全部初始化,但它们仍然传达了基本信息。在深入这个例子之前,让我们先来看看构成一个成功的蓝牙应用的大部分元素。

首先,您必须初始化与平板电脑内蓝牙连接器的链接。请遵循以下步骤:

  1. 将清单 7-1 所示的代码包含在MainActivity.javaonCreate()函数中。

    清单 7-1。onCreate()

    `BlueAdapter = BluetoothAdapter.getDefaultAdapter();

    if (BlueAdapter == null) {     Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();     return; }`

BlueAdapter成为设备中蓝牙功能的手柄。if语句用于确定蓝牙是否可用。然后,该函数向用户发布消息,提醒他们不能使用该程序。

2.你的启动的另一部分发生在一个你以前没有处理过的方法中:在MainActivity.java中,紧随onCreate()之后的onState()函数;参见清单 7-2 。您还需要导入android.intent.Intent,它让活动发送消息。

清单 7-2。??onStart()

`    @Override     public void onStart() {         super.onStart();

        if (!BlueAdapter.isEnabled()) {             Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);             startActivityForResult(enableIntent, REQUEST_ENABLE_BT);         } else {             if (game_running == null) startgame();         }     }`

清单 7-2 中的代码检查蓝牙设备看它是开着还是关着。它通过调用来启动蓝牙设备。(您很快就会看到这个新活动的表现。)如果蓝牙打开,你检查游戏是否已经开始。如果没有,你调用一个新的函数来初始化游戏。请注意,您的大部分额外代码都围绕着这样一个事实,即游戏的许多方面都必须在开始之前等待正确的蓝牙连接。

3.当活动被发送消息时,使用清单 7-3 中的代码。

清单 7-3。??onActivityResult()

`public void onActivityResult(int requestCode, int resultCode, Intent data) {     switch (requestCode) {     case REQUEST_CONNECT_DEVICE:

        if (resultCode == Activity.RESULT_OK) {

            String address = data.getExtras()                                  .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);

            BluetoothDevice device = BlueAdapter.getRemoteDevice(address);

            mGameView.connect(device);         }         break; case REQUEST_ENABLE_BT:

        if (resultCode == Activity.RESULT_OK) {

            startgame();         } else {

        Toast.makeText(this, “Bluetooth failed to initiate”, Toast.LENGTH_SHORT).show();             finish();         }     } }`

上面的代码做了两件简单的事情。首先,如果您用连接另一个设备的请求来调用它,它会完成收集另一个设备的地址并创建一个到它的蓝牙设备的链接的步骤。然后它调用mGameView中的一个新函数将两个设备绑定在一起。

4.现在你有了一个非常简短的startgame()函数。清单 7-4 展示了游戏是如何开始的。

清单 7-4。??startgame()

`    private void startgame() {

        mGameView = new GameView(this, mHandler);         setContentView(mGameView);

}`

这个函数很大程度上并不令人兴奋,但是需要注意的是,您正在向GameView构造函数发送一个新的参数。处理程序是你从蓝牙通道向游戏发送数据的手段。理解这是如何工作的可能是蓝牙编程最重要的方面。

5.清单 7-5 中的代码围绕着处理发送和接收数据的不同任务的处理器。

***清单 7-5。*处理经办人

`private final Handler mHandler = new Handler() {     @Override     public void handleMessage(Message msg) {         switch (msg.what) {         case MESSAGE_STATE_CHANGE:             switch (msg.arg1) {             case BluetoothChatService.STATE_CONNECTED:                 break;             case BluetoothChatService.STATE_CONNECTING:                 Toast.makeText(this, “Connecting to Bluetooth”, Toast.LENGTH_SHORT).show();

                break;             case BluetoothChatService.STATE_LISTEN:             case BluetoothChatService.STATE_NONE:                 Toast.makeText(this, “Not Connected to Bluetooth”, Toast.LENGTH_SHORT).show();                break;            }            break;

       case SEND_DATA:            byte[] writeBuf = (byte[]) msg.obj;

           String writeMessage = new String(writeBuf);

           break;         case RECEIVE_DATA:             byte[] readBuf = (byte[]) msg.obj;

            String readMessage = new String(readBuf, 0, msg.arg1);

            break;         case MESSAGE_DEVICE_NAME:

            mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);             Toast.makeText(getApplicationContext(), "Connected to "                            + mConnectedDeviceName, Toast.LENGTH_SHORT).show();             break;         case MESSAGE_TOAST:             Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST),                            Toast.LENGTH_SHORT).show();             break;         }     } };`

因为处理程序的初始化做了很多工作,所以下面列出了各种活动供您查看。一旦你真正创建了自己的项目,你就会回到这个问题上来。基本上,处理程序被传递一个特定的消息或事件,它必须处理或忽略。它有各种各样的反应,你必须编码。请记住,这些是从GameView类发送的:

  • MESSAGE_STATE_CHANGE: The first case is the change of Bluetooth connection status. In most cases, if the status becomes disconnected, you will remind the user. For example, if the service is trying to connect, you will remind the user of this. If the connection cannot be established in an unfortunate event, you can also remind the user by explaining the problem. This also helps to debug problems.
  • SEND_DATA: The next event is to send data to the other device. Here, you have collected the code string and are ready to send it to another device. You didn't actually send it here; You can come back later to add this feature.
  • RECEIVE_DATA: Similar to you calling to write messages to the other device, you also accept data from the other device. Similarly, when you decide what you want to accomplish, there will be more code in this area.
  • MESSAGE_DEVICE_NAME: The penultimate message is a call, which just reminds the user that they have connected to a specific device. You remind the user through a small pop-up box.
  • MESSAGE_TOAST: Finally, you have a general method to send messages from the GameView class to users.
管理蓝牙连接

随着GameView.java的增加,你将回到更熟悉的领域。请记住,您需要将大部分代码放在这里,因为您可以在这里根据平板电脑之间来回发送的数据来更改精灵的位置。

清单 7-6 、 7-7 和 7-8 显示了三个小线程的代码,您必须将它们添加到GameView中,以处理两个玩家交互时出现的各种蓝牙操作:AcceptThreadConnectThreadConnectedThreadAcceptThread处理初始连接,ConnectThread处理复杂的设备配对,ConnectedThread是设备在一起时的正常程序。

清单 7-6。??AcceptThread

`private class AcceptThread extends Thread {     // The local server socket     private final BluetoothServerSocket mmServerSocket;

    public AcceptThread() {         BluetoothServerSocket tmp = null;

        // Create a new listening server socket         try {             tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);         } catch (IOException e) {             Log.e(TAG, "listen() failed", e);         }         mmServerSocket = tmp;     } public void run() {         if (D) Log.d(TAG, "BEGIN mAcceptThread" + this);         setName("AcceptThread");         BluetoothSocket socket = null;

        // Listen to the server socket if you're not connected         while (mState != STATE_CONNECTED) {             try {                 // This is a blocking call and will only return on a                 // successful connection or an exception                 socket = mmServerSocket.accept();             } catch (IOException e) {                 Log.e(TAG, "accept() failed", e);                 break;             }

            // If a connection was accepted             if (socket != null) {                 synchronized (BluetoothChatService.this) {                     switch (mState) {                     case STATE_LISTEN:                     case STATE_CONNECTING:                         // Situation normal. Start the connected thread.                         connected(socket, socket.getRemoteDevice());                         break;                     case STATE_NONE:                     case STATE_CONNECTED:                         // Either not ready or already connected. Terminate new socket.                         try {                             socket.close();                         } catch (IOException e) {                             Log.e(TAG, "Could not close unwanted socket", e);                         }                         break;                     }                 }             }         }         if (D) Log.i(TAG, "END mAcceptThread");     }

    public void cancel() {         if (D) Log.d(TAG, "cancel " + this);         try {             mmServerSocket.close();         } catch (IOException e) {             Log.e(TAG, "close() of server failed", e);         }     } }`

AcceptThread是一段复杂的代码,但实际上它只是等待一个连接被接受。请注意,关键字socket频繁出现。这是设备或实体之间任何类型连接的标准,指的是交换信息的能力。这个代码不是我的;它是从 Android 文档的一个例子中重复使用的。一些 it 方法和代码块非常高效,不需要重做。

清单 7-7。??ConnectThread

`private class ConnectThread extends Thread {     private final BluetoothSocket mmSocket;     private final BluetoothDevice mmDevice;

    public ConnectThread(BluetoothDevice device) {         mmDevice = device;         BluetoothSocket tmp = null;

        // Get a BluetoothSocket for a connection with the         // given BluetoothDevice         try {             tmp = device.createRfcommSocketToServiceRecord(MY_UUID);         } catch (IOException e) {             Log.e(TAG, "create() failed", e);         }         mmSocket = tmp;     }

    public void run() {         Log.i(TAG, "BEGIN mConnectThread");         setName("ConnectThread");

        // Always cancel discovery because it will slow down a connection         mAdapter.cancelDiscovery();

        // Make a connection to the BluetoothSocket         try {             // This is a blocking call and will only return on a             // successful connection or an exception             mmSocket.connect();         } catch (IOException e) {             connectionFailed();             // Close the socket             try {                 mmSocket.close();             } catch (IOException e2) {                 Log.e(TAG, "unable to close() socket during connection failure", e2);             }             // Start the service over to restart listening mode             GameView.this.start();             return;         }

        // Reset the ConnectThread because you're done         synchronized (BluetoothChatService.this) {             mConnectThread = null;         }

        // Start the connected thread         connected(mmSocket, mmDevice);     }

    public void cancel() {         try {             mmSocket.close();         } catch (IOException e) {             Log.e(TAG, "close() of connect socket failed", e);         }     } }`

这个线程与前一个线程相似,它处理连接到另一个设备的尝试。Android 的例子也包括这个,所以我没有对它做任何修改。如果你好奇的话,它会尝试ping或者与另一个设备建立连接。如果失败,它可以通过try块调用继续尝试,失败会导致重启。

幸运的是,您真的只对来回发送数据感兴趣,不需要改变连接的建立方式。

清单 7-8。??ConnectedThread

`private class ConnectedThread extends Thread {     private final BluetoothSocket mmSocket;     private final InputStream mmInStream;     private final OutputStream mmOutStream;

    public ConnectedThread(BluetoothSocket socket) {         Log.d(TAG, "create ConnectedThread");         mmSocket = socket;         InputStream tmpIn = null;         OutputStream tmpOut = null;

        // Get the BluetoothSocket input and output streams         try {             tmpIn = socket.getInputStream();             tmpOut = socket.getOutputStream();         } catch (IOException e) {             Log.e(TAG, "temp sockets not created", e);         }

        mmInStream = tmpIn;         mmOutStream = tmpOut;     }

    public void run() {         Log.i(TAG, "BEGIN mConnectedThread");         byte[] buffer = new byte[1024];         int bytes;

        // Keep listening to the InputStream while connected         while (true) {             try {                 // Read from the InputStream                 bytes = mmInStream.read(buffer);

                // Send the obtained bytes to the UI Activity                 mHandler.obtainMessage(MainActivity.MESSAGE_READ, bytes, -1, buffer)                         .sendToTarget();             } catch (IOException e) {                 Log.e(TAG, "disconnected", e);                 connectionLost();                 break;             }         }     }

    /**      * Write to the connected OutStream.      * @param buffer  The bytes to write      */     public void write(byte[] buffer) {         try {             mmOutStream.write(buffer);

            // Share the sent message back to the UI Activity             mHandler.obtainMessage(MainActivity.MESSAGE_WRITE, -1, -1, buffer)                     .sendToTarget();         } catch (IOException e) {             Log.e(TAG, "Exception during write", e);         }     }

    public void cancel() {         try {             mmSocket.close();         } catch (IOException e) {             Log.e(TAG, "close() of connect socket failed", e);         }     } }`

类做了大量的工作。每当设备处于连接状态时,此代码都会运行。请注意,它首先收集输入和输出流,以便可以从其他设备访问数据,然后再发送自己的信息。

接下来,run()方法进入一个循环,不断检查它可以处理的新数据。大多数数据都是以整数的形式发送的,但是发送字符串作为设备之间的交换还是有一些好处的。首先,在一个复杂的游戏中,可能有许多数字需要发送,如生命值、弹药、位置和库存。仅仅发送数字意义不大。相反,可以快速解析像“a:10”这样的字符串,以查找冒号后面的数字和冒号前面的字符,从而确定必要的更改。

在循环之外,线程有一个方法将缓冲区上的消息发送到另一个设备。它是不言自明的,并按原样发送消息。

在这些线程之前,添加一些用于发送数据和调用线程来执行某些操作的方法。请记住,您还没有以任何方式初始化或利用线程。清单 7-9 中的代码启动了它们。

***清单 7-9。*连接到蓝牙设备

`public synchronized void start() {     if (D) Log.d(TAG, "start");

    // Cancel any thread attempting to make a connection     if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}

    // Cancel any thread currently running a connection     if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}

    // Start the thread to listen on a BluetoothServerSocket     if (mAcceptThread == null) {         mAcceptThread = new AcceptThread();         mAcceptThread.start();     }     setState(STATE_LISTEN); } public synchronized void connect(BluetoothDevice device) {     if (D) Log.d(TAG, "connect to: " + device);

    // Cancel any thread attempting to make a connection     if (mState == STATE_CONNECTING) {         if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}     }

    // Cancel any thread currently running a connection     if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}

    // Start the thread to connect with the given device     mConnectThread = new ConnectThread(device);     mConnectThread.start();     setState(STATE_CONNECTING); }

public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {     if (D) Log.d(TAG, "connected");

    // Cancel the thread that completed the connection     if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}

    // Cancel any thread currently running a connection     if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} // Cancel the accept thread because you only want to connect to one device     if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}

    // Start the thread to manage the connection and perform transmissions     mConnectedThread = new ConnectedThread(socket);     mConnectedThread.start();

    Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_DEVICE_NAME);     Bundle bundle = new Bundle();     bundle.putString(BluetoothChat.DEVICE_NAME, device.getName());     msg.setData(bundle);     mHandler.sendMessage(msg);

    setState(STATE_CONNECTED); }

public synchronized void stop() {     if (D) Log.d(TAG, "stop");     if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}     if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}     if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}     setState(STATE_NONE); }

public void write(byte[] out) {     // Create temporary object     ConnectedThread r;     // Synchronize a copy of the ConnectedThread     synchronized (this) {         if (mState != STATE_CONNECTED) return;         r = mConnectedThread;     }     // Perform the write unsynchronized     r.write(out); }

private void connectionFailed() {     setState(STATE_LISTEN);

    // Send a failure message back to the Activity     Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_TOAST);     Bundle bundle = new Bundle();     bundle.putString(BluetoothChat.TOAST, "Unable to connect device");     msg.setData(bundle);     mHandler.sendMessage(msg); }

private void connectionLost() {     setState(STATE_LISTEN);

    // Send a failure message back to the Activity     Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_TOAST);     Bundle bundle = new Bundle();     bundle.putString(MainActivity.TOAST, "Device connection was lost");     msg.setData(bundle);     mHandler.sendMessage(msg); }`

一旦你看到了线程,你就会明白这些函数主要用来启动线程。前三个函数启动三个线程(AcceptThreadConnectThreadConnectedThread)。当你的游戏遇到终点(也就是角色死亡)时,会调用stop()函数来确保没有线程继续。当你想发送一些东西到另一个设备时,你也可以使用write()方法。

最后,另外两种方法使用Handler在连接丢失或失败时显示消息。

为两个玩家改编游戏代码

您已经完成了处理建立连接并维护它们的大部分代码。现在你需要弄清楚你的游戏将如何使用蓝牙。这个示例游戏的整个代码太大了,不适合本书的篇幅,但是你可以从 code.google.com/p/android-t…

事不宜迟,你想在游戏过程中在屏幕上有两个球拍:一个在顶部,一个在底部。清单 7-10 包含了来自GameViewupdate()方法的重要代码。请注意,您必须在前面的函数中初始化paddle_other精灵,并将其添加到draw()函数中。它被放置在屏幕的顶部,与另一个球拍的图像相同。

***清单 7-10。*增加了球拍和碰撞检测,并更新了游戏状态

`//paddle input int val=0; for (int i=latest_input.length-1, j = 0; i >= 0; i--,j++)         {                 val += (latest_input[i] & 0xff) << (8*j);         } paddle_other.setX(val);

//paddle_other collision int paddle_other_bottom = paddle_other.getBitmap().getHeight(); if(paddle_other.collide(ball)){         if(ball_y < paddle_other_bottom && ball_y < paddle_other_bottom + 20){                 ball.setMoveY(-ball.getMoveY());         } }

//paddle output byte[] paddle_output; ByteBuffer bb = ByteBuffer.allocate(4); bb.putInt((int)paddle.getX()); paddle_output = bb.array(); write(paddle_output);`

清单 7-10 中的代码做了三件事。首先,它根据控制它的其他设备的输入将paddle_other移动到该位置。第二,它检测碰撞。第三,它将你控制的球拍的位置发送到另一个设备,这样你的对手就可以看到你最近的移动。

稍微分解一下,for循环将您作为输入获得的字节数组转换成一个整数,用于移动拨片。幸运的是,您还不需要将byte[]解析成更复杂的值。

碰撞检测与另一个球拍的碰撞检测类似,但是您颠倒了检测,因为您只对球撞击底部感兴趣,而不是顶部。如果你愿意,当球碰到顶部时,你可以使游戏重置或结束,给玩家 2 同样的强度。

最后,您将 paddle 的位置转换为一个字节数组,并将其发送到您的write()函数,该函数又将它发送到connectedThread进行处理。

测试游戏

测试一个使用蓝牙的多人游戏应用可能有点棘手。如果你有两个 Android 平板电脑,那么你可以利用它们的内置功能相互连接。然后将程序加载到两台设备上。如果你没有或者不想要几个平板,你必须做不同的安排。

显然,测试这些程序的另一种可能方式是借用别人的平板电脑,并将其与自己的平板电脑配对。请注意,要在另一台平板电脑上安装软件,您需要遵循附录 A 中针对所有平板电脑的说明。在你开始实验之前,确保你的朋友或亲戚明白你在对他们的平板电脑做什么!

将蓝牙 USB 加密狗插入您的计算机并期望您的仿真器能够处理蓝牙可能很有诱惑力。可悲的是,事实并非如此;模拟器目前不具备处理蓝牙的能力。在添加此功能之前,您必须使用真实设备进行测试。

总结

再次祝贺你:你在 Android 游戏开发的一些有趣的蓝牙和多人游戏方面取得了成功。这个话题是你在游戏编程中会遇到的最困难的话题之一。现在你已经准备好在这本书结尾的大型游戏上工作了。准备好接受更多的精灵和声音,以及更多的代码。

八、单人策略游戏第一部分:构建游戏

是时候做你的最后一个游戏了,一个单人策略游戏——Harbor Defender——在这个游戏中,你使用了你在前面章节中开发的概念和代码。大多数内容都是我们已经学过的东西。你利用你已经知道的东西。一些游戏开发书籍喜欢以华丽的 3D 游戏结尾。我选择不走这条路,因为没有足够的时间来教你添加第三维度的所有细微差别。编写一个 3D 游戏并不容易:当你在智能手机或平板电脑上玩一个游戏时,你可以相当肯定它是由一个大型团队创作的。我在这本书里的目标是教你如何创建你可以自己编程的游戏。这样,您不必与任何人分享您的利润,也不必与其他开发人员争论您的设计和实现决策!

在你构建的战略游戏中,用户必须保卫一个堡垒,防止敌人从海上攻击。游戏的设计允许你通过增加新类型的防御和增加敌人的数量来增加它的难度。还可以添加布局来创建更具挑战性的游戏关卡。

在这一章,两章中的第一章,你的重点是设置游戏和它的元素,并创建一个系统,使一切顺利运行。在下一章中,你将通过实现一个点计数器和一些有趣的用户控件来使游戏更加精彩。

images 注意因为你在为平板电脑开发游戏,你需要记住让它的开发不同于手机或桌面游戏的方面。这些差异包括使用触摸屏、应对屏幕尺寸以及设计直观的用户控件。一些不熟悉平板电脑的开发人员很想把他们以前的项目移植到平板电脑上。这可以很好地工作,但浏览一下 app store 会让你相信,那里的大多数游戏都是为平板电脑定制的,不能在任何其他硬件上很好地工作。通常情况下,用户只是在他们的游戏系统上玩原版游戏,他们希望他们的平板电脑体验有一个特殊的游戏。

让我们先来看看战略游戏的布局,然后组装构建它所需的元素。

介绍港湾卫士

港口保卫者是我为你在本章开始的游戏选择的名字。游戏表面由一个堡垒,一个由码头定义的港口,攻击船和可以用子弹击沉攻击者的大炮组成。图 8-1 是你在本章结束时组装的游戏面的图像。在第九章中,你添加了用户控件,但是现在你需要一个玩家最终会与之交互的界面。它让你知道机械是如何工作的。

images

***图 8-1。*港湾保卫者的测试版

港口守卫者的目标是在船只入侵要塞之前,摧毁通过港口接近要塞的船只。玩家通过发射位于港口周围的码头上的大炮来击退船只。每个桥墩都可以容纳一门大炮,但是使用者必须将它瞄准正确的方向。为了让游戏更具挑战性,用户不能制造无限数量的大炮。相反,用户得到的加农炮数量是有限的,因此他们必须明智地选择加农炮的位置。如前所述,在这一章你设置游戏环境;在下一章中,您将添加用户交互。

你可以让船以更快的速度靠近,为了最大化它们的效率,用户必须快速删除和移动加农炮。现在,让我们来看看为了使这个游戏成功,你必须创建的物品和活动。

集结港湾卫士

这里是一个港口防御者需要什么的细目分类。在本节中,您将探索这些元素以及如何处理它们:

  • Wharf: The stones of the wharf support your cannon and define the port through which the invading ships must pass. The pier itself doesn't do anything, but it is used for reference cannon placement. You use XML data to quickly encode the location of each item. Each part is realized as an elf; Sprite objects give you more functions than just displaying images on the screen.
  • Ground: The ground is part of the background, so you don't test it or use it. However, it is important for you to use it, because when the blue background is enough, it makes you unnecessary to use larger and resource-intensive images.
  • Castle: The castle will not react until it is hit by a boat. Otherwise, it is an immovable object that is relatively easy to implement. Similarly, you can choose to put the ground and the castle in an elf, but you use this method because it makes more sense in the game by limiting the size of the image.
  • Ship: The ship is one of only two mobile elves in the whole game. You create them according to a random number generator, adding some unpredictability to the game. You have to set their route and speed in advance. Bullet is another moving elf that you dealt with in Chapter 9.
  • Artillery: A cannon has a simple function, that is, it fires at ships. Their location is unique because players can make and destroy cannons in the game. Similarly, the function of the cannon will be realized in the next chapter.

这个游戏编码中最有趣的部分是船只和大炮没有固定的位置和数量。这意味着不是所有的精灵都像你习惯的那样在开始时被初始化。

在开始构建游戏环境之前,您需要打开一个新的 Eclipse 项目:

  1. Open a new Eclipse project named harbor defender .
  2. Copy all the files of PaddleGame (see Chapter 7 ) into your new project. This includes art, XML files and, of course, code.
建造桥墩

在您的上一个游戏中,您使用了一个 XML 页面来存储方块的位置。您可以重用这个页面来存储大量的码头坐标。因为你有这么多的码头,有些人会说一个循环可以处理快速排列的碎片。这是真的,但是桥墩的不规则形状适合这种手工编码。另外,请记住,如果您创建了另一个级别,更改这些数据是非常容易的。

请遵循以下步骤:

  1. 清单 8-1 显示了文件blocklocation.xml(与你用于 TabletPaddle 的文件完全相同)的内容,但是它包含了所有墩块的位置,而不是 Paddle 游戏中的方块。将该文件的内容添加到位于res下的blocklocation.xml中。我强烈建议从网站([code.google.com/p/android-tablet-games/](http://code.google.com/p/android-tablet-games/))下载这个文件,而不是输入这些代码。

    ***清单 8-1。*码头平台位置

    `         <integer name="blocknumber">32

            <integer-array name="x">

            180         280         380         480         580         680         780         880         980         1080         1180         1080         1180         380         480         580         680         780         1080         1180         680         780         1080         1180         680         780         1080         1180         680         780         1080         1180

            

            <integer-array name="y">

            0         0         0         0         0         0         0         0         0         0         0         100         100         200         200         200         200         200         200         200         300         300         300         300         400         400         400         400         500         500         500         500

            

        `

  2. 你用和以前一样的技巧解析这个文件。第一个项目列表是 x 坐标;y 坐标在第二个列表中。通过将 x 列表中的第一个条目与 y 列表中的第一个条目配对来创建每个 sprite,然后向下移动,直到创建了每个块。请注意,您必须在 XML 文件的顶部键入总块数——在本例中,您有 32 个桥墩。这个游戏需要一些新的精灵对象、整数和数组。在实现它们之前,您需要将它们添加到GameView类的顶部。清单 8-2 包含了新的声明;将它们放在文件的顶部。

    清单 8-2。初始化项目的对象/变量

    `//SpriteObjects private SpriteObject[] pier; private SpriteObject[] cannon; private SpriteObject ground; private SpriteObject castle; private SpriteObject[] boat;

    //Variables private Resources res; private int[] x_coords; private int[] y_coords; private int boat_count = 0; private int cannon_count = 3; private int pier_count;`

  3. Although these elves and integers look similar to those you created before, it should be noted that boat_count is set to 0. This allows you to start the game without any boats, and add boats during the game. Similarly, you set cannon_count to 3, because initially you only handled three cannons.

  4. 将清单 8-3 中的代码添加到GameView构造函数中。这段代码看起来应该非常像 TabletPaddle 代码;除了您正在创建的对象的名称之外,它们是相同的。然后,在onDraw()函数中,循环遍历每个桥墩,并将其绘制到屏幕上。

    ***清单 8-3。*建造码头

    //pier sprites pier_count = res.getInteger(R.integer.blocknumber); x_coords = res.getIntArray(R.array.x); y_coords = res.getIntArray(R.array.y); pier = new SpriteObject[pier_count]; for(int i = 0; i < pier_count; i++){         pier[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.pier), x_coords[i], y_coords[i]); }

  5. 将清单 8-4 中的代码放到GameViewonDraw()方法中。

    ??**清单 8-4。**绘制桥墩

    for(int i = 0; i < pier_count; i++){         pier[i].draw(canvas); }

因为 pier 块不需要做任何事情,所以您不需要在update()函数中为它们创建代码。让我们继续到地面和城堡。

添加地面和城堡

地面和城堡是更无生命的物体。你照顾他们就像你照顾码头一样。幸运的是,每种数据只有一个,这意味着您不需要使用更多的 XML 数据:

  1. 清单 8-5 显示了您在GameView构造函数中为两个精灵使用的代码。现在添加。

    ***清单 8-5。*创建地面和城堡

    ground = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.ground), 480, 500); castle = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.castle), 890, 500);

  2. 这两个精灵的诀窍是确保他们都在正确的地方。它们的绘制顺序也很重要。图 8-2 显示了调用onDraw()函数时发生的情况。您可以看到正在绘制的图像层。images

    ***图 8-2。*图像图层

  3. The ground must be lower than the pier and higher than the blue background. Similarly, the castle must be on the ground. In order to get the correct sequence, Listing 8-6 contains a new onDraw() routine. Make sure the order is correct: if the ground appears above the castle, then you will have an underground fortress that is not easy to use in the game!

***清单 8-6。*绘制城堡和地面

`canvas.drawColor(Color.BLUE); ground.draw(canvas); castle.draw(canvas);

for(int i = 0; i < pier_count; i++){         pier[i].draw(canvas); }`

您创建的下一个精灵对象将在绘制完桥墩后添加。这是有道理的,因为大炮必须在桥墩的顶部;船只沿着水面滑行,可能会撞到城堡。

制造船只

船是你必须对付的最复杂的精灵。用户无法控制它们,因此它们的运动必须按照特定的路线进行预编程。更复杂的是,你必须根据精灵的方向改变它的图像。这些都集中在update()函数中。但是现在,您可以创建一个数组来保存船只,而不必实际制作它们:

  1. 将清单 8-7 中的片段放到你的GameView构造函数方法中。

    ??**清单 8-7。**创造 12 个船精灵

    //boat sprites boat = new SpriteObject[12];

  2. 清单 8-8 显示了循环绘制可用船只的程序。在绘制完其他精灵后,将这段代码放入onDraw()函数中。

    清单 8-8。画船

    for(int i = 0; i < boat_count; i++){         boat[i].draw(canvas); }

  3. Here comes the wonderful part. Before you go forward, you need to understand the variable boat_count. Back to the GameView variable declaration, you initialize this integer by setting it to 0. Therefore, in the initial state, no ship spirit is drawn, because i is not less than boat_count. You can think of ?? as a collection of available ships.

  4. 因为你一开始没有船,所以他们的创作方法就有点复杂了。清单 8-9 包含了你需要添加到update()函数中的代码。之后我把它分解成关键部分。为了让它工作,在GameView.java的顶部导入java.util,Random

    ??**清单 8-9。**创建船只和随机间隔

    `Random random_boat = new Random(); int check_boat = random_boat.nextInt(100);

    if(check_boat > 97 && boat_count < 12){         int previous_boat = boat_count - 1;         if(boat_count == 0 || boat[previous_boat].getX() > 150){                 boat[boat_count] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.boat), 100, 150);                 boat[boat_count].setMoveX(3);                 boat_count++;         } }`

  5. 首先,你做一个随机数生成器。您调用一个nextInt()方法,该方法在 0 和参数之间选择一个整数。测试了check_boat变量,以便您以随机间隔创建船只。

    images 注意创建一个随机数发生器,并得到一个介于零和你自己的值之间的整数,这是给你的游戏增加一些随机性的完美方法。你不再需要担心小数,因为整数更容易处理。如果你使用随机元素,请记住在测试中多次运行你的游戏,因为如果随机数与你预期的不同,你可能会发现意想不到的行为第一个if语句只有在随机数大于 97 的情况下才继续执行,这种情况不太可能发生,但可以将船只的冲击降到最低。然后你要求boat_count小于 12。这可以防止许多船只同时出现在赛场上。如果这对玩家来说太容易了,你可以增加这个数字,让游戏更有挑战性。

  6. The second if statement checks whether the new ship is the first ship or whether it has a certain distance from the previous ship. Therefore, add 1 to boat_count and test whether the X coordinate of the previous ship is greater than 150. Otherwise, these boats may appear on top of each other, which is detrimental to the appearance of the game (although it may make the game more challenging! ).

  7. If the ship passes all the if statements, it is initialized to the starting x position 100. You move it at a slow speed of three pixels per update() function. This is another good opportunity to increase the difficulty. When the player reaches a certain score or other achievements, slowly increase the speed of the boat.

  8. 最后,boat_count递增,让draw()函数处理新添加的船。你的舰队扩大了。你需要改变船只的方向,让它们能够适当地转向它们的目的地:城堡。清单 8-10 中的代码就是这样做的;将其添加到update()方法中。

    ??**清单 8-10。**改变船的方向

    for(int i = 0; i < boat_count; i++){         if((int)boat[i].getX() > 950){                 boat[i].setMoveX(0);                 boat[i].setMoveY(3);                 boat[i].setBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.boatdown));         } }

  9. When the ship reaches the X position of 950 pixels, it stops moving to the right and begins to descend. Notice the last line: you changed the sprite image, because ships rarely move without changing direction. Therefore, the original ship image is rotated by 90 degrees and saved as a new resource named boatdown.

就是这样。当你添加加农炮时,你会看到船只随机出现并驶向你的城堡。

加农炮

对船来说也是如此,大炮的数量在游戏中会发生变化。现在,你只担心证明这个概念。请遵循以下步骤:

  1. 将清单 8-11 中的代码放入GameView创建器中。你可以在声明中改变cannon_count的值来创建更多的加农炮。不是响应用户输入,而是让加农炮出现在三个连续的墩上,每次快速循环移动加农炮 100 个单位。

    ***清单 8-11。*改变加农炮计数的数值

    //cannon sprites cannon = new SpriteObject[cannon_count]; for(int i = 0; i < cannon_count; i++){         cannon[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannonup), (580 + i * 100), 200); }

  2. In order to prepare yourself for creating additional cannon elves, name the original picture cannonup. When the user wants to change the direction of the cannon, it will be easier.

  3. Add the code in Listing 8-12 to the onDraw() function, and your cannonball will appear when the game is running.

***清单 8-12。*拉炮

for(int i = 0; i < cannon_count; i++){         cannon[i].draw(canvas); }

你有一些简单的遗留问题要处理。游戏的框架完成了。

添加图像

我使用的图片可以在code.google.com/p/android-tablet-games/买到,或者你可以自己创作。图 8-3 到图 8-7 显示了我用来构建 Harbor Defender 的图片;它们的尺寸在图标题中有规定。后来,我提出了一些关于如何创建你的图像的建议。请记住,有时您需要旋转或翻转它们,以交替状态显示它们。

images

图 8-3。 Castle : 200 × 100

images

图 8-4*。** Boatboatdown:分别为 50 × 30 和 30×50*

images

图 8-5。 Ground : 800 × 250

images

图 8-6。 Pier : 100 × 100

images

图 8-7。 Cannon : 100 × 100

调试海港卫士

通过一个简单的游戏外观方法,你就可以开始尝试了。像任何游戏一样加载它,你会看到船只慢慢出现并向城堡游去。如果你等得够久,他们会穿过城堡,离开屏幕。

如果事情不是这样,或者你得到一个类似于图 8-8 中显示的错误,或者游戏在启动时关闭,那么你需要做一些工作。本节致力于解决 Android 游戏开发中的常见问题。它没有深入到具体的问题中,因为没有办法预测每一个错误。如果 Eclipse 捕捉到错误,如何修复代码中的错误应该是相当明显的,但是运行时问题可能更加困难。

以下是要使用的流程:

  1. Make sure you are using LogCat to get the information on the simulator. When you use Log.d in the program to remind you that certain events have been triggered, this is very important. LogCat A fairly detailed report on errors is also displayed.

  2. 出错时不要关闭模拟器。查看图 8-2 中的问题。你可能很想立即平仓,但这样做会抹去LogCat的结果。相反,请等待,以便您可以诊断问题。images

    ***图 8-8。*海港保卫者运行时出错

  3. 向上滚动LogCat读数,如图图 8-9 所示,你应该可以找到红色字体的短语,表示错误发生的位置。幸运的是,该错误指出了问题所在的确切行号。images

    图 8-9 .??NullPointerException??

  4. In most cases, you only need to pay attention to the previous error line. In this case, when the cannon is pulled out, the onDraw() function fails. The reason is that I commented out the initialization of the cannon elf. This is a common problem when you deal with a game where elves are created and destroyed. Please make sure that all the wizards you refer to for drawing or updating really exist. The last suggestion for handling errors is to make your simulator smaller. If you have a relatively small screen size, then your simulator may occupy most of the screen. This can prevent you from looking at LogCat at work. To solve this problem, choose to run Run_configuration. Then go to the target tab and scroll down. In the command line option, type scale.8 . This reduces the simulator to 80% of its original size.

images 注意如果你解决问题的最大努力没有成功,尝试在 stack overflow(stackoverflow.com/)上搜索解决方案。不过,将来在测试之间做些小的改变。通过这种方式,您可以回到以前的工作状态。准备好总是回到你知道有效的事情上来。

下一章涉及到游戏的许多不同的修正和更新。最值得注意的是,你使用户能够移动和旋转大炮。在你以前的游戏中,玩家从来没有这么多的选择,这将是一个独特的练习。

另一个增加的是一个积分系统,玩家每摧毁一艘船就获得奖励。物理也必须更新,因为一旦船撞上城堡,你需要结束游戏,而不是让船直接通过。

你还得担心一个新的因素:不恰当的用户交互。对于用户来说,点击码头的一部分来放置大炮是有意义的,但是如果他们错过了码头而点击了海洋呢?这就要求你快速有效地评估每一个输入,并立即对用户做出反应,同时还要防止大炮出现在不该出现的地方。

为了完成你的工作,你添加输入和逻辑来润色游戏的整体外观。

总结

你已经经历了设置一个真实游戏的过程。有了这些元素,你就可以添加让游戏成为有趣的用户体验的特性了。你应该习惯于计划一个游戏和组织处理精灵和组成它的物体的方式。

当你展望未来时,你会更加关注玩家的体验,而不是你技术能力的极限。为一个游戏创作美术作品通常也是一个限制因素,但是一个有趣和有创意的游戏可以弥补许多缺点。现在,让我们为您的游戏部署做好准备。

九、单人策略游戏第二部分:游戏编程

有了框架,现在你可以编写代码来创建一个可玩的游戏。这里的诀窍是让你的代码尽可能高效。当游戏变得越来越复杂,涉及到更多的精灵时,它们可能会因为处理器难以跟上而开始变慢。你可以用一些巧妙的技术来减轻负担,从而避免这种情况。

随着你的进展,牢记最终目标也很重要,因为你必须有一个正常运行的游戏,然后才能添加使你的工作与众不同的附加功能。事实上,以我的经验来看,知道何时停止开发一款游戏并发布它总是最棘手的部分。太简单的游戏和不可玩的游戏之间有一条细微的界限,因为它的功能和附加功能太多了,普通用户没有时间去学习。

images 注意当你阅读本章中的代码时,回想一下,将Log.d语句放入代码中有助于澄清正在发生的事情和正在调用的函数。有些代码可能相当复杂,我仍然使用这种技术来帮助我逐步完成这些方法,尤其是当我没有得到想要的行为时。

以下是你必须在本章中完成的功能列表,以便拥有一个可用的游戏:

  • Augmented elf object
  • Shoot a bullet from a cannon
  • Destroy the hit ship.
  • Restart the game when the ship hits the castle.

其中一些——比如当子弹击中一艘船时降低它的健康——很容易完成,但是其他的需要一些思考和聪明的编码。为了简化你的编辑,我已经贴出了这一章的全部方法。这样可以保证你之前的作品正是最终游戏所需要的。这也有助于您了解每个函数如何调用其他函数,以及它们之间共享哪些信息。

下一节从我们对SpriteObject.java的改进开始。您只做了很少的修改,但是您所做的更改将会简化您在GameView.java中的工作。

增强游戏精灵

在这个游戏中,你对你的精灵要求很多。为了处理新功能,您需要一些所有精灵都使用的新方法和变量。虽然实际上只有一个精灵可以利用一个特定的特性,而不是创建额外的类,但是你可以让每个游戏精灵从SpriteObject继承,因为精灵在很大程度上是相同的——没有必要混淆项目。

然而,如果你扩展游戏,你想让船能够开火还击,改变方向,或者产生更小的船,那么你可能想创建一个特殊的船类来体现这些能力。每当一个精灵或对象使用两个或更多独特的函数时,我通常会为它创建一个新类。

按照以下步骤修改SpriteObject.java:

1.清单 9-1 显示了要添加的新变量以及你赋予它们的值。将这段代码添加到SpriteObject.java的顶部。

清单 9-1。 SpriteObject变数

private int health = 3; private int Orientation = -1; public int LEFT = 0; public int RIGHT = 1; public int UP = 2; public int DOWN = 3; private boolean stack = false;

2.清单 9-1 中变量的使用在清单 9-2 中所示的函数中显而易见。在SpriteObject的末尾键入所有这些代码。新方法被你的精灵们自由地使用。

***清单 9-2。*新增功能为SpriteObject

`public boolean cursor_selection(int cursor_x, int cursor_y){

        int sprite_right = (int)(getBitmap().getWidth() + getX());         int sprite_bottom = (int)(getBitmap().getHeight() + getY());         if(cursor_x > getX() && cursor_x < sprite_right && cursor_y > getY() && cursor_y < sprite_bottom){                 return true;         }         else{                 return false;         }

}

public void setStacked(boolean s){         stack = s; } public boolean getStacked(){         return stack; } public void diminishHealth(int m){         health -= m; } public int getHealth(){         return health; } public void setOrientation(int o){         Orientation = o; } public int getOrientation(){         return Orientation; }`

cursor_selection()函数是一个非常强大的方法,如果用户触摸了一个 sprite,它将返回 true,如果用户没有触摸,它将保持 false。它基本上是一个简单版本的collide()方法,但是它只关心用户给出的输入。您通过用户选择要添加的加农炮类型来实现它。

与子画面是否堆叠相关的函数用于确定一块桥墩上是否已经有大炮。如果那里有一门加农炮,你要阻止用户在它上面放置另一门。有些地方比其他地方更好,所以让玩家放置大炮是不公平的。

添加两个函数来处理精灵的健康状况。游戏中唯一健康的精灵是船。当他们被击中三次时,他们将被移出游戏。

3.你需要修改SpriteObject update()函数来检查一个精灵是否已经失去了所有的健康。用清单 9-3 中的代码替换现有代码。

***清单 9-3。*改变update()方法

public void update(int adj_mov) {         if(state == ALIVE){                 x += x_move;                 y += y_move;                 if(health <= 0){                         state = DEAD;                 }         } }

最后一个加法检查精灵面向哪个方向。你用这个装大炮。例如,如果一门大炮朝下,你必须向屏幕底部发射子弹,而一门指向右边的大炮应该向屏幕右侧发射子弹。

让我们把这些功能付诸行动吧!

创建用户控件

GameView.java的构造器方法有几个新人。本节剖析了主要用于用户交互的新精灵,并向您展示了一个新概念。不是创建四个不同的指向所有主要方向的大炮图标,而是为四个不同的精灵旋转一个图像。这可以节省机器上的空间,但也会在启动时导致一些额外的处理器工作。

为了证明这一点,主炮都是不需要旋转的独立精灵。在这种情况下,您使用的方法取决于您的资源和磁盘空间。

请遵循以下步骤:

1.在开始使用新精灵之前,必须先在构造函数之前声明对象。将清单 9-4 中的代码放到GameView.java中。

清单 9-4。 SpriteObject s 为海港保卫者

private SpriteObject trash; private SpriteObject dock; private SpriteObject marker; private SpriteObject cannonrightsmall; private SpriteObject cannonleftsmall; private SpriteObject cannonupsmall; private SpriteObject cannondownsmall;

2.在GameView构造函数中,初始化trashdockmarker图标,如清单 9-5 所示。这三个精灵创建了用户控件的基础。在屏幕右下角,有一个存放选项的 dock。在码头的前面是垃圾桶,让用户摧毁他们建造的大炮。标记精灵在图标后面跳来跳去,向玩家显示当前选择的是哪一个。

***清单 9-5。*设置图标

trash = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.trash), 50, 650); dock = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.dock), 0, 650); marker = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.marker), 50, 650);

3.下一步是创建小炮图标。将清单 9-6 中的代码添加到GameView构造函数中。这是你的码头的基础。

***清单 9-6。*制作微型大炮图标

`Bitmap bcannonupsmall = BitmapFactory.decodeResource(getResources(), R.drawable.cannonupsmall); int w = bcannonupsmall.getWidth(); int h = bcannonupsmall.getHeight(); Matrix mtx = new Matrix(); mtx.postRotate(90);

Bitmap bcannonrightsmall = Bitmap.createBitmap(bcannonupsmall, 0, 0, h, w, mtx, true); Bitmap bcannondownsmall = Bitmap.createBitmap(bcannonrightsmall, 0, 0, w, h, mtx, true); Bitmap bcannonleftsmall = Bitmap.createBitmap(bcannondownsmall, 0, 0, h, w, mtx, true);

cannonrightsmall = new SpriteObject(bcannonrightsmall, 110, 650); cannonleftsmall = new SpriteObject(bcannonleftsmall, 180, 650); cannondownsmall = new SpriteObject(bcannondownsmall, 240, 650); cannonupsmall = new SpriteObject(bcannonupsmall, 300, 650);`

如果你觉得这段代码有点像希腊语,不用担心。你创建了微型加农炮精灵,并收集其高度和宽度。然后你启动一个新的矩阵,旋转 90 度。通过旋转cannondownsmall三次创建三个新位图。然后用新图像创建精灵。位置非常具体,将所有图标放在屏幕左下方的小 dock 上。

4.要使 dock 有用,您需要用变量存储用户的选择(换句话说,如果用户选择正面朝下的大炮,您需要知道如何创建该类型的大炮)。你可以通过将清单 9-7 中的变量放在GameView的顶部来实现。User_choice存储用户的选择。

***清单 9-7。*存储用户选择的变量

Private int TRASH = 1; Private int CANNON_LEFT = 2; Private int CANNON_RIGHT = 3; Private int CANNON_UP = 4; Private int CANNON_DOWN = 5; Private int user_choice;

5.您已经创建了一个不错的 dock,有几个选项供用户选择,但是您需要跟踪用户指向的位置。您使用四个变量来引用用户的选择。将清单 9-8 中的变量添加到GameView.java的顶部。

***清单 9-8。*收集关于最后一次触摸事件的位置的数据

private int cursor_x; private int cursor_y; private boolean selection_changed; private boolean addboat;

6.编辑ProcessMotionEvent()看起来像清单 9-9 中的代码。这包含了您在步骤 5 中声明的前三个变量。

***清单 9-9。*存储用户的输入

`public void processMotionEvent(InputObject input){         selection_changed = true;         cursor_x = input.x;         cursor_y = input.y;

}`

有了这些代码,当平板电脑上发生触摸时,将selection_changed设置为true,并用变量cursor_xcursor_y存储触摸的位置。

7.在update()函数中,您使用来自步骤 6 的数据来确定是否需要处理用户输入事件以及用户在哪里交互。将清单 9-10 中的代码添加到GameView.javaupdate()方法中。这就是处理用户输入的方式。

***清单 9-10。*update()功能中处理用户输入

`if(selection_changed){         selection_changed = false;         if(trash.cursor_selection(cursor_x, cursor_y)){                 user_choice = TRASH;                 marker.setX(50);                 addboat = false;         }         if(cannonrightsmall.cursor_selection(cursor_x, cursor_y)){                 user_choice = CANNON_RIGHT;                 marker.setX(110);                 addboat = true;         }         if(cannonleftsmall.cursor_selection(cursor_x, cursor_y)){                 user_choice = CANNON_LEFT;                 marker.setX(180);                 addboat = true;         }         if(cannondownsmall.cursor_selection(cursor_x, cursor_y)){                 user_choice = CANNON_DOWN;                 marker.setX(240);                 addboat = true;         }         if(cannonupsmall.cursor_selection(cursor_x, cursor_y)){                 user_choice = CANNON_UP;                 marker.setX(300);                 addboat = true;         }         else if(addboat){                 if(cannon_count < 10){                         for(int i = 0; i < pier_count; i++){                                 if(pier[i].cursor_selection(cursor_x, cursor_y)){                                         if(pier[i].getStacked() == false){                                                 switch(user_choice){                                                 case 2:                                                         cannon[cannon_count] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannonleft), (int)pier[i].getX(), (int)pier[i].getY());

cannon[cannon_count].setOrientation(cannon[cannon_count].LEFT);                                                         break;

                                                case 3:                                                         cannon[cannon_count] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannonright), (int)pier[i].getX(), (int)pier[i].getY());

cannon[cannon_count].setOrientation(cannon[cannon_count].RIGHT);                                                         break;

                                                case 4:                                                         cannon[cannon_count] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannonup), (int)pier[i].getX(), (int)pier[i].getY());

cannon[cannon_count].setOrientation(cannon[cannon_count].UP);                                                         break;

                                                case 5:                                                         cannon[cannon_count] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannondown), (int)pier[i].getX(), (int)pier[i].getY());

cannon[cannon_count].setOrientation(cannon[cannon_count].DOWN);                                                         break;                                                 }

                                                cannon_count++;                                                 pier[i].setStacked(true);                                         }                                         else if(pier[i].getStacked() == true){                                                 if(user_choice == 1){                                                         for(int u = 0; u < cannon_count; u++){                                                                         if(cannon[u].getX() == pier[i].getX() && cannon[u].getY() == pier[i].getY()){

cannon[u].setstate(cannon[u].DEAD);                                                                 }                                                         }                                                 }                                         }                                 }                         }                 }         } }`

这段代码处理的是 dock 图标。用户交互的另一面是船在屏幕上的实际位置。当玩家选择任何一艘船或垃圾桶时,他们设置addboattrue。这意味着你需要寻找用户在游戏中做什么。变量user_choice存储用户选择的最后一个停靠图标。

处理器循环通过墩件;当它发现用户触摸了墩块时就停止。然后,它会询问码头是否堆叠。你早些时候看到,在这种情况下,被堆叠意味着码头已经容纳了一门大炮。如果不是,那么用户可以自由添加一门大炮到那个码头。然后代码进入一个switch语句。

switch的事例编号对应于您在构造函数方法中分配的变量(例如,大炮是否指向左边)。当你找到玩家想要的加农炮的方向时,你使用码头的位置创建新的精灵。很重要的一点是,你的码头和大炮要占同一个面积(100 × 100)。这使得定位变得很简单。

然而,放置大炮并不是玩家唯一能做的事情。他们还可以选择值为 1 的垃圾桶。垃圾的表现与你之前看到的相反:它寻找一个堆放的墩块,找到放在那里的大炮,然后移走它。

就这样。用户现在可以控制你的游戏了。接下来的部分将为你的子弹和船只添加新的功能。

把一切都显示在屏幕上

现在你已经有了很多很棒的特性,比如你的用户界面控件和船只,你需要把它们添加到屏幕上。为此,onDraw()功能需要调整。清单 9-11 包含了这个函数的全部代码。

确保你的 onDraw 函数看起来与清单 9-11 中的完全一样,否则图像不会被绘制到屏幕上。

清单 9-11。??onDraw()

`@Override public void onDraw(Canvas canvas) {         canvas.drawColor(Color.BLUE);         ground.draw(canvas);

        //the user controls         dock.draw(canvas);         marker.draw(canvas);         trash.draw(canvas);         cannonleftsmall.draw(canvas);         cannonrightsmall.draw(canvas);         cannondownsmall.draw(canvas);         cannonupsmall.draw(canvas);

        for(int i = 0; i < pier_count; i++){                 pier[i].draw(canvas);         }         for(int i = 0; i < boat_count; i++){                 boat[i].draw(canvas);         }         for(int i = 0; i < cannon_count; i++){                 cannon[i].draw(canvas);         }         for(int i = 0; i < 50; i++){                 bullets[i].draw(canvas);         }         castle.draw(canvas); }`

查看标题为“用户控件”的精灵组。这些图标包括用户可以选择的 dock、标记以及垃圾桶和大炮图标。这里需要注意的是,dock 显然是先画的,然后是标记,然后是图标。这样就可以一直在背景中看到 dock。然后,标记可以自由地从后面高亮显示所有图标。图 9-1 显示了码头的样子。

images

***图 9-1。*包含用户控件的 dock,用户可以与之交互

在函数的最后,四个for循环遍历精灵列表。最后画出城堡。

你总是画出每一颗子弹,即使它们可能正在移动,也可能不在移动。这是由SpriteObject类负责的,它在绘制精灵之前检查以确保精灵是活的。随着子弹准备摧毁船只,我们必须创造和跟踪即将到来的敌人。下一节将介绍操纵船只的来龙去脉。

部署和管理攻击艇

清单 9-12 包含了处理船只的整个GameView.java update()方法的代码..如果你不明白它的一部分,键入它的全部并运行游戏。你可以根据游戏的行为来看它是如何工作的。

1.确保您的 update()方法包含这里的所有代码。上市后,你会发现它的解释。

***清单 9-12。*update()功能中设置船只

`public void update(int adj_mov) {

        for(int i = 0; i < boat_count; i++){                 if((int)boat[i].getX() > 950){                         boat[i].setMoveX(0);                         boat[i].setMoveY(3);                         boat[i].setBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.boatdown));                 }         }

        Random random_boat = new Random();         int check_boat = random_boat.nextInt(100);

        if(check_boat > 97 && boat_count < 12){                 int previous_boat = boat_count - 1;                 if(boat_count == 0 || boat[previous_boat].getX() > 150){                         boat[boat_count] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.boat), 100, 150);                         boat[boat_count].setMoveX(3);                         boat_count++;                 }         }`

来自清单 9-12 的代码在第八章中完成..第一个for循环确定船只是否向右移动过多。如果有,那么使用一个新的精灵图像,它开始沿着屏幕向城堡移动。

下一个块处理随机船的创建。最重要的部分是使用一个if语句来确保前一艘船与新船充分分离。同样,你增加船只的数量,并设置新船上路,如清单 9-12 所示。

现在我们将检查与城堡的碰撞,这将导致玩家的损失。

2.在 update()方法中添加清单 9-13 中的 for 循环。

***清单 9-13。*测试与城堡的碰撞,重置游戏

for(int i = 0; i < boat_count; i++){         if(boat[i].collide(castle)){                 reset();         } }

如果用户失败,船撞上了城堡,那么你调用一个名为reset()的新函数。你看一下这个简单的函数做了什么。(我本可以在这里包含所有的代码,但是我发现添加额外的函数来处理不同的任务在视觉上更容易。)

随着船只的航行和子弹的准备发射,我们需要研究我们的大炮。没有它们你无法打败船。看看下一节,我们如何操纵和使用加农炮。

开炮

在用户输入之后,子弹是游戏中最复杂的部分。跟踪 50 个可以向四个不同方向移动的精灵是一件棘手的事情,这些精灵目前可能活着,也可能不活着。大炮将会变得更加精彩。在本节中,您将添加子弹并编写代码来处理大炮如何以及何时发射炮弹。

请遵循以下步骤:

1.将清单 9-14 中的代码添加到GameView构造函数中。这个代码处理大炮发射的新子弹。为了简单起见,屏幕上的项目符号数量限制为 50 个。有两个数组:一个包含子弹精灵(bullets[]),另一个包含当前没有使用的子弹列表(available_bullet[])。

清单 9-14。Additions to the onCreate() method that handle the bullets.

`available_bullet = new int[50]; for(int i = 0; i < 50; i++){         available_bullet[i] = i; }

bullets = new SpriteObject[50]; for(int i = 0; i < 50; i++){         bullets[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.bullet), 10, 10);         bullets[i].setState(bullets[i].DEAD); }`

您声明了一个整数数组,其中包含所有可用的子弹,因为您知道还没有子弹射出。项目符号精灵也被初始化。您将它们的状态设置为DEAD,因为您不希望子弹没有发射就出现。

2.将清单 9-15 中的代码添加到update()方法中。首先,你设置available_bullet数组等于零;这将使你进行计算时更容易。然后你创建了一个非常重要的变量:g = 0g用于指定哪些项目符号可用,哪些不可用。

***清单 9-15。*重置可用子弹列表

`for(int f = 0; f < 50; f++){         available_bullet[f] = 0; }

int g = 0;`

3.在清空数组后,立即将清单 9-16 中的代码放到update()方法中。

***清单 9-16。*处理子弹的变化

`for(int i = 0; i < 50; i++){

if(bullets[i].getY() > 800 || bullets[i].getX() > 1280 || bullets[i].getY() < 0 || bullets[i].getX() < 0){                 bullets[i].setstate(bullets[i].DEAD);         }

        for(int b = 0; b < boat_count; b++){                 if(bullets[i].collide(boat[b])){                         boat[b].diminishHealth(1);                         bullets[i].setstate(bullets[i].DEAD);                 }         }

bullets[i].update(adj_mov); if(bullets[i].getstate() == bullets[i].DEAD){                 available_bullet[g] = i;                 g++;         }

}`

每个子弹精灵都有一个循环。第一个if语句检查子弹是否已经离开屏幕;如果有,就将其状态设置为DEAD。这意味着它可以在下一次迭代中作为可用的项目符号被重用。一个for回路处理船只碰撞。如果船被击中,那么它的健康就会下降一,你就摧毁了子弹。同样,子弹现在可以重复使用。一个简单的update()调用根据它的moveXmoveY改变了子弹的位置。

如果子弹是死的,那么你把它列为可用子弹。如果仔细观察if语句,您会注意到第一个失效的项目符号被赋予了available_bullet数组中的第一个位置,g被递增,下一个失效的项目符号被赋予了下一个位置。

4.子弹准备好了,该担心发射机制了。五十次迭代的update()函数从比赛场上的每一门大炮中释放一颗子弹。清单 9-17 中的代码通过调用新函数createBullet()来执行这些操作,这个函数有四个参数。将这段代码放在已经添加到方法中的代码之后的update()方法中。

***清单 9-17。*计算何时发射一排子弹

shooting_counter++; if(shooting_counter >= 50){         shooting_counter = 0;         int round = 0;         for(int i = 0; i < cannon_count; i++){                 if(cannon[i].getOrientation() == cannon[i].LEFT){                         int x = (int)(cannon[i].getX());                         int y = (int)(cannon[i].getY() + cannon[i].getBitmap().getHeight()/2);                         createBullet(x,y,cannon[i].LEFT, round);                         round++;                 }                 if(cannon[i].getOrientation() == cannon[i].RIGHT){                         int x = (int)(cannon[i].getX() + cannon[i].getBitmap().getWidth());                         int y = (int)(cannon[i].getY() + cannon[i].getBitmap().getHeight()/2);                         createBullet(x,y,cannon[i].RIGHT, round);                         round++;                 }                 if(cannon[i].getOrientation() == cannon[i].UP){                         int x = (int)(cannon[i].getX() + cannon[i].getBitmap().getWidth()/2);                         int y = (int)(cannon[i].getY());                         createBullet(x,y,cannon[i].UP, round);                         round++;                 }         if(cannon[i].getOrientation() == cannon[i].DOWN){                         int x = (int)(cannon[i].getX() + cannon[i].getBitmap().getWidth()/2);                         int y = (int)(cannon[i].getY() + cannon[i].getBitmap().getHeight());                         createBullet(x,y,cannon[i].DOWN, round);                         round++;                 }         } }

这段代码创建了变量round,它跟踪哪颗子弹已经发射。第一门大炮发射第一轮,第二门大炮发射第二轮,以此类推。这一系列的if语句使用了您在SpriteObject.java中创建的新的getOrientation()函数。然后将每门加农炮炮管末端的 x 和 y 坐标传递给createBullet()方法。得到坐标需要一些计算,因为你知道枪管在大炮的中心。

子弹的结构在createBullet()中更有意义,你将在下一节中写它;清单 9-17 中的代码只是将必要的信息发送给那个方法。因为你已经初始化了所有的子弹精灵,这不会浪费处理时间,因为你只是在更新精灵。

5.要完成update()方法,确保调用了各种精灵的update()函数,如清单 9-18 所示。

***清单 9-18。*包括基本的update()功能

`castle.update(adj_mov); ground.update(adj_mov); for(int i = 0; i < boat_count; i++){         boat[i].update(adj_mov); }        

}`

接下来的部分通过处理游戏重置和发射子弹来解决遗留问题。

管理游戏结果

当玩家输掉游戏,一艘船撞上城堡,你呼叫reset()。这是一个简单快捷的功能。

请遵循以下步骤:

1.将清单 9-19 中的代码添加到GameView中其他函数的下面。

清单 9-19。 reset()

`private void reset(){         for(int i = 0; i < boat_count; i++){                 boat[i].setstate(boat[i].DEAD);         }         boat_count = 0;

}`

你所做的就是毁掉那些船。这实际上重新开始了游戏,因为船又一次被随机创建了。你不移除大炮,因为没有必要担心它们。如果用户愿意,他们可以删除它们。如果您想向用户显示一条消息,您可以创建一个 sprite 并在此时将其绘制在屏幕上。在update()功能中,等待大约 30 个周期,然后删除消息。

2.createBullet()方法有点复杂,正如你在清单 9-20 中看到的,但它绝对是可管理的。把这个方法直接放在reset()函数下面。

清单 9-20。 createBullet()

`private void createBullet(int x, int y, int direction, int r){         if(r >= 0){                 int index = available_bullet[r];                 if(direction == bullets[index].RIGHT){                         bullets[index].setMoveX(10);                         bullets[index].setMoveY(0);                         bullets[index].setX(x);                         bullets[index].setY(y);                         bullets[index].setstate(bullets[index].ALIVE);                 }                 if(direction == bullets[index].LEFT){                         bullets[index].setMoveX(-10);                         bullets[index].setMoveY(0);                         bullets[index].setX(x);                         bullets[index].setY(y);                         bullets[index].setstate(bullets[index].ALIVE);                 }                 if(direction == bullets[index].UP){                         bullets[index].setMoveY(-10);                         bullets[index].setMoveX(0);                         bullets[index].setX(x);                         bullets[index].setY(y);                         bullets[index].setstate(bullets[index].ALIVE);                 }                 if(direction == bullets[index].DOWN){                         bullets[index].setMoveY(10);                         bullets[index].setMoveX(0);                         bullets[index].setX(x);                         bullets[index].setY(y);                         bullets[index].setstate(bullets[index].ALIVE);                 }         }

}`

子弹精灵是对称的,所以你不用担心它们的方位,只需要担心它们移动的方向。别忘了每个if块的最后一行,让子弹活起来。否则,它们将永远不会被绘制出来,并且您将很难找出哪里出错了。

你终于完成了游戏项目。下一节给你一些未来计划的想法。

分析游戏

如果你还没有,运行游戏。当船开始来的时候,放置你的大炮保卫城堡。祝你在战斗中好运。

以下是您用来构建 Harbor Defender 的功能和技术列表。为你在代码、错误和工作中坚持不懈的努力感到自豪:

  • Game cycle
  • Multiple elves
  • Draw an image on the screen
  • Bitmap manipulation
  • User interaction
  • Some AI
  • Collision detection
  • XML data parsing
  • And more.

写完整个游戏后,你可以放松,把游戏改成你想要的样子。如果你做了足够多的改变,也许你可以在 Android 市场上赚点钱。本书的最后一章讨论了这种可能性。

拥有一款可扩展的游戏至关重要。如果游戏开发商不得不从头开始制作每一款游戏,他们永远不会发布足够的游戏来支付租金。相反,他们将框架转化为许多独特的、看似不同的创造。你所做的有潜力转化为迷宫游戏,平台游戏,回合制策略游戏,或者其他许多可能性。

SpriteObject类是完全可重用的,并且GameView可以很容易地调整成其他类型。如果你需要想法,我觉得浏览其他游戏开发书籍并为 Android 创建它们的样本很有趣。任何语言的任何游戏都可能在 Android 上创建。如果游戏是为电脑设计的,并且使用键盘控制,这可能是一个挑战。要有创造性,我相信你能写出一些非常不同的程序。

图 9-2 显示完成的游戏。看看你能否想象它被转化成十几个不同的项目。

images

***图 9-2。*你完成的项目

总结

你的辛苦完成了,你也学到了很多。在本章的最近部分,你看到了如何使用矩阵来旋转位图。您还了解了如何跟踪 50 个精灵并维护另一个列表,其中列出了哪些精灵已经死亡并准备再次创建。这一章也标志着你第一次尝试创建一个用户界面,它包括几个图标和一个标记来显示用户当前选择的内容。

如果你厌倦了代码,有一个好消息:下一章处理发布你的游戏,提供更新,并处理业务结束。你看看什么游戏卖得好,平板电脑如何改变计算领域。当你理解了商业方面,就轮到你创造自己的杰作了!

十、发布游戏

你的游戏已经为大众做好了准备,但是在应用被大众消费之前,你还有几个步骤。对代码有几处修改,可以用来润色您的工作。然后这一章讲述了出售或赠送游戏的步骤。最后,您将了解如何确保在竞争激烈的移动应用市场中取得成功。

制作一款高质量的游戏只是在 Android 应用市场获得畅销地位的第一步。到目前为止,你所做的一切都可以融入到你展示最终产品的过程中。这款应用的图形、声音和外观都融入了你向消费者销售的方式中。

打磨应用

虽然你的游戏可以玩,但它可能需要一些润色。欢迎屏幕将是一个很好的补充,这样玩家可以在进入游戏之前了解游戏。当谈到添加这个功能时,你有很多选择,但制作一个基本的入口屏幕很容易,你可以针对每个游戏进行微调。在本节中,您将添加一个屏幕,然后添加一个按钮来启动游戏。

添加闪屏

因为GameView.java负责实际的游戏及其外观,所以你的启动页面由MainActivity.java处理。不是设置屏幕显示GameView,而是呈现一个快速布局,然后给用户进入游戏的能力。这使您的工作更专业,对用户来说也更容易。为了扩展这个概念,你可以播放一小段视频来介绍这个游戏,但是我会留给你自己去想象。

看看图 10-1 看看你的闪屏是什么样子的。如果你想要一个更完整的介绍界面,这一节将讨论添加特性和项目的方法。

images

***图 10-1。*游戏简介

为了达到图 10-1 中的效果,让我们回到第一章中的概念。应用的外观是在main.xml中生成的,在这里你可以通过拖动按钮和文本到屏幕上来创建界面。然后编辑文本和元素。以下步骤显示了如何做到这一点:

  1. Navigate to the folder: res layout main.xml to find your main.xml file in the Harbor Defender project.
  2. Open main.xml and select "10.1-inch WXGA" from the drop-down menu near the top. The first task is to check the code of the main.xml file.
  3. Select main.xml on the small tab near the bottom of the screen. Replace the existing code with the code in Listing 10-1 .

清单 10-1Main.xml

`

<AbsoluteLayout           xmlns:android="schemas.android.com/apk/res/and… `          android:orientation="vertical"*           android:layout_width="fill_parent"           android:layout_height="fill_parent"         >

`

你用一个AbsoluteLayout替换现有的LinearLayout。这两者都是可以添加布局元素的框架。然而,AbsoluteLayout让您快速指定元素的确切位置,而LinearLayout将所有项目向左对齐。当您添加欢迎屏幕的各个部分时,这一点非常重要。

5.选择屏幕底部的小图形布局选项卡,返回图形布局。

6.您可以使用左侧的项目面板来创建您的布局。图 10-2 显示了这将会是什么样子。将一个Button和一个TextView拖到您的屏幕上。它们现在包含填充文本,但是您很快就会编辑它。

images

图 10-2*。*使用左边的调色板拖动TextViewButton对象到屏幕上

7.是时候回到代码的视图了。选择屏幕底部的main.xml选项卡。您应该观察到两个新元素(ButtonTextView)已经出现在您的AbsoluteLayout元素中。

8。您需要插入文本并更改按钮的id。查看清单 10-2 中的粗体代码。您可以使用不同的词,但重要的是记住您分配给按钮的id的名称或其他标识符。

清单 10-2Main.xml

`

<AbsoluteLayout           xmlns:android="schemas.android.com/apk/res/and…"           android:orientation="vertical"           android:layout_width="fill_parent"           android:layout_height="fill_parent"         >

    <Button android:text="Start Game" android:layout_width="wrap_content" android:id="@+id/startgame" android:layout_height="wrap_content" android:layout_x="557dip" android:layout_y="249dip">

    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="When you are ready to begin this game, please click on the button above." android:id="@+id/textView1" android:layout_x="310dip" android:layout_y="361dip">

`

layout_xlayout_y行指定项目的位置。如果您想要精确地确定按钮和文本的位置,您可以编辑这些值。您使用id标签来引用代码中的对象,就像您在下一节中所做的那样。

响应开始游戏按钮的按下

现在你已经有了一个很好的展示给用户,你需要让它具有交互性。让玩家快速开始游戏至关重要。这对于一个回归的玩家来说尤其重要。请记住,如果这个人回来玩你的游戏,他们希望很快开始玩,不希望看到说明或被介绍视频打扰。

为了显示你的新布局,然后让用户导航到真正的游戏,让我们回到MainActivity.java。这里你做一个简单的输入测试,然后展示实际的游戏。然而,最初你需要把Main.xml而不是GameView.java作为游戏的视图。请遵循以下步骤:

  1. Open MainActivity.java in the edit pane of Eclipse.
  2. Add the following import statement to the top of the file:

import android.widget.Button;

3.更改MainActivity.javaonCreate()方法,使其看起来像清单 10-3 中的。粗体部分是对你之前作品的修改。你必须进口Android.view.View才能让它工作。

清单 10-3。??MainActivity.java

`@Override public void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);

    mGameView = new GameView(this);

    setContentView(R.layout.main);

    mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);     mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

    final Button button = (Button) findViewById(R.id.startgame);     button.setOnClickListener(new View.OnClickListener() {         public void onClick(View v) {

            setContentView(mGameView);         }     });

}`

第一个setContentView()告诉应用加载main.xml作为布局。按钮部分监听按钮的点击。一旦发生这种情况,你调用另一个setContentView()在屏幕上显示GameView。这是你用来初始化游戏的简单方法。

当你给按钮赋值时,使用函数findViewById;作为参数,您使用按钮的id。这就是为什么你要把id按钮设计成一个很容易识别的启动游戏的物品。

4.运行游戏,你会看到一个欢迎界面。按下开始游戏按钮继续,应用正常运行。

恭喜你:你终于完成了这本书的代码部分!下一部分处理游戏的最终编译和发行的准备工作。您离与其他用户共享您的创作越来越近了。

包装游戏

在游戏完成并准备发布之前,你必须注意几件事情。这一节讲述了如何清理代码,并最终将产品编译成一个 APK 文件,以备分发。APK 是包含所有游戏代码、图像和资源的包装。

请遵循以下步骤:

  1. The first thing to do is to delete any Log.d statements in the code. I usually perform global search and replacement to delete them. You don't want the retail version to waste processing power and send our debugging warning. You must fix the code version in the Android manifest file. Find this file by going to the root directory of HarborDefender folder and opening AndroidManifest.xml. The code should be similar to the tag shown in Listing 10-4 .

清单 10-4。??AndroidManifest.xml

` <manifest xmlns:android="schemas.android.com/apk/res/and…"       package="com.gameproject.harbordefender"       android:versionCode="1"       android:versionName="1.0">     <uses-sdk android:minSdkVersion="11" />

    <application android:icon="@drawable/icon" android:label="@string/app_name">         <activity android:name=".MainActivity"                   android:label="@string/app_name">                              <action android:name="android.intent.action.MAIN" />                 <category android:name="android.intent.category.LAUNCHER" />                      

     `

注意粗体部分。你可以设置自己的版本代码和版本名称,但这是惯例,因为这是你的第一个游戏,使用 1.0 作为版本。还要确保 SDK 的最低版本是 11。

3.在 Eclipse 中选择文件>导出。

4.选择导出 Android 应用作为您要执行的导出类型。

5.在下一页,输入你最终项目的名称: Harbor Defender

6.您必须创建一个密钥库,这是保护您的应用的安全性所必需的,并且被 Android 应用市场用作标识符。选择创建新的密钥库,如图图 10-3 所示,并使用浏览按钮打开一个窗口,让您将文件放入文件夹中..键入类似于 harbordefenderkey 的文件名,并接受默认位置。

images

***图 10-3。*生成密钥的提示

7.创建一个唯一且难懂的密码来保护自己,如图图 10-3 所示。

8.在图 10-4 所示的密钥创建页面中填写适用信息。(图为我是如何完成的。)密码可以与您在上一页中使用的密码相同。

images

***图 10-4。*填写开发商信息

9.下一页是最后一页。单击浏览,并输入 HarborDefender 作为 APK 目的地。关闭对话框,并完成该过程。

就这样,你完成了这个项目。下一节将讨论如何将这个项目引入应用市场并送到消费者手中。你还将讲述如何在拥挤的应用市场中做最好的营销和工作。

部署游戏

我希望你对你的游戏感到满意,并且相信其他人也会喜欢它。本节介绍如何使用 Android 应用市场。您将了解如何上传应用以及营销和定价的基本原理。有了这些信息,您就可以继续制作更多的待售应用。

首先,看一下图 10-5 ,图中显示了[market.android.com/](https://market.android.com/)的 Android 应用市场主页。

images

***图 10-5。*安卓应用市场

在这个页面上,Android 移动设备和平板电脑的用户可以下载和购买应用。特别值得注意的是标签上写着特色平板电脑应用。Android 正在大力吸引平板电脑的买家,因此它将专门为平板电脑设计的应用与手机应用分开。这对你来说是个好消息,因为你面临的竞争少了很多。

在如何提供课程方面有很大的自由。你可以为你的应用设定一个介于 1 美元到 200 美元之间的价格,或者免费赠送。当一个顾客买了它,你得到了销售额的 70%;剩下的就是将应用发送到设备上的费用。谷歌不收取任何收益,但设备制造商和在线分销商收取处理交易的费用,就像信用卡公司对每笔交易收取费用一样。iPhone 和 iPad 应用只给开发者 60%的收入,所以从这个意义上说,Android 比苹果应用商店还有一个优势。

安卓市场上 57%的应用是免费的。竞争应用商店的免费应用比例要低得多。对你的暗示是,你必须意识到,要求用户付费的节目必须表现出优越的质量,并提供许多小时的播放时间。

你现在知道了应用市场的基本情况。您必须创建一个 Android 应用市场帐户,才能看到您自己的作品。下一节将介绍如何创建帐户并上传您的第一个应用。

开设谷歌开发者账户

没有什么比看到自己的工作掌握在他人手中更让应用开发人员高兴的了。在这里,您可以创建您的 Android 应用市场帐户,并向全世界发布您的程序:

  1. Go to [market.android.com/](https://market.android.com/). At the bottom of the screen, click Developer.
  2. Select the option to publish the application.
  3. Log in to your Google account, or create a new account. You should create a new account specifically for your application business, which is different from your regular email or Google+ activities.
  4. The next screen is shown in Figure 10-6 . Fill in with accurate and professional information. If you don't have a website, that's fine, but you might want one. images ?? * ?? 】 Figure 10-6. Create your robot application market account*
  5. You are prompted to pay the registration fee. This is $25, which must be paid through Google checkout.

注册完成后,您的帐户就有了一个分类配置文件。你可以做很多事情,从添加一个谷歌结账账户以便获得付款,到上传一个应用。

现在你可以将你的游戏上传到谷歌市场了。

上传游戏到谷歌市场

虽然大多数开发人员都想出售他们的应用,但这一部分介绍了如何将您的应用免费上传给公众。如果你想收到付款并为你的工作收费,请访问这个关于市场的惊人指南:www . Google . com/support/androidmarket/developer/bin/topic . py?topic=15866

在完成上传游戏的简单过程之前,您必须准备好几个项目,包括:

  • Include applications
  • Two beautiful application screenshots of APK file highlight its characteristics.
  • A high-resolution icon that users choose to play your game.

上传游戏是一个简单的命题。在您的在线开发人员控制台上,单击上传应用。在这里,您将看到一个向导,询问刚才列出的项目。在您存储文件的目录中找到这些文件。

关键是要有一个有吸引力的截图和描述,以及任何你想显示的附加图表;你的成功将关系到你的游戏吸引了多少用户。下一部分着眼于如何准备在市场上取得最大的成功。

营销您的游戏

营销你的应用需要将你的产品展示给尽可能多的人。如果你创造了一个像样的游戏,那么如果人们有机会看到它,他们就会购买。第一个问题是如何让你的应用脱颖而出。与 iPads 和 iPhones 的应用商店不同,Android 程序可以从任何网站下载,而不仅仅是谷歌官方市场。这意味着拥有自己网站的开发者更容易销售他们的产品,因为他们不会与市场上过多的类似应用混淆。用户可以直接来到他们的网站,看到视频、图片和程序说明,这些在 Android Market 上的简短描述中是不可能的。

利用这一事实,创建自己的网站,吸引潜在买家。创建一个脸书页面或推特账户也能增加关注度。不要把读者引向你在 Android 应用市场上的页面,而是把他们引向你自己网站上的一个页面,这样就不会那么混乱了。

如果你做过网上营销,你就会知道邮件列表有多有用。在您的网站上,为访问者提供注册更新您的应用和免费附加服务的机会。这样,即使他们没有立即购买,你也可以继续吸引他们,说服他们购买你的产品。看看 AWeber ( [www.aweber.com/](http://www.aweber.com/))网站,它提供了一个很棒的邮件系统,你可以用它来向你的用户分发时事通讯。它每月收费,但许多营销人员发现,客户从简讯中获得的收益超过了成本。

最后,通过将你的公司或游戏放入更传统或可信的媒体来解决营销问题。请关注技术的杂志对其进行评论,或将相关信息发送给在线新闻来源。当你这样做的时候,确保你的游戏提供一些非常独特的东西。也许输入控制是完全创新的,或者游戏发生在零重力室内。让应用有新闻价值。这也可以由你的公司整体来做。举例来说,如果你所有游戏中的艺术作品都来自一位著名的画家,那绝对是一个值得一个网站谈论的独特故事。

所有这些技术都可以追溯到广告中使用的基本漏斗方法。它在各种各样的初级营销和公共关系书籍中有所阐述,但也需要包括在这里。你吸引的用户越多,时间越长,你的销售额就越多。图 10-7 显示了这是如何工作的。

images

***图 10-7。*将你的访客引向买家

这就是营销技巧。通过反复试验,你会找到最适合你的方法。我发现在应用市场上的成功很少是在你的第一款甚至第二款游戏上实现的。你必须坚持下去,在获得金牌之前,建立对你的产品的期待和兴奋。

总结

恭喜你!你已经完成了这本书。你从发现什么是 Android 以及如何在其中编程,到编写一个完整的游戏,再到将你的工作投入应用市场。

这是一本有趣的书,我希望你也喜欢。从事一项发展如此迅速的技术工作既令人畏惧又令人振奋;理想情况下,这本书给了你一些关于如何为 Android 平板电脑创建自己的游戏的想法。

凭借 Android 过去的成功和光明的未来,我相信平板电脑对更好游戏的需求将会持续很长一段时间。确保你在那里抓住这个令人兴奋的浪潮。