プチコン3号 19日目 SmileBASICでOOP(2)ポリモーフィズム

ゲームプログラムでは、例えばシューティングなら自機、敵機、自分の弾、敵の弾など、さまざまなオブジェクトが登場します。
各オブジェクトは挙動が異なりますが、一方で画面上での位置などの更新、ゲーム開始時の初期化、イベント発生時の処理(たとえば自機がやられた場合シーンの最初からリスタートするなど)など、共通性の高い面もあります。
そういった処理は、実際の処理内容が異なっても、同じ名称(「更新」「初期化」「リスタート」など)で呼ぶのが自然に思われます。

オブジェクト指向プログラミングでは、手続きや関数はオブジェクトの属性の一種なので、異なるオブジェクトが同一の名称の手続きや関数を持ち、それらの処理内容がオブジェクトによって異なるのは普通です。
よく言われる例えは

・オブジェクト「犬」が「鳴く」→ワン
・オブジェクト「猫」が「鳴く」→ニャア

というもので、異なるオブジェクト「犬」「猫」が、同じ「鳴く」という名称の処理をすることができ、その結果は犬では「ワン」猫では「ニャア」となります。

このような特性をポリモーフィズム(多相性とか多態性と訳されています)と呼びます。

SmileBASICはオブジェクト指向言語ではないので、ポリモーフィズムを備えていません。
そのため、一般的には以下の2つのどちらかの手段を使うことになります。

(1)処理「ワン」や処理「ニャア」にそれぞれ別の名称を与える

DEF 犬_鳴く
ワン
END

DEF 猫_鳴く
ニャア
END

(2)処理「鳴く」を定義し、IF文で分岐する

DEF 鳴く(動物)
IF 動物==犬 THEN ワン :RETURN
IF 動物==猫 THEN ニャア :RETURN
END

これらはどちらも利用できますが、

(1)の場合:処理の名称が多数になり、覚えにくい。呼び出し側でも使い分けが必要。
(2)の場合:動物の種類が増えるたびにIF文を追加しなければならない。

という問題があります。

実際には、(2)の方法だけでは処理本体が長くなってしまいますので、(1)と組み合わせて

DEF 鳴く(動物)
IF 動物==犬 THEN 犬_鳴く:RETURN
IF 動物==猫 THEN 猫_鳴く:RETURN
END

DEF 犬_鳴く
ワン
END

DEF 猫_鳴く
ニャア
END

のように書かざるを得ず、(1)(2)両方の問題を抱えることになります。

ポリモーフィズムはこのような問題を解決してくれます。
BASICではポリモーフィズム自体は使うことができませんが、前回から解説している「スプライトを使ったオブジェクト指向風プログラミング」では、(2)の問題についてSmileBASICの「CALL」命令を使って対策しています。

CALL命令は文字列を命令として呼び出す命令で、

CALL STRING$,パラメータ

のように使います。
例えばSTRING$の中身が”PROC”であれば

PROC パラメータ

が実行されます。
この命令により、プログラム内で生成した文字列変数を命令として呼び出すことができます。

従って、クラス名(”犬”、”猫”)と処理名(”鳴く”)が文字列で与えられれば、これらからそれぞれの処理「”犬_鳴く”」「”猫_鳴く”」を生成して、それを命令として呼び出すことが可能です。

CALLを使うと、命令を直接呼び出す場合に比べてオーバーヘッドは生じますが、実測では直接呼び出す場合と比べて20%程度の処理時間増でしたので、それほど大きなオーバーヘッドではないと思います。

実際の使用例として、プチコン3号でリアルタイムゲームを作る場合を考えます。
ゲームのメインループを極端に単純化すると、以下のようになります。

@LOOP
FOR I=0 TO __MAXOBJ-1
SP_UPDATE I
NEXT
VSYNC 1
GOTO @LOOP

ここで、SP_UPDATEが、各オブジェクトの位置などを更新する処理です。

SP_UPDATEの処理を普通に書くと先ほどの(2)のように

DEF SP_UPDATE OBJECT
VAR CLASS
CLASS=SP_CLASS(OBJECT)
IF CLASS==CLASS1 THEN CLASS1_UPDATE OBJECT
IF CLASS==CLASS2 THEN CLASS2_UPDATE OBJECT
IF CLASS==CLASS3 THEN CLASS3_UPDATE OBJECT
       :
IF CLASS==CLASSN THEN CLASSN_UPDATE OBJECT
END

となってしまいますが、前回の記事にあるクラス名の配列__CNAME$とCALL命令を使うと、これを以下のように書くことができます。
__CNAME$[]は文字列の配列で、SP_NEWCLASSのパラメータで与えたクラス名が__CNAME$[CLASS]で参照できます。

DEF _SP_BIND_METHOD(SELF,METHOD$)
VAR CLASS,FM$
CLASS=SP_CLASS(SELF)
IF CLASS == _S_NULL THEN RETURN ""
IF CLASS != SELF THEN
FM$ = METHOD$+__CNAME$[CLASS]
ELSE
FM$ = METHOD$+__CNAME$[CLASS]+"_C"
ENDIF
RETURN FM$
END

DEF _SP_CALL_METHOD0 SELF,METHOD$
VAR FM$
FM$ = _SP_BIND_METHOD(SELF,METHOD$)
IF CHKCALL(FM$) THEN CALL FM$,SELF
END

DEF SP_UPDATE SELF
_SP_CALL_METHOD0 SELF,"UPDATE_"
END

_SP_BIND_METHODは、オブジェクトと呼び出したい処理名を受け取り、オブジェクトのクラス名を処理名の後に”_”で連結した文字列を返します。
オブジェクトがクラスオブジェクトだった場合には、さらに末尾に”_C”を付加します。

_SP_CALL_METHOD0は、_SP_BIND_METHODで生成した文字列にあたる命令が存在するかどうかチェックし、存在していればその命令をパラメータ無しで呼び出します。
(「_SP_CALL_METHOD0」の名称の最後の「0」は「パラメータ無し」を表すために付けました。)

SP_UPDATEは、与えられたオブジェクトについて、_SP_CALL_METHOD0で”UPDATE”処理を行います。

これをメインループの中から呼び出すと、全てのオブジェクトについて、そのオブジェクトの所属クラスに応じて”クラス名_UPDATE オブジェクト”という命令が実行されます。

この方法の良いところは、クラスを追加したときに、”クラス名_UPDATE””クラス名_UPDATE_C”という命令をDEFで定義しておけば、その命令はメインループから勝手に呼び出されるという点です。
メインループには何も書き加える必要がありません。

なお、本当は”クラス名_UPDATE”というDEFを書くよりも、例えばrubyのように

class 犬
def 鳴く
end
end

class 猫
def 鳴く
end
end

という記述をしたいところです。
ソースコード操作関連の命令を使えば、この記述をSmileBASICでの記述に変換するようなプリプロセッサを作ることは可能かもしれません。

コメント