読者です 読者をやめる 読者になる 読者になる

スクリプト仕様の再設計3

フリーゲーム作製


# 脳内メモ.現在進行形で考えていることを適当に書き連ねてみる.

車輪の再発明の意味

# 前回偉そうに「再利用性を考慮しない」とか書いておいて,なんですが.
BulletML の再発明をしてみたいと思います.
...というのは,もちろん言い過ぎなのですが,シューティングのスクリプトを考える上で,
その核となる仕様は,ある程度 BulletML に似たものになってきちゃうと思います.
よく「車輪の再発明は無駄,有るものは有効に使っていこう」という話があります.
自分は stl や boost を知らない頃,よく「車輪の再発明」をしてましたが,
その頃考えた経験や,書いたコードは,現在でも非常に役立っています.
車輪の再発明は非常に有効な学習手段であると思います.
# 3日に渡る言訳終了


# と言うわけでここから先は,いよいよ本格的にチラシの裏
# リファクタリングするにあたって,まず現行スクリプト仕様を書きなぐって,
# で,おかしいなと思った所を修正してみました.



# 基本的にはリファクタリングが大目的なので,スクリプト仕様はそこまで変化しません.
# ので,「シューティング用スクリプト2」で晒しているスクリプトが多少参考になるかもしれません.

■ スクリプトのフォーマット

メモ帳で書けるのが絶対条件.
極力特殊な外部アプリケーションに依存しないようにする.
1. 1行1コマンドで上から下に順に処理.コーディングが楽だから.
2. コマンドはスペース区切り,引数はカンマ区切り.
3. 大文字/小文字は区別しない.直感で書けるから.
4. 行頭"//" でコメントアウト.コメント行は絶対必要.
5. 数値引数は,多くても4個まで.無指定なら0を代入.覚えられないから.
6. 階層構造が可視である必然性は無いと思うので,XMLは不採用.
7. include "file_name" で,他スクリプトの読み込み.スクリプトの分散は重要.
シンプルに,↓形式を採用

// Comment
[COMMAND]  [Param1], [Param2], ...
[COMMAND]  [Param1], [Param2], ...
...

■ スクリプトの管理方法

スクリプトは,アプリケーションの初期化時に読み込み,メモリ上に中間コードとして展開する.
毎回実行時に,文字列→中間コードの変換を行う,いわゆるインタプリタ形式を採用.
コンパイラを通してバイナリデータにするステップは,毎回やるには結構うざい.
ただし,リリース時には,バイナリデータに変換する可能性あり.
中間コードは,一つの巨大な単方向チェインリストとして管理する.
この中間コードを順次参照することで,スクリプトが実行される.
分岐コマンドが有るので,必要に応じて分岐先ポインタも記録する(通常はNULL).

// 中間コードクラス
class CCommand
{
//  単方向チェインリスト
private:
  CCommand* pNext;        // 次
  CCommand* pJump;        // 分岐先

//  コマンド要素
public:
  int     iCommandID;     // コマンド ID
  DWORD   dwValueID;      // 結果を代入する変数の ID
  // パラメータ
  char*   strParam;       // 文字列パラメータ
  union {
    float         fParam[_MIDCODE_PARAM_MAX]; // パラメータは float 限定
    struct {                                  // ↓数式の場合に限りこっち
      float*      pfTarget;                   // 数式の答えの代入先
      CFormula*   pForm;                      // 数式本体
    };
  };
};

数式は1コマンドとして管理.コマンドのパラメータ内に数式が入っている場合,
そのコマンドの前に数式コマンドを挿入して,計算して結果をコマンドパラメータに渡す.

スクリプト; 
  vel      rank*10, a+b*5, 0
  ↓3コードに分解.
中間コード; 
  Formula  (pfTarget = &param1) "rank*10"
  Formula  (pfTarget = &param2) "a+b*5"
  vel      param1, param2, 0

■ ファイバとオブジェクト

「ファイバ」は,スクリプトを実行するスレッドのような存在.
「オブジェクト」は,「敵」とか「弾」とか「ショット」とか,ゲーム内に存在する物体.
これら二つは,バラバラに管理/実行する.
これは,一つのオブジェクトが複数のスクリプトを並行して実行できるようにするため.
「40 Frame でジグザグ動く敵が rank*20+10 Frame 間隔で弾を発射する」
という動きをなるべくシンプルに表現するには,一つの敵オブジェクトが,
動きスクリプト と 弾発射スクリプト の2つを並行して実行出来る方が良い.
「オブジェクト=ファイバ」の方がシンプルだが↑こういった自由度が無くなるので不採用.
(Nomltestは,オブジェクト=ファイバ)

■ スクリプトの実行方法

ファイバは,中間コード内の現在実行中の位置をポインタとして保持する.
ファイバは,必ず1つの操作対象オブジェクトを持っている.
操作対象オブジェクトの無いファイバは,スクリプトを実行する事が出来ない.

// ファイバの基本クラス
class IInstance
{
protected:
  CCommand*   m_pPointer;                 // 現在の実行位置を示すポインタ
  CCommand*   m_pJumpStac[_PTRSTAC_MAX];  // SubRoutine 実行用ポインタスタック
  int         m_iJumpStacCount;           // ポインタスタックの最表面インデックス
  int         m_iWaitCounter;             // wait用カウンタ
};
class CFiber : public IInstance
{
private:
  IFiberObject*   m_pObject;       // 操作対象オブジェクト
  LIFE_STATUS     m_enStatus;      // Fiberの状態
};


オブジェクトは,メンバ変数の値を参考に単純な行動のみ行う.

// オブジェクトの基本クラス(一部を抜粋)
class IFiberObject
{
public:
  CVector     m_pos;          // 座標
  CVector     m_vel;          // 速度
  CVector     m_acc;          // 加速度
  float       m_fAgingSpeed;  // 老いる速度
  float       m_fAge;         // 年齢(誕生;0/老衰;1)
	
  // 毎フレーム呼び出される.
  inline bool MoveObject()
  {
    m_pos += m_vel + (m_acc * 0.5f);
    m_vel += m_acc;
    
    m_fAge += m_fAgingSpeed;
    return (m_fAge > 1.0f);
  }
};

ファイバは,スクリプトを読んで,操作対象オブジェクトのメンバ変数を書きかえる.
見かけ上,オブジェクトがスクリプトに従った動きをするようになる.


例えば,敵オブジェクトについて,
コマンド「vel 0, 10, 0」によって,速度のメンバ変数 m_vel が,
m_vel = CVector(0.0f, 10.0f, 0.0f) と書きかえられる.すると,
オブジェクトは,上記MoveObject()関数内で,毎フレーム Vy = 10.0f の
等速運動を続けるようになる.

■ 変数

オブジェクトのメンバ変数(座標x,y,z,速度vx,vy,vz,年齢age など)は,
スクリプト中の数式内で変数として扱えるようにする.
gl.rank(ランク), gl.score(得点), gl.time(経過時間)なども扱えるようにする.


また,それ以外に,自由に扱える変数があった方が,何かと便利なので,
各オブジェクト内に,浮動小数点型の汎用レジスタを8個用意する.
これらは,"a-h" の名前で,スクリプトの数式内で参照/代入が可能.

スクリプト例)
a = gl.rank * 5 + 10
vel  x * 0.5, a, 0

汎用レジスタはファイバでは無く,オブジェクトが保持する.
そうすると,同一オブジェクトを操作しているファイバは,共通のレジスタを使うため,
連携が取りやすくなる.ただし,同じレジスタを使わないように工夫が必要.


# ...分かり難いなぁ...


(つづく)