A.I, Data and Software Engineering

re-dev the first video game pong with SurfaceView and gameloop

r
Pong game, android Kotlin
Pong game (prototype)

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 and shouldUpdate variables are to determine it is time to update.
  • The render and update 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

The result

Video tutorial will be available on Petamind channel.

Useful links

1 comment

A.I, Data and Software Engineering

PetaMinds focuses on developing the coolest topics in data science, A.I, and programming, and make them so digestible for everyone to learn and create amazing applications in a short time.

Categories