
In this article, we recreate the two-player game Pong – the first arcade video game 1972 – for Android using Kotlin. We will implement the game SurfaceView, game loop design pattern, and code optimization for better performance.
Game loop
Game loop is a common design pattern in game development. It is already integrated into different game engines. Nevertheless, we won’t use one in this project. Therefore, we can define a game loop interface in the newly created project.
interface GameLoop {
fun draw(canvas: Canvas? = null)
fun update()
}
However, if you want to create a more convenient interface with some more variables, we can have something like below.
import android.graphics.Canvas
interface GameLoop {
var frameRate: Int
var timeToUpdate: Long
val shouldUpdate: Boolean
get() = (System.currentTimeMillis() >= timeToUpdate)
fun render(canvas: Canvas? = null)
fun update()
}
- The
frameRate
variable (or update rate) is to control the time interval between each update. - The
timeToUpdate
andshouldUpdate
variables are to determine it is time to update. - The
render
andupdate
are two standard method that are used to draw and update relevant elements.
Sprite, Ball, and player classes
For easier to handle the drawing of the ball and players, we will create an abstract class named Sprite
. The benefit of doing this is to manage all objects’ collision and draw on the screen and help the code project easier to maintain.
The variables, such as location
, shape
, and movVec
, are to control the drawing and movement of the sprite. Also, it can accan accept any
abstract class Sprite : GameLoop {
constructor(context: Context, shapeId: Int?) {
this.context = context
this.shape = shapeId?.let { BitmapFactory.decodeResource(context.resources, it) }
if (shape != null) {
location.right = shape!!.width.toFloat()
location.bottom = shape!!.height.toFloat()
}
}
var movVec = PointF()
var paint = Paint()
var context: Context? = null
var location = RectF(0f, 0f, 80f, 80f)
var shape: Bitmap? = null
override fun render(canvas: Canvas?) {
if (shape != null && location != null) {
canvas?.drawBitmap(shape!!, null, location!!, paint)
} else {
canvas?.drawRect(location, paint)
}
}
abstract fun collide(o: Any?): Boolean
}
Next, the ball and player classes will then inherit Sprite
. The player class is implemented as follow:
class Player(context: Context, shapeId: Int?, isAI: Boolean = false) : Sprite(context, shapeId) {
var isAI = false
init {
this.isAI = isAI
location = RectF(0f, 0f, 200f, 50f)
if (isAI) {
movVec.x = 5f
}
}
override fun collide(o: Any?): Boolean {
TODO("Not yet implemented")
}
override var frameRate: Int = 30
override var timeToUpdate: Long = System.currentTimeMillis()
override fun update() {
if (!shouldUpdate) {
return
}
timeToUpdate += 1000L / frameRate
}
}
Similarly, we create the ball class. However, we handle some collision The ball class is implemented as follow:
class Ball(context: Context, shapeId: Int?) : Sprite(context, shapeId) {
var ratio: Float = 1f
override fun collide(o: Any?): Boolean {
var endgame = false
if (o is Rect) {
if (location.left <= 0 || location.right >= o.right) {
movVec.x = -movVec.x
if (location.left <= 0) {
location.offset(-location.left, 0f)
} else {
location.offset(o.right - location.right, 0f)
}
}
if (location.top <= 0 || location.bottom >= o.bottom) {
movVec.y = -movVec.y
if (location.top <= 0) {
location.offset(0f, -location.top)
} else {
location.offset(0f, o.bottom - location.bottom)
}
endgame = true
}
}
if (o is Player) {
if (o.location.contains(location.centerX(), location.bottom)) {
movVec.y = -movVec.y
location.offset(0f, o.location.top - location.bottom)
} else if (o.location.contains(location.centerX(), location.top)
) {
movVec.y = -movVec.y
location.offset(0f, o.location.bottom - location.top)
}
}
return endgame
}
override var frameRate: Int = 60
set(value) {
field = value
}
override var timeToUpdate: Long = currentTimeMillis()
set(value) {
field = value
}
init {
this.movVec.set(6f, 8f)
}
override fun update() {
if (shouldUpdate) {
val current = currentTimeMillis()
val delta = current - timeToUpdate;
ratio = 1f + delta.toFloat() * frameRate / 1000f
timeToUpdate = current + 1000L / frameRate
location.offset(
movVec.x * ratio,
movVec.y * ratio
)
}
}
}
The game view
The game view is a surface view and it also implements the game loop interface. Using SurfaceView
will let you have more control over animation but you also to work harder than using normal View
.
class GameView(context: Context?) : SurfaceView(context), SurfaceHolder.Callback, Runnable,
GameLoop {
override var frameRate: Int = 120
override var timeToUpdate: Long = System.currentTimeMillis()
private var mThread: Thread? = null
private var mRunning: Boolean = false
lateinit var mCanvas: Canvas
var mHolder: SurfaceHolder?
private var bounds: Rect = Rect()
set(value) {
field = value
setup()
}
private lateinit var ball: Ball
private lateinit var players: Array<Player>
init {
mHolder = holder
if (mHolder != null) {
mHolder?.addCallback(this)
}
}
private fun setup() {
ball = Ball(this.context, R.drawable.ball)
players = arrayOf(Player(this.context, null), Player(this.context, null))
players[0].location.offsetTo(
bounds.exactCenterX() - players[0].location.width() / 2,
bounds.bottom - players[0].location.height()
)
ball.location.offsetTo(
players[1].location.centerX() - ball.location.centerX(),
players[1].location.bottom
)
}
fun start() {
mRunning = true
mThread = Thread(this)
timeToUpdate = System.currentTimeMillis()
mThread?.start()
}
fun stop() {
mRunning = false
try {
mThread?.join()
} catch (e: InterruptedException) {
}
}
override fun surfaceCreated(p0: SurfaceHolder) {
}
override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {
Log.d(this.toString(), "surface changed")
bounds = Rect(0, 0, p2, p3)
start()
}
override fun surfaceDestroyed(p0: SurfaceHolder) {
stop()
}
override fun run() {
while (mRunning) {
while (shouldUpdate) {
update()
}
render()
}
}
override fun update() {
timeToUpdate = System.currentTimeMillis() + 1000L / frameRate
ball.update()
mRunning = !ball.collide(bounds)
for (p in players) {
p.update()
ball.collide(p)
}
}
override fun render(canvas: Canvas?) {
if (mHolder!!.surface?.isValid == true) {
mCanvas = mHolder!!.lockCanvas()
mCanvas.drawColor(Color.WHITE)
ball.render(mCanvas)
for (p in players) p.render(mCanvas)
mHolder!!.unlockCanvasAndPost(mCanvas)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event!!.y > bounds.exactCenterY()) {
players[0].location.offsetTo(event.x, players[0].location.top)
} else {
players[1].location.offsetTo(event.x, 0f)
}
return true
}
}
The result of Ping the Pong

Video tutorial will be available on Petamind channel.
Useful links
- Source code on Github
- Mobile game tutorials
- Subscribe to Petamind channel
[…] up the article “RE-DEV THE FIRST VIDEO GAME PONG WITH SURFACEVIEW AND GAMELOOP”, here is the video tutorial for another version of the Ping […]