Ich meine zu behaupten: „Jeder mag Computerspiele!“ Ok, zugegeben nicht jeder, aber die meisten von uns, nicht wahr?
Viele von uns sind mit Computerspielen aufgewachsen und genauso wie ich, haben bestimmt einige von euch auch daran gedacht, ein Spiel selbst zu programmieren. Das ist heutzutage gar nicht so schwer, solange man nicht gleich das nächste AAA-Spiel entwickeln möchte.
Es gibt unzählige Frameworks zur Entwicklung von Spielen und Multimedia Anwendungen. Manche Frameworks haben begrenzte Möglichkeiten, sind dafür aber einfacher, wenn es um die Entwicklung geht. Bei anderen wiederum stehen einem alle Türen und Tore offen, welches aber mit teilweise (extrem) hoher Komplexität daher kommt.
Ich möchte hier ein einfaches Open Source Framework vorstellen, genannt „KorGE“. Auf der KorGE Webseite steht: „KorGE Game Engine is a Kotlin Open Source modern Game Engine created in Kotlin designed to be extremely portable and really enjoyable to use“. Das „really enjoyable“ kann ich an dieser Stelle schon mal bestätigen. Auf der „GitHub-Seite“ sind einige interessante Features gelistet, wie z.B. Hot Reloading, Multiplatform, 100 % Kotlin. Diverse kleine Beispiele findet ihr auf der KorGE-Webseite im Bereich „KorGE in Action“. Hier könnt ihr euch durchklicken und ein paar Features entdecken.
Ich möchte hier ein kleines Memory Spiel entwickeln und zeigen, dass die Entwicklung eines Spieles nicht schwer sein muss.
Was benötigen wir, um loslegen zu können?
Als Erstes eine Entwicklungsumgebung. Die IntelliJ Community Edition ist vollkommen ausreichend für dieses Vorhaben. Ich hatte hier für dieses Beispiel die Version 2023.2.4 verwendet. Desweiteren braucht ihr das KorGE IntelliJ Plugin. Dieses einfach über die „Settings/Plugins“ innerhalb IntelliJ installieren. Meine verwendete Version war die zu diesem Zeitpunkt aktuellste Version 4.0.4. Zusätzlich benötigen wir noch 20 quadratische Bilder in der minimalen Auflösung von 100 × 100 Pixeln. Die Bilder für das Spiel habe ich von Game-Icons geladen (License CC BY 3.0 Unported).
Los gehts!
Allerdings noch nicht mit Memory. Ihr werdet die nächsten Schritte jedoch später benötigen, daher diese nicht überspringen.
Startet mit File/New/Projekt ein neues IntelliJ Projekt und wählt bei „Generators“ den Eintrag „KorGE Game“ aus. Bei Templates das Starterkit „Hello World“. Über den „Next“-Button erreicht man den Dialog für den Projektnamen (hier am besten gleich MemoryGame eingeben, wir werden wie gesagt das Projekt später wiederverwenden.) und die Projekt-Lokation. Nun über den „Create“-Button das Projekt erstellen. Direkt danach startet der Gradle build und lädt alle benötigten Depencencies herunter.
KorGE ist Multiplatform fähig und initial sind bereits einige „Targets“ vorkonfiguriert. Als Target wird die Zielplatform bezeichnet, für die das Spiel kompiliert und gebaut werden soll. Für dieses Beispiel brauchen wir die beiden „targetJvm()“ und „targetJs()“ und kommentieren alle anderen in der „build.gradle.kts“ aus. Dies verhindert, dass Gradle derzeit nicht benötigte Dependencies herunterlädt. Dadurch verschwindet auch die Fehlermeldung „Can’t find android sdk (ANDROID_HOME environment not set and Android SDK not found in standard locations)“.
Das ausgewählte „Hello World“-Template beinhaltet bereits eine kleine Demoanwendung, welche unter „src/commonMain/kotlin/main.kt“ zu finden ist. Einmalig starten können wir die Anwendung über den Gradle Task namens „runJvm“. Nichts ausgefallenes, oder? Aber immerhin ein hin und her schwingendes KorGE Logo, mit ein paar wenigen Zeilen Code.
Wo bleibt das Memory Spiel?
Keine Panik, es geht gleich los. Im Bereich „Los gehts!“ haben wir bereits ein Projekt angelegt, welches uns als Einstiegspunkt für das Memory-Spiel dienen wird.
Aufräumen
Zu Beginn ersetzt ihr den deprecated Aufruf der Methode „changeTo()“, löscht den Inhalt der Methode „fun SContainer.sceneMain()“ und benennt die Klasse „MyScene“ in „MemoryScene“ um. Der Code sieht nun folgendermaßen aussehen:
suspend fun main() = Korge(windowSize = Size(512, 512), backgroundColor = Colors["#2b2b2b"]) {
val sceneContainer = sceneContainer()
sceneContainer.changeTo { MemoryScene() }
}
class MemoryScene : Scene() {
override suspend fun SContainer.sceneMain() {
}
}
Vorbereitungen
Legt die quadratischen Bilder in ein neues Verzeichnis mit dem Namen „src/commonMain/resources/images/“ ab.
Definiert nun ein paar Konstanten noch vor der „main()“-Methode. Diese werden uns im weiteren Verlauf behilflich sein, „Magic Numbers“ im Code loszuwerden.
private const val WINDOW_WIDTH = 940
private const val WINDOW_HEIGHT = 600
private const val IMAGE_WIDTH_AND_HEIGHT = 100
private const val IMAGE_SPACING = 10
private const val IMAGES_PER_ROW = 8
private const val X_POSITION_START = 30
private const val Y_POSITION_START = 30
private val HIDE_IMAGES_AFTER_DURATION = 1.seconds
private const val RESTART_BUTTON_WIDTH = 75
private const val RESTART_BUTTON_HEIGHT = 25
private val IMAGE_FILE_NAMES = listOf(
"arrow-cursor.png",
"cancel.png",
"click.png",
<HIER ALLE HERUNTERGELADENEN BILDER EINFÜGEN>,
...,
)
Für das Ein- und Ausblenden der Bilder werden folgende beiden Extension Functions benötigt.
private fun Image.hide() {
this.alpha = 0.0
}
private fun Image.reveal() {
this.alpha = 1.0
}
Jetzt folgt der komplette Code der main-Methode mit Kommentaren an den interessanten Stellen zur weiteren Erklärung.
suspend fun main() = Korge(
title = "Memory",
windowSize = Size(WINDOW_WIDTH, WINDOW_HEIGHT),
backgroundColor = Colors["#1b1b1b"]
) {
val sceneContainer = sceneContainer()
sceneContainer.changeTo { MemoryScene() }
}
class MemoryScene : Scene() {
// Listen zum Speichern des Spielzustandes
val revealedIds = mutableListOf<UUID>() // Welche Bildpaare wurden bereits aufgedeckt
val openedIds = mutableListOf<UUID>() // Welche Bilder, dargestellt durch UUID's sind gerade aufgedeckt
val openedImages = mutableListOf<Image>() // Welche Bilder, dargestellt durch ein Image sind gerade aufgedeckt
// Hilfsmethode: Überprüft ob ein bestimmtes Bildpaar bereits aufgedeckt wurde
fun isAlreadyRevealed(id: UUID): Boolean = revealedIds.contains(id)
// Hilfsmethode: Verdeckt die im Spielverlauf aufgedeckten Bilder nach einer festgelegten Zeitdauer
suspend fun hideOpenedImagesAfterDelay() {
delay(HIDE_IMAGES_AFTER_DURATION)
openedImages.forEach { it.hide() }
openedImages.clear()
openedIds.clear()
}
// Hilfsmethode: Überprüft ob die identischen Bilder aufgedeckt wurden
fun isSameImageOpened(): Boolean = openedIds[0] == openedIds[1]
override suspend fun SContainer.sceneMain() {
initGame()
}
// Methode zum Initialisieren bzw. Neustarten des Spieles
private suspend fun Container.initGame() {
val container = this
// Button zum Neustarten des Spiels
uiButton {
size = Size(RESTART_BUTTON_WIDTH, RESTART_BUTTON_HEIGHT)
text = "Restart"
onClick {
container.removeChildren()
container.initGame()
}
}
// Initialisieren der Listen für den Spielzustand
revealedIds.clear()
openedIds.clear()
openedImages.clear()
// Startpositionen festlegen
var x = X_POSITION_START
var y = Y_POSITION_START
IMAGE_FILE_NAMES.flatMap {
val id = UUID.randomUUID() // Erstellen einer eindeutigen ID für ein Bildpaar
val bitmap = resourcesVfs["images/$it"].readBitmap() // Das gerade aktuelle Bild aus dem "/images" Verzeichnis laden
(1..2).map { _ ->
// image(...) ist eines der Hilfsmethoden um auf einfache Art und Weise ein Bild darzustellen
image(bitmap) {
size(IMAGE_WIDTH_AND_HEIGHT, IMAGE_WIDTH_AND_HEIGHT) // Bildgröße festlegen
position(-100, -100) // Positionieren des Bildes im nicht sichtbarem Bereich
onClick { _ -> // Klick Listener registrieren
if (!isAlreadyRevealed(id)) {
if (openedIds.size < 2) {
reveal()
openedIds.add(id)
openedImages.add(this)
}
if (openedIds.size == 2) {
if (isSameImageOpened()) {
openedIds.clear()
openedImages.clear()
revealedIds.add(id)
} else {
hideOpenedImagesAfterDelay()
}
}
}
}
hide() // Initial das Bild verstecken
}
}
}
.shuffled() // Mischen der Bilder
.forEachIndexed { index, image ->
// Platzhalter mittels Hilfsmethode erstellen
fastRoundRect(
size = Size(IMAGE_WIDTH_AND_HEIGHT, IMAGE_WIDTH_AND_HEIGHT),
color = Colors.BLACK
).position(x, y).zIndex = -1.0
// Bild positionieren
image.position(x, y)
// Berechnen der nächsten x und y Positionen
x += IMAGE_WIDTH_AND_HEIGHT + IMAGE_SPACING
if ((index + 1) % IMAGES_PER_ROW == 0) {
x = X_POSITION_START
y += IMAGE_WIDTH_AND_HEIGHT + IMAGE_SPACING
}
}
}
}
Das war es auch schon. Gratulation, ihr habt somit euer erstes KorGE Spiel in unter 150 Codezeilen implementiert. Nun könnt ihr das Spiel mit dem Gradle Task „runJvm“ starten und die erste Runde Memory spielen.
Hier seht ihr mein Ergebnis:
Den kompletten Code findet ihr auch auf meiner GitHub Seite.
An dieser Stelle noch ein paar Vorteile bzw. Nachteile, die mir während der Entwicklung aufgefallen sind.
Vorteile
- Schnell aufgesetzt und Einsatzbereit
- Einfach zu erlernen und dadurch schnelle Ergebnisse
- Mit wenig Code kann man viel erreichen
- Unterstützung für Websockets zur Entwicklung von Multiplayer Applikationen
- Engine ist Multiplatform fähig und laut Dokumentation lauffähig auf Android, IOS, Web und auf dem Desktop. Getestet habe ich nur die Desktop und die Web Versionen.
Nachteile
- Dokumentation nicht auf dem neuesten Stand
- Probleme mit dem HotReload. Hat sich bei mir nach dem Stoppen aufgehängt.
Fazit
Die KorGE Game Engine erlaubt es in kurzer Zeit ein Computerspiel zu entwickeln, ohne dass man sich zu tief in die Grafikprogrammierung einarbeiten muss. Das Framework bietet viele Features und die API ist ausreichend mit nützlichen Funktionen ausgestattet. Die Möglichkeit, das Spiel bzw. die Anwendung im Anschluss auf verschiedene Zielplattformen zu exportieren, ist sicherlich ein Pluspunkt.
Meine Meinung zu KorGE
Ich hatte viel Spaß mit KorGE, vor allem weil ich in meiner bevorzugten Sprache Kotlin entwickeln durfte. Die API finde ich gelungen und leicht zu verstehen. Ich kann mir vorstellen, dass Lernspiele oder sonstige kleinere Projekte gut, einfach und schnell umzusetzen sind. Bestimmt auch einige etwas komplexere.
Wenn du Kotlin magst und auch immer schon mal ein Spiel entwickeln wolltest, dann wirst auch du schnelle Ergebnisse erzielen und viel Freude haben.
Ich hoffe, ich konnte einige von euch dazu animieren, selbst ein kleines oder auch größeres Projekt zu starten. Und jetzt viel Spaß mit der Game Engine.