Kotlin Player Skeleton

This is the documentation for the Kotlin Player Skeleton. It is a good starting point for Kotlin developers. You find it here. If you want to use it, fork this repo to your own repo, and start working.

Table of Contents

Player Skeleton Kotlin

This is the Documentation of the Player Skeleton for the microservice dungeon, which is written in plain Kotlin. You can use this player as basis for your own player. It already implemented the basic functionality of a player:

  • Creating(Dev Mode), Registering, Joining, Starting, Ending and Persisting Games
  • Listening and Logging for incoming Events
  • Parsing basic Events into Kotlin classes
  • Handle Incoming events by calling the Handler of the specific event class
  • Domain Primitives you can use to build your player
  • Tests for the basic functionality

Requirements:

  • Kotlin 1.7

Preparation

To use this skeleton as the base for your player development, you need to accomplish the following steps.

First, fork this repository and create a new repository under the Player Teams subgroup which is named after your desired player name, for example player-constantine. Now you need to add your player-name to a few files. The required places are marked using TODO comments. Update the files in helm-chart/, pom.xml, src/main/kotlin/config/Appconfig.kt and .gitlab-ci.yml, wherever you see a TODO.

Configuration

The player can be configured using environment variables or by changing the Default values provided in the src/main/kotlin/config/Appconfig

Environment Variable Default
DATA_ENDPOINT_PORT 8090
RABBITMQ_HOST localhost
RABBITMQ_PORT 5672
RABBITMQ_USERNAME admin
RABBITMQ_PASSWORD admin
GAME_HOST http://localhost:8080
PLAYER_NAME player-skeleton-kotlin
PLAYER_EMAIL player-skeleton-kotlin@example.com
KTOR_LOG_LEVEL Info
SLF4J_LOG_LEVEL Info
DEV true

Dev mode

The dev mode is useful when you run the player LOCALLY as it saves you time spinning up games to test your player. On Startup it will stop all existing games and creates a new one, so you do not have to do it by yourself! It will also wait until the NUMBER_OF_PLAYERS constant in src/main/kotlin/dev/GameAutoInitializer.kt have joined the game and start it after. Additionally it makes sure that when you close your Application to stop all existing Games.

How does the player work

  • player listens to game events
  • parses them into event classes
  • each event class has an eventHandler which is executed upon receiving the event

Serialization

What is Serialization?

At its core, serialization is the process of converting an object or data structure into a format that can be easily stored or transmitted, and later reconstructed. Think of it as “packing” your data to send or store it, and “unpacking” it to use it again.

Serialization in Event Driven Context

In event-driven systems, especially distributed ones, events often need to be transmitted between different parts of the system, or even between different systems. This is where serialization comes into play.

Process

1. Event Creation

When specific actions occur, for example you’re sending a buy robot Command an event is created in one of the core Services.

2. Event Serialization

The event is then serialized, by one of the core services, into a format that can be transmitted, in our case JSON.

3. Event Transmission

The event is being transmitted to Kafka(Message Broker) which publishes it to the specific Player Queues via RabbitMQ( Message Broker).

4. Event Deserialization

The player receives the event on the RabbitMQ Queue he listens to and deserializes it into a class. In our case, we use Kotlinx Serialization to deserialize the JSON into a Kotlin class.

5. Event Handling

The player then handles the event by executing the Handler of the event class.

Why we chose Kotlinx Serialization over Jackson

While both Kotlinx Serialization and Jackson are powerful serialization libraries in the Kotlin ecosystem, there are a few reasons why we leaned towards Kotlinx Serialization for our project:

  1. Kotlin Native: Kotlinx Serialization is Kotlin’s native serialization library. This means it’s inherently more integrated with Kotlin’s language features, providing a better experience when working exclusively with Kotlin.
  2. Performance: Benchmarks have shown that Kotlinx Serialization can have performance benefits over Jackson in certain use-cases, especially when it comes to serializing and deserializing Kotlin data classes.
  3. Immutability: Kotlinx Serialization works seamlessly with Kotlin’s data classes and respects their immutability.

How to use Kotlinx Serialization

Basics

Converting an object to JSON is called serialization, and converting JSON to an object is called deserialization. Kotlinx Serialization provides a simple and easy to use API for both of these processes.

@Serializable
data class Person(val name: String, val age: Int)

fun main() {
    val person = Person("John", 25)
    val json = Json.encodeToString(person)
    println(json) // {"name":"John","age":25}
    val person2 = Json.decodeFromString<Person>(json)
    println(person2) // Person(name=John, age=25)
}

You have to annotate classes you want to serialize/deserialize with @Serializable.

Example:

@file:UseSerializers(UUIDSerializer::class)

package core.eventlistener.concreteevents.game

@Serializable
@SerialName(EventType.GAME_STATUS)
data class GameStatusEvent(
    override val header: GameEventHeader,
    val payload: GameStatusPayload
) : GameEvent()

@Serializable
data class GameStatusPayload(
    val gameId: UUID,
    val gameworldId: UUID? = null,
    val status: GameStatus
)

You can also annotate properties with @SerialName in case the property name in the JSON differs from the property name in the class. Example:

@Serializable
data class GameEventHeader(
    val type: String,
    val eventId: UUID,
    @SerialName("kafka-topic")
    val kafkaTopic: String = "",
    val transactionId: UUID,
    val version: Int,
    val playerId: String?,
    val timestamp: Instant
)

Here the property kafka-topic in the JSON is mapped to the property kafkaTopic in our class.

As you can see in the first example, we also have to register a custom serializer for non-primitive types like UUID. This is done by annotating the package with @file:UseSerializers(UUIDSerializer::class).

object UUIDSerializer : KSerializer<UUID> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: UUID) {
        encoder.encodeString(value.toString())
    }

    override fun deserialize(decoder: Decoder): UUID {
        return UUID.fromString(decoder.decodeString())
    }
}

Important

In order for KotlinX Serialization to know which event class to deserialize into from the JSON, you have to provide @SerialName(EventType.EVENT_TYPE_STRING) in the class annotation of the event.

The src/main/kotlin/core/eventlistener/EventType.kt Object contains all event types as strings, which are used as the @SerialName value above Event Classes.

So when you add a new event class, you have to add the event type string to the EventType Object, and annotate the class with @SerialName(EventType.EVENT_TYPE_STRING) and @Serializable.

Further information can be found in the Kotlinx Serialization Documentation

Dependency Injection

What is Dependency Injection?

Dependency Injection (DI) is a design pattern where objects are not responsible for creating their dependencies. Instead, dependencies are “injected” into them, usually by a DI framework or container. This promotes the SOLID principles, especially the Dependency Inversion Principle, making the codebase more modular, testable, and maintainable.

Why we chose Koin

While there are several DI frameworks available for Kotlin, Koin stood out for various reasons:

  • Kotlin-centric: Koin is a Kotlin-first DI framework. It leverages Kotlin’s features to the fullest, leading to concise and idiomatic code.
  • Simplicity: Koin is intuitive and decluttered, making it easy for new users to use and set up.
  • No Reflection: Unlike some other DI frameworks, Koin doesn’t use reflection. This leads to better performance and predictability.
  • Flexibility: Koin is not just for Android; it’s versatile enough to be used in any Kotlin project.
  • Testability: Koin is designed to be testable, making it easy to write unit tests for your code.

Other Options could’ve been

  • Dagger Is more performant than Koin, but also more complex to use.
  • Kodein Is more performant than Koin but also more complex to use.
  • Spring Is a huge framework, and we do not need all of its features. Spring is not Kotlin native and therefore does not support all Kotlin features.

How to use Koin

In koin you let classes implement the KoinComponent interface to be able to use the by inject() delegate.

class SomeClass() : KoinComponent {
    private val someService: SomeService by inject()
}

You can also use constructor injection

class SomeClass(private val someService: SomeService) : KoinComponent {
}

SomeService is a class that implements the KoinComponent interface and is defined in a module. For this project all the Koin modules are defined in the src/main/kotlin/di/appmodule.kt file.

All classes that should be automatically created at runtime have to be defined inside of that module. You have to define the class as a single or factory depending on your needs. A single is a class that is created once and then reused for every injection. A factory is a class that is created every time it is injected.

For further information check out the Koin Quickstart Guide

Difference between Koin and Spring

  • Koin is a lightweight DI framework, while Spring is a full-fledged framework.
  • Koin is Kotlin-native, while Spring is Java-based.
  • Koin is more lightweight and easier to use than Spring.
  • Koin is more performant than Spring.
  • Koin doesn’t use reflection, proxies and other stuff, while Spring does(so called Spring Magic). Which makes Koin more predictable and easier to debug.

How to start Developing your Player?

  • Get familiar with how the Game works. Check out the Further Reading Section for more information.
  • Setup Local Dev Environment to run & test games locally. Check out the Local Dev Environment Setup for more information.
  • Add Missing Event Classes that you want to handle
  • Add Missing Event Handlers for the Event Classes you want to handle
  • Add Missing Domain Entities (or write Tests first, in case you do TDD)
  • Add Missing Domain Services (or write Tests first, in case you do TDD)
  • Think about what you want to do / are able to do with the data you receive via the events and base the strategy for your robots on that, or just do random stuff =D
  • Make sure to call commands for your robots, or your player does nothing :D

Some Words of Advice

General Recommendations

  • Do not try to implement everything at once. Start small and build up from there.
  • Focus on the core functionality of your player first, and then add more features.
  • Write tests for your code. This will make it easier to debug and maintain.

Performance…

Some Events grow with the amount of robots there are in the game. So what people often run into is that their code is too slow to handle events in time. You can see what parts of your code could be potential bottlenecks with IntelliJ Profiler. IntelliJ Profiler Guide

You could think about using Coroutines to handle multiple events at the same time to tackle this problem, but be cautious as it adds complexity to your code and Race Conditions can easily happen.

Debugging…

I would advice you to implement the Logic to serve required data for the Map Visualization src/main/kotlin/dataendpoint early on in development because the visualization is going to help you to understand what is going on in the game. You can find more information about the Map Visualization in the Further Reading Section.

Before Deployments (Near end of project)

Deployments are probably the last thing you have to do before the project is finished. Make sure to adjust the values.yaml in the helm-chart folder to the projects needs! Speak with the devops team if you run into issues.

Further Reading

Authors

Last modified February 4, 2025: fix go & npm dependencies (8ff1fa0)