官术网_书友最值得收藏!

Using GameView

Until now, we have been using standard views and translating them to render the different elements of the game. While this is an easy way to draw elements on the screen, it is far from being efficient. We are relying on the system layout to do the drawing.

While this technique is fine for a turn-based game or any non-real-time game in general, it cannot render enough frames per second for a real-time game.

Note

Working with standard Views is fine for non-real-time games.

We are going to create a custom View that we are going to call GameView. This view will be responsible for drawing the sprites.

We already noted the duplication of code and mentioned the concept of sprite in the previous chapter. We will now move forward and create a Sprite class that will take care of drawing an image at specific coordinates inside the GameView.

There are two ways of drawing at low level on Android. They are:

  • Extending View and overriding onDraw
  • Extending SurfaceView and using SurfaceHolder

In both cases, we will get a Canvas and draw our GameObjects on it. The main difference is that the onDraw method of the View is executed on the UIThread, while SurfaceView and SurfaceHolder are designed to perform the draw on a separate thread.

Note

Low-level drawing on Android is always done using a canvas.

According to the official documentation, it is more efficient to use SurfaceView. But, since Android 4.0, view rendering is hardware-accelerated (while SurfaceView is not). In the case of modern phones with high-resolution screens and faster processors, this may not always be the case.

Note

SurfaceView is not hardware-accelerated and may perform worse than normal View.

Anyway, you should know both and be able to swap them easily, even if it is just for testing purposes. We will create an interface named GameView, which will be implemented by the two classes, so they can be changed easily. The classes we will make are:

  • StandardGameView: This will extend from View
  • SurfaceGameView: This will extend from SurfaceView

The GameView interface

The GameView interface will have all the methods that are needed by the GameEngine to handle the View:

public interface GameView {
  void draw();
  void setGameObjects(List<GameObject> gameObjects);
  // Generic methods from View
  int getWidth();
  int getHeight();
  int getPaddingLeft();
  int getPaddingRight();
  int getPaddingTop();
  int getPaddingBottom();
  Context getContext();
}

There are basically two methods we need, one to trigger the drawing and one to pass the list of game objects to the GameView, so they can be drawn there:

  • draw: This will trigger a draw on the GameView
  • setGameObjects: This will set the list of GameObjects for the View

The rest of the methods are implemented in the View. We need to declare them, because we are using them on the GameEngine.

Let's explore each implementation in detail.

StandardGameView

The StandardGameView class extends View. We just provide the basic constructors for View, override the onDraw method, and then implement the methods from GameView:

public class StandardGameView extends View implements GameView {

  private List<GameObject> mGameObjects;

  public GameView(Context context) {
    super(context);
  }

  public GameView(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  public GameView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    synchronized (mGameObjects) {
      int numObjects = mGameObjects.size();
      for (int i = 0; i < numObjects; i++) {
        mGameObjects.get(i).onDraw(canvas);
      }
    }
  }

  @Override
  public void draw() {
    postInvalidate();
  }

  @Override
  public void setGameObjects(List<GameObject> gameObjects) {
    mGameObjects = gameObjects;
  }
}

Basically, setGameObjects stores a reference to the game objects. The adding and removing of GameObjects are done in the GameEngine. When we draw the view, we iterate over the list of game objects, calling onDraw on all of them and passing the Canvas object on which we are drawing.

Note that the method is synchronized using the mGameObjects variable. This is important because, as we mentioned in Chapter 1, Setting Up the Project, the contents of the list can change during onUpdate and we do not want this to happen while we are iterating over the list.

The other important point is that the list of GameObjects is a reference to the one inside the GameEngine and not a copy, so whenever the list gets modified, the latest values are accessible from both places. This is also why synchronization is required.

Note

The list of GameObjects is shared between the GameEngine and the GameView.

Performance-wise, it would not make sense to copy all the elements in the list to a new one in each execution of onDraw.

To trigger a draw, we just need to call postInvalidate. Remember that invalidating a view has to be done on the UIThread. This is why we need to call postInvalidate. This method will post a Runnable to be run on the UIThread that will then invalidate the View.

As we mentioned in the earlier chapters, once the view gets invalidated, Android makes sure that the onDraw method of the View is called and then the UI is updated. This is the connection between invalidating the view and the onDraw method, where we draw the game objects.

The onDraw method is obviously time-critical. We should avoid all unnecessary operations. In particular, lint shows a warning if you create an object inside onDraw. This is, to reiterate, a best practice for game developers: to always create the objects in advance.

Note

Never do object creation inside onDraw.

Also, it is worth remembering that Android has a fallback mechanism to avoid overload on the drawing. If a view has been invalidated but not yet redrawn, the call to invalidate will be ignored (the view is already going to be redrawn).

SurfaceGameView

To implement a GameView that extends SurfaceView, we need to define a Callback for SurfaceHolder—the class used to access the SurfaceView—and then, whenever we want to draw, we lock the canvas, draw on it, and unlock it again so it can be rendered by SurfaceView.

Let's see the code of SurfaceGameView:

public class SurfaceGameView extends SurfaceView implements SurfaceHolder.Callback, GameView {

  private List<GameObject> mGameObjects;
  private boolean mReady;

  public SurfaceGameView(Context context) {
    super(context);
    getHolder().addCallback(this);
  }

  public SurfaceGameView(Context context, AttributeSet attrs) {
    super(context, attrs);
    getHolder().addCallback(this);
  }

  public SurfaceGameView(Context c, AttributeSet attrs, int defStyleAttr) {
    super(c, attrs, defStyleAttr);
    getHolder().addCallback(this);
  }

  @Override
  public void surfaceCreated(SurfaceHolder holder) {
    mReady = true;
  }

  @Override
  public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

  }

  @Override
  public void surfaceDestroyed(SurfaceHolder holder) {
    mReady = false;
  }

  @Override
  public void setGameObjects(List<GameObject> gameObjects) {
    mGameObjects = gameObjects;
  }

  @Override
  public void draw() {
    if (!mReady) {
      return;
    }
    Canvas canvas = getHolder().lockCanvas();
    if (canvas == null) {
      return;
    }
    canvas.drawRGB(0,0,0);
    synchronized (mGameObjects) {
      int numObjects = mGameObjects.size();
      for (int i = 0; i < numObjects; i++) {
        mGameObjects.get(i).onDraw(canvas);
      }
    }
    getHolder().unlockCanvasAndPost(canvas);
  }
}

First, we have three constructors with different arguments that are intrinsic to SurfaceView. Note that they all include a call to set a Callback to the SurfaceHolder, which is also implemented by SurfaceGameView. This callback will inform us of when SurfaceView is ready or when things have changed.

The next methods are the implementation of the Callback interface. Those are the methods that are called when the SurfaceView is created, modified, or destroyed. We store the status of the view to know whether it is ready or not, so that it can be used to draw. A View is ready any time after it is created until it is destroyed.

Then, we move into implementing the methods from GameView.

To set the GameObjects, we do exactly as we did for StandardGameView, also with the same implications when it comes to handling a reference.

The draw method is where things are a bit different. We have to check whether the view is ready. If so, we lock the Canvas so we can draw on it.

Once we have the canvas, we need to clean it before we draw each frame. The canvas will have the previous image on it. (If we do not clean it, we will get rendering artifacts as shown in the following screenshot.) This cleaning is done by filling the canvas with a solid color using drawRGB.

Once we have cleaned the canvas, we take the same drawing as for StandardGameView and just iterate over the game objects.

Finally, we unlock the canvas and post it. This is the point when we pass the Canvas back to the SurfaceView and post it to the UIThread. Note that all of the drawing has been done outside the UIThread. Only once the Canvas is fully rendered will it be passed back for drawing.

Note

SurfaceView performs the drawing on the Canvas outside the UIThread.

As mentioned before, SurfaceView is supposed to give better performance. But, since it is only software accelerated, in modern phones a standard View—with hardware acceleration—may be more efficient in some cases. A particular situation when SurfaceView performance is impacted is if we put other views on top of it (like the pause button), since a full alpha-blended composite will be performed each time the surface changes.

Updating GameEngine

The use of GameView has some implications from the GameEngine point of view. It means that it has to initialize the GameView and then trigger draws using the generic interface.

The GameView will be a parameter of the constructor of GameEngine. It will be initialized, passing a reference to the list of game objects. The updated constructor of GameEngine is like this:

public GameEngine (Activity a, GameView gameView) {
  mActivity = a;
  mGameView = gameView;
  mGameView.setGameObjects(mGameObjects);

  mWidth = gameView.getWidth()
    - gameView.getPaddingRight() - gameView.getPaddingRight();
  mHeight = gameView.getHeight()
    - gameView.getPaddingTop() - gameView.getPaddingBottom();

  mPixelFactor = mHeight / 400d;
}

From now on, we will also calculate the pixelFactor inside the GameEngine. We will store it in a public variable so it can be read by the game objects. This has several advantages, such as:

  • If we decide to change the number of units of the screen, this is done in a single place
  • Removing code duplications is always good for maintenance

On the other hand, the onDraw method of the GameEngine becomes extremely simple:

public void onDraw() {
  mGameView.draw();
}

Updating the game layout

Of course, we have to modify the fragment_game.xml layout to include the GameView. We will take this chance to do some other modifications to it, such as removing the TextView and changing the padding of the layout to be the margins on the pause button instead. This makes sure that the GameView is fullscreen while keeping the button margins as they were.

It is important to remember that in a FrameLayout, the order in the XML specifies the order in which the items are drawn (the z-index). We will put the GameView at the beginning of the layout to have the pause button drawn on top of it.

The new version of fragment_game.xml is as follows:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context="com.example.yass.counter.GameFragment">

  <com.example.yass.engine.SurfaceGameView
    android:id="@+id/gameView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

  <Button
    android:layout_gravity="top|right"
    android:id="@+id/btn_play_pause"
    android:layout_marginTop="@dimen/activity_vertical_margin"
    android:layout_marginRight="@dimen/activity_vertical_margin"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/pause" />

  <include layout="@layout/view_vjoystick" />

</FrameLayout>

Note that this is the place where we decide which variant of the GameView we are going to use. The rest of the code will access the methods via the GameView interface, so nothing else needs to be changed. We are going to use the SurfaceGameView from now on, but feel free to experiment with StandardGameView as well.

Note

The layout is where we set which variant of GameView we are going to use.

Finally, inside GameFragment, we update the creation of the GameEngine by adding the GameView parameter:

GameView gameView = (GameView) getView().findViewById(R.id.gameView);
mGameEngine = new GameEngine(getActivity(), gameView);

Now we have a GameEngine that relies on a GameView to do the rendering. We still need to update the GameObject class to make use of it.

Before we get to the GameObject class, let's take a moment to improve DrawThread as well.

主站蜘蛛池模板: 衡东县| 电白县| 潞西市| 遂宁市| 磴口县| 大英县| 宣汉县| 巴彦淖尔市| 都匀市| 峨眉山市| 双柏县| 汶川县| 临漳县| 凤城市| 涡阳县| 内黄县| 荣昌县| 红河县| 定日县| 若尔盖县| 凤翔县| 石台县| 长岛县| 杭州市| 大田县| 曲沃县| 杂多县| 邵阳县| 稷山县| 犍为县| 香河县| 蒙自县| 辉县市| 繁峙县| 织金县| 隆回县| 海丰县| 阿拉善左旗| 卢龙县| 新干县| 兴文县|