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.