Las herramientas de desarrollo

¡No intentes reinventar (del todo) la rueda!

El logo oficial del framework Love2d

Decisiones

En el artículo sobre el principio quizá leíste que había decidido hacer un juego.

Hay muchísimas formas de abordar algo así y cada situación pide un enfoque distinto. Lo único que sabía al 100% es que tenía que ser una aventura gráfica de apuntar y hacer clic de la vieja escuela, en 2D.

Con eso quedaron descartadas de golpe muchas herramientas de desarrollo, pero todavía quedaban algunas preguntas abiertas:

  • ¿Con qué lenguaje de programación?
  • ¿En qué plataforma tiene que funcionar el juego?
  • ¿Qué motor o framework vas a usar?
  • ¿Foro de soporte?

Lenguaje de programación

Ya sabía manejar PHP, C#, Python, Javascript, Node, Blitz 3D, Monkey X, Monkey 2. Con los dos últimos lenguajes ya había hecho un prototipo. Pero el desarrollador de ese lenguaje lo dejó, así que perdí la confianza en que acabara bien y en su momento yo también lo dejé.

Así que tenía que ser un lenguaje sencillo y fiable. Me gusta la sencillez y tampoco tenía tiempo para dominar bien un lenguaje nuevo.

Motor

Un motor de juego como Unreal Engine existe para hacer el trabajo mucho más fácil. Por eso mi búsqueda empezó estudiando la lista completa https://en.wikipedia.org/wiki/List_of_game_engines.

Muchos quedaron descartados enseguida, porque no quería gastar dinero en un motor, o al menos un pago único no era problema. Pero desde luego nada de construcciones de licencias raras que algunos motores se habían inventado. También quería que siguiera en desarrollo activo y que no fuera C ni C++. Y por supuesto 2D.

Sí, Unreal Engine también puede hacer 2D, pero eso va en contra de mi principio ’less is more’. Esos motores tienen demasiadas funciones que no voy a usar y que solo pueden romperse. Por eso esos motores quedan descartados.

  • Adventure Game Studio
    • Windows, y no puedo hacer ahí todo lo que tenía en mente.
  • Construct
    • Problemas de licencia, demasiado caro y pago mensual
  • Flixel
    • No quiero usar ActionScript
  • GameMaker Studio
    • Demasiado viejo, problemas de licencia y no me parecía lo bastante guay jaja
  • LÖVE
    • ¡Genial!, pero todavía lo estoy investigando
  • Monkey X
    • Con este había empezado, pero ya no tiene soporte
  • Pygame
    • Interesante, pero al final no fue el elegido
  • HaxeFlixel
    • No, simplemente no
  • Cocos2d
    • Interesante, pero no, para mí no era lo bastante básico
  • Godot
    • Interesante, pero no, para mí no era lo bastante básico
  • Unity
    • Problemas de licencia

La conclusión fue que, al parecer, de verdad quería hacer algo yo mismo. Tenía que tener una licencia abierta MIT o algo parecido y ser versátil. Investigando vi que algunos de los motores de arriba usan SDL2, y tenía que saber más de eso.

SDL2

Si buscas cómo funciona la relación entre software y hardware para mover una imagen en pantalla a través de la tarjeta gráfica (y el sonido), ves que Windows usa DirectX, Ubuntu OpenGL y Mac Metal/Vulkan. Y Windows y Mac también pueden usar OpenGL sin más…

Así que cada plataforma tiene su propia librería, escrita a su vez en su propio lenguaje. Muy incómodo, porque no quieres volver a escribir código para cada plataforma. Lo que quieres es escribir el código 1 vez y que funcione en todas partes.

La solución a este problema es la librería https://www.libsdl.org. Es gratuita con la licencia adecuada y con el mismo código puedes usar todas las plataformas. SDL2 está escrita en C y hay varios bindings con los que puedes usar SDL2 en otro lenguaje de programación.

Como no quería usar C, recorrí toda la lista de bindings para probar un poco:

  • C
    • No me parecía práctico
  • C#
    • Descartado, porque en casa usaba un Mac
  • GO
    • La verdad es que me gustaba y era sencillo, pero había que compilar cada vez
  • Haskell
    • Un horror, no era para nada lo que buscaba
  • Rust
    • Eeeh, esto era demasiado difícil, de repente tenía que programar de forma funcional
  • Lua
    • Interesante y sencillo
  • Python
    • Por algún motivo no encajaba bien, y tampoco me apetece poner un TAB en todas partes.
  • Nim
    • Parecía que nadie lo usaba

En ese punto quedaban GO y Lua, y en una búsqueda anterior de un motor ya había visto LÖVE.

LÖVE

El framework https://love2d.org cumplía todos mis requisitos.

No lo llamo motor sino framework, porque solo contiene funciones que usan SDL2 de forma básica. Y eso es justo lo que busco, cumple todos mis puntos:

  1. Hay un foro donde todo el mundo sigue siendo amable y dispuesto a ayudar.
  2. El framework es básico (less is more).
  3. El lenguaje Lua es sencillo y se usa desde hace años.
  4. Targets en las plataformas en las que va a funcionar el juego.
function love.draw()
  love.graphics.print("Hello: Signal From The Stars", 400, 300)
end

Lua

El logo oficial de Lua

El logo oficial de Lua

Solo tenía que aprender Lua, así que ¿qué haces? Vas a casa de tus padres a imprimir el manual completo de Lua… No les hizo mucha gracia.

Al final resultó que el lenguaje era tan sencillo que no necesitaba el manual. Creo que Lua se puede ver como un wrapper que habla con C. También es una ventaja no tener que esperar a un compilador después de cada cambio en el código.

Claro que también tiene desventajas, por ejemplo no es tan rápido como C, pero eso no es un problema.

Targets

El juego Signal From The Stars tiene que funcionar como mínimo en Mac, Windows e iOS (iPad). Además, más adelante se añadirán Nintendo Switch, Ubuntu y Android. Probablemente también podrá funcionar en Xbox y PlayStation, pero no directamente out of the box.

Imágenes

El juego también tendrá imágenes, y al principio usaba para ellas el programa Aseprite, porque tiene un modo pixel perfect. Es un estilo concreto que uso. Pero más adelante acabé usando Adobe Photoshop. Escribiré un artículo sobre esto más adelante.

Después, todas las imágenes se procesan de forma óptima con TexturePacker https://www.codeandweb.com/texturepacker, para luego procesar esa salida otra vez con un script.

Scripts

Hacen falta algunos scripts sueltos para poner ciertas cosas en orden. La mayoría están en Lua, pero más adelante se irán sumando más.

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

Elegí Node solo porque había un módulo que podía leer archivos de Adobe Photoshop (https://github.com/meltingice/psd.js). Todavía lo uso, y convierte el archivo de Photoshop en assets y en un Atlas para el juego.

Si entonces hubiera encontrado lo mismo en Python, no habría usado Node. Por algún motivo Node, con sus miles de millones de librerías, no encaja del todo con lo que busco. Me recuerda un poco al infierno de las DLL de Windows.

Python

Me arrepiento de no haber escrito más Python. Aquí abajo está, por ahora, el único script que uso en Python, pero es importante. Con él puedo encontrar el contorno de un archivo .png transparente y simplificar esas coordenadas.

# 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

Todo se controla y se guarda en git y en BitBucket, porque tiene varios repositorios privados (gratuitos).

Editor

Probé varios editores, pero a mí personalmente me funcionó mejor Visual Studio Code. Eso sí, combinado con varias extensiones.

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

Así puedo reiniciar el juego con solo pulsar una tecla y, si hace falta, depurarlo. Y el resaltado de sintaxis también es un alivio para mí. El autocompletado en realidad no lo uso nunca.

Escribir la historia

Y ahora viene lo raro. Pensarías en Word, LibreOffice, Markdown… Pero yo la escribo en archivos 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...

Todos los archivos .rst están en carpetas distintas y con el script de abajo hago una exportación a PDF. Para luego imprimirla.

Sobre esta aventura escribiré, claro, un artículo.

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

Diagrama

Toda la historia tiene, por supuesto, puzles, y los hago visibles para mí con el programa gratuito (https://app.diagrams.net).

Yaml

Para montarlo todo de forma sencilla al principio, decidí no dedicar tiempo a un editor de juego. Todo el juego está montado con archivos .yaml que se convierten a json y se usan en el juego.

Resumen

Si miras todo lo que he escrito hasta ahora, llegas al esquema de abajo. En él ves engine3 y game, que explicaré en otro post.

Diagrama de las herramientas finales

Diagrama de las herramientas finales