Utviklingsverktøyene

Ikke prøv å finne opp hjulet (helt) på nytt!

Den offisielle logoen til Love2d-rammeverket

Avgjørelser

I artikkelen om begynnelsen har du kanskje lest at jeg hadde bestemt meg for å lage et spill.

Det finnes utrolig mange måter å gripe an noe slikt på, og hver situasjon krever en annen tilnærming. Det eneste jeg visste 100 %, var at det skulle bli et oldschool pek og klikk eventyrspill i 2D.

Dermed falt mange utviklingsverktøy bort med en gang, men det var fortsatt noen åpne spørsmål:

  • Med hvilket programmeringsspråk?
  • På hvilken plattform skal spillet fungere?
  • Hvilken motor eller hvilket rammeverk skal du bruke?
  • Supportforum?

Programmeringsspråk

Jeg kunne allerede bruke PHP, C#, Python, Javascript, Node, Blitz 3D, Monkey X, Monkey 2. Med de to siste språkene hadde jeg allerede laget en prototype. Men utvikleren av språket sluttet, og dermed hadde jeg ikke lenger tro på et godt utfall og sluttet selv også den gangen.

Det måtte altså være et enkelt og pålitelig språk. Jeg liker enkelhet og hadde heller ikke tid til å lære et nytt språk skikkelig.

Motor

En spillmotor som Unreal Engine finnes for å gjøre arbeidet mye enklere. Derfor begynte letingen min med å studere hele listen https://en.wikipedia.org/wiki/List_of_game_engines.

Mye falt bort med en gang, siden jeg ikke ville bruke penger på en motor, i hvert fall var en engangsbetaling ikke noe problem. Men absolutt ingen rare, uklare lisenskonstruksjoner som noen motorer hadde funnet på. Jeg ville også at den fortsatt ble aktivt utviklet og at det ikke var C eller C++. Og selvfølgelig 2D.

Ja, Unreal Engine kan også 2D, men det går imot prinsippet mitt ’less is more’. Slike motorer inneholder altfor mange funksjoner jeg ikke kommer til å bruke, og som bare kan gå i stykker. Derfor faller slike motorer bort.

  • Adventure Game Studio
    • Windows, og jeg kan ikke gjøre alt jeg hadde tenkt der.
  • Construct
    • Lisensproblemer, for dyrt og månedlig betaling
  • Flixel
    • Jeg vil ikke bruke ActionScript
  • GameMaker Studio
    • For gammelt, lisensproblemer og syntes ikke det var kult nok haha
  • LÖVE
    • Nice! Men jeg holder fortsatt på å undersøke
  • Monkey X
    • Her begynte jeg en gang, men det har ikke support lenger
  • Pygame
    • Interessant, men det ble det ikke til slutt
  • HaxeFlixel
    • Nei, bare nei
  • Cocos2d
    • Interessant, men nei, i mine øyne ikke enkelt nok
  • Godot
    • Interessant, men nei, i mine øyne ikke enkelt nok
  • Unity
    • Lisensproblemer

Konklusjonen var at jeg tydeligvis virkelig ville lage noe selv. Det måtte ha en åpen MIT-lisens eller noe lignende og være allsidig. Undersøkelser viste at noen av motorene over bruker SDL2, og det måtte jeg vite mer om.

SDL2

Når du undersøker hvordan programvare og maskinvare samarbeider for å få et bilde til å bevege seg på skjermen via skjermkortet (og lyd), ser du at Windows bruker DirectX, Ubuntu OpenGL og Mac Metal/Vulkan. Og Windows og Mac kan også bare bruke OpenGL…

Så hver plattform har sitt eget bibliotek, som igjen er skrevet i sitt eget språk. Veldig upraktisk, for du vil ikke skrive kode på nytt for hver plattform. Det du vil, er å skrive koden 1 gang og at den fungerer overalt.

Løsningen på dette problemet er biblioteket https://www.libsdl.org. Det er gratis under riktig lisens, og med den samme koden kan du bruke alle plattformer. SDL2 er skrevet i C, og det finnes en rekke bindings som gjør at du kan bruke SDL2 i et annet programmeringsspråk.

Siden jeg ikke ville bruke C, gikk jeg gjennom hele listen over bindings for å teste litt:

  • C
    • Virket ikke praktisk
  • C#
    • Falt bort, for hjemme brukte jeg en Mac
  • GO
    • Likte det egentlig og det var enkelt, men du måtte kompilere hver gang
  • Haskell
    • Skrekk, dette var absolutt ikke det jeg lette etter
  • Rust
    • Øh, dette var for vanskelig, plutselig måtte jeg programmere funksjonelt
  • Lua
    • Interessant og enkelt
  • Python
    • På en eller annen måte passet det ikke, og jeg gidder heller ikke sette en TAB overalt.
  • Nim
    • Ingen så ut til å bruke det

På dette tidspunktet gjensto altså GO og Lua, og i et tidligere søk etter en motor hadde jeg allerede sett LÖVE.

LÖVE

Rammeverket https://love2d.org oppfylte alle kravene mine.

Jeg kaller det ikke en motor, men et rammeverk, siden det bare inneholder funksjoner som bruker SDL2 på en enkel måte. Og det er akkurat det jeg ser etter, det oppfyller alle punktene mine:

  1. Det finnes et forum der alle fortsatt er hjelpsomme og hyggelige.
  2. Rammeverket er enkelt (less is more).
  3. Språket Lua er enkelt og har vært brukt i mange år.
  4. Targets for plattformene spillet skal fungere på.
function love.draw()
  love.graphics.print("Hello: Signal From The Stars", 400, 300)
end

Lua

Den offisielle Lua-logoen

Den offisielle Lua-logoen

Jeg måtte bare lære Lua, så hva gjør du da? Du drar til foreldrene dine for å skrive ut hele Lua-manualen… Det var de ikke så glade for.

Til slutt viste det seg at språket var så enkelt at jeg ikke trengte manualen. Lua kan man vel se som en wrapper som snakker med C. Det er også en fordel at du ikke hele tiden må vente på en kompilator etter hver kodeendring.

Det finnes selvfølgelig også noen ulemper, det er for eksempel ikke like raskt som C, men det er ikke noe problem.

Targets

Spillet Signal From The Stars må minst fungere på Mac, Windows og iOS (iPad). Senere kommer Nintendo Switch, Ubuntu og Android til. Det kan sannsynligvis også fungere på Xbox og PlayStation, men ikke rett out of the box.

Bilder

Spillet får også bilder, og til det brukte jeg først programmet Aseprite, siden det har en pixel perfect modus. Det er en bestemt stil jeg bruker. Senere gikk jeg likevel over til Adobe Photoshop. Jeg skal skrive en artikkel om det senere.

Alle bildene behandles deretter optimalt via TexturePacker https://www.codeandweb.com/texturepacker, og det resultatet behandles så på nytt med et skript.

Skript

Det trengs noen enkeltstående skript for å få orden på visse ting. Det meste er i Lua, men etter hvert kommer det mer og mer.

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 av Node skyldes bare at det fantes en modul som kunne lese Adobe Photoshop-filer (https://github.com/meltingice/psd.js). Den bruker jeg fortsatt, og den gjør om Photoshop-filen til ressurser og en Atlas for spillet.

Hvis jeg den gangen hadde funnet det samme i Python, hadde jeg ikke brukt Node. På et vis passer ikke Node med sine milliarder av biblioteker helt til det jeg ser etter. Det minner meg litt om DLL-helvetet i Windows.

Python

Jeg angrer på at jeg ikke har skrevet mer Python. Under står akkurat nå det eneste, men viktige, skriptet jeg bruker i Python. Med det kan jeg finne omrisset av en gjennomsiktig .png-fil og forenkle koordinatene.

# 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 spores og lagres i git og på BitBucket, siden den har flere (gratis) private repoer.

Editor

Jeg har testet flere editorer, men personlig fungerte Visual Studio Code best for meg. Riktignok i kombinasjon med ulike utvidelser.

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

Da kan jeg starte spillet på nytt med ett tastetrykk og eventuelt feilsøke. Og syntaksuthevingen er også en lettelse for meg. Autofullføring bruker jeg egentlig aldri.

Å skrive historien

Og nå kommer det rare. Du 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-filene ligger i ulike mapper, og med skriptet under lager jeg en eksport til PDF. For så å skrive den ut.

Om dette eventyret skal jeg selvfølgelig skrive en artikkel.

#!/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 inneholder selvfølgelig gåter, og dem gjør jeg synlige for meg selv med det gratis programmet (https://app.diagrams.net).

Yaml

For å sette alt enkelt sammen i første omgang valgte jeg å ikke bruke tid på en spilleditor. Hele spillet er satt sammen av .yaml-filer som gjøres om til json og brukes i spillet.

Oversikt

Når du ser på alt jeg har skrevet så langt, ender du opp med skjemaet under. Her ser du engine3 og game, som jeg skal forklare i et annet innlegg.

Diagram over de endelige verktøyene

Diagram over de endelige verktøyene