Các công cụ phát triển

Đừng cố phát minh lại (hoàn toàn) bánh xe!

Logo chính thức của framework Love2d

Quyết định

Trong bài viết về lúc bắt đầu có lẽ bạn đã đọc rằng tôi đã quyết định làm một trò chơi.

Có vô số cách để làm việc này, và mỗi tình huống lại cần một cách tiếp cận khác. Điều duy nhất tôi biết chắc 100% là nó phải là một trò chơi phiêu lưu point and click kiểu cũ, dạng 2D.

Nhờ vậy rất nhiều công cụ phát triển bị loại ngay, nhưng vẫn còn vài câu hỏi:

  • Dùng ngôn ngữ lập trình nào?
  • Trò chơi phải chạy trên nền tảng nào?
  • Dùng engine hay framework nào?
  • Diễn đàn hỗ trợ?

Ngôn ngữ lập trình

Tôi đã biết dùng PHP, C#, Python, Javascript, Node, Blitz 3D, Monkey X, Monkey 2. Với hai ngôn ngữ cuối tôi đã từng làm một bản mẫu. Nhưng nhà phát triển ngôn ngữ đó ngừng lại, nên tôi không còn tin nó sẽ có kết cục tốt và lúc đó tôi cũng dừng.

Vì vậy phải là một ngôn ngữ đơn giản và đáng tin cậy. Tôi thích sự đơn giản và cũng không có thời gian để thành thạo một ngôn ngữ mới.

Engine

Một game engine như Unreal Engine có mặt để làm công việc dễ hơn nhiều. Vì vậy hành trình tìm kiếm của tôi bắt đầu bằng việc nghiên cứu danh sách đầy đủ https://en.wikipedia.org/wiki/List_of_game_engines.

Nhiều cái bị loại ngay vì tôi không muốn tốn tiền cho engine, ít nhất trả một lần thì không sao. Nhưng tuyệt đối không phải những kiểu giấy phép kỳ quặc, mập mờ mà một số engine nghĩ ra. Tôi cũng muốn nó vẫn đang được phát triển tích cực và không phải C hay C++. Và dĩ nhiên là 2D.

Đúng là Unreal Engine cũng làm được 2D, nhưng điều đó đi ngược nguyên tắc ’less is more’ của tôi. Những engine như vậy có quá nhiều chức năng mà tôi sẽ không dùng và chỉ có thể hỏng. Vì thế những engine như vậy cũng bị loại.

  • Adventure Game Studio
    • Windows, và tôi không thể làm mọi thứ mình đã hình dung ở đó.
  • Construct
    • Vấn đề giấy phép, quá đắt và trả tiền hằng tháng
  • Flixel
    • Tôi không muốn dùng ActionScript
  • GameMaker Studio
    • Quá cũ, vấn đề giấy phép và tôi thấy nó chưa đủ ngầu haha
  • LÖVE
    • Tuyệt! Nhưng tôi vẫn đang tìm hiểu
  • Monkey X
    • Tôi từng bắt đầu với nó, nhưng giờ không còn được hỗ trợ
  • Pygame
    • Thú vị, nhưng cuối cùng không chọn
  • HaxeFlixel
    • Không, đơn giản là không
  • Cocos2d
    • Thú vị, nhưng không, theo tôi chưa đủ tối giản
  • Godot
    • Thú vị, nhưng không, theo tôi chưa đủ tối giản
  • Unity
    • Vấn đề giấy phép

Kết luận là có vẻ tôi thật sự muốn tự làm một thứ gì đó. Nó phải có giấy phép mở MIT hoặc tương tự, và linh hoạt. Tìm hiểu thêm thì tôi thấy vài engine ở trên dùng SDL2, và tôi phải biết thêm về nó.

SDL2

Khi tìm hiểu phần mềm và phần cứng phối hợp thế nào để làm một hình ảnh di chuyển trên màn hình qua card đồ họa (và âm thanh), bạn sẽ thấy Windows dùng DirectX, Ubuntu dùng OpenGL và Mac dùng Metal/Vulkan. Và Windows với Mac cũng có thể dùng OpenGL luôn…

Vậy mỗi nền tảng có thư viện riêng, lại được viết bằng ngôn ngữ riêng. Rất bất tiện, vì bạn không muốn viết lại mã cho từng nền tảng. Điều bạn muốn là viết mã 1 lần và chạy được ở mọi nơi.

Giải pháp cho vấn đề này là thư viện https://www.libsdl.org. Nó miễn phí với giấy phép phù hợp và bạn có thể dùng cùng một mã cho mọi nền tảng. SDL2 được viết bằng C và có một số binding giúp bạn dùng SDL2 trong ngôn ngữ lập trình khác.

Vì không muốn dùng C, tôi đã thử qua toàn bộ danh sách binding:

  • C
    • Có vẻ không tiện
  • C#
    • Bị loại vì ở nhà tôi dùng Mac
  • GO
    • Thật ra tôi khá thích và nó đơn giản, chỉ là mỗi lần phải biên dịch
  • Haskell
    • Kinh khủng, hoàn toàn không phải thứ tôi tìm
  • Rust
    • Ờ, quá khó, bỗng nhiên tôi phải lập trình theo kiểu hàm
  • Lua
    • Thú vị và đơn giản
  • Python
    • Không hiểu sao không hợp lắm, và tôi cũng chẳng muốn phải đặt TAB khắp nơi.
  • Nim
    • Có vẻ không ai dùng

Đến lúc này chỉ còn GO và Lua, và trong lần tìm engine trước tôi đã thấy LÖVE.

LÖVE

Framework https://love2d.org đáp ứng mọi yêu cầu của tôi.

Tôi không gọi nó là engine mà là framework, vì nó chỉ chứa các hàm dùng SDL2 theo cách cơ bản. Và đó chính là điều tôi tìm, nó đáp ứng mọi tiêu chí của tôi:

  1. Có một diễn đàn mà ở đó mọi người vẫn nhiệt tình và tử tế.
  2. Framework tối giản (less is more).
  3. Ngôn ngữ Lua đơn giản và đã được dùng nhiều năm.
  4. Các nền tảng đích mà trò chơi sẽ chạy.
function love.draw()
  love.graphics.print("Hello: Signal From The Stars", 400, 300)
end

Lua

Logo chính thức của Lua

Logo chính thức của Lua

Tôi chỉ còn phải học Lua, vậy bạn làm gì? Bạn đến nhà bố mẹ để in toàn bộ sách hướng dẫn Lua… Bố mẹ tôi không vui lắm.

Cuối cùng hóa ra ngôn ngữ đơn giản đến mức tôi chẳng cần sách hướng dẫn. Tôi nghĩ có thể xem Lua như một wrapper giao tiếp với C. Một ưu điểm nữa là bạn không phải chờ trình biên dịch sau mỗi lần sửa mã.

Dĩ nhiên cũng có nhược điểm, chẳng hạn nó không nhanh bằng C, nhưng đó không phải vấn đề.

Nền tảng đích

Trò chơi Signal From The Stars tối thiểu phải chạy trên Mac, Windows và iOS (iPad). Sau này sẽ thêm Nintendo Switch, Ubuntu và Android. Có lẽ nó cũng chạy được trên Xbox và PlayStation, nhưng không phải cứ thế là chạy ngay.

Hình ảnh

Trò chơi sẽ có hình ảnh, và ban đầu tôi dùng chương trình Aseprite vì nó có chế độ pixel perfect. Đó là một phong cách nhất định mà tôi dùng. Tuy nhiên sau này tôi lại chuyển sang Adobe Photoshop. Tôi sẽ viết một bài về chuyện này sau.

Sau đó mọi hình ảnh được xử lý tối ưu qua TexturePacker https://www.codeandweb.com/texturepacker, rồi kết quả đó lại được xử lý tiếp bằng một script.

Script

Cần vài script riêng lẻ để sắp xếp một số thứ. Phần lớn viết bằng Lua, nhưng về sau sẽ ngày càng nhiều thêm.

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

Tôi chọn Node chỉ vì có một mô-đun đọc được tệp Adobe Photoshop (https://github.com/meltingice/psd.js). Tôi vẫn đang dùng nó, và nó chuyển tệp Photoshop thành tài nguyên và Atlas cho trò chơi.

Nếu lúc đó tôi tìm được thứ tương tự bằng Python thì tôi đã không dùng Node. Không hiểu sao Node với hàng tỷ thư viện của nó không thật sự hợp với thứ tôi tìm, làm tôi nhớ đến địa ngục DLL của Windows.

Python

Tôi tiếc là đã không viết nhiều Python hơn. Bên dưới là script duy nhất nhưng quan trọng mà tôi đang dùng bằng Python. Nhờ nó tôi có thể tìm đường viền của một tệp .png trong suốt và đơn giản hóa các tọa độ đó.

# 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

Mọi thứ được theo dõi và lưu trong git và BitBucket, vì BitBucket có nhiều kho riêng tư (miễn phí).

Trình soạn thảo

Tôi đã thử vài trình soạn thảo, nhưng với tôi Visual Studio Code là hợp nhất. Tất nhiên là kết hợp với nhiều tiện ích mở rộng.

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

Nhờ vậy tôi có thể khởi động lại trò chơi chỉ với một phím bấm, và gỡ lỗi nếu cần. Tô sáng cú pháp cũng giúp tôi dễ thở hơn nhiều. Còn tự động hoàn thành thì thật ra tôi chưa bao giờ dùng.

Viết câu chuyện

Và giờ đến phần kỳ lạ. Bạn sẽ nghĩ là Word, LibreOffice, Markdown… Nhưng tôi viết trong các tệp 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...

Mọi tệp .rst nằm trong các thư mục khác nhau, và với script bên dưới tôi xuất chúng ra PDF. Rồi in ra.

Về cuộc phiêu lưu này, tất nhiên tôi sẽ viết một bài.

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

Sơ đồ

Toàn bộ câu chuyện dĩ nhiên có các câu đố, và tôi làm chúng hiện ra trực quan cho chính mình bằng chương trình miễn phí (https://app.diagrams.net).

Yaml

Để ban đầu mọi thứ được ghép lại đơn giản, tôi chọn không dành thời gian làm trình chỉnh sửa trò chơi. Toàn bộ trò chơi được dựng bằng các tệp .yaml, được chuyển sang json và dùng trong trò chơi.

Tổng quan

Nếu nhìn lại mọi thứ tôi viết đến giờ, bạn sẽ có sơ đồ bên dưới. Trong đó có engine3 và game, tôi sẽ giải thích trong một bài khác.

Sơ đồ các công cụ cuối cùng

Sơ đồ các công cụ cuối cùng