开发工具
不要试图(完全)重新发明轮子!

决定
在关于最初的文章里,你也许读到过,我决定要做一款游戏。
做这样一件事有非常多的方法,每种情况的做法也各不相同。我唯一百分之百确定的是,它必须是一款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 的函数。 这正是我要找的,它满足了我所有的条件:
- 有一个论坛,大家仍然乐于助人又友善。
- 框架很精简(less is more)。
- Lua 语言简单,而且已经用了很多年。
- 支持游戏要运行的各个目标平台。
function love.draw()
love.graphics.print("Hello: Signal From The Stars", 400, 300)
end
Lua

Lua 的官方标志
我只需要再学会 Lua,那该怎么做?当然是去父母家把整本 Lua 手册打印出来… 他们对此可不太高兴。
结果发现这门语言简单到我根本不需要手册。
我觉得可以把 Lua 看作一个和 C 打交道的 wrapper。另一个好处是,每次改完代码不用等编译器。
当然也有一些缺点,比如它没有 C 那么快,但这不是问题。
目标平台
游戏 Signal From The Stars 至少要能在 Mac、Windows 和 iOS(iPad)上运行。之后还会加入 Nintendo Switch、Ubuntu 和 Android。它大概也能在 Xbox 和 PlayStation 上运行,但不是开箱即用。
图像
游戏当然也会有图像,一开始我用的是 Aseprite,因为它有像素完美模式。这是我采用的一种特定风格。不过后来我还是改用了 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 并不太符合我要找的东西,它让我有点想起 Windows 的 DLL 地狱。
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,我会在另一篇文章里解释。
最终工具示意图