Les outils de développement

N’essayez pas de réinventer (complètement) la roue !

Le logo officiel du framework Love2d

Décisions

Dans l’article sur le début, vous avez peut-être lu que j’avais décidé de créer un jeu.

Il existe énormément de façons d’aborder une telle chose, et chaque situation demande une approche différente. La seule chose que je savais à 100 %, c’est que ce devait être un jeu d’aventure point and click old school en 2D.

Beaucoup d’outils de développement ont donc été écartés d’emblée, mais il restait quelques questions :

  • Avec quel langage de programmation ?
  • Sur quelle plateforme le jeu doit-il fonctionner ?
  • Quel moteur ou framework utiliser ?
  • Un forum de support ?

Langage de programmation

Je savais déjà utiliser PHP, C#, Python, Javascript, Node, Blitz 3D, Monkey X, Monkey 2. Avec les deux derniers langages, j’avais déjà fait un prototype. Mais le développeur de ce langage a arrêté, alors je n’avais plus confiance en une bonne issue et j’ai arrêté moi aussi à l’époque.

Il fallait donc un langage simple et fiable. J’aime la simplicité et je n’avais pas non plus le temps de bien maîtriser un nouveau langage.

Moteur

Un moteur de jeu comme Unreal Engine sert à rendre le travail beaucoup plus simple. Ma recherche a donc commencé par l’étude de la liste complète https://en.wikipedia.org/wiki/List_of_game_engines.

Beaucoup ont été écartés tout de suite, car je ne voulais pas dépenser d’argent pour un moteur, ou du moins un paiement unique ne posait pas de problème. Mais surtout pas les montages de licences étranges que certains moteurs avaient imaginés. Je voulais aussi qu’il soit encore activement développé et que ce ne soit pas du C ou du C++. Et bien sûr en 2D.

Oui, Unreal Engine sait aussi faire de la 2D, mais ça va contre mon principe ’less is more’. Ce genre de moteur contient beaucoup trop de fonctions que je n’utiliserai pas et qui ne peuvent que casser. C’est pourquoi ces moteurs sont écartés.

  • Adventure Game Studio
    • Windows, et je ne peux pas y faire tout ce que j’avais en tête.
  • Construct
    • Problèmes de licence, trop cher et paiement mensuel
  • Flixel
    • Je ne veux pas utiliser ActionScript
  • GameMaker Studio
    • Trop vieux, problèmes de licence et je ne le trouvais pas assez cool haha
  • LÖVE
    • Sympa ! Mais je suis encore en train de l’étudier
  • Monkey X
    • J’avais commencé avec, mais il n’a plus de support
  • Pygame
    • Intéressant, mais finalement ce n’est pas celui-là
  • HaxeFlixel
    • Non, tout simplement non
  • Cocos2d
    • Intéressant, mais non, à mes yeux pas assez basique
  • Godot
    • Intéressant, mais non, à mes yeux pas assez basique
  • Unity
    • Problèmes de licence

La conclusion, c’est qu’apparemment je voulais vraiment créer quelque chose moi-même. Il fallait une licence ouverte MIT ou quelque chose d’approchant, et que ce soit polyvalent. Mes recherches ont montré que certains des moteurs ci-dessus utilisent SDL2, et je devais en savoir plus.

SDL2

Quand on cherche comment le logiciel et le matériel fonctionnent ensemble pour faire bouger une image à l’écran via la carte graphique (et le son), on voit que Windows utilise DirectX, Ubuntu OpenGL et Mac Metal/Vulkan. Et Windows et Mac peuvent aussi tout simplement utiliser OpenGL…

Chaque plateforme a donc sa propre bibliothèque, elle-même écrite dans son propre langage. Très peu pratique, car on ne veut pas réécrire du code pour chaque plateforme. Ce qu’on veut, c’est écrire le code 1 fois et qu’il fonctionne partout.

La solution à ce problème est la bibliothèque https://www.libsdl.org. Elle est gratuite sous la bonne licence et permet d’utiliser toutes les plateformes avec le même code. SDL2 est écrite en C, et il existe plusieurs bindings qui permettent d’utiliser SDL2 dans un autre langage de programmation.

Comme je ne voulais pas utiliser le C, j’ai parcouru toute la liste des bindings pour faire quelques essais :

  • C
    • Ça ne me semblait pas pratique
  • C#
    • Écarté, car à la maison j’utilisais un Mac
  • GO
    • Ça me plaisait bien et c’était simple, mais il fallait compiler à chaque fois
  • Haskell
    • L’horreur, ce n’était absolument pas ce que je cherchais
  • Rust
    • Euh, c’était trop difficile, je devais tout à coup programmer de façon fonctionnelle
  • Lua
    • Intéressant et simple
  • Python
    • D’une façon ou d’une autre, ça ne collait pas, et je n’ai pas envie de mettre une TAB partout.
  • Nim
    • Personne ne semblait l’utiliser

À ce stade, il restait GO et Lua, et lors d’une recherche précédente de moteur, j’avais déjà vu LÖVE.

LÖVE

Le framework https://love2d.org répondait à toutes mes exigences.

Je ne l’appelle pas moteur mais framework, car il ne contient que des fonctions qui utilisent SDL2 de manière basique. Et c’est exactement ce que je cherche, il coche toutes mes cases :

  1. Il y a un forum où tout le monde est encore serviable et sympathique.
  2. Le framework est basique (less is more).
  3. Le langage Lua est simple et utilisé depuis des années.
  4. Des targets pour les plateformes sur lesquelles le jeu va tourner.
function love.draw()
  love.graphics.print("Hello: Signal From The Stars", 400, 300)
end

Lua

Le logo officiel de Lua

Le logo officiel de Lua

Il ne me restait plus qu’à apprendre Lua. Alors que fait-on ? On va chez ses parents imprimer tout le manuel de Lua… Ils n’en étaient pas ravis.

Finalement, le langage s’est révélé si simple que je n’avais pas besoin du manuel. Je pense qu’on peut voir Lua comme un wrapper qui s’adresse au C. C’est aussi un avantage de ne pas devoir attendre un compilateur après chaque modification de code.

Il y a bien sûr aussi des inconvénients, par exemple il n’est pas aussi rapide que le C, mais ce n’est pas un problème.

Targets

Le jeu Signal From The Stars doit fonctionner au minimum sur Mac, Windows et iOS (iPad). Plus tard, Nintendo Switch, Ubuntu et Android seront ajoutés. Il pourra probablement aussi fonctionner sur Xbox et PlayStation, mais pas directement out of the box.

Images

Le jeu aura aussi des images, et pour ça j’utilisais d’abord le logiciel Aseprite, car il a un mode pixel perfect. C’est un style particulier que j’utilise. Mais plus tard, je suis finalement passé à Adobe Photoshop. J’écrirai un article à ce sujet plus tard.

Toutes les images sont ensuite traitées de façon optimale via TexturePacker https://www.codeandweb.com/texturepacker, puis ce résultat est à nouveau traité avec un script.

Scripts

Il faut quelques scripts séparés pour mettre certaines choses en ordre. La plupart sont en Lua, mais il y en aura de plus en plus.

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

Le choix de Node s’explique uniquement par l’existence d’un module capable de lire les fichiers Adobe Photoshop (https://github.com/meltingice/psd.js). Je l’utilise encore, et il convertit le fichier Photoshop en assets et en Atlas pour le jeu.

Si j’avais trouvé la même chose en Python à l’époque, je n’aurais pas utilisé Node. D’une manière ou d’une autre, Node et ses milliards de bibliothèques ne correspondent pas tout à fait à ce que je cherche. Ça me rappelle un peu l’enfer des DLL de Windows.

Python

Je regrette de ne pas avoir écrit plus de Python. Voici pour l’instant le seul script, mais important, que j’utilise en Python. Il me permet de trouver le contour d’un fichier .png transparent et de simplifier ces coordonnées.

# 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

Tout est suivi et conservé dans git et sur BitBucket, car il propose plusieurs dépôts privés (gratuits).

Éditeur

J’ai testé plusieurs éditeurs, mais personnellement c’est Visual Studio Code qui me convenait le mieux. En combinaison avec plusieurs extensions.

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

Je peux ainsi relancer le jeu en appuyant sur une seule touche, et le déboguer si besoin. Et la coloration syntaxique est aussi un vrai soulagement pour moi. L’autocomplétion, en fait, je ne l’utilise jamais.

Écrire l’histoire

Et maintenant, le plus bizarre. On penserait à Word, LibreOffice, Markdown… Mais je l’écris dans des fichiers 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...

Tous les fichiers .rst sont dans différents dossiers, et avec le script ci-dessous j’en fais un export en PDF. Pour ensuite l’imprimer.

J’écrirai bien sûr un article sur cette aventure.

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

Diagramme

Toute l’histoire contient bien sûr des énigmes, que je rends visibles pour moi-même avec le logiciel gratuit (https://app.diagrams.net).

Yaml

Pour tout assembler simplement au début, j’ai choisi de ne pas passer de temps sur un éditeur de jeu. Tout le jeu est construit avec des fichiers .yaml convertis en json et utilisés dans le jeu.

Vue d’ensemble

Si l’on regarde tout ce que j’ai écrit jusqu’ici, on arrive au schéma ci-dessous. On y voit engine3 et game, que j’expliquerai dans un autre article.

Diagramme des outils finaux

Diagramme des outils finaux