Udviklingsværktøjerne

Prøv ikke at opfinde den dybe tallerken (helt) igen!

Det officielle logo for Love2d frameworket

Beslutninger

I artiklen om begyndelsen har du måske læst, at jeg havde besluttet at lave et spil.

Der er utroligt mange måder at gribe sådan noget an på, og hver situation kræver en ny tilgang. Det eneste, jeg vidste 100 %, var, at det skulle være et oldschool point and click eventyrspil i 2D.

Dermed faldt en masse udviklingsværktøjer straks fra, men der var stadig nogle åbne spørgsmål:

  • Med hvilket programmeringssprog?
  • På hvilken platform skal spillet virke?
  • Hvilken engine eller hvilket framework skal du bruge?
  • Supportforum?

Programmeringssprog

Jeg kunne allerede bruge PHP, C#, Python, Javascript, Node, Blitz 3D, Monkey X, Monkey 2. Med de sidste to sprog havde jeg allerede lavet en prototype. Men udvikleren af sproget stoppede, så jeg havde ikke længere tillid til, at det ville ende godt, og stoppede også dengang.

Det skulle altså være et simpelt og pålideligt sprog. Jeg kan lide enkelhed og havde heller ikke tid til at lære et nyt sprog ordentligt.

Engine

En game engine som Unreal Engine findes for at gøre arbejdet meget lettere. Derfor begyndte min søgen med at studere hele listen https://en.wikipedia.org/wiki/List_of_game_engines.

Mange faldt fra med det samme, da jeg ikke ville bruge penge på en engine, i hvert fald var en engangsbetaling ikke et problem. Men bestemt ingen mærkelige, uklare licenskonstruktioner, som nogle engines havde fundet på. Jeg ville også have, at den stadig blev aktivt udviklet, og at det ikke var C eller C++. Og selvfølgelig 2D.

Ja, Unreal Engine kan også 2D, men det går imod mit princip ’less is more’. Den slags engines har alt for mange funktioner, jeg ikke kommer til at bruge, og som kun kan gå i stykker. Derfor falder den slags engines fra.

  • Adventure Game Studio
    • Windows, og jeg kan ikke lave alt det, jeg havde i tankerne.
  • Construct
    • Licensproblemer, for dyrt og månedlig betaling
  • Flixel
    • Jeg vil ikke bruge ActionScript
  • GameMaker Studio
    • For gammelt, licensproblemer og syntes ikke, det var sejt nok haha
  • LÖVE
    • Nice! Men jeg er stadig ved at undersøge det
  • Monkey X
    • Det var jeg engang startet med, men der er ingen support længere
  • Pygame
    • Interessant, men det blev det ikke til sidst
  • HaxeFlixel
    • Nej, bare nej
  • Cocos2d
    • Interessant, men nej, i mine øjne ikke enkelt nok
  • Godot
    • Interessant, men nej, i mine øjne ikke enkelt nok
  • Unity
    • Licensproblemer

Konklusionen var, at jeg åbenbart virkelig ville lave noget selv. Det skulle have en åben MIT-licens eller noget lignende og være alsidigt. Undersøgelser viste, at nogle af de engines ovenfor bruger SDL2, og det måtte jeg vide mere om.

SDL2

Når man undersøger, hvordan software og hardware arbejder sammen om at få et billede til at bevæge sig på skærmen via grafikkortet (og lyd), ser man, at Windows bruger DirectX, Ubuntu OpenGL og Mac Metal/Vulkan. Og Windows og Mac kan også bare bruge OpenGL…

Så hver platform har sit eget bibliotek, som igen er skrevet i sit eget sprog. Meget upraktisk, for man vil ikke skrive kode forfra til hver platform. Det, man vil, er at skrive koden 1 gang, og så virker den overalt.

Løsningen på det problem er biblioteket https://www.libsdl.org. Det er gratis under den rette licens, og med den samme kode kan man bruge alle platforme. SDL2 er skrevet i C, og der findes en række bindings, så man kan bruge SDL2 i et andet programmeringssprog.

Da jeg ikke ville bruge C, gik jeg hele listen over bindings igennem for at teste lidt:

  • C
    • Virkede ikke praktisk
  • C#
    • Faldt fra, for derhjemme brugte jeg en Mac
  • GO
    • Faldt faktisk i god jord og var simpelt, men man skulle kompilere hver gang
  • Haskell
    • Rædsel, det var slet ikke det, jeg ledte efter
  • Rust
    • Øh, det var for svært, så skulle jeg pludselig programmere funktionelt
  • Lua
    • Interessant og enkelt
  • Python
    • På en eller anden måde passede det ikke, og jeg gider heller ikke sætte en TAB alle steder.
  • Nim
    • Ingen lod til at bruge det

På det tidspunkt var der altså GO og Lua tilbage, og i en tidligere søgning efter en engine havde jeg allerede set LÖVE.

LÖVE

Frameworket https://love2d.org opfyldte alle mine krav.

Jeg kalder det ikke en engine, men et framework, da det kun indeholder funktioner, der bruger SDL2 på en enkel måde. Og det er præcis, hvad jeg leder efter, det opfylder alle mine punkter:

  1. Der er et forum, hvor alle stadig er hjælpsomme og venlige.
  2. Frameworket er enkelt (less is more).
  3. Sproget Lua er simpelt og har været brugt i mange år.
  4. Targets for de platforme, spillet skal køre på.
function love.draw()
  love.graphics.print("Hello: Signal From The Stars", 400, 300)
end

Lua

Det officielle Lua-logo

Det officielle Lua-logo

Jeg skulle bare lige lære Lua, så hvad gør man? Man tager hjem til sine forældre for at printe hele Lua-manualen… Det var de ikke så glade for.

Til sidst viste det sig, at sproget var så simpelt, at jeg slet ikke havde brug for manualen. Lua kan man vist se som en wrapper, der taler med C. Det er også en fordel, at man ikke hele tiden skal vente på en compiler efter hver kodeændring.

Der er selvfølgelig også nogle ulemper, det er fx ikke så hurtigt som C, men det er ikke et problem.

Targets

Spillet Signal From The Stars skal som minimum virke på Mac, Windows og iOS (iPad). Senere kommer Nintendo Switch, Ubuntu og Android til. Det kan sandsynligvis også køre på Xbox og PlayStation, men ikke bare out of the box.

Billeder

Spillet får også billeder, og til det brugte jeg i første omgang programmet Aseprite, da det har en pixel perfect tilstand. Det er en bestemt stil, jeg bruger. Senere gik jeg dog over til Adobe Photoshop. Det skriver jeg en artikel om senere.

Alle billeder bliver derefter behandlet optimalt via TexturePacker https://www.codeandweb.com/texturepacker, og det output bliver så igen behandlet med et script.

Scripts

Der skal bruges nogle enkeltstående scripts til at få styr på bestemte ting. Det meste er i Lua, men senere kommer der mere og mere til.

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

Valget af Node skyldes kun, at der fandtes et modul, der kunne læse Adobe Photoshop-filer (https://github.com/meltingice/psd.js). Det bruger jeg stadig, og det omdanner Photoshop-filen til assets og et Atlas til spillet.

Havde jeg dengang fundet det samme i Python, havde jeg ikke brugt Node. På en eller anden måde passer Node med alle sine milliarder af libs ikke helt til det, jeg leder efter. Det minder mig lidt om DLL-helvedet i Windows.

Python

Jeg fortryder, at jeg ikke har skrevet mere Python. Herunder står lige nu det eneste, men vigtige script, jeg bruger i Python. Med det kan jeg finde omridset af en gennemsigtig .png-fil og forenkle de koordinater.

# 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

Alt holdes styr på og gemmes i git og BitBucket, da den har flere (gratis) private repos.

Editor

Jeg har testet en række editorer, men personligt fungerede Visual Studio Code bedst for mig. Dog i kombination med forskellige udvidelser.

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

Så kan jeg genstarte spillet med ét tryk på en knap og eventuelt debugge. Og syntaksfremhævningen er også en lettelse for mig. Autofuldførelse bruger jeg faktisk aldrig.

At skrive historien

Og nu kommer det mærkelige. Man skulle tro Word, LibreOffice, Markdown… Men jeg skriver den i reStructuredText-filer (.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-filer ligger i forskellige mapper, og med scriptet herunder laver jeg en eksport til PDF. For så at printe den.

Om dette eventyr skriver jeg selvfølgelig en artikel.

#!/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

Diagram

Hele historien indeholder selvfølgelig gåder, og dem gør jeg visuelt synlige for mig selv med det gratis program (https://app.diagrams.net).

Yaml

For at sætte det hele enkelt sammen i første omgang valgte jeg ikke at bruge tid på en spileditor. Hele spillet er bygget op af .yaml-filer, som omdannes til json og bruges i spillet.

Overblik

Når man ser på alt det, jeg har skrevet indtil nu, ender man med skemaet herunder. Her ser du engine3 og game, som jeg forklarer i et andet indlæg.

Diagram over de endelige værktøjer

Diagram over de endelige værktøjer