How Do I Fix the Score Counter in Android Game Without Crashing It?

While working on a client project recently, I ran into a frustrating issue while trying to implement an image based score counter in a custom Pong style Android game. The idea was simple display the score as changing number images (like digital counters) instead of plain text. But the app kept crashing whenever the ball reached the screen edge.

This blog walks through the debugging process, the root cause, how I fixed it, and how I added additional functionality to make the counter smarter and more interactive.

Objective

Implement an image based score counter in a custom Android game and fix the crash occurring during runtime when the ball hits the top or bottom edge.

Initial Code & Problem Description

I had the image counter logic working in theory, and the app even compiled correctly. But when the ball hit the screen edge, it crashed with this error:

java.lang.NullPointerException
at com.Pong.GameState.update(GameState.java:154)

This was the line causing the issue:

TopCounter.setImageResource(myImageList[i+1]);

Root Cause – Explanation

The Problem: NullPointerException

The error occurred because TopCounter (and also BottomCounter) was null at runtime.

Why Did That Happen?

In GameThread.java, I initialized the GameState like this:

= new GameState();

But GameState was originally an Activity class, and creating it with new GameState() does not call onCreate(). Therefore, findViewById() was never executed, meaning the ImageView objects were never initialized. Boom NullPointerException.

Correct Approach

Don’t Use Activity for Game Logic

Activities are lifecycle bound UI components. You should never manually instantiate them using new. Instead, I refactored GameState into a plain Java class and passed in the required UI elements (ImageView) from the actual Activity that manages the layout.

Final Working Version: GameState.java

package com.Pong;

import android.graphics.*;
import android.widget.ImageView;

public class GameState {

private int _screenWidth = 270;
private int _screenHeight = 395;
private int _ballSize = 10;
private int _ballX = 100, _ballY = 100;
private int _ballVelocityX = 3, _ballVelocityY = 3;

private int _batLength = 75;
private int _batHeight = 10;
private int _topBatX = (_screenWidth / 2 + 25) - (_batLength / 2);
private int _topBatY = 54;
private int _bottomBatX = (_screenWidth / 2 + 25) - (_batLength / 2);
private int _bottomBatY = 392;

private int _toppoint = 0;
private int _bottompoint = 0;

private ImageView TopCounter;
private ImageView BottomCounter;

private int[] myImageList = {
R.drawable.counter00, R.drawable.counter01, R.drawable.counter02,
R.drawable.counter03, R.drawable.counter04, R.drawable.counter05,
R.drawable.counter06, R.drawable.counter07, R.drawable.counter08,
R.drawable.counter09
};

public GameState(ImageView topCounter, ImageView bottomCounter) {
this.TopCounter = topCounter;
this.BottomCounter = bottomCounter;
}

public void update() {
_ballX += _ballVelocityX;
_ballY += _ballVelocityY;

if (_ballY > _screenHeight) {
resetBall();
_toppoint = Math.min(_toppoint + 1, 9);
TopCounter.setImageResource(myImageList[_toppoint]);
}

if (_ballY < 50) {
resetBall();
_bottompoint = Math.min(_bottompoint + 1, 9);
BottomCounter.setImageResource(myImageList[_bottompoint]);
}

if (_ballX > _screenWidth || _ballX <= 40) {
_ballVelocityX *= -1;
}

if (_ballX > _topBatX && _ballX < _topBatX + _batLength && _ballY < _topBatY) {
_ballVelocityY *= -1;
}

if (_ballX > _bottomBatX && _ballX < _bottomBatX + _batLength && _ballY > _bottomBatY) {
_ballVelocityY *= -1;
}
}

private void resetBall() {
_ballX = _screenWidth / 2;
_ballY = _screenHeight / 2;
_topBatX = _screenWidth / 2 - 10;
_bottomBatX = _screenWidth / 2 - 10;
}

public void draw(Canvas c, Paint paint) {
paint.setARGB(255, 234, 222, 199);
c.drawRect(new Rect(_ballX, _ballY, _ballX + _ballSize, _ballY + _ballSize), paint);
c.drawRect(new Rect(_topBatX, _topBatY, _topBatX + _batLength, _topBatY + _batHeight), paint);
c.drawRect(new Rect(_bottomBatX, _bottomBatY, _bottomBatX + _batLength, _bottomBatY + _batHeight), paint);
}

public void resetScore() {
_toppoint = 0;
_bottompoint = 0;
TopCounter.setImageResource(myImageList[0]);
BottomCounter.setImageResource(myImageList[0]);
}
}

GameThread.java: How I Passed the ImageViews

From the main Activity, I passed the ImageView references directly into GameState:

 topCounter = findViewById(R.id.counterT);
ImageView bottomCounter = findViewById(R.id.counterB);
GameState state = new GameState(topCounter, bottomCounter);

Then, passed state into GameThread, like this:

gameThread = new GameThread(surfaceHolder, gameView, context, handler, state);

Inside GameThread:

 GameThread extends Thread {
...
public GameThread(..., GameState state) {
this._state = state;
}
}

Bonus Feature I Added for Practice

To go beyond just fixing the bug, I added the following:

Reset Button

A button in the UI that resets the score back to 0 for both players:

.setOnClickListener(v -> gameState.resetScore());

Game Over Check

Inside update():

if (_toppoint == 9 || _bottompoint == 9) {
// Stop thread or show Toast/Game Over dialog
}

Animated Score

Use AnimationDrawable to animate the score transition for a better UX.

Sound Effects

Trigger short audio clips on scoring or paddle bounce using SoundPool.

Final Thoughts

This small bug taught me a big lesson never instantiate Android components manually unless the framework is meant to manage them. The way I originally used new GameState() completely bypassed lifecycle methods like onCreate() causing my views to be null.

By restructuring GameState as a pure logic class and passing in view references, the design is now cleaner, safer, and reusable. The added features also make the game more enjoyable for players.

Related blog posts