Wie entwickelt man ein Computerspiel in unter einer Stunde?

Die Entwicklung eines Computerspieles muss nicht schwer sein. Wie das genau mit der Open Source Game Engine KorGE funktioniert, erfahrt Ihr in diesem Blog.

, Balazs Baltringer

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:

Memory2

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.

Allgemeine Anfrage

Wir freuen uns darauf, Ihre Herausforderungen zusammen in Angriff zu nehmen und über passende Lösungsansätze zu sprechen. Kontaktieren Sie uns – und erhalten Sie maßgeschneiderte Lösungen für Ihr Unternehmen. Wir freuen uns auf Ihre Kontaktanfrage!

Jetzt Kontakt aufnehmen