Battle for Wesnoth
プログラミングは問題解決の手段だと単純に考えられがちです。開発者は自身が抱える要件の解決法をコードにするのだというこの考え方では、コードの素晴らしさは技術的実装の美しさや効率の良さによって判断されます: この本には優れた例がたくさんあるでしょう。しかし計算という直接的な機能を越えて、コードが人々の暮らしに大きく影響する場合もあります。つまりコードを見た人を、プロジェクトに参加して新しいコンテンツを制作しようという気分にさせることができます。一方で残念ながら、一般の人々がプロジェクトに参加するには大きな障壁が存在します。
たいていのプログラミング言語は使いこなすのに長い技術的研鑽が必要であり、多くの人は手が出ません。さらにコードのアクセシビリティを高めるというのは技術的に難しく、多くのプログラムにおいて不必要です。アクセシビリティが綺麗なスクリプトや賢いプログラムになることはほとんどありません。アクセシビリティを達成するにはプロジェクトとプログラムの設計に関してたくさんのことを事前に考えておかなければならず、その多くは通常のプログラミング作法と衝突します。加えてたいていのプロジェクトはスキルの高いプロフェッショナルなメンバーが継続して取り組むので、コードのアクセシビリティについて考えるのは後になってからです。全く考慮されないことだってあります。
私たちのプロジェクト Battle for Wesnoth では、この問題を最初から俎上に載せました。このプログラムはターンベースのファンタジーストラテジーゲームであり、GPL2 ライセンスを使ったオープンソースモデルで開発されています。それなりの成功を収めており、執筆時点で 400 万回以上ダウンロードされています。この数字も素晴らしいのですが、プロジェクトの真価はその開発モデルにあると私たちは信じています。多様なスキルレベルを持つ大勢のボランティアが生産的に参加できます。
アクセシビリティの向上は開発者が何となく設定した目標ではなく、プロジェクトが生き残る上で不可欠な要素だとみなされています。Wesnoth はオープンソースなアプローチを採用するので、高いスキルを持った開発者をすぐに揃えられません。スキルの異なる多くのコントリビューターをプロジェクトへアクセス可能にすれば、プロジェクトの生存可能性を大きく高めるでしょう。
私たち開発者はアクセシビリティを高める基盤について開発の最初の反復から取り組んできました。これによってプログラムのアーキテクチャは間違いなく影響を受けており、大部分の決断はこの目標を念頭に置いて行われました。この章ではアクセシビリティの向上を焦点を当ててプログラムを詳細に見ていきます。
章の前半部分でプロジェクトの言語、依存関係、アーキテクチャといったプログラムの概要を示します。その次は Wesnoth のユニークなストレージ言語 (Wesnoth Markup Language, WML) を説明し、ゲームに登場するユニットへの影響を中心に WML の関数を紹介します。続いてマルチプレイヤー実装と外部プロトコルに触れ、最後はプロジェクトの参加層を広げる上での困難とプロジェクト構造についての観察を行います。
プロジェクトの概要
Wesnoth のコアエンジンは C++ で書かれ、執筆時点で 20,000 行ほどです。このコードはエンジンの中心部分であり、コードベースの半分はコンテンツと関係がありません。Wesnoth のゲームコンテンツは Wesnoth Markup Language (WML) と呼ばれるユニークなデータ言語で定義され、25,000 行の WML 言語が含まれます。 WML 言語の割合はプロジェクトと共に変化し、最初は C++ でハードコードされていたゲームコンテンツは WML で定義できるようプログラムが成熟に連れて書き直されました。図 25.1 に Wesnoth のアーキテクチャの概観を示します。灰色の部分は Wesnoth の開発者によって管理され、白い部分は外部の依存プログラムです。
アプリケーションの可搬性を高めるために、プロジェクトは全体として依存プロジェクトを減らします。こうすればプログラムの複雑性が抑えられ、開発者もサードパーティの API をいくつも習得しなくて済みます。そして同時に、依存プロジェクトを慎重に使うことでも同じ効果を期待できます。例えば Wesnoth は Simple Directmedia Layer (SDL) を動画と I/O そしてエラー処理に使いますが、これは複数のプラットフォームで共通の I/O インターフェースを用意するのが便利なためです。プラットフォーム独自の API をコーディングすることなく幅広いプラットフォームでポータブルなコードが作成できます。ただし依存プロジェクトの利用には、プラットフォーム独自の機能を使いにくくなるという代償もあります。Wesnoth は他にも次に示す SDL ライブラリを利用しています:
-
SDL_Mixer: オーディオとサウンド
-
SDL_Image: PNG などの画像フォーマットの読み込み
-
SDL_Net: ネットワーク I/O
Wesnoth はその他のライブラリも利用します:
-
Boost: 高度な C++ 機能
-
Pango と Cairo: 国際化対応のフォント
-
zlib: データの圧縮
-
Python と Lua: スクリプティングのサポート
-
GNU gettext: 国際化
Wesnoth エンジンでは WML オブジェクト ――文字列から子ノードへの辞書―― が至る所で使われます。多くのオブジェクトは WML ノードから作成され、WML ノードにシリアライズされます。エンジンの一部ではデータが WML 辞書ベースのフォーマットに直接保存され、C++ のデータ構造へのパースが行われません。
Wesnoth はいくつかの主要なサブシステムから構成されます。サブシステムは自立するようにできており、この分割された構造によってアクセシビリティが向上します。コントリビューターは他の部分を傷つけることなく特定領域のコードを変更できるからです。サブシステムの例を示します:
-
プリプロセッサ付きの WML パーサー
-
低レベルのライブラリとシステムコールを抽象化する基礎的な I/O モジュール ――ビデオモジュール、サウンドモジュール、ネットワークモジュール
-
ボタン、リスト、メニューなどのウィジェットを実装する GUI モジュール
-
ゲームボード、ユニット、アニメーションなどを実装するディスプレイモジュール
-
AI モジュール
-
六角形のゲームボードで役立つユーティリティ関数を含んだパス探索モジュール
-
異なる種類のランダムマップを生成するためのマップ生成モジュール
ゲームの流れを制御するためのモジュールもあります:
-
タイトル画面の表示を制御するタイトル画面モジュール
-
カットシーンを表示するストーリーラインモジュール
-
マルチプレイヤーのゲームの表示と設定を行うロビーモジュール
-
メインのゲームプレイを制御する “プレイゲーム” モジュール
Wesnoth で一番大きいのは “プレイゲーム” モジュールとメインのディスプレイモジュールであり、この二つは明確な仕様が存在しない常に変更されるモジュールでもあります。つまり Blob アンチパターン ――動作のはっきりしない巨大オブジェクト―― に陥る危険と隣り合わせです。コードは定期的にレビューされ、どこかを個別のモジュールに分割できないかがチェックされます。
プロジェクトには補助的な機能もありますが、メインのプログラムとは別になっています。例えばネットワーク越しのマルチプレイヤーゲームで使うマルチプレイヤーサーバーや、コンテンツを他の人と共有するためにサーバーへアップロードするコンテンツサーバーがあります。両方とも C++ で書かれています。
Wesnoth Markup Language
拡張可能なゲームエンジンとして、Wesnoth は全てのゲームデータの保存と読み込みに簡単なデータ言語を使います。最初は XML が選択肢にあったのですが、技術的知識を持たないユーザーに対してもう少しフレンドリーで、視覚データをもう少し簡単に扱えるものが望まれました。そこで開発されたのが独自のデータ言語 Wesnoth Markup Language (WML) です。この言語は技術的知識が最低限のユーザーを念頭に置いて、Python や HTML に手を出せないユーザーさえ WML ファイルを読み書きできるように開発されました。ユニット情報、キャンペーン、シナリオ、GUI 情報、ゲームロジックの設定といった Wesnoth のゲームデータは全て WML に保存されます。
WML は XML と同じ基本的な概念を持ちます: 要素 (element) と属性 (attribute) です。ただし要素の中のテキストはサポートされません。WML の属性は文字列から文字列への辞書として表現され、属性の解釈はゲームロジックが行います。ゲームに登場する Elvish Fighter ユニットの定義を例として示します:
[unit_type]
id=Elvish Fighter
name= _ "Elvish Fighter"
race=elf
image="units/elves-wood/fighter.png"
profile="portraits/elves/fighter.png"
hitpoints=33
movement_type=woodland
movement=5
experience=40
level=1
alignment=neutral
advances_to=Elvish Captain,Elvish Hero
cost=14
usage=fighter
{LESS_NIMBLE_ELF}
[attack]
name=sword
description=_"sword"
icon=attacks/sword-elven.png
type=blade
range=melee
damage=5
number=4
[/attack]
[/unit_type]
Wesnoth では国際化が重要なので、WML は国際化を直接サポートします。アンダースコアが最初に付いている属性の値は翻訳可能です。翻訳可能な文字列は、WML をパースするときに GNU gettext
を使って翻訳された文字列に変換されます。
Wesnoth では WML ドキュメントをたくさん持つのではなく、ゲームエンジンが利用する全てのメインゲームデータを一つのドキュメントに収めるアプローチを取っています。こうすると、このドキュメントを保持する単一のグローバル変数が全てのユニットの定義を持てるようになります。例えばこのドキュメントの読み込みが終われば、unit
要素を unit_type
と検索すれば全てのユニットの定義を取得できます。
最終的には全てのデータが単一の WML ドキュメントに保存されますが、本当に全てを一つのファイルに保存したのでは取り扱いが面倒です。このため Wesnoth では WML をパースする前に実行されるプリプロセッサがサポートされます。このプリプロセッサを使えば他のファイルあるいはディレクトリ全体のインクルードが可能です。例えば、
{gui/default/window/}
は gui/default/window/
にある .cfg
ファイルを全てインクルードします。
WML は非常に冗長になるので、プリプロセッサではマクロを使った省略表現が利用できます。例えば Elvish Fighter の定義で使われる {LESS_NIMBLE_ELF}
は、特定の条件 (森の中にいるなど) の下でエルフユニットの動きを高速にします:
#define LESS_NIMBLE_ELF
[defense]
forest=40
[/defense]
#enddef
この設計により、WML ドキュメント全体を複数ファイルに分割する方法についてエンジンは気にする必要がなくなります。全てのゲームデータを複数のファイルとディレクトリにどうやって分割するかは WML を書く人の責任です。
ゲームが WML ドキュメントを読み込むとき、ゲームの設定からプリプロセッサシンボルが定義されます。例えば Wesnoth のキャンペーンでは異なる難易度を定義できますが、難易度を変えると定義されるプリプロセッサシンボルが変化します。難易度を変えるために敵に割り当てる資源 (ゴールド) を変化させることがよくありますが、このために次の WML マクロが定義されています:
#define GOLD EASY_AMOUNT NORMAL_AMOUNT HARD_AMOUNT
#ifdef EASY
gold={EASY_AMOUNT}
#endif
#ifdef NORMAL
gold={NORMAL_AMOUNT}
#endif
#ifdef HARD
gold={HARD_AMOUNT}
#endif
#enddef
このマクロは例えば {GOLD 50 100 200}
のようにして呼び出し、ゴールドの量は難易度に応じて敵の定義などから決定されます。
WML は条件付いて読み込まれるので、WML ドキュメントに渡されるシンボルが Wesnoth エンジンの実行中に変更されたときにはドキュメント全体をもう一度読み込む必要があります。例えばユーザーがゲームを開始すると WML ドキュメントが読み込まれ、利用可能なキャンペーンといった情報が表示されます。そしてその後ユーザーがキャンペーンと難易度 (例えば EASY) を選択すると、WML ドキュメント全体が EASY を定義した状態で再度読み込まれます。
この設計により単一ドキュメントが全てのゲームデータを表せるようになり、シンボルを使った簡単な設定が可能になります。しかしプロジェクトが成功するにつれ Wesnoth のコンテンツは増えていき (DLC も追加され)、その全てがコアのドキュメントに追加されました。結果としてこの WML ドキュメントは数メガバイトとなり、Wesnoth でパフォーマンスの問題が発生しました。ゲーム中にドキュメントの再読み込みを行うたびにコンピューターによっては読み込みが数分にも及ぶようになり、さらにメモリ使用量も無視できなくなりました。これに抵抗するための手立てがいくつか取られ、例えばキャンペーンを読み込むときにはそのキャンペーンを表すシンボルがプリプロセッサで定義され、そのキャンペーンが必要とするコンテンツだけが #ifdef
で読み込まれるようになりました。
加えて Wesnoth には完全にプリプロセス済みの WML ドキュメントをキーの集合についてキャッシュするキャッシュシステムがあります。このキャッシュシステムは各 WML ファイルのタイムスタンプを保存し、更新された場合にドキュメントを再生成します。
Wesnoth のユニット
Wesnoth の主人公はユニットです。Elvish Fighter と Elvish Shaman が Troll Warrior と Orcish Grunt に対して戦闘を行うでしょう。全てのユニットに共通する動作もありますが、ゲームプレイを変化させる特殊アビリティもたくさんあります。例えば Troll は毎ターン一定のヘルスを回復し、Elvish Shaman はエンタグリング・ルートを使って敵の行動を遅くし、森の中にいる Wose は敵から見えません。
エンジンでこれを表現する最適な方法は何でしょうか? ベースとなる C++ クラス unit
を作って、他のユニットをこの派生クラスとしたくなるかもしれません。例えば wose_unit
クラスが unit
を継承し、デフォルトで false
を返す仮想関数 bool is_invisible() const
を unit
が持ち、森にいるときにだけ true
を返す関数で wose_unit
がこれをオーバーライドするというものです。
このようなアプローチは決められた規則しか持たないゲームであれば上手く行きます。残念ながら Wesnoth は巨大なゲームなので、このアプローチでは拡張性が足りなくなります。新しい種類のユニットの追加に新しい C++ クラスの定義が必要になるためです。さらにユニットの特性を組み合わせることもできません: ターンごとに回復し、網で敵の行動を遅くし、森にいる間は敵から見えなくなるユニットがいた場合、どうすればいいでしょうか? 他のクラスのコードをコピペした新しいクラスを作る必要があります。
Wesnoth のユニットシステムは継承を全く使っていません。その代わりユニットのインスタンスを表す unit
クラスと、ある種類のユニットが持つ不変の特性を表す unit_type
クラスを使います。unit
クラスはオブジェクトの種類への参照を持ちます。全ての可能な unit_type
オブジェクトはグローバルな辞書に保存され、メインの WML ドキュメントを読み込むときにロードされます。
ユニットタイプはそのユニットが持つアビリティのリストを持ちます。つまり Troll には毎ターン回復する “regeneration” アビリティがあり、Saurian Skirmisher には敵陣を通り抜ける “skirmisher” アビリティがあります。こういったアビリティはエンジンに組み込まれており、例えばパス探索アルゴリズムはユニットの “skirmisher” フラグをチェックして敵陣を通り抜けられるかどうかを確認します。こうするとエンジンが対応するアビリティを好きに組み合わせた新しいユニットの作成が WML の編集だけ行えます。もちろん全く新しいアビリティやユニットの振る舞いの追加にはエンジンの修正が必要です。
それから Wesnoth のユニットは好きなだけ攻撃手段を持てます。例えば Elvish Archer は長距離の弓攻撃と短距離の件攻撃があり、それぞれ攻撃力と特性が異なります。attack_type
クラスで攻撃を表し、unit_type
のインスタンスは attack_type
のリストを持ちます。
Wesnoth にはユニットに個性を持たせるための特性 (trait) という概念があり、仲間になるユニットには事前に定義された特性リストの中から二つが割り当てられます。例えば strong なユニットは近接攻撃のダメージが増え、intelligent なユニットは少ない経験値でレベルアップできます。またユニットはゲーム中に装備品を入手でき、例えば剣を装備して攻撃のダメージを増やすことが可能です。特性と装備品の実装には WML で定義されるユニットの改変を利用します。特定の攻撃手段にだけ適用される改変も定義でき、strong 特性が近接攻撃のダメージを増やし、遠距離攻撃には効果がないようにするといったことができます。
ユニットの振る舞いを WML から完全に設定可能にするのが望ましいのは明らかなので、Wesnoth がそうなっていない理由を説明するべきでしょう。任意の振る舞いを WML で記述できれば柔軟性が増すのは事実です。しかしそうすると WML はデータ指向言語ではなく完全な機能を持つプログラミング言語となってしまい、未来のコントリビューターの多くが興味を失ってしまうでしょう。
また C++ で書かれた Wesnoth AI は regeneration や invisibility といったゲーム中のアビリティを認識し、ユニットの持つアビリティを活かした行動を指示します。ユニットのアビリティは WML で作成されますが、その情報を AI が動的に検知して活用するというのは難しく、かといってアビリティを実装するのに AI はそれを無視するというのは優れた実装とは言えません。同様に WML でアビリティを実装するたびに C++ で書かれた AI を書き換えるというのも不自然です。そのため、ユニットは WML で定義でしつつアビリティはエンジンにハードコードするというのが Wesnoth の要件に合う道理にかなった解決法となりました。
Wesnoth のマルチプレイヤー実装
Wesnoth のマルチプレイヤー実装は「可能な限り単純な」アプローチを使います。サーバーへの悪意ある攻撃を避ける手立ては講じてありますが、本格的なチート対策はありません。Wesnoth におけるあらゆる動作 ――ユニットの移動、敵の攻撃、ユニットの登用など―― は WML ノードとして保存され、例えばユニットを移動する指令は次の WML として保存されます:
[move]
x="11,11,10,9,8,7"
y="6,7,7,8,8,9"
[/move]
プレイヤーの指令を受けて移動するユニットの軌跡が示されています。ゲームにはこういった WML コマンドを実行する機能があります。ゲームの初期状態とそれからのコマンドを保存すれば完全なリプレイを保存できるので、この機能は非常に便利です。ゲームのリプレイが可能であればプレイヤーがお互いのプレイを観戦できるようになり、さらにバグレポートが作成しやすくなります。
Wesnoth のネットワークマルチプレイヤー実装では、コミュニティは友好的でカジュアルなゲームに集中することを私たちは決定しました。チート対策システムの隙を突く反社会的なクラッカーと技術的な戦争を繰り広げるのはやめて、本格的なチート対策システムは実装しませんでした。他のマルチプレイヤーゲームの解析によると対戦ランキングシステムが反社会的な行動を引き起こす主要な原因だと分かっているので、そういった機能を意図的に外すことでチートを行う動機を減らしています。さらにモデレータは他のプレイヤーと親しくなって一緒にプレイするようなポジティブなゲームコミュニティを奨励し、競争よりも相互関係を重視しています。悪意を持ったゲームのハックはほぼなくなっており、この取り組みは成功と思われます。
Wesnoth のマルチプレイヤー実装は典型的なクライアント-サーバーの構造を持ちます。wesnothd とも呼ばれるサーバーは Wesnoth クライアントからの接続を受け付け、クライアントに参加可能なゲームの要約を送信します。Wesnoth は「ロビー」を表示し、プレイヤーはゲームに参加するか新しいゲームを作るかを選択します。プレイヤーが参加しゲームが開始されると、Wesnoth の各インスタンスはプレイヤーが行った操作を表す WML コマンドを生成してサーバーに送り、コマンドはサーバーによって全てのクライアントにリレーされます。ここでサーバーは非常に薄いリレーとして動作し、他クライアントの WML コマンドの実行にはリプレイシステムが使われます。Wesnoth はターンベースのゲームなので、ネットワーク通信で使われるのは全て TCP/IP です。
このシステムではゲームの観戦も可能です。観戦者が進行中のゲームに参加するとゲームの初期状態と開始時点から実行された全てのコマンド列がサーバーから送られ、ゲームの進行に付いていけるようになります。ゲームの履歴も閲覧できますが、ゲームの現在状態の復元には ――コマンド履歴の早送りも可能ですが、それでも―― 少し時間がかかります。こうする以外には、一つのクライアントにゲームの現在状態のスナップショットを定期的に計算させ、そのスナップショットを新しい観戦者に送るという方法も考えられます。しかしこのアプローチはクライアントに観戦者ごとにオーバーヘッドがかかり、大量の観戦者による DoS 攻撃が可能になってしまいます。
もちろん Wesnoth クライアントはゲームの状態を共有せずコマンドを送るだけなので、ゲームのルールについての合意が必要です。サーバーはバージョン付けされ、ゲームに参加できるのは同じバージョンを持つサーバーだけです。またプレイヤーが同期ずれを起こしたプレイヤーはすぐに警告を受け、これはチート対策にもなります。クライアントをいじるチートは簡単に行えるものの、バージョンの違いはすぐに通知されプレイヤーが処理しなければならないからです。
最後に
プログラムとしての Battle for Wesnoth の美しさは、コーディングが幅広い人々にとってアクセシブルな点にあると私たちは信じています。この目標のためにコードを「美しく」しない選択をすることもありました。効率的でないシンタックスに対してはプロジェクトの熟練したプログラマーからは反対意見が出たこともありましたが、それでもこの判断はプロジェクトの成功に大きく貢献しました。現在の Wesnoth にはユーザーが作成した数百個のキャンペーンと Era があり、その多くはプログラミングの経験をほとんどあるいは全く持たない人々によって作られました。さらに Wesnoth をプログラミングの学習ツールとして使って、プログラミングの職に就いた人もいます。他のプログラムでは見られない業績と言えるでしょう。
Wesnoth プロジェクトからリーダーに学んでほしい教訓の一つが、スキルの低いプログラマーが直面する課題について考えることです。このためにはコントリビューターがコーディングやスキルを学習するときの障害物を想像しなければなりません。例えばコントリビュートを志したユーザーはプログラミングのスキルを持たず、emacs
や vim
といった高機能なエディタは学習曲線が急すぎるかもしれません。WML は単純なテキストエディタから編集可能なように設計されているので、誰もがコントリビュート用のツールを持っています。
ただし、コードベースのアクセシビリティの向上だけが目標ではありません。アクセシビリティは杓子定規的に向上できるものではなく、ネガティブな影響を持つ様々な要素をコミュニティが主体となってバランスを取る必要があります。これが分かりやすく示されたのがプログラムと依存関係の付き合い方です。依存関係が参加の障壁となることもあれば、それによってコントリビュートが簡単になることもあります。全ての問題は一つ一つ個別に検討されなければなりません。
Wesnoth の成功を過剰に宣伝しないよう注意する必要もあるでしょう。このプロジェクトには他のプロジェクトが簡単には得られないアドバンテージがありました。例えば幅広いの人々にコードをアクセシブルにするというのはプログラムが置かれた環境の結果とも言えます。また Wesnoth はオープンソースプログラムであることからも様々な恩恵を得ています。法律に関して言うと、GNU ライセンスにより誰でも既存のファイルを探索して動作を調べ、変更を行えるようになります。この文化により実験・学習が促されましたが、これは他のプログラムでは不可能でしょう。しかしそれでも、全ての開発者の役に立ちコーディングの美しさを見出すのを助ける何かが本章にあることを私たちは願っています。