この記事は15分程度で読めます。
ゲームプログラミングになじみがない人向けの短めコラムです。
はじめに
この記事では、対戦ゲームにおける同着や相打ちをどのように実装すればよいか、その考え方を紹介します。プログラミング初学者の場合、同着処理が実装されていないことによって、複数のプレイヤーが同着となった場合に必ずプレイヤー1が勝ってしまう(負けてしまう)実装になっていることがあります。この記事では、複数のプレイヤーが存在する対戦ゲームのシナリオを例として紹介し、同着・相打ちについて、プログラミング初学者が気づきを得る手助けをしたいと思います。
プログラミング言語を知らない人でもある程度読めるように、疑似言語を使って短めのコードで記載するので、なんとなく英語の箇条書きで処理の流れが書かれているんだなと思いながら読んでみて下さい。
ゲームループ
ゲームが動作している間繰り返し動くコードのことを一般的にゲームループと呼びます。この処理は、ある時点における様々なイベントを観測したり(プレイヤーのキー入力を受け取ったり)、ゲームの状態を更新したり(重力を与えてものを動かしたり、ダメージを与えたり)、ゲームの状態を描画(現時点の世界をモニターに描画)したり、ゲームの基本構造を書く部分となります。基本的には以下の3つの処理をゲームが終わるまで無限にループするため、ゲームループと呼ばれています。
- イベント処理: プレイヤーからの入力やその他のゲームイベントを処理します
-
状態更新: ゲームの状態を変更します
(プレイヤーの移動、ダメージ処理、ゴール判定/死亡判定など) - レンダリング: ゲームの現在の状態を画面に反映します
※1と2は順番が逆なこともあります
疑似言語を使って書くとこんな感じになります。
while (Game.isRun()) {
Game.input()
Game.update()
Game.render()
}
ゲームが続く限り(while is running)3つの処理(input、update、render)を繰り返すということです。
同着や相打ちの処理はこの状態更新 Game.update() の箇所で行われます。プログラミング言語を全く知らない方は、中括弧で書かれた箇所が上から順番に繰り返し行われるのかな、と想像しておいてください。
状態更新
前述の 2.状態更新、ここでは例えば、各プレイヤーの位置、体力、所持アイテム、パラメータなどのゲーム内数値を計算したりすることで、ゲームの状態を1フレーム更新します(フレームというのはゲームにおける時間の単位です。現実世界における秒みたいなものです。1秒進むとりんごの位置が重力で下に10cm落ちた、とかそういうことをここで計算します)。
今回はプレイヤーの同着に焦点を当てているため、りんごの話は置いておいてプレイヤーの話をします。全てのプレイヤーの状態をそれぞれ更新していく必要があるため、プレイヤー1人ずつ順番に処理していくコードを書きます。これにより、一人一人の状態が個別に、かつ公平に管理されます。これはプレイヤーループなどと呼ばれ、例えば、以下のような疑似言語で書くことができます。
for (player in players) {
player.update()
if player.isDeath()
death(player)
}
全てのプレイヤーについて(for player)、順に update() を行い、死亡時(if player is death)には death() の処理を行うというコードです。
りんごの処理が必要ならそれも書いてください。
for (player in players) {
player.update()
if player.isDeath()
death(player)
}
for (apple in apples) {
apple.update()
}
処理順序と公平性
プログラミングにおいて、コードは記述された順序に従って上から下へと実行されます。ゲーム開発では、この特性がプレイヤー間での公平性に影響を与える可能性があります。例えば、前述のプレイヤーループをループを使わないコードに展開すると以下のような疑似言語で記載できます。プレイヤーが2人の場合、このコードは前述のコードと同じ意味になります。ループを無くした代わりに、プレイヤー2人分のコードを上から順番に並べただけです。
player1.update()
if player1.isDeath():
death(player1)
player2.update()
if player2.isDeath():
death(player2)
ここで、update() や death() でどんなコードを実装すべきかなんとなく想像してみて下さい。
プレイヤー1の状態を更新する際に、プレイヤー1が死亡しているかどうか?、ゴールしているかどうか?の判定を行う必要がありますが、ここでプレイヤー1の順位や得点、勝ち負けを確定できるでしょうか?
もちろんできません。なぜならプレイヤー2が死亡しているかどうか?、ゴールしているかどうか?の判定がまだ行われていないため、仮に同着・相打ちだった場合には、プレイヤー1の順位や得点、勝ち負けが変動する可能性があるからです。プレイヤー数が2人以上いるようなバトルロワイヤルゲームの場合も同様です。
相互作用
前述の状態更新は、プレイヤーのキー入力などのアクションに基づいて処理されますが、加えて、他のプレイヤーがどういう行動をとったか(攻撃した、防御した、ゴールした等)の相互作用により状態が変動することがあります。同着や相打ち処理はこの相互作用の1つです。
同着・相打ち処理が必要となる場合、前述の疑似言語はどのように記載するのが正解だったのでしょうか?プレイヤー1の状態を更新する際に、プレイヤー2の状態を知っている必要がありましたがどうすればよかったでしょう?
答えはシンプルですが、解決策の1つとしてプレイヤーに関するループを2回記載すればよいです。
for (player in players) {
player.detect()
}
for (player in players) {
player.update()
if player.isDeath()
death(player)
}
1回目のループでは全ての処理を行うのではなく、死亡しているかどうか?ゴールしたかどうか?などの判定のみ抜粋して行います。このループは形式上プレイヤーに関するループの記述に見えますが、やっていることはゲームの状況を一旦集計する事前作業のようなものになります。ここでは各プレイヤーに関するフラグの更新や、ゲーム全体の情報の集計(生存者数、確定済みの順位など)を行うことが多いです。
2回目のループでは、他に死んだプレイヤーが何人いたか、ゴールしたプレイヤーがいたか、といった事前作業の情報を元にプレイヤーの得点や勝ち負けを決めていく、プレイヤーごとの処理として記述できるようになります。
このように判定や集計を行う処理を元のループから分離して事前に処理(pre process)し、実際の処理(process)をその後に行い、最後にゲーム全体の事後処理(post process)をするようにコードを3つに分離すると、全体の Game.update() はざっくり3つのブロックに分かれます。
function update() {
// pre processing(プレイヤー全体に関する集計や判定など)
for (player in players) {
player.detect()
}
// player processing(集計結果に基づいてプレイヤーごとに処理)
for (player in players) {
player.update()
if player.isDeath()
death(player)
}
// post processing(今回のループの終了処理、次のループに向けた準備など)
・・・省略・・・
}
ここで重要なことは、同着・相打ちの処理を実現するためには、判定と処理を2つに分ける必要があるということです。
まとめ
これまで同着処理について考えたことがなかった人はこれを機に同着処理を実装してみて下さい。当たり前のことですが意外と実装されていないゲームもあります。