Buffering Solutions on BlackBerry, Part 3


The first two parts (here and here) of this series covered the basics on buffering and outlined a few fairly simple solutions for adding buffering into your game. Now we come to the fun stuff. Simple buffering only gets you so far. You’ve read about the various pros and cons of the full screen image buffering methods and why sometimes they’re just not feasible due to device limitations. Now let’s discover a variation I’ve used numerous times to achieve good frame rates on games with a lot of background graphics.

Note: when I originally started writing these articles I did not expect to think of so much useful information that was not covered in the DEVCON presentation. Instead of writing a book, I have left out some details that I expect a competent developer should be able to figure out on their own after having read the articles. Please feel free to contact me with questions and I’ll answer them or clarify anything that’s not clear.

Segmented Buffer

A segmented buffer is… well, just like it sounds. Conceptually a large buffer but broken up into smaller chunks. There is a lot of complexity that can be required when using segmented buffers to determine which segments are relevant, where a viewport is, how you decide on the configuration of segments, etc. I won’t answer all your questions here but given the examples below and the information provided, you should be able to take the next steps to create custom segmented buffers that meet your specific gaming and performance needs.

int numBuffers = (screenWidth/ bufferWidth) + 1;

Bitmap buffers[] = new Bitmap[numBuffers];
private boolean[] bufferIsDirty = new boolean[numBuffers];
private int leftBuffer = 0;
private int leftOffset = 0;public synchronized void setBufferDirty(int bufferId) {
  synchronized (buffers) {
    bufferIsDirty[bufferId] = true;
  }
}

Instead of allocating one large buffer, an array of smaller buffers is created, each with a unique flag to determine if the buffer is dirty. I’ll explain how to determine how many segments you need and how to decide if you are going to pre-determine their width or height (this example uses pre-determined width) a little later on in this article. Suffice it to say, figure out how many segments you need and then create the variables for them.

Since segmented buffers are usually used for buffers that exceed the dimensions of the screen, the concept of the viewport (as discussed in the previous article) is very important here. This example assumes the segments are the same height as the screen, but smaller in width. leftBuffer and leftOffset keep track of the viewport’s left-most edge. The leftBuffer variable tracks which of the segments contains the left edge of the viewport (the index into the buffers array) and the leftOffset variable tracks where within the segment the left edge of the viewport is (in case the edge of the viewport does not exactly align with the edge of the buffer). For example, if leftBuffer is referencing the first buffer in the array (buffers[0]) then leftOffset’s value is bound by 0 (the left edge of that segment) and bufferWidth (the width of each segment) such that it is always referencing a pixel location that falls within the area of buffers[leftBuffer]. I’ll discuss how those are leveraged in more detail later on.

The setBufferDirty method is very similar to past examples but because each segment’s dirty state is tracked independently, there’s a parameter passed to indicate which of the segments to set as dirty. I’ll leave it as an exercise for you to create a method that sets all flags dirty =).

private void repaintBuffer(int bufferId) {
  synchronized (buffers) {
    // ... draw all the static content onto the buffer ...
    bufferIsDirty[bufferId] = false;
  }
}

repaintBuffer is also similar to previous examples however like the setBufferDirty, a parameter is added to indicate which buffer to repaint. This is important. You do not always need to repaint all segments. Usually, only a subset of the segments will need to be repainted. In practise, you will probably want more complex repaintBuffer implementations or else need to map objects to buffer segments elsewhere in your game so you can determine which segments to repaint. Usually, you will do this by knowing the x/y coordinates of the object on-screen you are changing and then mapping that to the buffer segment via the viewport offsets (leftBuffer and leftOffset in this example).

public void paint(Graphics g) {
  synchronized (buffers) {
    for (int i = 0; i < numBuffers; i++) {
      if (bufferIsDirty[i]) { repaintBuffer(i); }
    }
  }
  int currentBuffer = leftBuffer;
  for (int i = 0; i < numBuffers; i++) {
    g.drawBitmap(... buffers[currentBuffer % numBuffers] ...);
    ++currentBuffer;
  }
  // ... draw all dynamic content ...
}

Painting the segmented buffer to the screen is also a little different. This example used the paint method to check each segment’s dirty state and repaint the segment as needed prior to rendering the buffer to screen. You can choose the other methods of dirty buffer checking I’ve explained before, I’m using this one as it’s simple for illustrative purposes.

Instead of a simple drawBitmap with a single full screen image, each of the segments that are visible in the viewport need to be drawn on-screen.

Segment Configuration Options

One of the most important decisions to make when implementing a segmented buffering solution, is the number and type of segments you need. There are three primary types of segments you could consider: Vertical, Horizontal, Grid.

Vertical

The example above used vertical segments. Conceptually, it’s like splitting the screen into slices that have a constant width, and a height equal to (or greater than) the height of the screen. These are most useful when the horizontal movement of the viewport on-screen will be significant. Think of Super Mario Bros. (the original). The maximum number of segments you will need is the width of your level divided by the width you choose for each segment… +1. The extra buffer is to accommodate the transition of the viewport between buffers. I.e. usually one buffer is moving off-screen while simultaneously a buffer is moving on-screen on the opposite side of the screen during scrolling. The width of each segment and the number of segments are the variables that you can change to affect the functionality and performance of the segmented buffer in this case.

Horizontal

This type of segment is the exact opposite of the vertical segments. Instead of thinking of splitting up your screen into slices with constant width, you’re splitting the screen into slices with constant height. If you’re mostly scrolling vertically in your level, you probably want horizontal segments. Think of a top-down shooter where the ground scrolls down past you as you fly around shooting things. The height of each segment, in this case, is analogous to the width in the vertical segments.

Grid

Combining the concepts of the vertical and horizontal segments, you could create a 2D array of segments in a grid that allows equally for horizontal and vertical scrolling… but that’s a configuration I’ll let you explore (though it’s not complicated if you understand everything so far).

Recycling Segments

One of the great benefits of segmented buffers is that you can scale the number of buffers dynamically based on the available memory on your device. The minimum number of segments you need is the minimum required to cover the entire screen, plus 1. The extra segment is for rendering off-screen the next section to come on-screen as you scroll.

BlackBerry Game Development Challenges - Slide 12

Slide 12

In many games, you’re usually scrolling in one direction most of the time. Say you’re scrolling right through the level and rarely go back left. Why keep around the buffered content that goes off the left edge of the screen? If you don’t need those extra buffers, you may as well reuse them! This is where recycling of buffers comes in and why that leftBuffer variable was needed in the above example.

As you scroll around the screen and a buffer goes off-screen, instead of repainting all the segments, or physically moving each reference in the array so that index 0 is always the left buffer on-screen, simply offset the leftBuffer index to indicate where the left edge of the screen is (this is for vertical segments, horizontal segments would obviously track the top or bottom of the screen) and then paint the number of buffers required (wrapping around from the end of the array to the beginning) to cover the screen. Any remaining buffers between the leftBuffer index and (leftBuffer+(screenWidth/bufferWidth))%numBuffers are not used to render the current content on-screen and can be freely changed without worrying about conflicting with the screen rendering in the current frame.

This allows you to pre-render buffers as they go off-screen when there is idle time in the game — a huge potential performance impact, if even only perceptually to the user.

Performance Implications

The choices you make on the number and size of the buffers has a direct impact on the performance of your game. Slide 12 from the presentation shows an example of Indiana Jones and the Kingdom of the Crystal Skull by THQ Wireless (BlackBerry version by Magmic Games). Run the animation and you’ll see the viewport moving over the various segments as well as the content that would be rendered on each.

If you chose the have more buffers within a screen area, you will get smoother scrolling as the time to recycle a buffer as the player moves throughout the level will be lower (smaller buffers are faster to repaint). However the time required to render the screen increases with more buffers (the worst case being the same number of buffers as tiles, in which case you gain nothing by buffering) so your fixed frame rates will be lower.

Less buffers significantly improves the frame rate where there’s little or no scrolling happening but can take longer to repaint when they are recycled so you can end up with some choppy scrolling as one segment moves off-screen and gets repainted.

More Variations

Segments are very flexible. You can make vertical segments that are taller than the screen, horizontal segments that are wider than the screen, or many other combinations and variations that allow you to tweak the performance of your segmented buffer optimally for your specific gaming needs. If you understand the basic concepts, with a little experimentation, you can come up with custom buffer sizes and configurations that optimize your particular game’s needs.

Tying it all together…

The best way to summarize all of these articles is to work through a case study where we combined multiple buffering options to optimize a game for BlackBerry. In Part 4, we will profile “Worms: A Space Oddity” and show how these strategies (among other things) were critical to even having a playable game; and how, by combining them, we achieved an amazing gaming experience on BlackBerry.

Advertisements
This entry was posted in Game Development and tagged , , , , , , , , , , , . Bookmark the permalink.

One Response to Buffering Solutions on BlackBerry, Part 3

  1. Cool post very informative I just found your blog and read through a few posts although this is my first comment, i’ll be including it in my favorites and visit again for sure

Comments are closed.