The second part of this series will show you how you can scroll smoothly over the simple 2D Map which was created in the first part.

Note: I changed my coding style to fit the Java/Android coding style. Please be aware that variables like _mapSize are now mMapSize.

The performance issue we discovered in the first part was awful and no one will play a game which needs seconds to draw another frame. But why do we have this performance issue?
Do you remember how we draw the Map? We go trough the map in a loop and draw each cell. If our map has only a size of 10, everything is fine, but if we go to 100 and more, we draw a lot of cells and most of them are not on our display. And thats the mistake: We use resources and time to draw cells we don’t see.

To fix that, we should add a calculation to define the start and end of our loop.
Thats the current onDraw():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void onDraw(Canvas canvas) {
    // help variables
    int x = 0;
    int y = 0;
 
    for (int i = 0; i < mMapSize; i++) {
        // get the row
        mRow = mMapCells.get(i);
 
        // calculate the correct y for the row
        y = i * mCellSize - mYOffset;
        // go through the row
        for (int j = 0; j < mMapSize; j++) {
            // calculate the x coordinate for the columns
            x = j * mCellSize - mXOffset;
 
            mRow.get(j).draw(canvas, paint, x, y);
        }
    }
}

We see that we always go through the full map.

Ok before we change the start and stop of the loop, we need to calculate these borders. Therefore we create a function to do that.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// loop borders
private int mStartRow;
private int mMaxRow;
private int mStartCol;
private int mMaxCol;
 
// atomic offset for pixelwise scrolling
private int mXScrollOffset = 0;
private int mYScrollOffset = 0;
 
private void calculateLoopBorders() {
    mStartRow = mYOffset / mCellSize;
    mMaxRow = (int) Math.min(mMapSize, mStartRow + Math.floor(getHeight() / mCellSize) + 2);
    mStartCol = mXOffset / mCellSize;
    mMaxCol = (int) Math.min(mMapSize, mStartCol + Math.floor(getWidth() / mCellSize) + 2);
    mXScrollOffset = (int) (((mXOffset / (float) mCellSize) - (mXOffset / mCellSize)) * mCellSize);
    mYScrollOffset = (int) (((mYOffset / (float) mCellSize) - (mYOffset / mCellSize)) * mCellSize);
}

On line 2 and 4 we simply calculate the start of both loops by dividing the offset by the cell size. The result is the number of cells we have above or left of the point of origin.
On line 3 and 5 we calculate how many rows/columns have space on the screen with a extra buffer of 2 to make sure that we always see a cell at the edge, even if we have only one pixel left.
Line 6 and 7 are the most important, because they calculate the atomic scroll offset we need to refine our scrolling. (Hint: If you remove that variable, the scrolling will be by cell, not by pixel)

The next step is, we need to call this calculation whenever we scroll the map. We only scroll the map while we have the ACTION_MOVE event, so we will call the function at the last line in our if/else construct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public boolean onTouchEvent(MotionEvent event) {
    // touch down
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        // snipped
    } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
        // snipped
 
        // store the last position
        mXTouch = (int) event.getX();
        mYTouch = (int) event.getY();
 
        calculateLoopBorders();
    } else if (event.getAction() == MotionEvent.ACTION_UP) {
        // snipped
    }
    return true;
}

The borders are by default 0 or at least the display size + 2. To be sure that getWidth() and getHeight() are returning the right sizes, we need to add the very first call to calculate the borders inside the onSurfaceCreated() method. If we don’t do that, the very first screen will be black and we have to make a touch to display the map.

1
2
3
4
5
6
7
8
9
@Override
public void surfaceCreated(SurfaceHolder holder) {
    if (!mMapThread.isAlive()) {
        mMapThread = new MapThread(this);
    }
    mMapThread._run = true;
    mMapThread.start();
    calculateLoopBorders();
}

So, after we have created the borders and the call for their calculation, we have to refactor the loop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// help variables
private int mX = 0;
private int mY = 0;
private int mI = 0;
private int mJ = 0;
 
@Override
public void onDraw(Canvas canvas) {
    mI = 0;
    for (int i = mStartRow; i < mMaxRow; i++) {
        mJ = 0;
        // get the row
        mRow = mMapCells.get(i);
 
        // calculate the correct y for the row
        mY = mI * mCellSize - mYScrollOffset;
        // go through the row
        for (int j = mStartCol; j < mMaxCol; j++) {
            // calculate the x coordinate for the columns
            mX = mJ * mCellSize - mXScrollOffset;
 
            mRow.get(j).draw(canvas, mPaint, mX, mY);
 
            mJ++;
        }
        mI++;
    }
}

Now step by step:
The onDraw() method will be called as often as possible, so we should reuse objects and variables. To do that, we changed the helper variables to be member variables. We also added mI and mJ, which will help us to store the row/column index on the screen because the “display index” is not the index we can use while going through the map.
On line 10 we start the loop with the calculated start and end and on line 13 we get the complete row to go through, again with the calculated borders and draw the cells (on line 18).
The code comments should explain the rest.

Now you can change the mMapSize to 300 and you can scroll smoothly.
If you think, we have done everything right, take a look at the Garbage Collector in your LogCat.
On the Nexus One I can use a mMapSize of 500 but I am near the limit, the GC frees every 600ms at least 1.6MB.

Project source: ScrollMap – Part II

  • Share/Bookmark