Gli strumenti di sviluppo

Non cercare di reinventare (del tutto) la ruota!

Il logo ufficiale del framework Love2d

Decisioni

Nell’articolo sull’inizio forse hai letto che avevo deciso di creare un gioco.

Ci sono tantissimi modi per affrontare una cosa del genere e per ogni situazione l’approccio è diverso. L’unica cosa che sapevo al 100% era che doveva essere un’avventura punta e clicca old school in 2D.

Così molti strumenti di sviluppo sono stati subito esclusi, ma restavano alcune domande aperte:

  • Con quale linguaggio di programmazione?
  • Su quale piattaforma deve funzionare il gioco?
  • Quale motore o framework usare?
  • Un forum di supporto?

Linguaggio di programmazione

Sapevo già usare PHP, C#, Python, Javascript, Node, Blitz 3D, Monkey X, Monkey 2. Con gli ultimi due linguaggi avevo già fatto un prototipo. Ma lo sviluppatore di quel linguaggio si è fermato, quindi non avevo più fiducia in un buon esito e all’epoca mi sono fermato anch’io.

Doveva quindi essere un linguaggio semplice e affidabile. Amo la semplicità e non avevo nemmeno il tempo di imparare bene un nuovo linguaggio.

Motore

Un motore di gioco come Unreal Engine esiste per rendere il lavoro molto più semplice. Per questo la mia ricerca è cominciata studiando l’elenco completo https://en.wikipedia.org/wiki/List_of_game_engines.

Molti sono stati subito esclusi, perché non volevo spendere soldi per un motore, o almeno un pagamento unico non era un problema. Ma di sicuro niente strane costruzioni di licenza che alcuni motori si erano inventati. Volevo anche che fosse ancora sviluppato attivamente e che non fosse C o C++. E ovviamente 2D.

Sì, Unreal Engine può fare anche il 2D, ma va contro il mio principio ’less is more’. Motori del genere contengono troppe funzioni che non userò e che possono solo rompersi. Per questo motori del genere sono esclusi.

  • Adventure Game Studio
    • Windows, e non ci posso fare tutto quello che avevo in mente.
  • Construct
    • Problemi di licenza, troppo caro e pagamento mensile
  • Flixel
    • Non voglio usare ActionScript
  • GameMaker Studio
    • Troppo vecchio, problemi di licenza e non lo trovavo abbastanza figo haha
  • LÖVE
    • Bello! Ma lo sto ancora studiando
  • Monkey X
    • Avevo cominciato con questo, ma non ha più supporto
  • Pygame
    • Interessante, ma alla fine non l’ho scelto
  • HaxeFlixel
    • No, semplicemente no
  • Cocos2d
    • Interessante, ma no, secondo me non era abbastanza essenziale
  • Godot
    • Interessante, ma no, secondo me non era abbastanza essenziale
  • Unity
    • Problemi di licenza

La conclusione era che evidentemente volevo davvero creare qualcosa io stesso. Doveva avere una licenza aperta MIT o qualcosa di simile ed essere versatile. Dalle ricerche è emerso che alcuni dei motori qui sopra usano SDL2, e su questo dovevo saperne di più.

SDL2

Se cerchi come funziona il rapporto tra software e hardware per far muovere un’immagine sullo schermo tramite la scheda video (e il suono), vedi che Windows usa DirectX, Ubuntu OpenGL e Mac Metal/Vulkan. E Windows e Mac possono usare anche semplicemente OpenGL…

Quindi per ogni piattaforma c’è una libreria propria, scritta a sua volta nel proprio linguaggio. Molto scomodo, perché non vuoi riscrivere il codice per ogni piattaforma. Quello che vuoi è scrivere il codice 1 volta e che funzioni ovunque.

La soluzione a questo problema è la libreria https://www.libsdl.org. È gratuita con la licenza giusta e con lo stesso codice puoi usare tutte le piattaforme. SDL2 è scritta in C e ci sono diversi binding che permettono di usare SDL2 in un altro linguaggio di programmazione.

Visto che non volevo usare il C, ho provato tutto l’elenco dei binding:

  • C
    • Non mi sembrava pratico
  • C#
    • Escluso, perché a casa usavo un Mac
  • GO
    • In realtà mi piaceva ed era semplice, ma dovevi compilare ogni volta
  • Haskell
    • Un orrore, per niente quello che cercavo
  • Rust
    • Ehm, troppo difficile, all’improvviso dovevo programmare in modo funzionale
  • Lua
    • Interessante e semplice
  • Python
    • In qualche modo non era la scelta giusta, e non ho voglia di mettere un TAB ovunque.
  • Nim
    • Sembrava che nessuno lo usasse

A quel punto restavano GO e Lua, e in una ricerca precedente di un motore avevo già visto LÖVE.

LÖVE

Il framework https://love2d.org soddisfaceva tutti i miei requisiti.

Non lo chiamo motore ma framework, perché contiene solo funzioni che usano SDL2 in modo semplice. Ed è proprio quello che cerco, soddisfa tutti i miei punti:

  1. C’è un forum dove tutti sono ancora disponibili e gentili.
  2. Il framework è essenziale (less is more).
  3. Il linguaggio Lua è semplice ed è usato da anni.
  4. Target sulle piattaforme su cui funzionerà il gioco.
function love.draw()
  love.graphics.print("Hello: Signal From The Stars", 400, 300)
end

Lua

Il logo ufficiale di Lua

Il logo ufficiale di Lua

Dovevo solo imparare Lua, e allora cosa fai? Vai dai tuoi genitori a stampare tutto il manuale di Lua… Non ne erano molto contenti.

Alla fine si è scoperto che il linguaggio era così semplice che il manuale non mi serviva. Lua si può vedere, credo, come un wrapper che parla con il C. È anche un vantaggio non dover aspettare ogni volta un compilatore dopo ogni modifica al codice.

Ovviamente ci sono anche degli svantaggi, per esempio non è veloce come il C, ma non è un problema.

Target

Il gioco Signal From The Stars deve funzionare almeno su Mac, Windows e iOS (iPad). Più avanti verranno aggiunti Nintendo Switch, Ubuntu e Android. Probabilmente potrà funzionare anche su Xbox e PlayStation, ma non subito out of the box.

Immagini

Il gioco avrà anche delle immagini, e per queste all’inizio usavo il programma Aseprite, visto che ha una modalità pixel perfect. È uno stile particolare che uso. Più avanti però sono passato ad Adobe Photoshop. Scriverò un articolo su questo più avanti.

Tutte le immagini vengono poi elaborate in modo ottimale tramite TexturePacker https://www.codeandweb.com/texturepacker, per poi elaborare di nuovo quell’output con uno script.

Script

Servono alcuni script separati per sistemare certe cose. La maggior parte è in Lua, ma più avanti se ne aggiungeranno sempre di più.

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

La scelta di Node è dovuta solo al fatto che esisteva un modulo in grado di leggere i file di Adobe Photoshop (https://github.com/meltingice/psd.js). Lo uso ancora e converte il file Photoshop in asset e in un Atlas per il gioco.

Se allora avessi trovato la stessa cosa in Python, non avrei usato Node. In qualche modo Node, con i suoi miliardi di librerie, non è del tutto quello che cerco. Mi ricorda un po’ l’inferno delle DLL di Windows.

Python

Mi dispiace di non aver scritto più Python. Qui sotto c’è al momento l’unico script, ma importante, che uso in Python. Con questo posso trovare il contorno di un file .png trasparente e semplificarne le coordinate.

# 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

Tutto viene tracciato e salvato in git e su BitBucket, perché offre più repository privati (gratuiti).

Editor

Ho provato diversi editor, ma per me personalmente Visual Studio Code funzionava meglio. In combinazione con diverse estensioni.

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

Così con la pressione di un tasto posso riavviare il gioco ed eventualmente fare debug. E anche l’evidenziazione della sintassi per me è una boccata d’aria. Il completamento automatico in realtà non lo uso mai.

Scrivere la storia

E ora arriva la parte strana. Penseresti a Word, LibreOffice, Markdown… Ma io la scrivo in file reStructuredText (.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...

Tutti i file .rst sono in cartelle diverse, e con lo script qui sotto ne faccio un’esportazione in PDF. Per poi stamparla.

Su questa avventura scriverò ovviamente un articolo.

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

Diagramma

Tutta la storia contiene ovviamente degli enigmi, e li rendo visibili per me stesso con il programma gratuito (https://app.diagrams.net).

Yaml

Per mettere insieme tutto in modo semplice all’inizio, ho scelto di non spendere tempo in un editor di gioco. Tutto il gioco è costruito con file .yaml che vengono convertiti in json e usati nel gioco.

Panoramica

Se guardi tutto ciò che ho scritto finora, arrivi allo schema qui sotto. Ci vedi engine3 e game, che spiegherò in un altro post.

Diagramma degli strumenti finali

Diagramma degli strumenti finali