Kotlin Player Skeleton
Hint
This following part of the documentation is embedded from the README of the player-skeleton-kotlin repository. If you experience any issues on this page, just visit the repository directly.Table of Contents
- Player Skeleton Kotlin
- Requirements:
- Preparation
- Configuration
- How does the player work
- Serialization
- Dependency Injection
- How to start Developing your Player
- Some Words of Advice
- Before Deployments(Near end of project)
- Further Reading
- Authors
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:
- 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.
- 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.
- 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
- Kotlin Player Skeleton Repository
- Learning About Asynchronous Communication
- Kotlinx Serialization Documentation
- Koin Documentation
- Kotlin Documentation
- Kotlin Coroutines Documentation