Die Entwicklungswerkzeuge

Versuch nicht, das Rad (komplett) neu zu erfinden!

Das offizielle Logo des Love2d Frameworks

Entscheidungen

Im Artikel über den Anfang hast du vielleicht gelesen, dass ich beschlossen hatte, ein Spiel zu machen.

Es gibt enorm viele Möglichkeiten, so etwas anzugehen, und jede Situation verlangt einen anderen Ansatz. Das Einzige, was ich zu 100 % wusste, war, dass es ein Oldschool Point and Click Adventure in 2D werden sollte.

Damit fielen gleich viele Entwicklungswerkzeuge weg, aber ein paar Fragen blieben offen:

  • Mit welcher Programmiersprache?
  • Auf welcher Plattform soll das Spiel laufen?
  • Welche Engine oder welches Framework nimmst du?
  • Ein Support-Forum?

Programmiersprache

Ich konnte schon mit PHP, C#, Python, Javascript, Node, Blitz 3D, Monkey X, Monkey 2 umgehen. Mit den letzten beiden Sprachen hatte ich schon einen Prototyp gebaut. Aber der Entwickler der Sprache hat aufgehört, dadurch hatte ich kein Vertrauen mehr in einen guten Ausgang und habe damals auch aufgehört.

Es musste also eine einfache und zuverlässige Sprache sein. Ich mag es einfach und hatte auch keine Zeit, eine neue Sprache richtig zu lernen.

Engine

Eine Game Engine wie die Unreal Engine gibt es, um die Arbeit deutlich einfacher zu machen. Meine Suche begann deshalb damit, die komplette Liste https://en.wikipedia.org/wiki/List_of_game_engines durchzugehen.

Vieles fiel sofort weg, weil ich kein Geld für eine Engine ausgeben wollte, zumindest war eine einmalige Zahlung kein Problem. Aber auf keinen Fall diese seltsamen, undurchsichtigen Lizenzkonstruktionen, die sich manche Engines ausgedacht hatten. Außerdem wollte ich, dass noch aktiv entwickelt wird und dass es kein C oder C++ ist. Und natürlich 2D.

Ja, die Unreal Engine kann auch 2D, aber das widerspricht meinem Prinzip ’less is more’. Solche Engines haben viel zu viele Funktionen, die ich nicht nutzen werde und die nur kaputtgehen können. Solche Engines fallen deshalb weg.

  • Adventure Game Studio
    • Windows, und ich kann dort nicht alles umsetzen, was ich im Kopf hatte.
  • Construct
    • Lizenzprobleme, zu teuer und monatliche Zahlung
  • Flixel
    • Ich will kein ActionScript benutzen
  • GameMaker Studio
    • Zu alt, Lizenzprobleme und fand ich nicht cool genug haha
  • LÖVE
    • Nice! Aber ich bin noch dabei, es mir anzusehen
  • Monkey X
    • Damit hatte ich einmal angefangen, aber es gibt keinen Support mehr
  • Pygame
    • Interessant, aber es ist am Ende nicht geworden
  • HaxeFlixel
    • Nein, einfach nein
  • Cocos2d
    • Interessant, aber nein, in meinen Augen nicht schlicht genug
  • Godot
    • Interessant, aber nein, in meinen Augen nicht schlicht genug
  • Unity
    • Lizenzprobleme

Die Schlussfolgerung war, dass ich offenbar wirklich etwas selbst bauen wollte. Es sollte eine offene MIT-Lizenz oder etwas Ähnliches haben und vielseitig sein. Bei der Recherche stellte sich heraus, dass einige der Engines oben SDL2 verwenden, darüber musste ich mehr wissen.

SDL2

Wenn man nachschaut, wie Software und Hardware zusammenarbeiten, um über die Grafikkarte ein Bild auf dem Bildschirm zu bewegen (und Ton abzuspielen), sieht man, dass Windows DirectX nutzt, Ubuntu OpenGL und Mac Metal/Vulkan. Und Windows und Mac können auch einfach OpenGL…

Pro Plattform gibt es also eine eigene Bibliothek, die wiederum in einer eigenen Sprache geschrieben ist. Sehr unpraktisch, denn man will nicht für jede Plattform neuen Code schreiben. Man will Code 1-mal schreiben, und er soll überall funktionieren.

Die Lösung für dieses Problem ist die Bibliothek https://www.libsdl.org. Sie ist unter der richtigen Lizenz kostenlos, und mit demselben Code kann man alle Plattformen nutzen. SDL2 ist in C geschrieben, und es gibt einige Bindings, mit denen man SDL2 in einer anderen Programmiersprache verwenden kann.

Da ich kein C verwenden wollte, bin ich die komplette Liste der Bindings durchgegangen, um einiges zu testen:

  • C
    • Schien mir nicht praktisch
  • C#
    • Fiel weg, weil ich zu Hause einen Mac benutzte
  • GO
    • Gefiel mir eigentlich gut und war einfach, nur musste man jedes Mal kompilieren
  • Haskell
    • Horror, das war absolut nicht, was ich suchte
  • Rust
    • Äh, das war zu schwierig, ich hätte plötzlich funktional programmieren müssen
  • Lua
    • Interessant und einfach
  • Python
    • Irgendwie passte das nicht gut, und ich habe auch keine Lust, überall einen TAB setzen zu müssen.
  • Nim
    • Niemand schien es zu benutzen

An diesem Punkt blieben also GO und Lua übrig, und bei einer früheren Suche nach einer Engine hatte ich LÖVE schon gesehen.

LÖVE

Das Framework https://love2d.org erfüllte alle meine Anforderungen.

Ich nenne es keine Engine, sondern ein Framework, da es nur Funktionen enthält, die SDL2 auf einfache Weise ansprechen. Und genau das suche ich, es erfüllt alle meine Punkte:

  1. Es gibt ein Forum, in dem alle noch hilfsbereit und freundlich sind.
  2. Das Framework ist schlicht (less is more).
  3. Die Sprache Lua ist einfach und wird seit Jahren verwendet.
  4. Targets für die Plattformen, auf denen das Spiel laufen soll.
function love.draw()
  love.graphics.print("Hello: Signal From The Stars", 400, 300)
end

Lua

Das offizielle Lua-Logo

Das offizielle Lua-Logo

Ich musste nur noch Lua lernen. Was macht man also? Man geht zu seinen Eltern, um das komplette Lua-Handbuch auszudrucken… Die waren darüber nicht so glücklich.

Am Ende stellte sich heraus, dass die Sprache so einfach war, dass ich das Handbuch gar nicht brauchte. Lua kann man, denke ich, als Wrapper sehen, der C anspricht. Ein Vorteil ist auch, dass man nicht nach jeder Codeänderung auf einen Compiler warten muss.

Natürlich gibt es auch Nachteile, es ist zum Beispiel nicht so schnell wie C, aber das ist kein Problem.

Targets

Das Spiel Signal From The Stars muss mindestens auf Mac, Windows und iOS (iPad) laufen. Später kommen Nintendo Switch, Ubuntu und Android dazu. Wahrscheinlich wird es auch auf Xbox und PlayStation laufen können, aber nicht einfach out of the box.

Bilder

Das Spiel bekommt natürlich auch Bilder, und dafür habe ich zuerst das Programm Aseprite verwendet, weil es einen Pixel Perfect Modus hat. Das ist ein bestimmter Stil, den ich verwende. Später bin ich aber doch zu Adobe Photoshop gewechselt. Darüber schreibe ich später einen Artikel.

Alle Bilder werden anschließend optimal über TexturePacker https://www.codeandweb.com/texturepacker verarbeitet, und diese Ausgabe wird dann wieder mit einem Skript weiterverarbeitet.

Skripte

Es braucht ein paar einzelne Skripte, um bestimmte Dinge in Ordnung zu bringen. Das meiste ist in Lua, aber später kommt immer mehr dazu.

Shell

#!/bin/bash

cd ../
luacheck --no-cache --config scripts/.luacheckrc src

cd src/lib/engine3/scripts/

echo "UNIT TEST"
./unittest.sh

echo "INTEGRATION TEST"
./integrationtest.sh

Node

Die Wahl von Node liegt nur daran, dass es ein Modul gab, das Adobe Photoshop-Dateien auslesen konnte (https://github.com/meltingice/psd.js). Ich nutze es immer noch, und es wandelt die Photoshop-Datei in Assets und einen Atlas für das Spiel um.

Hätte ich damals dasselbe in Python gefunden, hätte ich Node nicht verwendet. Irgendwie passt Node mit seinen Milliarden Libraries nicht ganz zu dem, was ich suche. Es erinnert mich ein bisschen an die DLL-Hölle von Windows.

Python

Ich bereue, dass ich nicht mehr Python geschrieben habe. Unten steht im Moment das einzige, aber wichtige Skript, das ich in Python verwende. Damit kann ich den Umriss einer transparenten .png-Datei finden und diese Koordinaten vereinfachen.

# python3 outline.py
# pip3 install pillow
# pip3 install opencv-python
import sys
import cv2
import numpy as np
import json
import os
import argparse

def find_outer_contours(image_path, simplify=0):
    img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
    mask = img[:, :, 3] > 0
    contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    min_contour_size = 5
    filtered_contours = [contour for contour in contours if cv2.contourArea(contour) > min_contour_size]

    if simplify > 0:
        simplified_contours = []
        for contour in filtered_contours:
            epsilon = simplify * cv2.arcLength(contour, True)
            simplified_contour = cv2.approxPolyDP(contour, epsilon, True)
            simplified_contours.append(simplified_contour)
        return simplified_contours
    else:
        return filtered_contours

def draw_and_save_contours(contours, output_path, original_size):
    img = np.zeros((original_size[1], original_size[0]), dtype=np.uint8)
    cv2.drawContours(img, contours, -1, (255), thickness=1)
    cv2.imwrite(output_path, img)

def contour_coordinates_to_json(contours):
    contour_coordinates_list = []
    for contour in contours:
        contour_coordinates = []
        for point in contour:
            x, y = point[0]
            contour_coordinates.append([int(x), int(y)])
        
        if len(contour_coordinates) % 2 != 0:
            contour_coordinates.append([contour_coordinates[-1][0], contour_coordinates[-1][1]])

        contour_coordinates_list.append(contour_coordinates)
    return json.dumps(contour_coordinates_list)

def apply_convex_hull(contours):
    combined_contour = np.concatenate(contours)
    hull = cv2.convexHull(combined_contour)
    return [hull]

def create_rectangles(contours):
    rectangles = []
    for contour in contours:
        x, y, w, h = cv2.boundingRect(contour)
        rectangles.append((x, y, x + w, y + h))

    return rectangles

def rectangles_to_json(rectangles):
    json_output = json.dumps(rectangles)
    return json_output

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Find outer contours of a transparent image.')
    parser.add_argument('image_path', type=str, help='Path to the image file.')
    parser.add_argument('--simplify', type=float, default=0.01, help='Simplify factor for contour approximation. Set to 0 to disable simplification.')
    parser.add_argument('--hull', action='store_true', help='Apply convex hull to the contours.')
    parser.add_argument('--rects', action='store_true', help='Return contour x1,y2,x2,y2 outline rects')
    parser.add_argument('--output_path', type=str, default='', help='Path to save the output image. If not provided, the image will not be saved.')
    args = parser.parse_args()

    image_path = args.image_path

    original_img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
    original_size = (original_img.shape[1], original_img.shape[0])
    contours = find_outer_contours(image_path, simplify=args.simplify)

    if args.hull:
        contours = apply_convex_hull(contours)

    if args.output_path:
        draw_and_save_contours(contours, output_path, original_size)

    if args.rects:
        rectangles = create_rectangles(contours)
        json_output = rectangles_to_json(rectangles)
        print(json_output)
    else:
        json_output = contour_coordinates_to_json(contours)
        print(json_output)

Git

Alles wird in git und BitBucket verwaltet und gespeichert, da es dort mehrere (kostenlose) private Repos gibt.

Editor

Ich habe einige Editoren getestet, aber persönlich hat Visual Studio Code für mich am besten funktioniert. Allerdings in Kombination mit verschiedenen Erweiterungen.

  • Code Runner
  • Love2D Support
  • Markdown Preview Enhanced
  • reStructuredText Syntax highlighting
  • vscode-lua

So kann ich das Spiel mit einem Tastendruck neu starten und bei Bedarf debuggen. Und das Syntax-Highlighting ist für mich eine echte Erleichterung. Die Autovervollständigung nutze ich eigentlich nie.

Die Geschichte schreiben

Und jetzt kommt das Seltsame. Man würde an Word, LibreOffice oder Markdown denken… Aber ich schreibe sie in reStructuredText-Dateien (.rst).

===========================
Scene 20: Nederland - Intro
===========================

.. image:: ../project/story/scene/20/resources/scene20.png
    :width: 100%
    
.. contents::
   :depth: 1

.. raw:: pdf

   PageBreak


Omschrijving
============

Een bos in Nederland in de len...

Alle .rst-Dateien liegen in verschiedenen Ordnern, und mit dem Skript unten exportiere ich sie als PDF. Um sie dann auszudrucken.

Über dieses Abenteuer werde ich natürlich einen Artikel schreiben.

#!/bin/bash
set -e

source config

if [ -n "$1" ]; then
    TARGET_SCENE="$1"
    TARGET_PDF="scene_${TARGET_SCENE}.pdf"
    echo "Generating PDF for scene $TARGET_SCENE"
else
    echo "Generating all PDF files"
fi

mkdir -p ../temp/docs/
for dir in ../project/story/scene/*; do 
    SCENE_NUMBER=$(basename "$dir")
    PDF_NAME="scene_${SCENE_NUMBER}.pdf"
    
    if [ -z "$TARGET_PDF" ] || [ "$PDF_NAME" = "$TARGET_PDF" ]; then
        echo -e "Create: $PDF_NAME"

        # merge *.rst scene files
        first_iteration=true
        TEMP_FILE=$(mktemp tempfile.XXXXXX)
        for md_file in $(ls "${dir}"/*.rst | sort); do
            TEMP_CONTENT=$(mktemp /tmp/tempcontent.XXXXXX)

            if $first_iteration; then
                first_iteration=false          
            else
                item_number=$(basename "$md_file" .rst | cut -d'-' -f1)
                item_description=$(basename "$md_file" .rst | sed -E 's/^[0-9]+-//; s/-/ /g')
                title=""
                # inventory item ends with 9
                if [ ${#item_number} -eq 6 ] && [[ "$item_number" == *"9" ]]; then
                    title="$item_number $item_description (inventory)"
                else
                    title="$item_number $item_description"
                fi
                title_length=${#title}
                separator=$(printf '=%.0s' $(seq 1 $title_length))
                echo -e "$title\n$separator\n" >> "$TEMP_CONTENT"
            fi

            cat "$md_file" >> "$TEMP_CONTENT"
            echo -e "\n\n.. raw:: pdf\n\n    PageBreak\n" >> "$TEMP_CONTENT"

            cat "$TEMP_CONTENT" >> "$TEMP_FILE"
            rm "$TEMP_CONTENT"
        done

        rst2pdf --footer="###Page### | ###Title###" "$TEMP_FILE" --output="../temp/docs/${PDF_NAME}"  
        rm "$TEMP_FILE"

        if [ $# -eq 1 ]; then
            echo "Opening $PDF_NAME..."
            open "../temp/docs/${PDF_NAME}"
        fi
    fi
done

if [ $# -eq 0 ]; then
    echo -e 'Create: concept_document.pdf'
    rst2pdf --footer="###Page### | ###Title###" ../project/story/concept-document.rst --output="../temp/docs/concept_document.pdf"  

    echo -e 'Create: story_notes.pdf'
    rst2pdf --footer="###Page### | ###Title###" ../project/story/notes.rst --output="../temp/docs/story_notes.pdf"  
fi

Diagramm

Die ganze Geschichte enthält natürlich Rätsel, die ich mir mit dem kostenlosen Programm (https://app.diagrams.net) visuell sichtbar mache.

Yaml

Um anfangs alles einfach zusammenzusetzen, habe ich beschlossen, keine Zeit in einen Spieleditor zu stecken. Das ganze Spiel ist aus .yaml-Dateien aufgebaut, die in json umgewandelt und im Spiel verwendet werden.

Überblick

Wenn man sich alles ansieht, was ich bisher geschrieben habe, kommt man auf das Schema unten. Darin siehst du engine3 und game, die ich in einem anderen Beitrag erkläre.

Diagramm der endgültigen Werkzeuge

Diagramm der endgültigen Werkzeuge