A.I, Data and Software Engineering

Smooth nature snake game – re-dev with Kotlin (part 1)

S

When my students proposed a snake game for their projects, I thought it is kind of boring. They are all the same old ideas. Nevertheless, how you want it to be do matter as the game mechanics and animations can be much varied.

snake game
snake game

Why I redev the snake game?

Did I try to reinvent the wheel – Not quite! What I include in this post addresses several challenges to simulate a fully smooth nature snake. Below, I detail the motivation for this small project:

Firstly, there are tons of tutorials out there either on YouTube or some blogs. However, many have low quality. Namely, most of the tutorials are for beginners. The solutions are quick and dirty. They show how to complete the game without considering many important aspects of the software design.

Secondly, the cool movement pattern (slithering) of nature snakes is not covered in any tutorial that I found. Many snakes are drawn in blocks and the movements are based on grids. Some game developers, of course, really want to secure their algorithms from their rivals. But I found it is not a big deal for this, just some mathematic equations.

Thirdly, I wanted to use pure Kotlin for this project as I felt bored with Java. Also, it is now official for Android development. There is no game engine involved. You may have a hard time to find a similar reference, especially it is written in this new programming language (read more for the Basics).

The general class design

Instead of placing all code in one or two files, we will separate game objects to different classes, i.e., menu activity, game activity, game view, obstacles, worm, game state, etc.

Game Scene(Game View) class contains several game objects, e.g. sprites or obstacles, dashboard. The game-loop pattern is used to loop through different events. Below is a simplified class structure

class diagram
class diagram

Game structure and camera view

Regarding game design pattern, we adopt game-loop with top-down third-person camera perspective.

Top-down camera view (Src: gamasutra)

The game loop allows the game to run smoothly regardless of a user’s input or lack thereof. There are several versions of game-loop. The most simple game loop looks like this:

while (playing)
{
  process Input
  update
  render
}

A simplified version of a game-loop for action game can be like this:

while( user doesn't exit )
  check for user input
  run AI
  move enemies
  resolve collisions
  draw graphics
  play sounds
end while

I created game-loop interface as follow:

interface GameLoop {
    fun start()
    fun pause()
    fun resume()
    fun update()
    fun draw()
    fun stop()
}

The Game View

The game view implements several interfaces as shown in the diagram. It will run on 30 FPS (frames per second).

class GameView : SurfaceView, SurfaceHolder.Callback, GameLoop, Runnable {
    private var mPaint: Paint? = null
    private var mContext: Context? = null
    private var mHolder: SurfaceHolder? = null
    private var mWidth: Int = 0
    private var mHeight: Int = 0
    private var mThread: Thread? = null
    private var mCanvas: Canvas? = null
    private var mRunning: Boolean = false
    private var worm : Worm? = null
    private var FPS = 30
        set(value) {field = value}
    private var timeToUpdate = 0L
     //...
}

We use game-loop in the run() method with a smart update check.

  override fun run() {
        while (mRunning) {
            if(timeToUpdate()) {
                update()
                draw()
                Thread.sleep(16)
            }
        }
    }

To improve the performance of drawing on SurfaceView with thread, we use surface holder lockCanvas() and unlockCanvasAndPost().

  override fun draw() {
        if (mHolder?.surface?.isValid == true) {
            mCanvas = mHolder?.lockCanvas()
            mCanvas?.drawColor(Color.WHITE)
            worm?.draw(mCanvas)
            mHolder?.unlockCanvasAndPost(mCanvas)
        }
    }

Draw the shape of the snake

In the implementation, I called it “WORM” instead of “snake”. I used purely points and path for drawing the shape of the snake. The head of the snake has a diamond shape, the body of the snake is just the connections of points.

The snake shape (not a sperm)

To make it smooth, I divided the snake into 50 parts (BODY_SEGMENTS) and calculate the points along the body path of the snake. Also, I need to keep the movement of the snake constant by logging its body ratio to the “worm_pace

//vars for body part
private var bodyPoints = ArrayList<PointF>()
private val BODY_SEGMENTS = 50
private val head = RectF(-15f, 15f, 15f, -15f)
private var mHeadPoints: Array<PointF>? = null
//init
init {
       ...
       val pm = PathMeasure(mPath, false)
       for (f in 0..BODY_SEGMENTS) {
            val aBodyPoint = FloatArray(2)
            pm.getPosTan(pm.length * f / BODY_SEGMENTS, aBodyPoint, null)
            val aBodyPointF = PointF(aBodyPoint[0], aBodyPoint[1])
            bodyPoints.add(aBodyPointF)
        }
        worm_pace = pm.length / BODY_SEGMENTS
}

The draw of the head and the body is easy if we already have body points ready. The code snippet below shows drawing of the head, body, and its nose.

fun draw(c: Canvas?) {
    ...
    //Draw head
    if (p1 != null && p2 != null && p3 != null && p4 != null) {
            mPath.moveTo(bodyPoints[0].x + p1.x, bodyPoints[0].y + p1.y)
            mPath.lineTo(bodyPoints[0].x + p2.x, bodyPoints[0].y + p2.y)
            mPath.lineTo(bodyPoints[0].x + p3.x, bodyPoints[0].y + p3.y)
            mPath.lineTo(bodyPoints[0].x + p4.x, bodyPoints[0].y + p4.y)
            mPath.close()
        }
        mPaint.style = Paint.Style.FILL_AND_STROKE
        mPaint.setColor(Color.BLACK)
        c?.drawPath(mPath, mPaint)
        //draw body
        mPaint.style = Paint.Style.STROKE
        mPaint.setColor(Color.BLACK)
        mPath.moveTo(bodyPoints[0].x, bodyPoints[0].y)
        for (point in bodyPoints) {
            mPath.lineTo(point.x, point.y)
        }
        c?.drawPath(mPath, mPaint)
        //draw nose
        mPaint.style = Paint.Style.FILL_AND_STROKE
        val nose = PointF(bodyPoints[0].x + p1!!.x, bodyPoints[0].y + p1.y)
        mPaint.setColor(Color.GREEN)
        c?.drawPoint(
            nose.x, nose.y,
            mPaint
        )
}

In the code, the “bodyPoints[0]” is the head’s centred point.

To be continued…

Please follow part 2, for the complex movement of the snake as demonstrated below.

serpentine movement
serpentine movement

Source code: Github

1 comment

💬

  • Simply desire to say your article is as astounding. The clarity in your post is simply spectacular and that i can assume you’re a professional in this subject.
    Fine together with your permission let me to seize your feed to
    stay up to date with coming near near post.
    Thanks one million and please keep up the enjoyable
    work.

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