Инструменты разработки

Не пытайтесь (полностью) изобретать велосипед заново!

Официальный логотип фреймворка Love2d

Решения

В статье о самом начале вы, возможно, читали, что я решил сделать игру.

Подойти к такому можно огромным количеством способов, и для каждой ситуации подход свой. Единственное, что я знал на 100 %, это что должен получиться олдскульный квест в жанре point and click в 2D.

Из-за этого сразу отпало множество инструментов разработки, но оставалось несколько вопросов:

  • На каком языке программирования?
  • На какой платформе должна работать игра?
  • Какой движок или фреймворк использовать?
  • Форум поддержки?

Язык программирования

Я уже умел работать с PHP, C#, Python, Javascript, Node, Blitz 3D, Monkey X, Monkey 2. На последних двух языках я уже делал прототип. Но разработчик этого языка прекратил работу, из-за этого я больше не верил в хороший исход и тогда тоже остановился.

Значит, язык должен был быть простым и надёжным. Я люблю простоту, а времени хорошо освоить новый язык у меня не было.

Движок

Игровой движок вроде Unreal Engine существует, чтобы сильно упростить работу. Поэтому мои поиски начались с изучения полного списка https://en.wikipedia.org/wiki/List_of_game_engines.

Многое отпало сразу, потому что я не хотел тратить деньги на движок, по крайней мере разовая оплата была не проблемой. Но уж точно никаких странных и мутных лицензионных схем, которые придумали некоторые движки. Ещё я хотел, чтобы он активно развивался и не был на C или C++. И, конечно, 2D.

Да, Unreal Engine умеет и 2D, но это противоречит моему принципу ’less is more’. В таких движках слишком много функций, которые я не буду использовать и которые могут только сломаться. Поэтому такие движки отпадают.

  • Adventure Game Studio
    • Windows, и там нельзя сделать всё, что я задумал.
  • Construct
    • Проблемы с лицензией, слишком дорого и ежемесячная оплата
  • Flixel
    • Не хочу использовать ActionScript
  • GameMaker Studio
    • Слишком старый, проблемы с лицензией, и мне он показался недостаточно крутым, ха-ха
  • LÖVE
    • Круто! Но я ещё изучаю
  • Monkey X
    • С него я когда-то начинал, но поддержки больше нет
  • Pygame
    • Интересно, но в итоге выбор пал не на него
  • HaxeFlixel
    • Нет, просто нет
  • Cocos2d
    • Интересно, но нет, на мой взгляд недостаточно простой
  • Godot
    • Интересно, но нет, на мой взгляд недостаточно простой
  • Unity
    • Проблемы с лицензией

Вывод был такой: похоже, я действительно хотел сделать что-то сам. Нужна была открытая лицензия MIT или что-то похожее, и универсальность. Исследование показало, что некоторые из движков выше используют SDL2, и мне нужно было узнать об этом больше.

SDL2

Если разобраться, как софт и железо вместе двигают картинку на экране через видеокарту (и звук), видно, что Windows использует DirectX, Ubuntu OpenGL, а Mac Metal/Vulkan. А Windows и Mac могут использовать и просто OpenGL…

То есть для каждой платформы своя библиотека, написанная на своём языке. Очень неудобно, ведь не хочется переписывать код под каждую платформу. Хочется написать код 1 раз, чтобы он работал везде.

Решение этой проблемы библиотека https://www.libsdl.org. Она бесплатна при правильной лицензии, и с одним и тем же кодом можно использовать все платформы. SDL2 написана на C, и есть ряд привязок, позволяющих использовать SDL2 в другом языке программирования.

Поскольку писать на C я не хотел, я прошёл весь список привязок, чтобы кое-что попробовать:

  • C
    • Показалось неудобным
  • C#
    • Отпал, потому что дома у меня был Mac
  • GO
    • Вообще-то понравился и был простым, только каждый раз нужно было компилировать
  • Haskell
    • Ужас, совсем не то, что я искал
  • Rust
    • Э-э, слишком сложно, пришлось бы вдруг программировать функционально
  • Lua
    • Интересно и просто
  • Python
    • Почему-то не подошёл, да и неохота везде ставить TAB.
  • Nim
    • Похоже, им никто не пользовался

На этом этапе остались GO и Lua, а при более раннем поиске движка я уже видел LÖVE.

LÖVE

Фреймворк https://love2d.org отвечал всем моим требованиям.

Я называю его не движком, а фреймворком, потому что в нём только функции, которые просто обращаются к SDL2. И это именно то, что я ищу, он подходит по всем пунктам:

  1. Есть форум, где все до сих пор отзывчивые и доброжелательные.
  2. Фреймворк простой (less is more).
  3. Язык Lua простой и используется уже много лет.
  4. Целевые платформы, на которых будет работать игра.
function love.draw()
  love.graphics.print("Hello: Signal From The Stars", 400, 300)
end

Lua

Официальный логотип Lua

Официальный логотип Lua

Оставалось только выучить Lua. И что делаешь? Едешь к родителям распечатать всё руководство по Lua… Они были не очень рады.

В итоге оказалось, что язык настолько простой, что руководство мне не понадобилось. Lua, по-моему, можно считать обёрткой, которая обращается к C. Ещё плюс в том, что не нужно каждый раз ждать компилятор после каждого изменения кода.

Конечно, есть и недостатки, например он не такой быстрый, как C, но это не проблема.

Целевые платформы

Игра Signal From The Stars должна работать как минимум на Mac, Windows и iOS (iPad). Позже добавятся Nintendo Switch, Ubuntu и Android. Вероятно, она сможет работать и на Xbox и PlayStation, но не просто из коробки.

Изображения

В игре будут изображения, и для них я сначала использовал программу Aseprite, потому что в ней есть режим pixel perfect. Это определённый стиль, который я использую. Но позже я всё же перешёл на Adobe Photoshop. Об этом я напишу статью позже.

Затем все изображения оптимально обрабатываются через TexturePacker https://www.codeandweb.com/texturepacker, а этот результат снова обрабатывается скриптом.

Скрипты

Нужно несколько отдельных скриптов, чтобы навести порядок в некоторых вещах. Большинство на Lua, но со временем их становится всё больше.

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

Node я выбрал только потому, что был модуль, умеющий читать файлы Adobe Photoshop (https://github.com/meltingice/psd.js). Я до сих пор им пользуюсь, и он превращает файл Photoshop в ассеты и Atlas для игры.

Если бы тогда я нашёл то же самое на Python, я бы не использовал Node. Почему-то Node со своими миллиардами библиотек не совсем то, что я ищу. Он немного напоминает мне DLL-ад в Windows.

Python

Жалею, что не писал больше на Python. Ниже пока единственный, но важный скрипт, который я использую на Python. С его помощью я могу найти контур прозрачного файла .png и упростить эти координаты.

# 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

Всё отслеживается и хранится в git и на BitBucket, потому что там есть несколько (бесплатных) приватных репозиториев.

Редактор

Я попробовал несколько редакторов, но лично мне лучше всего подошёл Visual Studio Code. Правда, вместе с разными расширениями.

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

Так я могу одним нажатием клавиши перезапустить игру и при необходимости отладить. И подсветка синтаксиса для меня просто спасение. Автодополнением я, по правде, никогда не пользуюсь.

Написание истории

А теперь самое странное. Можно подумать про Word, LibreOffice, Markdown… Но я пишу её в файлах 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...

Все файлы .rst лежат в разных папках, и с помощью скрипта ниже я экспортирую их в PDF. Чтобы потом распечатать.

Об этом приключении я, конечно, напишу статью.

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

Диаграмма

Во всей истории, конечно, есть головоломки, и я делаю их наглядными для себя в бесплатной программе (https://app.diagrams.net).

Yaml

Чтобы поначалу собрать всё просто, я решил не тратить время на редактор игры. Вся игра собрана из файлов .yaml, которые превращаются в json и используются в игре.

Обзор

Если посмотреть на всё, что я написал до сих пор, получается схема ниже. На ней видны engine3 и game, о них я расскажу в другом посте.

Схема итоговых инструментов

Схема итоговых инструментов