はじめに
この記事は、ニンテンドーSwitch「ナビつき!つくってわかる はじめてゲームプログラミング」(以下、はじプロ)において、ボードゲーム/パズル系のゲームを開発する際のノウハウについて紹介します。また、説明の都合上、はじプロのプログラミング言語的な側面を一般的な命令型言語と比較して述べていきたいと思います。
本記事で説明する手法や考え方は、あくまでも私個人が自分の経験をもとに勝手に整理したものですので、不完全な部分も多々あるかと思います。同じ課題についても実現方法は様々ありますので、ここで紹介するやり方が必ずしも正解ではないし効率が良いとも限りませんが、はじプロのプログラマが引き出しを増やしたり新たな発想のとっかかりになったりしたら幸いです。
なお、本記事では読者がチュートリアル(ナビつきレッスン)終了程度のはじプロの基本知識を持っているものと想定して、はじプロ自体の基本事項の説明は省略します。
~目次~
0. 自己紹介
1. はじめての「はじめてゲームプログラミング」
1.1 最初の題材
1.2 はじプロで向いているゲーム/向いていないゲーム
2. はじプロってどんなプログラミング言語?
2.1 データフロープログラミング
2.2 有向グラフ
2.3 シグナル
2.4 はじプロにおけるプログラミングとは
3. 命令型言語と比較したはじプロの仕様
3.1 値・データ型
3.2 変数(状態の保持)
3.2.1 状態を保持する方法
3.2.2 ワールド上のモノで状態を表現する方法1:モノの種類
3.2.3 ワールド上のモノで状態を表現する方法2:モノのサイズ
3.2.4 ワールド上のモノの状態の更新・参照
3.3 データ/処理のフロー制御
3.3.1 はじプロにおけるデータフロー制御
3.3.2 はじプロにおける処理フロー制御
3.3.3 単発動作と連続動作
3.3.4 単発シグナルと連続シグナル
3.3.5 単発シグナルと連続シグナルの演算・変換
3.3.6 単発シグナルによる処理フローの制御
3.4 処理フロー制御:順次
3.5 処理フロー制御:選択
3.6 処理フロー制御:反復
3.6.1 while文
3.6.2 for文
3.6.3 反復処理の注意事項
3.7 関数・サブルーチン
3.7.1 再利用可能な処理の塊
3.7.2 処理の塊に名前を付ける
4. ボードゲーム/パズル系のゲームの開発
4.1 全体の検討
4.2 盤
4.3 駒
4.4 カーソル
4.4.1 機能
4.4.2 ユーザ入力の移動方法
4.5 進行制御
4.5.1 ターン制御とフェーズ制御
4.5.2 入力制限
4.6 ゲームのロジック(ルール/仕様)の実現
4.6.1 データフロー的な手法
4.6.2 処理フロー的な手法
4.6.3 数式を用いる手法
4.7 処理の一般化とノードングラフの共有
4.7.1 先手と後手の処理の共通化
4.7.2 繰り返しの処理の共通化
4.8 待ち時間への対応
4.9 ルール説明・操作説明
4.10 はじプロの制約による仕様検討
4.11 相手CPUの思考ルーチン
4.11.1 実現方法
4.11.2 実現可能な思考ルーチンの制約
最後に
参考図書
付録:筆者の作成したゲーム
0. 自己紹介
トロ(@toro0000toro)と申します。はじプロのプログラマIDは P 003 P0K NMM
です。
本職ではとある業界向けのパッケージソフトの開発をしていて、普段は主にC++, C#を使っています。
プログラミングは好きですが創造することが苦手で、はじプロでは主にクラシックなボードゲーム/パズルやレトロゲームなどの再現作品を作っています。
1. はじめての「はじめてゲームプログラミング」
1.1 最初の題材
私は新しいプログラミング言語やフレームワークに触れる際には、最初の取っ掛かりとしてよくリバーシ(CPUの思考ルーチンが無い単純な二人対戦型)を作ります。それはリバーシが比較的シンプルながら基本的な文法や機能をざっと試すのに手ごろな規模・複雑さだからです。
はじプロで最初に「フリープログラミング」モードをプレイした際、いつものように軽い気持ちでリバーシを作ってみようと思ったわけですが、さて、どうやって実現すればよいのかと頭を悩ませることになりました。
1.2 はじプロで向いているゲーム/向いていないゲーム
はじプロの大きな特徴の1つは、3Dの物理エンジンを備えていることです。また、ある程度高機能な操作キャラクタの部品もあります。チュートリアルではこれらを利用して、簡単なアクション、シューティング、レースなどのゲームを作ります。これらのゲームがとても自然に学習できるのは、おそらく、はじプロというプラットフォームが初心者でもこれらのタイプのゲームを作りやすいようにと想定された造りになっているためと思われます。3D物理エンジンというベースの上に、登場するキャラクタやフィールドを追加し、ユーザ操作や自動で動くギミックなどのイベントおよびアクションをプログラムで記述することで、ゲームの基本的な形が作られます。
一方、リバーシのようなアクション性やリアルタイム性のないロジカルなゲームは、作り方が大きく異なってきます。このタイプのゲームは、一般的なプログラミング言語でゲーム開発をする場合には比較的容易な題材ですが、はじプロにおいてはあまり向いていない(作りにくい)題材と言えるかもしれません。単純に作り手と遊び手の需要が少ないだけかもしれませんが、実際にこのタイプのゲームを作成されている方は少ないようです。
それでは、なぜはじプロではリバーシのようなタイプのゲームが作りにくいかと言えば、プログラミング言語としてのはじプロの特徴が関連していると思われます。次節ではプログラミング言語としてのはじプロについて考えてみたいと思います。
なお、向いていないとはいえ、はじプロはそのようなゲームを作るのに必要十分な機能を取り揃えています。使用できる機能やリソースの制限がある中で課題を解決して目的を達成するのも、プログラミングの楽しさの一つです。
2 はじプロってどんなプログラミング言語?
私がこれまで経験してきたプログラミング言語のほとんどは命令型に分類される言語でしたので、プログラミングというと自然と思考がそちらに引きずられてしまいます。しかし、はじプロはまったく異なるパラダイム(ものの見方、捉え方)の言語でした。作り始めて最初に戸惑ったのは、これまでの考え方がそのままでは通用しないことでした。
まずは、はじプロが一般的な命令型のプログラミング言語と比較してどのような特徴があるかを整理します。(そんなことを意識しなくても、たいていの人は直感的にはじプロの仕様を理解して使いこなしていると思います。本記事では 私の性格上、 説明の流れの都合上、いったん回りくどい説明をしています(^^;)
2.1 データフロープログラミング
はじプロはビジュアルプログラミングの一種ですが、パラダイムとしてはデータフロープログラミングに該当すると思われます。
命令型の言語では、実行したい処理の流れを記述します。その流れの中で、メモリ上に記録されたデータを参照・更新したり、何らかの出力を行ったりする命令が含まれます。
一方、データフロープログラミングでは、データの流れがプログラムの中心となります。何らかの入力やイベント等で発生したデータが、分岐や合流を含む道筋を流れ、その過程で演算やアクションが行われます。
一般的にボードゲームやパズルのルール・仕様は、こういう手順でゲームを進行する、こういう場合にはこう判断しこういう操作をする、というように処理の流れとして説明されるかと思います。命令型の言語で実装する場合はそれをそのままプログラミング言語で記述すればよいのですが、データフロープログラミングではものの見方が異なるため、そのまま記述するというわけにはいかなかったのです。
以下では、データフロープログラミングとして見たはじプロの特徴を述べます。
2.2 有向グラフ
はじプロではノードンと呼ばれる部品をワイヤーで接続することでプログラムを記述します。ここで各ノードンは入力/出力のポートのいずれか一方または両方を持っており、ワイヤーによってあるノードンの出力ポートから別のノードンの入力ポートに向けてシグナル(=データ)が流れます。このノードンとワイヤーによって形作られる一塊の構造は有向グラフと見なすことができます。
有向グラフはデータフロープログラミングでよく使用される構造です。
はじプロではノードンを「入力」「中間」「出力」「モノ」の4グループにカテゴリ分けしています。本記事では少し異なる観点として、有向グラフでのデータの流れにおける位置づけに着目し、以下の4通りの分類で呼ぶことがあります。
分類 | 特徴 | 該当するノードン(例) |
---|---|---|
始点 | 出力ポートのみを持ち、一連のワイヤーの始点になる。 | ・「入力」ノードンのすべて ・「モノ」ノードンの一部(例:センサー類) ・ワイヤーワープ出口ノードン ・ゲームきりかえノードン(出口側) |
中間点 | 入力ポートと出力ポートを持ち、一連のワイヤーの中間点になる。 | ・「中間」ノードンの大部分 ・マーカー表示関連とスポイトのセット |
終点 | 入力ポートのみを持ち、一連のワイヤーの終点になる。 | ・「出力」ノードンの大部分 ・「モノ」ノードンの一部(例:連結やアクション、表示に関連するモノ) ・ワイヤーワープ入口ノードン ・ゲームきりかえノードン(入口側) |
単独 | 入力/出力ポートを持たない(=データフローに関与しない) | ・「モノ」ノードンの一部(例:シンプルな/オシャレなモノ、ワールド) ・自分メモノードン |
こうしてノードンとワイヤーで構成された複数の有向グラフが集まって、1つのプログラムとなります。1つの有向グラフは、「あるイベントや入力情報を起点として始まり、結果として何らかのアクションや出力情報を発生させる一塊の処理」となります。
ここで、ワイヤーワープ入口/出口ノードンは、対応する入口/出口の間を見えないワイヤーで接続したことと同義であり、実質的にはそれぞれの属する有向グラフを結合して1つの有向グラフを構成することと同じです。しかしプログラムの構造を考える際に意味のまとまった小さな塊ごとに分割して整理した方が理解しやすいため、本記事ではそれぞれ個別の有向グラフであるとして扱います。
逆に、大きな一つの有向グラフを、意味のまとまりごとに分けた複数の部分グラフが重なったものと見なすこともできます。例えばノードン数の節約のためにプログラムの別の場所に出現するノードンを共有化していくと、内容的に関連性の低い処理を行う有向グラフが結合して巨大な有向グラフになることもあります。この場合、意味や内容的に分離できる部分ごとに概念的に個別の有向グラフと考えて、それらの一部分が共有され重ねられているとみなすことができます。
これ以降、本記事ではノードンとワイヤーで構成されたひとまとまりの意味を持つ有向グラフ(有向グラフ全体またはその部分グラフ)をノードングラフと呼びます。
2.3 シグナル
すべてのノードンは常に入力ポートに入ってくるシグナルを受けつけ、また常に出力ポートにシグナルを出し続けています。はじプロのフレームワークは毎秒60フレームで動作していますが、この1フレームの中では、プログラム内のすべてのノードンのシグナルの伝達が同時に平行して行われています。
つまり、「始点」となるノードンは毎フレーム、シグナルを生成し続け、「中間点」のノードンを経由し、「終点」となるノードンで毎フレーム何らかの動作が行われます。ノードンの中には入力の値が0の場合には何もしないものがありますが、この場合も「値0というシグナルが流れ、何もしないという動作をしている」と考えることができます。
なお、1フレーム内ではワイヤー16ステップ分のシグナルが進みます。つまり、1フレームの1/16の時間(=1サブフレーム)の間にすべてのノードンから次のノードンへ1ステップ分のシグナル渡しが同時並行で行われます。通常、サブフレーム単位の内部処理をプログラマが意識する必要はなく、本記事の中でも簡単のため、1フレームごとにすべてのノードングラフが同時並行で始点から終点までシグナルを流しているという理解で進めます。
サブフレーム単位のシグナルの伝搬を利用してはじプロで関数や配列のような通常の方法では使用できない機能を実現する「おめくり式」と呼ばれるテクニックがありますが、難解であり本記事では触れません。詳細は参考図書を参照ください。
2.4 はじプロにおけるプログラミングとは
はじプロでプログラミングするということは、ある入力(=ユーザ操作やモノの状態・変化)に対して演算や加工をおこない、なんらかの出力を得る(=モノが動く、絵や音の変化など)という一連の処理の塊(ノードングラフ)をたくさん作る、ということになります。
この一文だけを見ると、命令型のプログラミング言語で関数やサブルーチンを複数作成してプログラムを構成するのとよく似ています。しかし決定的に異なるのは、命令型言語では処理の流れを記述するのに対し、はじプロではデータの流れを記述することです。そしてもう1つ、命令型言語ではプログラムの実行は1命令単位で順に実行され、処理の流れの中のある時点では常に1か所の命令のみが処理されるのに対し、はじプロでは、すべてのノードンの演算が同時並行で実行され続けている、ということです。
3 命令型言語と比較したはじプロの仕様
以下では、思考が命令型言語に引きずられがちな私が、命令型言語との比較という視点ではじプロの仕様を理解していった過程を説明します。
3.1 値・データ型
一般的なプログラミング言語では整数、小数、文字列などの様々な型の値を扱いますが、はじプロではシンプルに数値(小数)のみを扱います。ノードンから出力され、別のノードンが入力として受け付けるシグナル(=ワイヤーを流れるデータ)はすべて数値です。
ただし、入力ポートの一部は入力された数値を0(=false)かそれ以外(=true)として評価し、また出力ポートの一部は0(=false)または1(=true)のみを出力する仕様となっています。つまりノードングラフを流れる数値データは文脈によってbool値として扱われます。本記事でも数値とbool値を区別する文脈ではデータ型があるものとして説明します。
3.2 変数(状態の保持)
3.2.1 状態を保持する方法
一般的な命令型言語では変数を用いてプログラムの実行過程の状態を保持します。また、多数の情報や複雑な情報を扱うため、配列や構造体といったデータ構造がよく使われます。一方はじプロでは、プログラムの状態を保持するために以下のものが使用されます。
- カウンターノードン/フラグノードン
- 数値/bool値を保持できる
- ワールドに配置したモノノードン
- シンプルな/オシャレなモノの位置、種類(かたち、みため)、サイズに意味を持たせて情報を保持する
カウンターノードン/フラグノードンは一般的な命令型言語における変数とよく似ており、データの流れの中で状態を保持しておき、演算や条件判定等で使用できます。更新した値が参照結果に反映されるのに1フレームの遅延があることに注意が必要ですが、命令型言語における変数と同じような感覚で使用できます。ただし、配列のような機能はなく、一つ一つの独立した変数のようなものなので、多数のデータや複雑な情報をまとめて扱うには向いていません。
ボードゲームで盤に配置した駒のように多数のまとまった情報を管理するような場合、はじプロではプログラムのコード(ノードンガレージ)内で記述するのではなく、プログラム実行時の「ワールド」にモノを配置することで状態を保持することができます。
例えばボードゲームで駒の種類を識別したい場合、盤面に配置する駒の外観を表す物体の1つ1つに、種類識別用のモノを「見えない」状態にして連結します。後者が前者を兼ねる場合はそのまま見えるモノとして使用でき、使用ノードン数も少なくて済みます。
また、ワールドに配置するモノは必ずしもカメラに映っている必要はないため、カメラに映らない場所でデータ管理のためだけにモノを配置するということも可能です。
3.2.2 ワールド上のモノで状態を表現する方法1:モノの種類
触っているセンサーを使用することで、ワールド上に配置したシンプルなモノの「かたち」とオシャレなモノの「みため」を識別できます。これらは以下のように目的や制約で使い分けます。
分類 | 種類 | 最大数 | 動的生成 |
---|---|---|---|
シンプルなモノ | 3 | 512 | 可能(モノを発射) |
オシャレなモノ | 32 | 32 | 不可 |
触っているセンサーは検知対象をプログラムの作成時に決定しておく必要があるため、識別したいモノの種類の数だけそれぞれの対象のみを検知するセンサーを作成し、1つのモノにまとめて連結しておきます。このように複数のモノを識別して検知できるようにしたセンサー群を本記事では複合センサーと記述します。
以下の図はチェスで「先手が自分の移動する駒を選択」する際に使用する複合センサーの例です。フリースライドで移動するカーソルに各駒を識別するセンサーを連結し、それらの出力シグナルを合算したものがカーソルの位置に存在する駒の種類を表します。
3.2.3 ワールド上のモノで状態を表現する方法2:モノのサイズ
シンプルな/オシャレなモノの種類で識別を行う場合、モノの種類の数や配置できる上限数の制約があり、目的を達成できない場合があります。そこで、より複雑ですが適用範囲の広い方法として、モノのサイズで状態を表す方法があります。
例えばN通りの物体を識別したい場合、同じ種類のシンプルなモノを少しずつ異なるN通りのサイズで用意して対象の物体に連結しておきます。そして同様にサイズを少しずつ変えたN通りの触っているセンサーを1つのモノに連結して複合センサーを構成します。こうすることで、反応した触っているセンサーの個数によって識別用に連結したシンプルなモノの大きさが分かる、すなわちN通りの識別ができることになります。
3.2.4 ワールド上のモノの状態の更新・参照
ワールド上のモノを利用して状態を保持する場合、通常はモノを配置する座標を決めておき、その座標に存在するモノの種類・サイズで情報を表します。
モノを意図した位置に配置しその情報を更新するには、以下のノードンを連結したモノをフリースライド等で動かしてから、モノの移動または生成/消滅を行います。
- モノワープ入口/出口ノードン(移動する、または消滅させる)
- モノを発射ノードン(生成する、またはモノをぶつけて壊す)
同様に、モノに保持した情報を参照したいときには、モノを識別するための複合センサーを取り付けたモノをフリースライド等で所定の座標に移動させ、センサーの出力を確認します。
このように、ワールド上に配置したモノによって情報を保持する場合、情報の更新・参照ともにモノを所定の位置に移動させてそれが完了してから動作を行う必要があるため、1フレームの中で処理が完結しません。ノードングラフでこのような処理を実現する方法は後述します。
3.3 データ/処理のフロー制御
3.3.1 はじプロにおけるデータフロー制御
命令型言語では、制御文によって実行したい処理の流れを制御しますが、データフロープログラミングではデータが流れる有向グラフの接続関係でデータの流れを制御します。また、命令型言語では「どうなすべきか」を記述するのに対し、データフロープログラミング(を含む宣言型言語)では、「何をなすべきか」を記述します。
データフロープログラミングであるはじプロでは、プログラムを設計するということは、ワールド上で「何をなすべきか」をノードンガレージで記述するということになります。ワールド上で何かをなすということは、具体的にはモノを意図した場所に移動したり、特定の条件で音やエフェクトを起動したりということです。つまり、はじプロでプログラミングをするには、ゲーム上で表現したい対象物(モノ、こと)となる終点のノードンをノードンガレージに配置し、それらに対して意図したふるまいをさせるためのシグナルを流し込むようにノードングラフを構成する(=データの流れを制御する)ということが、基本的な考え方になります。
3.3.2 はじプロにおける処理フロー制御
プログラムを記述する際、一般的にはそのプログラミング言語のパラダイムに沿って思考や設計をすることが自然で分かりやすい良いプログラムを作ることにつながります。よってリバーシのように仕様が手続き的に記述されるゲームの開発においても、データフロープログラミング的な設計に落とし込んでから開発するという方が良いのかもしれません。しかし、リソース制限の厳しいはじプロでは、目的を達成するためには必ずしも良い作法とされている方法が使えるわけではありません。
本記事では、あえて「命令型言語における処理の流れをはじプロのデータフロープログラミングに適用するにはどうするか」を考えたいと思います。なお、プログラム全体を命令型言語的に記述することが目的ではありません。基本的にはデータフロー的に実装し、必要に応じて命令型言語的な処理フローを取り入れるという想定です。
命令型言語の基本的な制御構造として以下の3つがあります。
- 順次:処理Aを行った後、処理Bを行う。
- 選択:条件が真 (true) であれば処理Aを、偽 (false) であれば処理Bを行う。
- 反復:条件が真 (true) である間、処理を行う。
はじプロでは、命令型言語における「処理」は、ノードングラフの中間点のノードンによる演算や状態の更新、終点のノードンにおけるアクション等が該当します。また、ノードングラフを流れるデータ(シグナル)は常に流れ続けており、その流れを止めることはできません。つまり「処理」は常に同時並行して実行され続けている状態です。
しかし、流れるデータの見方を変えることで、命令型言語の制御構造のようにノードングラフの中で実行される処理の流れを仮想的に制御することができます。
以下では、はじプロで「処理」の流れを制御するための前提として、シグナルの性質やそれに対するノードンの反応の仕方について整理します。
3.3.3 単発動作と連続動作
入力ボートに入ってくるシグナルに対してどう反応するかによって、本記事では中間点および終点のノードンを以下のように分類して呼ぶこととします。
- 単発動作ノードン
- 通常時は値0 (false) が入力され何もしない(何も変化しない)が、特定の条件で非0 (true) が入力されると何らかのアクションを起こす。
- 例:フラグ/カウンター
- モノを発射/モノをこわす/モノワープ入口
- エフェクト
- 音を鳴らす(ループ音以外)
- リトライ/ゲームおわる/ゲームきりかえ
- 連続動作ノードン
- 0を含めたすべての入力値に対して常時意味のある反応をする。
- 例:演算を行う「中間」ノードン
- れんけつに関する「モノ」ノードン
- うごかせる/まわせる/のばせるモノ
- 音を鳴らす(ループ音)/BGM
- テクスチャ/数つきモノ
この分類は厳密なものではなく、プログラム内でのノードンの使用方法(プログラマの意図)によって変わってきます。例えばモノワープ入口は入力値が0かどうかでモノをワープさせるかどうかを切り替えますが、ある条件を満たした特定のタイミングだけワープをさせる場合は単発動作ノードン、触れたらワープさせる状態を継続させたり停止したりするような使い方をする場合は連続動作ノードンと見なすとします。
3.3.4 単発シグナルと連続シグナル
始点および中間点のノードンから出力されるシグナルについて、先ほどのノードンの分類と同じ観点で分類します。ノードンの場合と同様に、これらの分類はプログラム上で明確に区別されているわけではなく、プログラマの意図によって意識的に分類するものとなります。
- 単発シグナル
- 通常は0(false)が流れており、特定のタイミングで意味のある値(0(false)または非0(true))が流れる
- 連続シグナル
- 常時、意味がある値(値0を含む)が流れている
シグナルはそのデータ型によって数値とbool値に分類できますが、それと単発/連続の概念は直行しており、組み合わせられます。
ノードングラフの始点となるノードンは、ノードンの種類やプログラマの設計意図によって、単発シグナルまたは連続シグナルを発します。それが中間点のノードンを経由する過程で単発/連続が切り替わることもあり、最終的に終点のノードンに流れ込みます。
ノードングラフの終点においては、単発動作ノードンには、そのアクションを意図したタイミングで適切に動作させるための単発シグナルを入力させる必要があります。同様に、連続動作ノードンには、その動作を常に適切に制御するための連続シグナルを入力させる必要があります。
ワイヤーワープ入口/出口についても、シグナルの種類を対応付ける必要があります。ワイヤーワープ入口に流れ込むシグナルが単発シグナル(または連続シグナル)ならば、ワイヤーワープ出口側は単発シグナル(または連続シグナル)が入力されるものと想定したノードングラフを構成することになります。
3.3.5 単発シグナルと連続シグナルの演算・変換
ノードングラフを流れるシグナルは、経由する中間点のノードンによって連続/単発を変化させます。代表的なケースは以下の通りです。
- フラグノードン/カウンターノードン
- 単発シグナルを入力すると、連続シグナルが出力される。
- 0から変わったしゅんかんノードン
- 連続シグナルを入力すると、単発シグナルが出力される。
- 二項演算子
- 演算子に入力する2つのシグナルの組み合わせにより、出力されるシグナルが以下のように変化する。
演算 | 入力1 | 入力2 | 出力 |
---|---|---|---|
AND | 単発 | 単発 | 単発(bool) |
AND | 単発 | 連続 | 単発(bool) |
AND | 連続 | 連続 | 連続(bool) |
× | 単発 | 単発 | 単発(数値) |
× | 単発 | 連続 | 単発(数値) |
× | 連続 | 連続 | 連続(数値) |
値が一定のbool値型の連続シグナルAに対してtrueの単発シグナルBとのANDを取ると、連続シグナルAの値を単発シグナルBのタイミングで発するbool値型の単発シグナルが生成されます。
同様に、値が一定の数値型の連続シグナルAに対して値1の単発シグナルBとの掛け算を行うと、連続シグナルAの値を単発シグナルBのタイミングで発する数値型の単発シグナルが生成されます。
3.3.6 単発シグナルによる処理フローの制御
ここまでノードンの動作やシグナルを単発/連続に分類して考えてきましたが、終点の連続動作ノードンには連続シグナルを与えて、常に正しい状態・ふるまいを維持する必要があります。つまり、順次、選択、反復を意識して処理のフローを制御する必要があるのは、単発動作ノードンとなります。そして、この処理フローの制御を実現するのが、単発シグナルです。
命令型言語の処理フローでは、記述された処理の並びを1つずつ順に実行していき、ある瞬間にはどこの命令を実行しているか(現在、どの命令を実行しているか)という概念があります。一方、はじプロのシグナルは、それぞれのノードングラフの中を同時並行で常時流れ続けており、現在の実行位置という概念はありません。
ここで、ノードングラフに単発シグナルが流れる道筋において非0の値が流れていく過程を現在の実行位置と見なすことで、はじプロにおいて仮想的に処理フローを制御することができるようになります。
次に、命令型言語の基本的な3つの制御構造(順次、選択、反復)の実現方法について述べていきます。
ここで、ノードングラフの例を示す際には以下の凡例に従います。
3.4 処理フロー制御:順次
「順次:処理Aを行った後、処理Bを行う。」
はじプロで実行する「処理」の最小単位はノードンです。ノードンはワイヤーで接続されてデータが伝搬するため、直列に接続されたノードン同士(一方からもう一方へデータが流れ込む道がある)については、同一フレーム内で処理が「順次」実行されます。つまり、一つながりのデータの流れる道筋にある「中間点」のノードンによる演算と「終点」のノードンでのアクションは、順次実行されます。
逆に、並列の位置関係にあるノードン同士では、処理の順序は規定されません。そこで、単発動作ノードン、および単発シグナルの観点に着目します。2つの並列な単発動作ノードンA, Bの処理が実行される順序を制御するには、それらに流れ込む単発シグナルに非0の値が現れるタイミングを制御する必要があります。
以下の図は、2つの処理A, Bを「順次」処理するための基本的なパターンです。タイマーノードンを用いて、処理Aが完了するのに十分な時間待った後に処理Bを実行します。
具体的なコードを例に説明します。
下図は、チェスにおいて駒の移動先を選択したときに実行される処理の流れを表したものです。
この図の前半を処理1(駒の移動がルールと照らし合わせて有効な指定であるかを判定する)、後半を処理2(判定の成否によって対応するアクションを実行する)とします。それぞれ以下のように「順次」の処理が実行されます。
- 処理1 : 駒の移動の成否判定
- 駒の移動の成否判定は複数の条件で構成され、それらすべてを満たす必要があります。ここで、その条件の1つに「移動元と移動先の間に他の駒がないこと」の判定処理(1a)がありますが、この処理では触っているセンサーを移動させて盤上の駒の有無を判定するため数フレームを要します。この処理が完了する前に複数の条件の判定結果のANDをとってそのシグナルを参照しても、正しい判定結果が得られません。
- そこで、処理1aの判定結果(単発シグナル)をいったんフラグに入力して連続シグナル1bとします。処理1aが完了するのに十分な時間を待ってタイマー①の単発シグナルを発し、1bとのANDをとることで、処理1aの結果を表す単発シグナル1cが生成されます。駒移動成否に関する他の条件判定結果の連続シグナル1dとさらにANDを取って、最終的な判定結果の単発シグナル1eを生成します。
- この単発シグナル1eに対して、移動可否判定の成功時の処理、失敗時の処理を実行することになります。
- 処理2 : 駒移動成功時処理
- 駒の移動が成功した場合、以下の2つの処理を行います。
- ・(2a)移動先に敵の駒があれば破壊する(モノを発射)。
- ・(2b)移動元の自分の駒を移動先にワープする。
- ここで、これら2つが同時に行われると移動した自分の駒も一緒にモノ発射に当たって破壊されてしまうため、タイミングをずらす必要があります。そこで②のタイマーでタイミングをずらした単発シグナルをそれぞれの処理に入力することで、処理2aの完了後に処理2bが実行されるようにします。
この図の処理において、単発シグナルによって「処理の現在位置」の推移が行われています。
決定ボタン押下によって生成された単発シグナルが起点となり、ここに1フレーム分のtrueの値が発生します。このtrueの値は、まずタイマー①によって0.5秒分のフレーム数だけ遅れて伝搬し、待ちが必要な1bとのAND演算で同期を取ります。その次にAND演算(1e)、そして2a, 2bの処理へと伝搬し、それぞれの演算や処理が順次行われることになります。この単発シグナルのリレーによって運ばれるtrueの値が、処理の現在位置を表しています。
なお、リレーの途中の演算で単発シグナルの中のtrueがfalseに変化した場合、そこで単発シグナルの伝搬は途切れ、それ以降の処理は実行されないことになります。また、この例では伝搬させる値がbool値でしたが、数値の情報を伝搬させる場合にはAND演算の代わりに、true(=1)との掛け算で値をリレーさせていく必要があります。
3.5 処理フロー制御:選択
「選択:条件が真 (true) であれば処理Aを、偽 (false) であれば処理Bを行う。」
処理フロー「順次」で単発シグナルを用いて処理の実行順序、現在の実行位置を制御したのと同様に、「選択」についても単発シグナルで処理の流れを考えます。
上図左側は、命令型言語のif文に相当する「条件がtrueであれば処理Aを(単発シグナルがtrueになるタイミングで)行う」を表しています。
条件Pがtrueであることを表す連続シグナルaに対して、起点となる(=処理を実行するタイミングを制御する)単発シグナルbとのANDを取り、その結果の単発シグナルcによって処理Aを実行させます。
上図右側は、if-else文に相当する処理で、「条件がtrueであれば処理Aを、そうでなければ処理Bを(単発シグナルがtrueになるタイミングで)行う」ということを表しています。
ここで、単発シグナルcをNOTで反転したシグナルdは、起点となる単発シグナルがtrueとなるタイミング以外で値がtrueであるため、これをそのまま処理Bへの入力とすることができません。シグナルdに対して起点となる単発シグナルbとのANDを取ってその結果の単発シグナルeを処理Bへの入力とします。これにより、条件Pの真偽によって処理Aまたは処理Bを所定のタイミングで実行することができます。
3.4節のチェスのコードにおいても、このif-else文の構造が利用されています。
3.6 処理フロー制御:反復
「反復:条件が真 (true) である間、処理を行う。」
以下の図は、処理Aを繰り返すための基本的なパターンを示しています。反復処理開始の単発シグナルがtrueになったらフラグをONにしてカウンタを「ループ」モードで回し続け、ループするたびに「処理A」を起動する単発シグナルを発します。反復処理終了の単発シグナルがtrueになったら、フラグとカウンタをリセットして反復処理が停止されます。
反復処理には様々なバリエーションがあります。以下ではいくつかの実装例を示します。
3.6.1 while文
以下の図は「while (条件Pがtrue) { 処理A; } 」に相当します。
条件Pがtrueになった瞬間に反復処理を開始し、条件Pがfalseになると反復処理を終了します。
3.6.2 for文
以下の図は「for (i = 1 ; i <= N ; ++i) { 処理A; } 」に相当します。
処理AはN回実行され、その際にループ変数iが1, 2, ..., Nと変化します。
ループ内で繰り返される処理を起動する単発シグナルaは、まずカウンタbをカウントアップします。これがループ変数「i」に相当します。カウンタbの初期値(=ループ実行前)は0で、最初のイテレーションで1にカウントアップされます。
それと併せて、単発シグナルaでフラグcをいったんONにし、続けて0から変わった瞬間ノードンの出力でフラグcをOFFに戻します。これにより単発フラグdを生成します。これは、カウンタb(ループ変数i)が更新されて値が1に変わるのに1フレームを要するため、それとタイミングを同期させた単発フラグdを生成することが目的です。
この単発シグナルdと「変数i≦N」の条件でANDを取ったものが、「処理A」を起動する単発シグナルeとなります。ループ内でのn回目(1≦n≦N)の処理Aの実行中、ループ変数iの値がnとなっています。
単発シグナルdと「変数i>N」の条件でANDを取ったものが、ループ終了を表す単発フラグfとなります。これを用いてループに使用したフラグやカウンタをリセットします。また、この反復処理を終了した後に実行する次の処理の起点としても利用できます。
3.6.3 反復処理の注意事項
ループ内で繰り返して実行される「処理A」は、繰り返し実行されることを前提とした造りにしておく必要があります。
「処理A」のノードングラフの中にカウンターやフラグを含む場合、処理の開始時の前提の状態となるように適切な値にリセットしておく必要があります。処理の開始となる単発シグナルでリセットする方法が考えられますが、カウンターやフラグの値が更新されるのは1フレーム後となるため、処理開始の最初のフレームが正しく処理できない場合があるので注意が必要です。そこで、処理の終了時に、次の使用時に備えてリセットしておく方法もあります。
3.7 関数・サブルーチン
命令型言語における関数やサブルーチンにそのまま相当する機能ははじプロにはありません。しかし、関数が持つ様々な側面の一部は、はじプロでも再現することが可能です。
3.7.1 再利用可能な処理の塊
はじプロでは、一般的な命令型言語で使用される関数やサブルーチンのような、プログラムの複数の箇所で再利用可能な処理の塊を定義することは(通常の方法では)できません。プログラムの複数の箇所から同じノードングラフを共有しようとすると、同時に異なる目的のシグナルが合算されて流れ込むことになり正しく処理できません。
逆に言えば、複数の箇所から接続しても、同時に最大1か所からのみ有効なシグナルが流れ込むならば、同じノードングラフを共有することができます。
以下の図の左側はリバーシにおいて駒を置いたときに相手の駒を裏返す処理を、先手の番と後手の番で共有して使用する例を表します。この共通処理を起動する起点となるシグナルは同時に片方しかtrueになることはないため、先手と後手で処理を共有できます。
ここで、この共有処理の中では以下の点に注意する必要があります。
-
共有するノードングラフの中から別のノードングラフの処理に接続している(ワイヤーまたはワイヤーワープ)場合、後者の処理も同じタイミングで使い分けがされる共有処理(またはいつ参照しても問題ない処理)として実装する必要があります。上図の例では、共有処理の中で「カーソルの位置に敵の駒があるか」を判定する必要が出てきますが、先手の場合と後手の場合で「敵」が指すものが変わります。そこで敵の駒があるかの判定処理では、先手/後手のいずれの場合に実行されてもそれぞれのタイミングで適切な結果を生成するようにしておきます。
-
共有処理のノードングラフの中にカウンターやフラグを含む場合、共有処理の開始時の前提の状態となるように適切な値にリセットしておく必要があります。これは、反復処理において繰り返される処理でカウンターやフラグを適切にリセットする必要があることと同じです。
3.7.2 処理の塊に名前を付ける
関数・サブルーチンのもう一つの目的として、まとまった処理に名前を付けるということがあります。はじプロでは処理の塊に明示的に名前を付けることはできませんが、それに代わる方法としてワイヤーワープを用いることができます。
前述の図では、ワイヤーワープ入口(ID=Q)に「カーソル位置に敵駒があるか」を判定した結果のシグナルを流し込みます。つまり、プログラムの別の箇所でワイヤーワープ出口(ID=Q)を参照することは、「Qと名付けられた、カーソル位置に敵駒があるかを判定する関数」を呼び出してその結果を取得することに相当します。
複雑で大規模なロジックを構成する際に、多数のノードンで構成された塊の代わりに1つのワイヤーワープ出口ノードンでまとまった意味を表すことで、コードの見通しが良くなります。
ワイヤーワープはプログラムの可読性を上げるための物であり、ノードン数上限の制約が厳しいはじプロでは、整理の対象となりがちです。最終的には削除することになるとしても、実装の初期段階ではワイヤーワープを活用してプログラムを組むことでロジックを抽象化して見通しをよくすることができ、バグを作りこむ可能性を軽減できると思われます。
4. ボードゲーム/パズル系のゲームの開発
以下では私が開発した経験のあるリバーシやチェス、マンカラなどを例にとりながら、ボードゲーム/パズル系のゲームを開発する際に役立ちそうなテクニックや考え方を紹介します。
対象のゲームによってルールや必要な要素は変わってくるためそのまま再利用できない汎用性が低いものもありますが、考え方の参考にはなるかと思われます。
4.1 全体の検討
ボードゲーム/パズルに限った話ではありませんが、ゲームを開発する際にはまず初めに作りたいゲームの要素を分割して洗い出し、それぞれをどう実現するかを検討する必要があります。ここでゲームの要素とは、ゲーム内で使用する物だけでなくルールや制約を構成するロジカルな要素を含みます。
この段階で、実現不可能な問題はないか、リソース的に足りそうかを確認し、問題がある場合には回避策を検討することになります。
以下では、ゲームを構成する様々な要素ごとに見ていきます。
4.2 盤
多くのボードゲームでは、マス目など何らかの形状の盤を使用します。盤の実装にはテクスチャを使用することになると思いますが、その際に次のような工夫ができます。
- 大きなモノに小さな1マス分のテクスチャを貼ると、繰り返しパターンで描画されて複数のマス目となります。ここでモノの中心にテクスチャの中心が来るように配置されるため、テクスチャの描き方に注意が必要です。
- パターンを繰り返す必要がない場合、テクスチャをそのままワールドに配置することでノードン節約できます。
4.3 駒
3.2.1節で示したように、ゲームで使用する駒はワールド上に配置するモノとして実現します。実現手段はいくつか考えられますが、ゲームのルールの要求事項を満たせる方法を選択する必要があります。
例として、私が作成したボードゲーム系の作品で採用した駒の実現方法を示します。
ゲーム | 駒の実現方法 | 理由 |
---|---|---|
リバーシ | テクスチャを貼ったモノを発射ノードン100 ×2つ(円柱/球) | 先手/後手のコマがそれぞれ最大で64個必要になるため。 |
チェス | オシャレなモノ 12種類(=6種類×2), 合計32体 ヒトノードン 2体 テクスチャを貼ったモノを発射ノードン10 ×2つ(円柱/球) |
チェスの駒としての見た目も兼ねてオシャレなモノとヒトノードン(キング)を割り当てる。先手と後手のキングを区別するために見えないオシャレなモノを連結。 プロモーションでクイーンになった駒はモノ発射で対応。 |
四川省※ | 直方体のモノに識別用の異なるサイズの見えないシンプルなモノを連結 | 36種類の麻雀牌を識別する |
※四川省は@pochittoさんが開発された「上海」をベースに、改造して作成しました。麻雀牌を識別するしくみもpochittoさんの作成されたものをそのまま使用させていただきました。
4.4 カーソル
操作する駒や移動先などをユーザに選択させるために、カーソルを使用することが良くあります。ここではその実装方法を整理します。
4.4.1 機能・実現方法
カーソルの主目的はユーザーに盤上の位置を指定させることです。これは以下のように実現します。
- カーソルの論理的な座標(マス目の座標)をカウンタで保持する。
- カウンタの範囲制限でカーソルの移動範囲を制限する。
- ユーザ入力に応じてカウンタを更新する。
- マス目の原点(見えない/動かないモノ)とカーソル(見える/動くモノ)をフリースライドで連結する。
- マス目の座標にマス目の寸法を掛けてワールド座標上の距離に変換したものをフリースライドへの入力とする。
カーソルで位置を指定した後は、その位置を対象に駒の判定や移動などの処理を行うことになります。よってカーソルは以下の機能を持つものを連結して実現することになります。
- 複合センサー(モノの識別)
- モノワープ入口/出口、モノ発射(モノの生成、移動、消滅)
4.4.2 ユーザ入力によるカーソル移動方法
ユーザ入力に対しカーソルを移動させる方法はいくつかあります。ゲームの仕様や使用可能なノードン数に応じて使い分けます。
- ボタンによる1マス単位の移動
- ボタンの押した瞬間によるシグナルで、カーソルを1マス単位で上下左右に移動します。シンプルでノードン数も少なく済みますが、多数のマス目を移動するにはボタンを押す回数が増えてしまいます。また、(特に二人対戦型の場合は)多くのボタンを移動に割り当てることになり、他の用途で使用できるボタンが少なくなります。
- スティックによる1マス単位の移動
- スティックを傾けて連続移動ができます。操作しやすいですが、移動する速さを適切に調節するために多少のノードンを必要とします。
- スティックによる自由移動
- カーソルをマス目単位ではなく自由に移動させます。カーソル移動自体の実装は容易ですが、カーソルの座標に応じて対象物をフォーカスするための計算が必要になります。
下図はスティックによる1マス単位のカーソル移動の実装例です。スティックを傾けた際に適切な速度でカーソルが移動するようにカウントするフレーム数を調整します。
4.4.3 内部処理用カーソル
盤上の指定した位置に移動して、そこにあるモノを識別したりモノの生成/移動/消滅を行うという機能は、ユーザが操作するカーソル以外にもプログラムの内部処理として必要になります。以下に例を挙げます。
- リバーシで、駒を置いた際に敵駒を挟むことができるかの判定、および敵駒を裏返すためのモノの生成/消滅。
- マンカラで、指定した穴の石を拾って、それ以降の穴に順に石を配っていく(モノの移動)
これらは基本的にはユーザ操作のカーソルと同じ仕組みで実現することになります。違いは、カーソルの移動をユーザの入力に基づいて行うのではなく、プログラム内で適切なロジックを実装して行うという点です。
4.5 進行制御
4.5.1 ターン制御とフェーズ制御
二人用の対戦型ボードゲームでは、基本的にプレイヤー1のターン(番)、プレイヤー2のターンが交互に行われます。また、片方のプレイヤーのターンの中で、ゲームの状態がいくつかのフェーズに分かれます。また一人プレイ用のパズルゲームの進行も同様にフェーズに分かれます。
このフェーズは、ユーザの入力操作の段階だけではなく、内部処理の中での段階も含まれます。また、ゲーム開始前のタイトル画面や説明画面、ゲーム決着後の結果表示画面などもそれぞれフェーズとして考えることになります。
ゲームの実行中は、現在のフェーズとターンによって実行する処理の流れを切り替えます。ターンやフェーズに番号を付けてそれらの情報をカウンター等で保持しておき、条件判定や2Dマーカー表示ノードンなどを使用してデータの流れを制御します。
4.5.2 入力制限
ユーザのキー入力に対し、プログラム内部ではカーソルの移動や複雑な判定ロジックなど、数フレームを要する処理が起動されることがあります。この時、処理が完了しないうちに想定外のキー入力が入ってしまうと、プログラムが予期せぬ動作をしてしまいます。それを避けるには、以下のような点に気を付けます。
- キー入力に関するノードンからのシグナルは、すべてターン制御、フェーズ制御のためのシグナルとAND(bool値の場合)または掛け算(数値の場合)を行ってから使用し、入力を受けつけているタイミングでのみ有効に処理されるようにする。
- 受け付けたキー入力によって時間のかかる処理(実行中にキー入力が割り込まれてはならない処理)を開始する場合は、直ちに次のフェーズ(=処理実行中)となるようにフェーズ管理を行う。
4.6 ゲームのロジック(ルール/仕様)の実現
ボードゲームやパズルのルールや仕様(の構成要素)には様々なものがあり、適した実現方法も様々です。ここでは代表的な考え方として以下の3つの例を挙げます。
- データフロー的な手法
- 処理フロー的な手法
- 数式を用いる手法
4.6.1 データフロー的な手法
ゲームのルールを実現する際にもっとも基本的な方法は、はじプロらしくデータのフローで「何をなすべきか」を表現することです。以下に例を示します。
【ドット&ボックスのルール】
「マス目を囲む4本目の線を引いたら、そのマス目を塗りつぶして自分のマス(=得点)となる」
線を表すモノを検知するための4つのさわっているセンサーをカーソルの上下左右に連結しておきます。これにより、あるマス目を囲む線がすでに3つ描かれていて自分が4本目を書き足そうとしていることを、さわっているセンサーの出力で判断できます。
よって、線を描く決定ボタンを起点として、さわっているセンサーの出力などを加味しながら必要な条件判定を行い、4本目の線である場合にモノ発射ノードン(マス目の塗りつぶし)やカウンター(得点計算)に適切なシグナルを流し込むようにノードングラフを構成することで、このルールを実現できます。
このように、ある操作を起点として、処理の対象となるものを中心に近くの少数のモノだけで判別や手続きが完了できるようなルールの場合は、ノードングラフで適切なデータフローを記述することで実現することができます。
4.6.2 処理フロー的な手法
次に、データフロー的な手法では実現が難しい例を見てみます。
【リバーシのルール】
「自分が新たに置く駒とすでに置いてある自駒で敵駒を挟んだら、裏返して自駒にする」
敵駒を挟んでいる状態の盤面は、さまざまなケース(方向、個数)が考えられます。これを効率的に一度に漏れなく判定するようなセンサーや条件判定の使い方は思いつきません。(ノードン数に制限がなく膨大な量を使用できるなら、可能かもしれませんが。)
そこで、命令型言語的な処理フローを用いて「どうなすべきか」の観点で上記のルールを実現する方法を考えることにします。
まず、上記ルールを以下のような手順として表します。
・以下の手順を8方向について繰り返す。
(1つの方向の判定処理)
・開始点から前方に進みがら、1つ以上の敵駒があることを確認する。(無ければ終了)
・その次に自駒があることを確認する。(無ければ終了)
・開始点から前方に進みながら、敵駒があれば、敵駒を除去し自駒を生成する。
・自駒が現れたら終了。
これを命令型言語的な処理フローとして整理し、制御の基本となる順次、選択、反復の組み合わせで表現します。そこで3.4節、3.5節、3.6節で述べたように、これらの制御構造をノードングラフに置き換えることで、上記ルールを実現することができます。
このように、ルールを実現するために広範囲または多数のモノを対象として判断が必要となるケースの場合、その手順をいったん命令型言語的な処理フローとして表すことにより、ノードングラフで実現することが可能となります。
4.6.3 数式を用いる手法
データフロー/処理フローの観点とは別に、ルールをプログラム上で効率的に表現するテクニックとして、数式を用いる方法があります。特に幾何学的な位置関係などの表現に有効です。
【チェスのルール】
「駒は種類によって移動できるマスに制限がある」
移動元と移動先の座標が特定の位置関係にあることは、数式で表して判定できます。こうすることで、ゲームのルールとして言葉や図で表現されている仕様をプログラムに落とし込みます。
以下の図は、チェスの駒の移動可否判定を数式で表した例です。
また、チェスではナイト以外の駒は他の駒を飛び越えて移動できません。移動経路上に他の駒がないことをチェックする場合も、数式を使うことで省ノードンで実現できます。
- 移動元のセルから移動先のセルまで、1マスずつカーソルをずらしながら他の駒が存在するかをチェックする。
- 1マス分のカーソルの移動量は以下の計算式で表せる。
- ΔX = (x2 - x1) / |x2 - x1|
- ΔY = (y2 - y1) / |y2 - y1|
この計算式は、ルーク、ビショップ、クイーンによる縦横斜めのすべての場合で共通です。
ここで、はじプロの割り算では割る数が0のとき割られる数をそのまま出力することを利用しています。
4.7 処理の一般化とノードングラフの共有
命令型のプログラミング言語では、実現すべき様々なケースの処理をシンプルで自然な形に一般化して整理することで、処理を関数やループ内部の処理として共有化し、見通しをよくすることができます。これはデータフロープログラミングのはじプロでも同様です。
特に使用できるノードン数の制限が厳しいはじプロでは、複雑なロジックのノードングラフを共有化して、省ノードンで実現することが重要になります。
4.7.1 先手と後手の処理の共通化
対戦型のボードゲームにおいて、先手と後手の処理はほぼすべてが共通となります。基本的には、3.7.1節で述べたように、共有する処理の起点となる単発シグナルを先手の番、後手の番でそれぞれ入力します。また、共有される処理の中で先手と後手で切り替えるべき部分がある場合、起点となる単発シグナルと同期して先手・後手の一方のための処理が実行されるようにします。
以下の図は、マンカラで先手と後手のカーソル移動のロジックを共有している例です。マンカラは二人のプレイヤーが盤を挟んで向かい合い、盤面の中心で点対称となっています。先手について実装したほぼすべてのロジックが、原点を中心とした点対称の位置に座標を変更するだけで後手の番にも使用できます。フリースライドのオフセット座標を保持するカウンターや、それらの値を更新するロジックは1つ分だけを実装し、後手の番では符号を反転して使用するだけで済みます。
4.7.2 繰り返しの処理の共通化
ボードゲームやパズルのルールでは、同じ操作や概念を複数のケースに適用して繰り返すことが良くあります。それらを個別に記述していては非効率であるため、共通の表現で記述するようにします。
4.6.3節で縦横斜め方向のチェック処理を共通の数式で表したのもその一例です。
ここではもう1つの例として、リバーシにおいて、置いた駒によって相手の駒を裏返す処理を一般化して、縦横斜めの各ケースを1つの共通処理とする方法を説明します。
- 方向i(=1~8)の8回分、同じ処理を繰り返す。
- 1つの方向の処理として、起点から進行方向にカーソルを1マスずつ「前進」「後退」させて自駒、敵駒の有無をチェックするロジックとする。
- 方向iにおける1マス分の「前進」はX方向、Y方向の増分として考える。
1右=(1,0), 2右上=(1,1), 3上=(0,1), 4左上=(-1, 1), ... 8右下=(1,-1)
上記のX増分、Y増分は論理演算子やマーカー表示で場合分けして求めるとノードン数が大きくなりますが、以下のように一般化して数式で表すことで少しのノードンで実現できます。
4.8 待ち時間への対応
ゲームのルールに関する判定処理や相手CPUの思考ロジックの計算に数秒程度の時間を要する場合、何もないままユーザを待たせるとユーザは待ち時間を長く感じたり、ゲームがフリーズしているのではないかと思ったりします。それを緩和するための工夫の一例として、内部処理での判定位置を表すカーソルが見えるようにテクスチャを設定することで、敢えて内部処理の思考の過程をプレイヤーに見せるという方法があります。これにより、何か頑張って計算しているように感じてもらうことができるかと思います。
4.9 ルール説明・操作説明
ボードゲームやパズルは、シューティングやアクションゲーム等に比べてゲームのルールや目的が複雑で直感的には分からないことも多いです。そのため、ルール説明や操作説明をゲーム内でユーザに伝えることが必要になります。
ルールを説明するにはことばつきモノノードンが便利ですが、使用できる個数制限が厳しくすべてを書ききることができないこともあります。そのような場合、最低限伝えるべき目的や基本ルールを説明し、あとは実際に触ってもらえば分かるというようにします。テクスチャで模式図などを描くのも効果的です。
ゲームの進行状況に合わせて操作方法を明確に説明することも重要です。特に操作に複数のキー入力やステップがある場合、今現在は何を求められていて、そのためにどう操作すればよいのかを、状況に応じて切り替えながら表示していくと明確に伝わります。
4.10 はじプロの制約による仕様検討
はじプロでは使用できるノードン数や処理性能の問題で、どうしても実現できないことがあります。既存のゲームの再現またはオリジナルゲームを開発するにあたり、作成したい仕様はすべて実現できるか、できないとしたらどこを変更または削除して実現可能な範囲でゲームを成立させるか、悩みどころとなります。
4.10.1 ノードン数による制約
作成したいゲームの仕様がノードン数512の制限の中に納まりきらない場合、部分的に仕様を削除したり変更したりする必要があります。ケースバイケースなので一概には言えませんが、ゲームとして成立するための必要最低限の部分、ゲームの面白さに直結する部分は優先的に残し、優先度の低い部分を削除対象として検討します。
一例として、私がチェスを作成した際の仕様の取捨選択を示します。チェスを嗜む人からすれば許容できない変更かとは思いますが、一応駒の動かし方くらいは知っている、という私と同程度の意識のプレイヤーを想定しています。(^^;
- 優先度が高い仕様
- 各駒の基本動作、交互に操作して駒を取り合う
- 優先度が低い仕様
- プロモーション(成り)での変更後の駒の選択 → プロモーション後はクイーン固定とした
- キャスリング(ルークとキングの位置の入れ替え)→ 削除
- アンパッサン(特殊な条件でのポーンの動き)→ 削除
4.10.2 処理時間による制約
ワールド上に配置したモノの情報を参照・更新したり手続き的なロジックをノードンで構成すると、1つの処理に数フレームを要します。1つのボードゲームやパズルのルール(=仕様)の中には複数の項目がありますが、それを実現するために処理すべき対象の駒やマス目などの数が多いと、それを実装することが可能だとしても実際にプレイする際に許容できない時間がかかってしまうことになります。そのような場合、はじプロにあった仕様に変更してゲームとして成立させる必要が出てきます。
下表では例として、私が作成したゲームで経験した実装困難な処理とその対策を示します。
ゲーム | 時間がかかる判定処理 | 筆者がとった対策 |
---|---|---|
リバーシ | 相手の駒を取ることが可能なマスが1つ以上存在するか(存在しない場合に自動でスキップしたい) | 相手の駒を取れない場合、ユーザが手動でパスを行うものとする(取れる駒がある場合にパスをするのを許容する) |
チェス | 盤上のどこかで王手をかけている箇所があるか(チェックメイト) | チェックメイトの宣言はなくし、通常の駒移動で相手キングを取れば勝利とする |
四川省 | 選択された2つの牌を結ぶ有効な経路(他の牌を通らず最大2回まで曲がれる)が存在するか | 2つ目の牌をユーザが選択する際に、1つ目の地点からのカーソルの移動で3回曲がったらやりなおしとする |
チェッカー | 相手の駒が取れる状況でパスをしてはならない | 実装を断念(^^; |
4.11 相手CPUの思考ルーチン
二人対戦型のゲームを一人でプレイしてもらうためには、対戦相手のCPUのロジックをプログラムで実装する必要があります。
4.11.1 実現方法
多くの場合、二人プレイ用のゲームにおいてそれぞれのプレイヤーがキー入力で決定する部分のノードングラフの一部分を改変し、一方のプレイヤーの入力処理の代わりにプログラムのロジックで決定を行うように置き換えたものとなります。
下図の実装例では、黒字の部分が先手(プレイヤー)の操作に関するロジックで、赤字の部分が後手(CPU)の思考ロジックに関する操作です。
CPUの思考ロジックの実装方法は、ゲームのルールの判定や内部処理のロジックの実装と同様です。思考ロジックを命令型言語的な処理の手順として整理し、それをデータフロー的なノードングラフに置き換えて実装します。
4.11.2 実現可能な思考ルーチンの制約
賢く高度なAIを記述することははじプロのリソース制限の中ではそもそも実装が不可能ですので、ゲームとして成立する程度の最低限の簡易的なAIを目指すことになります。それでも、打てる手の選択肢が多く、ルールに従った有効な手を1つ見つけるのにも必要な探索処理の実行回数が多くなるような場合、その処理の実行にゲームとして許容できない時間がかかってしまい、断念せざるを得ないことになります。駒やマス目の個数が多いゲームのほとんどはこれに該当すると思われます。
逆に、マンカラのようにプレイヤーの手の選択肢が少ない(6つの穴から1つを選択する)ような場合には、実現が可能でした。例として、私が作成した一人プレイ用のマンカラではCPUの思考ルーチンを以下のように設定しました。
- 自分の左側の穴から順に、その穴を選択した場合に相手の石を取れるか(以下の4つの条件を満たすか)を判定し、取れる場合はその穴に決定する。
- 穴に石があるか。
- その穴から種蒔きを開始して、終点が自陣の穴になるか。
- 終点の穴は(種蒔き実行前に)空であるか。
- 終点の向かい側の敵陣の穴に石があるか。
- 右端に到達しても相手の石を取れる穴が見つからなかった場合、逆順に戻って石のある穴を探す。最初に見つけた石のある穴を選択する。
最後に
ボードゲーム/パズル系のゲームの開発を通して私が学んだノウハウを整理して記事にまとめました。
元々ゲーム開発時にはそこまで体系立てて考えていたわけではなく、なんとなく経験的に蓄積した方法であり、とても漠然としたものでした。今回、このアドベントカレンダーに参加するため初めてQiitaの記事を書いたのですが、漠然とした理解を文章として出力するうちに、読者に伝わるようにするにはもう少し補強や具体例が必要だ、と加筆修正を繰り返し、最終的には結構なボリュームとなってしまいました。
最後まで読んでいただきありがとうございました(^^)。
参考図書
@cabbagestoleさん著の、はじプロの定石から「おめくり式」のような高度なテクニックまでを紹介している貴重な参考書です。
付録:筆者の作成したゲーム
私が作成したボードゲーム/パズル系の作品リストです。プログラムが読みやすい作りになっていなかったり、本記事の解説とは異なる実装になっている部分もあったりしますが、よかったら参考にしてみてください。
ゲーム | ジャンル | プレイ人数 | ゲームID |
---|---|---|---|
リバーシ | ボードゲーム | 2 | G 003 4DD W8N |
チェス | ボードゲーム | 2 | G 001 9NK CYY |
ドット&ボックス | ボードゲーム | 2 | G 005 KL2 44B |
マンカラ(二人用) | ボードゲーム | 2 | G 004 X3P K50 |
マンカラ(一人用) | ボードゲーム | 1 | G 008 R9D LHR |
ペグ・ソリテール | パズル | 1 | G 003 0GL PF3 |
四川省 | パズル | 1 | G 006 KP5 X18 |