As ferramentas de desenvolvimento

Não tente reinventar (totalmente) a roda!

O logo oficial do framework Love2d

Decisões

No artigo sobre o começo você talvez tenha lido que eu tinha decidido fazer um jogo.

Existem inúmeras formas de fazer algo assim, e cada situação pede uma abordagem diferente. A única coisa que eu sabia 100% era que tinha que ser uma aventura point and click old school em 2D.

Com isso, muitas ferramentas de desenvolvimento já ficaram de fora, mas ainda restavam algumas perguntas:

  • Com qual linguagem de programação?
  • Em qual plataforma o jogo precisa funcionar?
  • Qual engine ou framework usar?
  • Fórum de suporte?

Linguagem de programação

Eu já sabia usar PHP, C#, Python, Javascript, Node, Blitz 3D, Monkey X, Monkey 2. Com as duas últimas linguagens eu já tinha feito um protótipo. Mas o desenvolvedor dessa linguagem parou, e com isso perdi a confiança num bom resultado e na época parei também.

Então precisava ser uma linguagem simples e confiável. Gosto de simplicidade e também não tinha tempo para dominar bem uma linguagem nova.

Engine

Uma game engine como a Unreal Engine existe para deixar o trabalho bem mais simples. Por isso minha busca começou estudando a lista completa https://en.wikipedia.org/wiki/List_of_game_engines.

Muita coisa já ficou de fora, porque eu não queria gastar dinheiro com uma engine, ou pelo menos um pagamento único não era problema. Mas nada daquelas construções de licença estranhas que algumas engines inventaram. Eu também queria que ainda estivesse em desenvolvimento ativo e que não fosse C ou C++. E claro, 2D.

Sim, a Unreal Engine também faz 2D, mas isso vai contra meu princípio ’less is more’. Engines assim têm funções demais que não vou usar e que só podem quebrar. Por isso engines assim ficam de fora.

  • Adventure Game Studio
    • Windows, e não consigo fazer ali tudo o que eu tinha em mente.
  • Construct
    • Problemas de licença, caro demais e pagamento mensal
  • Flixel
    • Não quero usar ActionScript
  • GameMaker Studio
    • Velho demais, problemas de licença e não achei legal o suficiente haha
  • LÖVE
    • Legal! Mas ainda estou pesquisando
  • Monkey X
    • Eu tinha começado com ele, mas não tem mais suporte
  • Pygame
    • Interessante, mas no fim não foi o escolhido
  • HaxeFlixel
    • Não, simplesmente não
  • Cocos2d
    • Interessante, mas não, na minha visão não era básico o suficiente
  • Godot
    • Interessante, mas não, na minha visão não era básico o suficiente
  • Unity
    • Problemas de licença

A conclusão foi que, pelo jeito, eu queria mesmo fazer algo por conta própria. Precisava ter uma licença aberta MIT ou algo parecido e ser versátil. Pesquisando, vi que algumas das engines acima usam SDL2, e eu precisava saber mais sobre isso.

SDL2

Quando você pesquisa como software e hardware funcionam juntos para mover uma imagem na tela pela placa de vídeo (e o som), vê que o Windows usa DirectX, o Ubuntu OpenGL e o Mac Metal/Vulkan. E Windows e Mac também podem usar OpenGL normalmente…

Então cada plataforma tem sua própria biblioteca, escrita na sua própria linguagem. Muito incômodo, porque você não quer reescrever código para cada plataforma. O que você quer é escrever o código 1 vez e ele funcionar em todo lugar.

A solução para esse problema é a biblioteca https://www.libsdl.org. Ela é gratuita com a licença certa e com o mesmo código você usa todas as plataformas. O SDL2 é escrito em C e existem vários bindings que permitem usar o SDL2 em outra linguagem de programação.

Como eu não queria usar C, percorri a lista completa de bindings para testar algumas coisas:

  • C
    • Não me pareceu prático
  • C#
    • Ficou de fora, porque em casa eu usava um Mac
  • GO
    • Na verdade gostei, e era simples, só que era preciso compilar toda vez
  • Haskell
    • Um horror, não era nada do que eu procurava
  • Rust
    • Hum, isso era difícil demais, de repente eu teria que programar de forma funcional
  • Lua
    • Interessante e simples
  • Python
    • De algum jeito não combinava, e também não tenho vontade de colocar TAB em todo lugar.
  • Nim
    • Ninguém parecia usar

Nesse ponto sobraram GO e Lua, e numa busca anterior por uma engine eu já tinha visto o LÖVE.

LÖVE

O framework https://love2d.org atendia a todos os meus requisitos.

Não chamo de engine, mas de framework, porque ele só contém funções que usam o SDL2 de forma básica. E é exatamente isso que procuro, ele atende a todos os meus pontos:

  1. Tem um fórum onde todo mundo ainda é prestativo e gentil.
  2. O framework é básico (less is more).
  3. A linguagem Lua é simples e usada há anos.
  4. Targets para as plataformas em que o jogo vai rodar.
function love.draw()
  love.graphics.print("Hello: Signal From The Stars", 400, 300)
end

Lua

O logo oficial do Lua

O logo oficial do Lua

Eu só precisava aprender Lua, então o que você faz? Vai à casa dos seus pais imprimir o manual completo do Lua… Eles não ficaram muito felizes com isso.

No fim, a linguagem era tão simples que nem precisei do manual. Acho que dá para ver o Lua como um wrapper que conversa com o C. Também é uma vantagem não ter que esperar um compilador depois de cada mudança no código.

Claro que também há desvantagens, por exemplo não é tão rápido quanto C, mas isso não é problema.

Targets

O jogo Signal From The Stars precisa funcionar pelo menos em Mac, Windows e iOS (iPad). Mais tarde serão adicionados Nintendo Switch, Ubuntu e Android. Provavelmente também vai poder rodar no Xbox e no PlayStation, mas não direto out of the box.

Imagens

O jogo também vai ter imagens, e para isso eu usava no início o programa Aseprite, porque ele tem um modo pixel perfect. É um estilo específico que uso. Mas depois acabei passando para o Adobe Photoshop. Vou escrever um artigo sobre isso mais tarde.

Depois, todas as imagens são processadas de forma otimizada pelo TexturePacker https://www.codeandweb.com/texturepacker, e essa saída é processada de novo com um script.

Scripts

São necessários alguns scripts avulsos para colocar certas coisas em ordem. A maioria é em Lua, mas com o tempo vão surgindo mais.

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

A escolha do Node foi só porque existia um módulo capaz de ler arquivos do Adobe Photoshop (https://github.com/meltingice/psd.js). Ainda uso, e ele converte o arquivo do Photoshop em assets e em um Atlas para o jogo.

Se na época eu tivesse encontrado o mesmo em Python, não teria usado Node. De algum jeito o Node, com seus bilhões de libs, não combina muito com o que procuro. Ele me lembra um pouco o inferno das DLLs do Windows.

Python

Me arrependo de não ter escrito mais Python. Abaixo está, por enquanto, o único script que uso em Python, mas é importante. Com ele consigo encontrar o contorno de um arquivo .png transparente e simplificar essas 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

Tudo é versionado e guardado no git e no BitBucket, porque ele tem vários repositórios privados (gratuitos).

Editor

Testei vários editores, mas pessoalmente o Visual Studio Code funcionou melhor para mim. Junto com várias extensões.

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

Assim, com um só toque de tecla, reinicio o jogo e, se precisar, faço debug. E o destaque de sintaxe também é um alívio para mim. O autocompletar, na verdade, nunca uso.

Escrevendo a história

E agora vem a parte estranha. Você pensaria em Word, LibreOffice, Markdown… Mas eu escrevo em arquivos 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 os arquivos .rst ficam em pastas diferentes, e com o script abaixo faço uma exportação para PDF. Para depois imprimir.

Sobre essa aventura vou, claro, escrever um artigo.

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

A história toda tem, claro, quebra-cabeças, e eu os deixo visíveis para mim com o programa gratuito (https://app.diagrams.net).

Yaml

Para montar tudo de forma simples no começo, escolhi não gastar tempo com um editor de jogo. O jogo inteiro é montado com arquivos .yaml que são convertidos para json e usados no jogo.

Visão geral

Olhando tudo o que escrevi até agora, você chega ao esquema abaixo. Nele você vê engine3 e game, que vou explicar em outro post.

Diagrama das ferramentas finais

Diagrama das ferramentas finais