A.I, Data and Software Engineering

Game Frame rate and character animations (part 2)

G

In part 1, we covered the principle of game frame rate and a useful game programming pattern for performance. This part shows the detailed implementation of a game scene with hundreds of moving characters using Android Studio and Kotlin.

Image asset

This game uses a free image from wild-refuge.net.

Xmas girl 3 (URL)

You can download any similar image (4 x 4 frames) and place it in the res/drawable of Android project.

The gameloop interface

For simplicity, we create a minimal game loop. In a real game, you should implement additional methods to synchronize with view life-cycle (Android/iOS).

interface GameLoop {
    fun draw()
    fun update()
}

The Sprite class

This class handle the appearance of a character as well as its movement. It has its own thread to update the walking animation. The declaration of the class and some related fields look like this:

class Sprite : Runnable {
    private val frameInterval = 1000L/4
    var movingDirection = Direction.DOWN
    private var frameToDraw = 0
    private var frameSize: Point
    private var sprite: Bitmap
    private var dst: Rect
    private var src: Rect
    private val pace = 1
    //...
}

There will be two animations in this class, the walking animation and the movement. The walking animation has 4fps meanwhile the moving pace is 1 pixel every update. We continue with a constructor which prepare the bitmap and frame to draw. The draw method draws an src frame to the dst rectangle. The setDst method set the character to a specified location.

   constructor(ctx: Context, id: Int, n_horizontal: Int = 4, n_vertical: Int = 4) {
        sprite = BitmapFactory.decodeResource(ctx.resources, id)
        frameSize = Point(sprite.width / n_horizontal, sprite.height / n_vertical)
        dst = Rect(0, 0, frameSize.x, frameSize.y)
        src = Rect(0, 0, frameSize.x, frameSize.y)
        Thread(this).start()
    }
    fun draw(canvas: Canvas) {
        canvas.drawBitmap(sprite, src, dst, null)
    }
    fun setDst(p: Point){
        this.dst.set(p.x - dst.width()/2, p.y - dst.height()/2, p.x + dst.width()/2, p.y + dst.height()/2)
    }

The function move will be called within the update method of class GameView.

fun move(){
        when (movingDirection) {
            Direction.RIGHT -> {
                dst.right += pace
                dst.left += pace
            }
            Direction.LEFT -> {
                dst.right -= pace
                dst.left -= pace
            }
            Direction.UP -> {
                dst.top -= pace
                dst.bottom -= pace
            }
            Direction.DOWN -> {
                dst.top += pace
                dst.bottom += pace
            }
        }
    }

The GameView class

This class manage all the game objects including sprites, obstacles, etc. Similar to Sprite class, this class has its own thread where we implement a game loop.

class GameView : SurfaceView, Runnable, SurfaceHolder.Callback, GameLoop {
    private var sprites = ArrayList<Sprite>()
    private var mCanvas: Canvas? = null
    private var mHolder: SurfaceHolder? = null
    private var mThread: Thread
    private var myGestureDetector: GestureDetector
    val FRAME_RATE = 60
    val MS_PER_SECOND = 1000L / FRAME_RATE
    constructor(ctx: Context) : super(ctx)
    constructor(context: Context?, attributeSet: AttributeSet) : super(context, attributeSet)
    init {
        mHolder = holder
        myGestureDetector = GestureDetector(context, MyGestureListener())
        mHolder?.addCallback(this)
        mThread = Thread(this)
        mThread.start()
    }
    //...
}

We implement the update and draw methods from the game loop interface to update and draw all sprites stored in an array list.

override fun draw() {
        mCanvas?.drawColor(Color.WHITE)
        for (sprite in sprites)
            sprite.draw(mCanvas!!)
}
override fun update() {
        for (sprite in sprites)
            sprite.move()
}

Next, we implement the game loop as mentioned in part 1.

override fun run() {
        var previous = System.currentTimeMillis()
        var lag = 0.0
        while (true) {
            val current = System.currentTimeMillis()
            val elapsed = current - previous
            previous = current.toLong()
            lag += elapsed
            if (mHolder?.surface?.isValid!!) {
                mCanvas = mHolder?.lockCanvas()
                synchronized(sprites)
                {
                    while (lag >= MS_PER_SECOND) {
                        update()
                        lag -= MS_PER_SECOND;
                    }
                    draw()
                }
                mHolder?.unlockCanvasAndPost(mCanvas)
            }
        }
}

There are synchronized blocks to avoid the concurrentmodificationexception as we add new sprites with taps.

We also added some code for gesture detection. A single tap will place a new sprite on the screen and a double-tap will change all sprites’ moving direction.

And the animation result

Hundreds of sprites walking with no lag - 60FPS
Hundreds of sprites walking with no lag

To sum up

This article demonstrates the implementation of a game loop pattern for enhancing the animations of a video game. You can find the source code below.

Source code: Github

Add 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