どうもです、タドスケです。
ゲーム開発で使えそうな部品を提供するこのシリーズ。
今回はシンプルなタスクシステムを作ってみました。
この記事内で公開しているコードは全て自作なので、個人/商用問わずご自由に流用いただいて構いません。 ただしコードを利用して損害が出た場合でも、その責任は負いかねます。
コード
コードは以下の場所に公開しています。
使いかた
タスククラスの基本的な使い方は以下です。
- TaskBase を継承したクラスを作る
- _update 関数をオーバーライドする
- 一番親のタスクの update 関数を呼ぶ
- 親子関係にある全てのタスクの _update が順番に実行される
class TaskA(TaskBase):
def _update(self, delta_sec: float) -> None:
print('A')
class TaskB(TaskBase):
def _update(self, delta_sec: float) -> None:
print('B')
a = TaskA()
b = TaskB(a)
a.update(0)
# A
# B
タスクの機能
タスク(TaskBaseクラス)には以下の機能があります。
- 更新処理
- 親子関係の制御
- 終了フラグ
更新処理
TaskBase クラスの更新処理(update 関数)は2種類あります。
def update(self, delta_sec: float) -> None:
"""更新.
:param 前回更新からの経過時間(秒)
"""
if self._exit:
return
# 自分の更新
self._update(delta_sec)
if self._exit:
return
# 子の更新
for child in self._children:
child.update(delta_sec)
# 終了している子を削除する
self._apply_exit_children()
def _update(self, delta_sec: float) -> None:
"""自分の更新."""
pass
アンダーバーが付いていないほう(public)が子も含めた更新処理で、アンダーバーが付いているほう(protected)が自分のみの更新処理です。
update 関数内では、まず自分の更新処理(_update 関数)を呼んでから、子の更新処理を順番に呼んでいます。
引数の delta_sec には前回更新からの時間(秒)が入るのですが、今回のサンプルでは使っていないので説明を省きます。
親子関係の制御
タスクは複数の子を持つことができます。
今回は、子タスクをリストで持つようにしています。
self._children: list[TaskBase] = []
親タスクの update が実行されると、子→弟の順で _update が実行されます。
このとき兄弟関係よりも親子関係の方が優先されることに注意してください。
例えばテストコード側にある以下のコードでは、abdc の順で各タスクの update が呼ばれます。
# a
# -b
# --d
# -c
a = TaskA()
b = TaskB(a)
c = TaskC(a)
d = TaskD(b)
a.update(0)
self.assertEqual('abdc', _order)
図にすると以下のようになります。
このような実行順を「深さ優先」とも言います。
詳しくは以下の記事で解説していますので、参考にしてください。
終了フラグ
exit 関数を呼ぶと、タスクの終了フラグが立ちます。
終了フラグが立っているタスクは、update 後に親がまとめて削除します。
その場で削除しないのは、子タスクが親の意図しないタイミングで子のリストを勝手に操作して、別の子の処理に支障が出てしまうのを防ぐためです。
割り込み処理をできるだけ減らすことで、難解なバグを起こしにくいプログラムにできます。
活用のアイデア
アクションゲームであれば、キャラクター一体の処理をそのままタスクにできます。
おともキャラクターの妖精、オーラエフェクトなどを子タスクにすれば、キャラクターの移動に合わせて一緒に移動させたりすることもできます。
キャラクターのタスクが終了したら、妖精やエフェクトのタスクも一緒に止まるため、「親が消えているのに、エフェクトだけが取り残される」などのバグは起きなくなります。
以上、タスクシステムの解説でした!
コメント