Skip to content

TobseF/HitKlack_LibGDX

Repository files navigation

Hit Klack - LibGDX

Retro console game remake of Mephisto Hit Klack

📢 Check out the NEW HitKlack Repo based on Korge framework.

This is a just for fun work in progress game remake. It's based on a German console, the Hit Klack from Mephisto. It runs on multiple platforms: Android, Windows, Linux, and Mac. For me it's a way to test the latest features of Kotlin, a programming language by Jetbrains. It uses the LibGDX Java game development framework.

Kotlin Coding Hitlist

Here is a list of nice noticeable Kotlin features which are already in use for HitKlack. Some of them are only available in the newest EAP of Kotlin 1.1. For a better understanding it may be help to follow direct into the sources.

Model cheatsheet: GameField consist out of ten Rings. A Ring contains four Blocks which each can hold one Stone.

fun Orientation.toControl(): Controller.Control =
  when (this) {
      Orientation.Left -> Controller.Control.Left
      Orientation.Right -> Controller.Control.Right
      Orientation.Up -> Controller.Control.Top
      Orientation.Down -> Controller.Control.Bottom
  }

This adds the toControl() function to the Orientation class as extension. It maps an Orientation enum (Left,Right,Up,Down) to the Orientation enum (Left,Right,Top,Bottom). So we can write val control = block.orientation.toControl().

private fun firstFull() = gameField.find(Ring::isFull)

private fun renderField() {
	game.getStones().forEach(renderer::renderStone)
}

private fun getTouchPointers() = (0..6).filter(input::isTouched)
	.map { viewport.unproject(TouchPoint(it)) }
	.filter { !it.isZero }

This demonstrates two different types of method references. The Ring::isFull points to the isFullof the Iterable<Ring>. Hopefuly Ring can be omitted due a smart compiler. In the second example the renderer::renderStone points a function of the renderer. That’s called a bound reference which points to it’s reviever. The last example, the getTouchPointers(), shows the combination of ranges, lamdas, Kotlins stream API and method references. It returns a list of all unprojected touchpoints which are not zero (0, 0).

class Controller(point: Point, val viewport: Viewport) : InputProcessor by InputAdapter(), Point by point {
    //..
}

This class implements the InputProcessor and the Point interface. Callers will be delegated to the implementation declared with the by statement. In pure Java I had to determine one parent because only a simple 1:1 inheritance is possible. And tying to implement both would result in a bunch of boilerplate code. With the power of delegations this predicament can be solved with a minimum and clear syntax.

data class Resolution(var width: Float, var height: Float) {
	fun getCenter() = Point2D(width / 2, height / 2)
}

Kotlin automatically generates the equals(), hashCode(), toString() and even a copy() for our data class. How kindly, isn’t it? GitHub will miss hundreds of hand written boilerplate code in Kotlin project.

typealias TexturePair = Pair<TextureRegion, TextureRegion>

private val red: TexturePair
private val blue: TexturePair
private val yellow: TexturePair
private val green: TexturePair

init {
  green = Pair(buttons[0], buttons[1])
  blue = Pair(buttons[2], buttons[3])
  yellow = Pair(buttons[4], buttons[5])
  red = Pair(buttons[6], buttons[7])
}

The Pair is used as alias of Pair<TextureRegion, TextureRegion>. So it’s possible to define a scope where a succinct type name can be used. This reduces boilerplate and can enhance the readability.

fun render(controller: Controller) {
  val radius = width / 2F
  fun draw(textureRegion: TextureRegion, touchArea: Controller.TouchArea) {
  	val pos = touchArea.rect.getCenter(Vector2()).sub(radius, radius)
  	batch.draw(textureRegion, pos.x, pos.y)
  }

  fun button(button: TexturePair, control: Control): TextureRegion {
  	return if (controller.isPressed(control)) button.second else button.first
  }

  draw(button(red, Control.Left), controller.touchAreas[0])
  draw(button(blue, Control.Right), controller.touchAreas[1])
  draw(button(yellow, Control.Bottom), controller.touchAreas[3])
  draw(button(green, Control.Top), controller.touchAreas[2])
}

Inline functions give us the freedom to place functions in the scope where they're really needed: Often inside another function. This offers a better way for a meaningful encapsulation.

override fun toString() =  "Block [$row ${orientation.char()} $stone]"

Get rid of the awful and illegible String concatenation and use it’s template feature. Besides that you can see that’s possible to write short funtions into one line without any brackets.

val next = geameField[block.row - 1][block.orientation]

It looks like an elagant array acces. field[block.row - 1] returns a Ring and the [block.orientation] retunrs the Array<Block>. Possible doe the two methods:

operator fun get(orientation: Orientation) = //[...]
operator fun get(index: Int) = //[...]
private var activeRing: Ring? = null

private fun resetRing() {
	activeRing?.reset()
}

private fun randomFreeOrientation(): Orientation = activeRing?.randomFreeSide() ?: Orientation.random()

With the ? we can safly call reset(). Trying to acces without this check is even forbitten by the compiler: Only safe (?.) or non-null asserted (!!.) calls are allowed on nullable reciever of type Ring?

The second method randomFreeOrientation() demonstrates the use case for the Elvis Operator (?:). The right side will be only elevated and returned, if the left side was null.

With this null safety, it’s clear if a Kotlin type can be nullable. And the compiler does its best to block us from doing unsafe operations. So welcome to world in peace without nullpointer exceptions. NPE RIP