タスクシステム解説

始めてまともなゲームプログラムっぽいネタかもしれない.
え〜,ゲームでよく使われている技術のひとつにタスクシステムというものがあります.ただ呼び方は今ひとつ定着していないらしく,単にタスクと読んだり,ゲームタスク,タスクコントローラ,珍しいところではジョブコントローラという呼び方もあったようです.日本以外では,Actor,GameEntity,GameObjectなどの呼ばれ方でタスクシステムという呼び方はしていないそうです.私はアクターという呼び方が好きですが,あえてここではタスクという言い方で統一しています.

■タスクシステムとは

簡単に言えば汎用的作業をひとまとめにしたものがタスクシステムと言われますが,その中は単なるリストなどで構成される配列やコンテナである事が一般的です.STLを使うならstd::listで実装されたものを使うのが一般的です.もちろん自前のリストやコンテナを使うというのもありです.boostには便利なライブラリがあり,タスクを作るのにも色々適しています.


タスクでやりたい事というのは様々ですが,主に以下のようなサブシステムがあります.

  • 周期的なリアルタイムアップデート
  • メッセージとイベント処理
  • Luaなどのスクリプトとの連携
  • ゲームフローと直結した一元の管理
  • タスク固有IDからのクエリー参照


ここでは実装自体に関しては解説しませんが,それぞれが一体どういう役割でどういうメリットがあるのかという解説をしてみたいと思います.実際にタスクとして実装する際には以下の記事などが役に立つかもしれません.


近代的タスクシステムの構築

■周期的なリアルタイムアップデート

どこかのタイミングで一斉更新をかけて,タスク自身の自律行動を行います.ここでは場合によっては描画も必要になるかもしれません.しかし描画と行動は切り分けておいた方がいいでしょう.例えば描画が常に行なわれていても行動は常に行なわれているとは限らないからです.


よくあるケースはゲームのポーズ画面です.ここでは描画だけを行い,全てのタスクの行動を停止させるかもしくは一部のタスクを除いて完全に処理を行わせないようにします.結果的にポーズ画面では絵だけが描画されている状態で,全く動きがない状態を作れます.これは逆手にとるとプレイヤーだけが動いて敵の動きを停止させるような攻撃を実装する際にも使う事が出来ます.とにかく描画と行動を切り分けておいて損するような事はないでしょう.

■メッセージとイベント処理

タスク同士での相互メッセージのやりとりが必要になるケースがあります.例えばボスが雑魚に対して攻撃命令を発する時や,人がドアを開けたりする際などそのケースは様々です.メッセージ自体を独自のイベントシステムとして作ってしまうのもありなんですが,イベントというのはとても数が多いものなので,タスク上で一元管理されていた方が何かと都合が良いものです.


実装自体は様々な方法がありますが,単純にタスク側へとメッセージが発生した際にコールバックとして呼び出す方法が最もわかりやすく,一般的な方法だと思います.イベントメッセージ用の定義リストだけを作って,イベントが増えるたびに登録していくようにしましょう.

Luaなどのスクリプトとの連携

昔のタスクと現在のタスクとの最大の違いはここかもしれません.タスクの動作に対してフック出来るように作っておき,C++のコード上にLuaなどに代表されるスクリプト言語を使用出来るようにします.ゲームによっては行動部分の大部分をスクリプトで書く場合もありますので,重要な部分でもあります.ただ何でもスクリプト化すればいいというものでもなく,当然ボトルネックになるような処理が出てきた場合はC++のコードで書く必要もあります.


上手いスクリプトとの連携はゲームの開発効率と直結します.これが上手く出来ているだけで開発時間が半分以下になるというのも珍しくありません.タスクシステムと連携出来るスクリプトは非常に有用ですので自身の技術に自信があれば実装してみることをオススメします.

■ゲームフローと直結した一元の管理

ゲームシーンというのは様々な状況がありますので,それと連携出来る管理があれば便利です.例えば状況によって雑魚だけを全滅させたい,もしくは特定のタスクだけを生き残らせたいというのであればタスクの属性に無敵などのようなものをもたせておくと便利です.進行フローによってタスクの生死状態も様々なものが起こりえますし,万が一に死ぬような状態にならなくなってしまったタスクがでてしまった場合でも一律に処理出来たりします.(この場合はむしろそういう状態を作るべきではないですが)


タイトル画面には敵はいりませんし,エンディングでプレイヤーが動きまわるような事はまずありません.そういった場合でも抑制がかけられるかそういう機能があるだけでもバグがとても出にくくなり結果的に開発はよりスムーズに進められる事になるでしょう.

■タスク固有IDからのクエリー参照

タスク自身が持つ固有IDからタスク本体の情報を取り出します.またID以外の複合条件も使用出来ればより様々な状況に対して参照をする事が可能となります.相手の情報がわからないとAIを作る時などにどう行動すればいいのかわかりません.最低限の情報だけでも知るためにもこの機能は必ず必要となってきます.しかし膨大なタスクの中から特定のタスクを探しだすという作業は決して軽い処理ではないので場合によっては控える必要もあります.


スマートポインタやハンドルなどを用いて実装します.情報が知りたい側からは相手が生きているか確認する術がないからです.もし,削除されて開放されたポインタにアクセスしようものなら一瞬にしてゲームは停止します.これはごく当たり前の事ですが,これが原因でハングアップしているゲームはとても多いです.とにかく実装する際は注意してスマートポインタやハンドルの理解を深めておきましょう.

■その他の問題

特に生成と削除が多いタスクはメモリに優しくありません.ゲームによっては固定長の数しかタスクを作れなくしていたりして最初から静的なメモリしか使わないというケースもあります.メモリプールという固定長のメモリを動的に確保し,その中でタスクを回し続けるという手段もあります.とにかくメモリが少ないゲーム機では死活問題となり,メモリのフラグメント化は死への一直線なので注意する必要があります.PCで開発する際はそこまで気にする必要はないかもしれません.


タスクの数が異常に増えたりする際は複数のタスクリストやコンテナに分けた方が管理面でも速度面でも良くなったりします.特に大量に当たり判定の計算などをする際は注意が必要です.タスクの数が増える場合は多少面倒でも分けておいた方が後々の苦労は少なくなるでしょう.


タスクの並列化はなかなか魅力的な話ですが,簡単な事ではありません.もちろん不可能ではありませんので,速度を追求するとか,異常な数のタスクを走らせてみたい!!という方にはオススメしたいところです.ただ,今回はそれを語るところがないのでまたの機会に.少しだけなら以下の記事でも触れられています.


西川善司の3Dゲームファンのための「ロスト プラネット」グラフィックス講座

■タスクとの付き合い方

タスクシステムは決して必須なものではありませんが,ある程度大規模な開発をしようものならほぼ必須なものとなってきています.便利なものであればあるほど,開発の効率も上がり,チーム開発では作業の分担がしやすくなっていい事尽くめになってきますので,ぜひ難しく考えずに習得しておきたいところです.