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.
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 Ring
s. A Ring contains four Block
s 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 isFull
of 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