Redpoll's 60
 Home / 3Dプログラミング入門 / 第2章 $§$2-16
第2章 2D空間におけるオブジェクトの運動
$§$2-1 オブジェクトの初期状態$§$2-17 衝突判定 5
$§$2-2 行列による変換の詳細 1$§$2-18 初期状態における頂点情報の取得について
$§$2-3 行列による変換の詳細 2$§$2-19 衝突判定 6 (軸平行な長方形同士の衝突)
$§$2-4 自転と公転$§$2-20 衝突判定 7 (円盤 vs 長方形)
$§$2-5 一体化したオブジェクトの運動 1$§$2-21 衝突判定 8 (回転した長方形同士の衝突)
$§$2-6 一体化したオブジェクトの運動 2$§$2-22 衝突判定 9 (「倉庫番」プログラムの作成)
$§$2-7 一体化したオブジェクトの運動 3$§$2-23 衝突判定 10 (円盤 vs 三角形)
$§$2-8 指定方向へのオブジェクトの移動 1$§$2-24 衝突判定 11 (直線 vs 長方形、円盤、直線)
$§$2-9 指定方向へのオブジェクトの移動 2$§$2-25 円と直線による補間曲線 1
$§$2-10 指定方向へのオブジェクトの移動 3$§$2-26 円と直線による補間曲線 2
$§$2-11 指定方向へのオブジェクトの移動 4 (連射プログラムの実装)$§$2-27 その他の重要事項 1 (UnityのTransformクラスによる記述 ; カメラ移動の基本)
$§$2-12 指定方向へのオブジェクトの移動 5$§$2-28 その他の重要事項 2 (画面に表示されるXY平面の範囲 ; ミニマップの実装)
$§$2-13 衝突判定 1 (点 vs 円盤、長方形 ; ローカル座標からワールド座標への変換 2D)$§$2-29 その他の重要事項 3 (スクリーン座標からワールド座標への変換 2D; スクリーンショットの撮影範囲)
$§$2-14 衝突判定 2$§$2-30 課題 1
$§$2-15 衝突判定 3$§$2-31 課題 2
$§$2-16 衝突判定 4

$§$2-16 衝突判定 4


前節までは、シーン内に配置されているオブジェクトに対して Shellを発射するのみで、オブジェクトの方から Shellを撃ってくることはなかった。いわば片方向のみの衝突判定であった。本節及び次節においてはシーン内に置かれているオブジェクトが自動的に Shellを発射するようにして、キー操作によって動かしている Planeも衝突判定の対象に含めた双方向の衝突判定を実装していく。


# Code1
図1、図2はオブジェクト Cannon、Battery の初期状態である。

図1
図2

Cannonは Shellを発射するオブジェクトであり、Batteryはその親オブジェクトである。まずは、Cannonが親オブジェクト Battery上において単振動を行うプログラムを以下に示す (Batteryは初期状態から動かさない)。

[Beta1]  (実行結果 図3)
i_shm += 2;
float x = 3.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
THMatrix3x3 localCannon = TH2DMath.GetTranslation3x3(x, 0);

THMatrix3x3 localBattery = THMatrix3x3.identity;

THMatrix3x3 worldCannon = localBattery * localCannon;
THMatrix3x3 worldBattery = localBattery;

Cannon[0].SetMatrix(worldCannon);
Battery[0].SetMatrix(worldBattery);

実行結果(図3)に見られるように Cannonが Batteryの内側の領域において振幅$3$の単振動を行っている。単振動のコードは何度も使われているので特に解説は不要であろう。一体化したオブジェクトの運動のプログラムであるため、例によってプログラム中の行列には「local」や「world」といった接頭辞が付けられている。Batteryはここでは動かさないのでセットされる行列 worldBattery の内容は identity行列である。
なお、本節で使われる Cannon、Battery は1つではなく、それぞれ複数個が使われる。そのため、プログラムにおける Cannon、Battery は配列であり、上記のように Cannon[0]Battery[0] の形で使われる。

では次に Batteryをシーン内の適当な位置に移動させ、移動した Battery上において Cannonが単振動を行うように上のプログラムBeta1に変更を加える。

[Code1]  (実行結果 図4)
i_shm += 2;
float x = 3.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
THMatrix3x3 localCannon = TH2DMath.GetTranslation3x3(x, 0);

THMatrix3x3 rotBattery = TH2DMath.GetRotation3x3(-125);
THMatrix3x3 traBattery = TH2DMath.GetTranslation3x3(-8, 4);
THMatrix3x3 localBattery = traBattery * rotBattery;

THMatrix3x3 worldCannon = localBattery * localCannon;
THMatrix3x3 worldBattery = localBattery;

Cannon[0].SetMatrix(worldCannon);
Battery[0].SetMatrix(worldBattery);

先程のプログラムからの変更点は5~7行目の localBattery の計算部分である。Beta1では Batteryは動かさなかったので実行される行列の内容は identity行列であったが、今回は初期状態においてまず $125$°回転させ、次に $(-8, 4)$ だけの平行移動を実行している。この変換を実行した結果、Batteryは図4に示される位置に移動することになる。子オブジェクトである Cannonにも同じ変換が実行されるので、今回の Cannonの単振動は新しい位置の Battery上で行われるようになる。

図3 Beta1 実行結果
図4 Code1 実行結果


# Code2
ここからは、Cannonから Shellが自動的に発射されるプログラムを作成していく。本節及び次節においての Shellの発射はすべて連射である。連射のプログラムは 2-11節、2-12節で作成したが、再度ここでその内容について簡単に復習する。

[2-11節 Code5]  
// Shell
for (int i = 0; i < Shell.Length; i++)
{
    if (Shell[i].active)
    {
        Vector2 newShellPos = Shell[i].GetPosition() + 0.80f * Shell[i].direction;
        if (Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12)
        {
            Shell[i].active = false;
        }

        Shell[i].SetPosition(newShellPos);
    }
    else
    {
        if (bShoot)
        {
            Shell[i].active = true;
            Shell[i].direction = direBody;

            Vector2 startPos = newP + 1.25f * direBody;
            Shell[i].SetPosition(startPos);

            bShoot = false;
        }
    }
}

このプログラムは 2-11節 Code5 から Shellの連射の部分を抜粋したものである。2-11節 Code5 は移動中のヘリコプターから、Aキーを押し続けることで Shellを連続的に発射するというものであった。
forループの処理は、全てのShellのうちでactiveなShellの場合は発射された方向に一定距離進ませ、activeでないShellについては、(そのフレームにおいてShellが発射される場合に)ループ中に最初に見つかったactiveでないShellを発射開始位置に移動させるというものである。ここでいう「activeなShell」とはShell配列のうちでヘリコプターから発射され、画面内を移動しているShellのことで、つまり画面内に存在するShellのことである。「activeでないShell」とはShell配列のうちで画面内に存在していないShellのことである。Aキーを押し続けていると(毎フレームではないが)bool型ローカル変数bShoottrueになる。この変数はShellを発射させるためのフラグである。forループ中にactiveでないShellが見つかった場合、bShoottrueであれば64行目のifブロックに入り、そのactiveでないShellを発射開始位置に移動させ、そのShellを「activeなShell」にする。次のフレームからは、そのShellはactiveなので52行目のifブロックに入り、発射された方向に一定距離ずつ進んでいき、55行目のif文で指定される範囲を超えた時点で(画面から見えなくなった時点で)再び activeでないShell になる。
以上がこのコードの概要である (詳しくは 2-11節参照)。

図5
図EはヘリコプターからShellが発射されるときの様子を示したものである。プログラム69行目の変数newPは、ヘリコプターのその時点での位置のことであるが、図中においては ヘリコプター中央部分の赤い印の位置のことである。67行目の変数direBodyは、ヘリコプターのその時点での向きのことであるが、図中においては緑色の矢印で表示されている方向のことであり、ShellはこのdireBodyの方向に発射される。
69行目の startPos はその時点での Shellの発射開始位置のことであり、 図5におけるヘリコプター先端のShellの位置のことである。1.25f という数値は ヘリコプターの位置(赤い印)から Shellの発射開始位置までの距離のことであり、正確にはその時点でのヘリコプターの位置からdireBodyの方向に1.25だけ離れた位置から Shellは発射される。
発射されたShellはactiveとなるので次のフレームからは52行目のifブロック内に入ることになるが、54行目の 0.80f という数値は発射されたShellの毎フレームの移動速度のことである。

2-14節では、Shellの発射開始位置及び発射方向の計算を、オブジェクトに実行された変換行列を使う方法に書き換えたが、ここでもその書き換えを行ってプログラムの見通しを改善しよう。
ヘリコプターは Body、Rotorの2つのオブジェクトによって構成されているが、Shellの発射は Bodyから発射される。したがって、ある時点において Bodyから発射される Shellの発射開始位置は、その時点で Bodyに実行されている変換行列と初期状態のBodyにおけるShellの発射開始位置の積を計算することで求められる。また、ある時点において Bodyから発射される Shellの発射方向は、その時点で Bodyに実行されている変換行列と初期状態のBodyにおけるShellの発射方向の積を計算することで求められる。

図6
図7

図6、図7ではヘリコプターの先端に Shellが置かれているが、その位置が初期状態のBodyにおけるShellの発射開始位置であり、図6に示されるようにその位値は $(0.0,\ 1.25)$ である。図7は Shellの発射される方向として緑色の矢印が表示されているが、これが初期状態のBodyにおけるShellの発射方向であり、その方向はy軸プラス方向、すなわち $(0, 1)$ である。
プログラム内において、これらの発射開始位置、発射方向を Body.initShootPositionBody.initShootDirection で取得できるものとすれば、上のプログラムは以下のように書き換えられる (initShootPositioninitShootDirection は同次座標化された3次元ベクトルであるとし、そのz成分はそれぞれ $1$、$0$ とする)。

// Shell
for(int i=0; i<Shell.Length; i++)
{
    if (Shell[i].active)
    {
        Vector2 newShellPos = Shell[i].GetPosition() + 0.80f * Shell[i].direction;
        if(Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12    )
        {
            Shell[i].active = false;
        }
 
        Shell[i].SetPosition(newShellPos);
    }
    else
    {
        if (bShoot)
        {
            THMatrix3x3 M = Body.GetMatrix();
            Vector2 startPos = M * Body.initShootPosition;
            Vector2 shootDir = (M * Body.initShootDirection).normalized;
    
            Shell[i].active = true;
            Shell[i].direction = shootDir;
 
            Shell[i].SetPosition(startPos);
            bShoot = false;
        }
    }
}

変更点は64行目のbShootifブロック内における発射開始位置(startPos ; 67行目)、発射方向(shootDir ; 68行目)の計算のみである。
2-14節(あるいは 2-13節)でも述べたが、変換行列実行後の位置を計算する場合には、計算で使われる同次座標のz成分は $1$ であるが、変換行列実行後の方向を計算する場合には、計算で使われる同次座標のz成分は $0$ である。

また、2-11節 Code5ではAキーを押し続けることで連射が行われるが、その部分の処理は次のようなものである。
[2-11節 Code5]  
bool bShoot = false;
if (Input.GetKey(KeyCode.A))
{
    if (i_frameCount >= Body.lastShootFrame + 3)
    {
        bShoot = true;
        Body.lastShootFrame = i_frameCount;
    }
}

20行目のインスタンス変数 i_frameCount は現在のフレーム番号であり、Body.lastShootFrame は最後に Shellが発射されたときのフレーム番号である。この20行目のifブロックは 最後に Shellが発射されてから$3$フレーム以上経過していれば、Shellの発射フラグであるbShoottrueにするという処理を行っている。つまり、Aキーを押し続けていても Shellは$3$フレームごとにしか連射されないわけである。

以上をまとめると、ヘリコプターから Shellを連射するにあたっては次のデータが必要となる (具体的には、ヘリコプターを構成するオブジェクトの1つであるBodyから Shellは発射される)。
(1)  Bodyに実行された変換行列 (書き換えられたプログラム66行目の M)
(2)  Bodyの初期状態におけるShell発射開始位置 (書き換えられたプログラム67行目の Body.initShootPosition)
(3)  Bodyの初期状態におけるShell発射方向 (書き換えられたプログラム68行目の Body.initShootDirection)
(4)  発射されたShellの毎フレームの移動速度 (プログラム54行目の 0.80f)
(5)  連射のためのインターバルカウント (何フレームごとに連射するか ; プログラム20行目の 3)
(6)  最後に Shellを発射したときのフレーム番号 (プログラム20行目の Body.lastShootFrame)

しかし、ヘリコプターだけではなく他のオブジェクトが Shellを連射する場合でも、これらのデータがあればそのオブジェクトから Shellを連射することができる。
つまり、あるオブジェクトから Shellを連射するためには、以下のデータが必要となる。
(1)  オブジェクトに実行された変換行列
(2)  オブジェクトの初期状態におけるShell発射開始位置
(3)  オブジェクトの初期状態におけるShell発射方向
(4)  発射されたShellの毎フレームの移動速度
(5)  連射のためのインターバルカウント (何フレームごとに連射するか)
(6)  最後に Shellを発射したときのフレーム番号

衝突判定で使われるオブジェクトは THAlphaObject2Dクラスのインスタンスであるが、THAlphaObject2Dクラスにはこれらのデータを取得するためのメソッド及びプロパティが用意されている。
THMatrix3x3  GetMatrix()
  :  オブジェクトに実行された変換行列を取得するメソッド。
initShootPosition
  :  オブジェクトの初期状態におけるShell発射開始位置(Vector3型プロパティ。同次座標を表し、z成分は $1$ )。
initShootDirection
  :  オブジェクトの初期状態におけるShell発射方向(Vector3型プロパティ。同次座標を表し、z成分は $0$ )。
shellSpeed
  :  発射されたShellの毎フレームの移動速度(float型プロパティ)。
shootInterval
  :  連射のためのインターバルカウント(int型プロパティ)。
lastShootFrame
  :  最後に Shellを発射したときのフレーム番号(int型プロパティ)。

では実際に、Cannonを使って上で述べてきた連射処理を実装しよう。
状況を簡単にするため、Cannonのみを使用し、Cannonは初期状態から変化させず Shellを連射するだけのプログラムである。
[Beta2]  (実行結果 図9)
if (!i_INITIALIZED)
{
    Cannon[0].lastShootFrame = 8;
    Cannon[0].shootInterval = 12;
    Cannon[0].initShootDirection = new Vector2(0, 1);
    Cannon[0].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[0].shellSpeed = 0.40f;

    i_INITIALIZED = true;
}

i_frameCount++;

// Cannon
Cannon[0].SetMatrix(THMatrix3x3.identity);

// Shell
bool bShoot = false;
if (i_frameCount >= Cannon[0].lastShootFrame + Cannon[0].shootInterval)
{
    Cannon[0].lastShootFrame = i_frameCount;
    bShoot = true;
}

foreach (var shell in EShell)
{
    if (shell.active)
    {
        Vector2 newShellPos = shell.GetPosition() + shell.speed * shell.direction;
        if (Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12)
        {
            shell.active = false;
        }

        shell.SetPosition(newShellPos);
    }
    else
    {
        if (bShoot)
        {
            THMatrix3x3 M = Cannon[0].GetMatrix();
            Vector2 startPos = M * Cannon[0].initShootPosition;
            Vector2 shootDir = (M * Cannon[0].initShootDirection).normalized;

            shell.active = true;
            shell.direction = shootDir;
            shell.speed = Cannon[0].shellSpeed;

            shell.SetPosition(startPos);

            bShoot = false;
        }
    }
}

1行目から10行目は初期化ブロックであり、プログラム実行開始時点で1度だけ処理が行われる。ここでは、連射に必要な各データの値を設定している。たとえば、shootInterval の値は 12 であるから、Shellの連射は$12$フレームごとに行われることになる。shellSpeed の値は 0.40f であるが、これは発射されたShellが毎フレーム $0.40$ずつ移動することを意味している。つまり、実行結果である図8においては Shellは$12$フレームごとに発射され、毎フレーム $0.40$ずつ移動しているのである。
また、initShootPosition の値には (0.0f, 1.75f) をセットしているが、このプロパティを取得する際には自動的に同次座標化されて (0.0f, 1.75f, 1.0f) として返される。initShootPosition は Cannonの初期状態におけるShellの発射開始位置であり、それは図9に示されている。
initShootDirection の値には (0, 1) をセットしているが、このプロパティも取得の際には自動的に同次座標化されて (0, 1, 0) として返される。initShootDirection は Cannonの初期状態におけるShellの発射方向で、図9における緑色の矢印の方向である。

図8 Beta2 実行結果
図9 初期状態における発射開始位置、発射方向

18行目以降が Shellの連射処理の部分である。
本節において使われるShellは図8や図9に見られるように、今までと色の異なるShellであり、プログラム中では「EShell」という配列変数名で使われている (以下の文章中において「Shell配列」と書かれていても、具体的にはその配列は EShell のことを指している)。
19行目のifブロックでは Shellを発射させるためのフラグであるbool型変数bShoottrueにする。今回はキーを押し続けることで Shellを発射させるのではなく、自動的に発射されるように変更されており、Cannonが最後に Shellを発射したフレームから一定時間経過した時点で自動的にこのブロックに入るようになっている。具体的には、初期化ブロックにおいて lastShootFrame は $8$ が設定され、shootInterval には $12$ が設定されているので、プログラム開始後 $8 + 12 = 20$ フレーム目において このifブロックに初めて入ることになり、それ以降は$12$フレームごとに自動的にこのブロックに入るようになる。
25行目のforeach文は Shell配列の要素を1つ1つループしていき、activeなShellに対しては移動処理を行い、activeでないShellのうち最初に見つかったShellに対しては発射開始位置に移動させるという処理を行う。この foreach文は、上記の書き換えの行われたヘリコプターの連射プログラムでは for文で記述されていた処理であるが、その内容はほとんど同じであるから特に解説は不要であろう。
このプログラムでは、Cannonは初期状態から動かさないので15行目では毎フレーム identity行列をセットしている。そのため、41行目の Cannon[0].GetMatrix() で取得される行列の内容は毎フレーム identity行列であり、42行目、43行目で計算される startPosshootDir の値はプログラム実行中常に図9の初期状態における値が返される。

では続いて、Cannonに簡単な運動を行わせてみよう。
以下のプログラムは、指定の範囲を Cannonが単振動をしながら、一定間隔で Shellを連射するものである。
[Code2]  (実行結果 図10)
if (!i_INITIALIZED)
{
    Cannon[0].lastShootFrame = 8;
    Cannon[0].shootInterval = 12;
    Cannon[0].initShootDirection = new Vector2(0, 1);
    Cannon[0].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[0].shellSpeed = 0.40f;

    i_INITIALIZED = true;
}

i_frameCount++;

// Cannon
i_shm += 2;
float x = 3.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
THMatrix3x3 T = TH2DMath.GetTranslation3x3(x, 0);
Cannon[0].SetMatrix(T);

// Shell
bool bShoot = false;
if (i_frameCount >= Cannon[0].lastShootFrame + Cannon[0].shootInterval)
{
    Cannon[0].lastShootFrame = i_frameCount;
    bShoot = true;
}

foreach (var shell in EShell)
{
    if (shell.active)
    {
        Vector2 newShellPos = shell.GetPosition() + shell.speed * shell.direction;
        if (Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12)
        {
            shell.active = false;
        }

        shell.SetPosition(newShellPos);
    }
    else
    {
        if (bShoot)
        {
            THMatrix3x3 M = Cannon[0].GetMatrix();
            Vector2 startPos = M * Cannon[0].initShootPosition;
            Vector2 shootDir = (M * Cannon[0].initShootDirection).normalized;

            shell.active = true;
            shell.direction = shootDir;
            shell.speed = Cannon[0].shellSpeed;

            shell.SetPosition(startPos);

            bShoot = false;
        }
    }
}

Beta2からの変更点は、15行目から18行目の Cannonの運動に関する部分のみである。Beta2では Cannonには identity行列がセットされるだけであったが、ここでは x軸方向に振幅$3$の単振動を行うように変更されている。
それ以外の部分(初期化ブロック、Shellの連射処理など)については、先程のBeta2と全く同じである。

図10 Code2 実行結果
図11

図11はある時点において、Cannonから Shellが発射されたときの様子である。図中の Cannon先端の Shellの位置はプログラム45行目で計算される startPos に等しく、緑色の矢印で表される Shellの発射方向は 46行目の shootDir に等しい。今回は Cannonは単振動を行っているため毎フレーム startPos の値は異なるが、Cannonの向きは初期状態から変化しないため shootDir の値は初期状態の値から変わらない。


# Code3
Code2における Cannonの運動は単一オブジェクトの運動であった。今回は、Code1で扱ったように Batteryを Cannonの親オブジェクトとし、2つのオブジェクトが一体化した状態で Cannonから Shellを発射させる。
単一オブジェクトの場合でも、階層構造に含まれるオブジェクトの場合でも、ある時点において Shellを発射する場合には、その時点における Shellの発射開始位置、Shellの発射方向を求めるわけだが、その求め方は2つの場合で変わらないことが以下のプログラムによって示される。

[Code3]  (実行結果 図12)
if (!i_INITIALIZED)
{
    Cannon[0].lastShootFrame = 8;
    Cannon[0].shootInterval = 12;
    Cannon[0].initShootDirection = new Vector2(0, 1);
    Cannon[0].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[0].shellSpeed = 0.40f;
    
    i_INITIALIZED = true;
}

i_frameCount++;

// <-- Code1と同じ記述
// Cannon
i_shm += 2;
float x = 3.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
THMatrix3x3 localCannon = TH2DMath.GetTranslation3x3(x, 0);

THMatrix3x3 rotBattery = TH2DMath.GetRotation3x3(-125);
THMatrix3x3 traBattery = TH2DMath.GetTranslation3x3(-8, 4);
THMatrix3x3 localBattery = traBattery * rotBattery;

THMatrix3x3 worldCannon = localBattery * localCannon;
THMatrix3x3 worldBattery = localBattery;

Cannon[0].SetMatrix(worldCannon);
Battery[0].SetMatrix(worldBattery);
// Code1と同じ記述 -->


// Shell
bool bShoot = false;
if (i_frameCount >= Cannon[0].lastShootFrame + Cannon[0].shootInterval)
{
    Cannon[0].lastShootFrame = i_frameCount;
    bShoot = true;
}

foreach (var shell in EShell)
{
    if (shell.active)
    {
        Vector2 newShellPos = shell.GetPosition() + shell.speed * shell.direction;
        if (Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12)
        {
            shell.active = false;
        }
        
        shell.SetPosition(newShellPos);
    }
    else
    {
        if (bShoot)
        {
            THMatrix3x3 M = Cannon[0].GetMatrix();
            Vector2 startPos = M * Cannon[0].initShootPosition;
            Vector2 shootDir = (M * Cannon[0].initShootDirection).normalized;
            
            shell.active = true;
            shell.direction = shootDir;
            shell.speed = Cannon[0].shellSpeed;
            
            shell.SetPosition(startPos);
            
            bShoot = false;
        }
    }
}

1行目の初期化ブロック及び 33行目以降の Shellの連射処理の部分はCode2と同じものである。Code2から変更されている箇所は、16行目から28行目のオブジェクトの運動に関する記述である。この部分のコードはCode1と同じものであり、シーン内の適当な位置に配置された Battery上において Cannonが単振動を行うという内容である。
実行結果(図12)に見られるように、Cannonが Battery上で単振動をしながら Shellを発射するようになっている。

図12 Code3 実行結果
図13

今回は Cannonは Batteryを親オブジェクトとして持っているわけだが、その場合でも ある時点における Shellの発射開始位置、発射方向の計算の仕方は変わらない。すなわち、その時点において Cannonに実行されている変換行列を取得し、それを Cannonの初期状態における発射開始位置、発射方向に掛けることによって求められる。具体的には、プログラム56行目で取得される変換行列 M が Cannonに実行されている変換行列であり、57行目、58行目で計算される startPosshootDir がその時点における Shellの発射開始位置、発射方向である。
図13はCode3実行中のある時点において Shellが発射されたときの様子であるが、この図でいえば startPos が表すものが図中の Cannon先端の Shellの位置であり、shootDir が表すものが緑色の矢印で表される Shellの発射方向である。


# Code4
では続いて Shellを発射するオブジェクトを2つにした場合を考えてみよう。ここで使用するオブジェクトは2つのCannonであるが、初期化ブロックの設定は以下のように同じものにする。

if (!i_INITIALIZED)
{
    Cannon[0].lastShootFrame = 8;
    Cannon[0].shootInterval = 12;  
    Cannon[0].initShootDirection = new Vector2(0, 1);
    Cannon[0].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[0].shellSpeed = 0.40f;
    
    Cannon[1].lastShootFrame = 8;
    Cannon[1].shootInterval = 12;
    Cannon[1].initShootDirection = new Vector2(0, 1);
    Cannon[1].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[1].shellSpeed = 0.40f;

    i_INITIALIZED = true;
}

この設定によって2つのCannonは同じタイミングで Shellを発射し、発射されるShellも同じ速度で同じ方向に進んでいくことになる。

以下は Code2の Shellの連射部分の処理であるが、このコードは今回のように複数のCannonがShellを発射するケースでは使えない。
// Shell
bool bShoot = false;
if (i_frameCount >= Cannon[0].lastShootFrame + Cannon[0].shootInterval)
{
    Cannon[0].lastShootFrame = i_frameCount;
    bShoot = true;
}

foreach (var shell in EShell)
{
    if (shell.active)
    {
        Vector2 newShellPos = shell.GetPosition() + shell.speed * shell.direction;
        if (Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12)
        {
            shell.active = false;
        }

        shell.SetPosition(newShellPos);
    }
    else
    {
        if (bShoot)
        {
            THMatrix3x3 M = Cannon[0].GetMatrix();
            Vector2 startPos = M * Cannon[0].initShootPosition;
            Vector2 shootDir = (M * Cannon[0].initShootDirection).normalized;

            shell.active = true;
            shell.direction = shootDir;
            shell.speed = Cannon[0].shellSpeed;

            shell.SetPosition(startPos);

            bShoot = false;
        }
    }
}

今回の2つのCannonは lastShootFrame の初期値も shootInterval の値も同じなので、同じタイミング(同じフレーム)で Shellが発射される。22行目のifブロックは Cannon[0]が Shellを発射する際に bShoottrueになるが、同じフレームで Cannon[1]の Shellの発射も行われるので bShoottrueになるフレームでは、常にCannon[0]とCannon[1]の2つのオブジェクトがShellを発射することになる。
問題なのは Shellの発射フラグであるbShootbool型の変数であるという点である。たとえば、あるフレームにおいて2つのCannonからShellが発射されることになったとしよう。このとき、やはり22行目のifブロック内でbShoottrueになり、その下のforeachループ内で42行目のifブロックに入ることになる。42行目のifブロックは activeでないShellのうちで最初に見つかったものを発射開始位置に移動させる処理である。bShoottrueの際には、このブロック内で確かに activeでないShellの1つを発射開始位置に移動させるが、その後にこのブロック内の54行目においてbShootfalseになる。bShootfalseになってしまうと、次のループからはもうこの42行目のifブロックには入ることはない。つまり、それは新たにShellを発射開始位置に移動させるという処理は行われなくなることを意味する。
Cannonが1つだけの場合は、あるフレームでShellを発射する場合でも、そのフレームにおいて Shellを1つだけ発射開始位置に移動させればよかったので発射フラグがbool型のbShootでも問題はないが、Cannonが2つになり、同じフレームにおいて2つのShellを発射させる場合には、このbShootが問題となってしまうのである。foreach文繰り返し中のあるループにおいて activeでないShellが見つかり、42行目のifブロックにおいてそのShellを発射開始位置に移動させるまでは問題はないが、その後に54行目でbShootfalseになるために、もうその後のループにおいてはこの42行目のifブロックには入らない。つまり、最初に見つかった activeでないShellを発射した後は、2度とShellは発射されることはなくなってしまうのである。

そこで、複数のShellを同一フレームにおいて発射できるように、以下のプログラムでは連射処理の部分が修正されている。
このプログラムは2つのCannonを適当な間隔で配置し、一定間隔で Shellを発射するものであるが、Shellの発射タイミングは同じであり、2つのCannonから同時に発射される。
[Code4]  (実行結果 図14)
if (!i_INITIALIZED)
{
    Cannon[0].lastShootFrame = 8;
    Cannon[0].shootInterval = 12;  
    Cannon[0].initShootDirection = new Vector2(0, 1);
    Cannon[0].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[0].shellSpeed = 0.40f;
    
    Cannon[1].lastShootFrame = 8;
    Cannon[1].shootInterval = 12;
    Cannon[1].initShootDirection = new Vector2(0, 1);
    Cannon[1].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[1].shellSpeed = 0.40f;
    
    i_INITIALIZED = true;
}

i_frameCount++;

// Cannon
THMatrix3x3 T0 = TH2DMath.GetTranslation3x3(-2.0f, 0.0f);
Cannon[0].SetMatrix(T0);
THMatrix3x3 T1 = TH2DMath.GetTranslation3x3(2.0f, 0.0f);
Cannon[1].SetMatrix(T1);


// Shell
var curShooters = new List<THAlphaObject2D>();

foreach (var obj in Cannon)
{
    if (i_frameCount >= obj.lastShootFrame + obj.shootInterval)
    {
        obj.lastShootFrame = i_frameCount;
        curShooters.Add(obj);
    }
}

foreach (var shell in EShell)
{
    if (shell.active)
    {
        Vector2 newShellPos = shell.GetPosition() + shell.speed * shell.direction;
        if (Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12)
        {
            shell.active = false;
        }
        
        shell.SetPosition(newShellPos);
    }
    else
    {
        if (curShooters.Count > 0)
        {
            THAlphaObject2D obj = curShooters[0];
            THMatrix3x3 M = obj.GetMatrix();
            Vector2 startPos = M * obj.initShootPosition;
            Vector2 shootDir = (M * obj.initShootDirection).normalized;
            
            shell.active = true;
            shell.direction = shootDir;
            shell.speed = obj.shellSpeed;
            
            shell.SetPosition(startPos);
            
            curShooters.RemoveAt(0);
        }
    }
}

冒頭の初期化ブロックは先ほど示したものと同じものである。この設定によって、2つのCannonからは Shellが$12$フレームごとに同じタイミングで発射される (Shellが発射されるフレームは $20$フレーム目、$32$フレーム目、$44$フレーム目 ... と続く)。
28行目以降が Shellの連射処理の部分である。28行目のList型ローカル変数 curShooters は、Shell発射用のフラグの役割を果たすもので、同一フレームにおいて複数のオブジェクトからのShellの発射を可能にするために今回用意された変数である。
具体的には、あるフレームにおいて Shellを発射するCannonがあれば、そのCannonを30行目のforeach文内で curShooters に追加する。たとえば、このプログラムでは実行後 $20$フレーム目が最初の発射になるが、このフレームにおいて 30行目のforeach文内で curShooters には2つのCannonが追加されることになる。
2つのCannonは常に同じタイミングで Shellを発射するので、あるフレームで Shellを発射する場合には curShooters には必ず2つのCannonが追加されている。それ以外のフレーム(Shellを発射しないフレーム)では curShooters の要素数は $0$ である。
Shell配列を1つ1つ調べるforeachループ(39行目)で修正された部分は 53行目のifブロックである。このifブロックは修正前はbool型変数bShootが使われていた箇所である。今回はbShootではなく、List型変数curShootersが使われている。このifブロックの条件式は if(curShooters.Count > 0) であるが、これは curShooters に要素がある限りこのifブロックに入ることを意味する。そして、このifブロックの最後66行目では curShooters.RemoveAt(0) によって curShooters の要素を1つ削除している。つまり、このifブロックに入るたびに curShootersの要素を1つ削除していくので、この53行目のifブロックには curShootersの要素数分入ることになる。言い換えれば、そのフレームにおいてShellを発射するオブジェクトの数と同じ回数だけ、このifブロックが実行されるということである。この実装によって「同一フレームにおいて複数のオブジェクトからのShellの発射」が可能になるわけである。
なお、このifブロックでは curShooters の先頭要素のオブジェクトを取り出し(55行目)、そのオブジェクトから発射されるShellの発射開始位置を算出し(57行目)、Shellをその位置へ移動させ(64行目)、それらの処理の後にそのオブジェクトは curShooters から削除される。つまり、curShootersに格納されているオブジェクトは このifブロックで Shellを発射した時点で curShooters から削除されるのである (このプログラムではcurShootersの中身は 空でない場合には、Shellを発射する2つのCannonである)。

図14 Code4 実行結果
図15 Code4 実行結果 (Cannon[0]のintervalを6にした場合)

図14はCode4の実行結果であるが、2つのCannonからShellが同じタイミングで発射され、同じ速度で同じ方向に進んでいく。これは1行目の初期化ブロックにおける設定が2つのCannonで同じ値になっているためである。もし、Cannon[0]の発射間隔の値(Cannon[0].shootInterval)を次のように $12$ から $6$ にすると、
if (!i_INITIALIZED)
{
    Cannon[0].lastShootFrame = 8;
    Cannon[0].shootInterval  = 6;    // <-- 12から6に変更
    .. ..
Cannon[0]の発射間隔が半分になるので、図15に表示されるように Cannon[1](右側)が1回発射する間に、Cannon[0](左側)は2回発射するという結果になる。


# Code5
次のプログラムでは2つのCannonが運動をしており、運動中のCannonから自動的に Shellが発射されるものである。今回、2つのCannonはBatteryを共通の親オブジェクトとしており、2つのCannonの運動は親オブジェクトBattery上における単振動である。

[Beta5]  (実行結果 図16)
if (!i_INITIALIZED)
{
    Cannon[0].lastShootFrame = 8;
    Cannon[0].shootInterval = 15;
    Cannon[0].initShootDirection = new Vector2(0, 1);
    Cannon[0].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[0].shellSpeed = 0.40f;

    Cannon[1].lastShootFrame = 12;
    Cannon[1].shootInterval = 18;
    Cannon[1].initShootDirection = new Vector2(0, 1);
    Cannon[1].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[1].shellSpeed = 0.40f;

    i_INITIALIZED = true;
}

i_frameCount++;

// Cannon & Battery
THMatrix3x3 rotBattery = TH2DMath.GetRotation3x3(-125);            
THMatrix3x3 traBattery = TH2DMath.GetTranslation3x3(-8, 4);    
THMatrix3x3 localBattery = traBattery * rotBattery;

i_shm += 2.0f;
float x = 3.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
THMatrix3x3 localCannon0 = TH2DMath.GetTranslation3x3(x - 2, 0);
THMatrix3x3 localCannon1 = TH2DMath.GetTranslation3x3(x + 2, 0);

THMatrix3x3 worldCannon0 = localBattery * localCannon0;
THMatrix3x3 worldCannon1 = localBattery * localCannon1;
THMatrix3x3 worldBattery = localBattery;

Cannon[0].SetMatrix(worldCannon0);
Cannon[1].SetMatrix(worldCannon1);
Battery[0].SetMatrix(worldBattery);


// Shell  
var curShooters = new List<THAlphaObject2D>();

foreach (var obj in Cannon)
{
    if (i_frameCount >= obj.lastShootFrame + obj.shootInterval)
    {
        obj.lastShootFrame = i_frameCount;
        curShooters.Add(obj);
    }
}

foreach (var shell in EShell)
{
    if (shell.active)
    {
        Vector2 newShellPos = shell.GetPosition() + shell.speed * shell.direction;
        if (Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12)
        {
            shell.active = false;
        }

        shell.SetPosition(newShellPos);
    }
    else
    {
        if (curShooters.Count > 0)
        {
            THAlphaObject2D obj = curShooters[0];
            THMatrix3x3 M = obj.GetMatrix();
            Vector2 startPos = M * obj.initShootPosition;
            Vector2 shootDir = (M * obj.initShootDirection).normalized;

            shell.active = true;
            shell.direction = shootDir;
            shell.speed = obj.shellSpeed;

            shell.SetPosition(startPos);

            curShooters.RemoveAt(0);
        }
    }
} 

図16
1行目の初期化ブロックではCode4と同じく、2つのCannonのShell発射に関するデータ設定を行っている。今回は2つのCannonの発射間隔(shootInterval)の値が異なるので、実行結果図16に見られるように Shellは2つのCannonから異なるタイミングで発射されている。21行目から36行目までが Cannon及びBatteryの運動の記述であるが、今回の運動は Code1で見た Battery上における Cannonの単振動と同じであり、Code1では1つのCannonの単振動であったのに対し、ここでは2つのCannonに単振動を行わせるように変更しただけである。
そして、40行目以降が Shell発射の処理であるが、この部分は Code4の28行目以降と同じである。

今までに見てきたプログラムでは、オブジェクトからの自動的なShellの発射を実装してきたが、プログラムは以下のような構成になっていた。
if (!i_INITIALIZED)
{
    // (1) 初期化ブロック 
    .. ..
}

// (2) オブジェクトの運動
.. ..

// (3) Shellの運動(発射を含む)
.. ..


以降では、(2)オブジェクトの運動 及び (3)Shellの運動 をメソッドとして独立させる。これは、いろいろなオブジェクトがShellを発射することを考慮しての一般化である。Shellを発射するオブジェクトが変わっても、Shellの運動(発射を含めた)に関しては処理的にはどのオブジェクトでもほとんど変わらないため、適当に変数の値を変えるだけでどのオブジェクトからでもShellを発射できるようにする。

オブジェクトの運動は次のメソッドに記述するものとする。
void EnemyMotion(){ .. }

Shellの運動は次のメソッドに記述するものとする。
void EShellMotion(){ .. }

今回の例では、EnemyMotion()EShellMotion() は次のような内容になる。
[EnemyMotion5()]
void EnemyMotion5()
{
    THMatrix3x3 rotBattery = TH2DMath.GetRotation3x3(-125);        
    THMatrix3x3 traBattery = TH2DMath.GetTranslation3x3(-8, 4);    
    THMatrix3x3 localBattery = traBattery * rotBattery;

    i_shm += 2.0f;
    float x = 3.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
    THMatrix3x3 localCannon0 = TH2DMath.GetTranslation3x3(x - 2, 0);
    THMatrix3x3 localCannon1 = TH2DMath.GetTranslation3x3(x + 2, 0);

    THMatrix3x3 worldCannon0 = localBattery * localCannon0;
    THMatrix3x3 worldCannon1 = localBattery * localCannon1;
    THMatrix3x3 worldBattery = localBattery;

    Cannon[0].SetMatrix(worldCannon0);
    Cannon[1].SetMatrix(worldCannon1);
    Battery[0].SetMatrix(worldBattery);
}

[EShellMotion()]
void EShellMotion()  
{
    var curShooters = new List<THAlphaObject2D>();

    foreach (var obj in i_shootObjects)
    {
        if (i_frameCount >= obj.lastShootFrame + obj.shootInterval)
        {
            obj.lastShootFrame = i_frameCount;
            curShooters.Add(obj);
        }
    }

    foreach (var shell in EShell)
    {
        if (shell.active)
        {
            Vector2 newShellPos = shell.GetPosition() + shell.speed * shell.direction;
            if (Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12)
            {
                shell.active = false;
            }

            shell.SetPosition(newShellPos);
        }
        else
        {
            if (curShooters.Count > 0)
            {
                THAlphaObject2D obj = curShooters[0];
                THMatrix3x3 M = obj.GetMatrix();
                
                Vector2 startPos = M * obj.initShootPosition;
                Vector2 shootDir = (M * obj.initShootDirection).normalized;

                shell.active = true;
                shell.direction = shootDir;
                shell.speed = obj.shellSpeed;

                shell.SetPosition(startPos);

                curShooters.RemoveAt(0);
            }
        }
    }
} 

EnemyMotion5() は上記プログラムBeta5の21行目から36行目と全く同じである (EnemyMotion5()5 はCode5の EnemyMotion という意味である)。EShellMotion() はBeta5の40行目以降と同じであるが、Beta5の42行目の foreach では Cannon となっていた部分が、EShellMotion()の5行目では i_shootObjects となっている。これはプログラム実行中に Shellを発射するオブジェクトがセットされた配列(インスタンス変数)であり、今回は次のように Cannonがセットされている (以下のCannonは要素2つの配列)。
// Shellを発射するオブジェクトを設定 ; 以下のCannonは配列
i_shootObjects = Cannon;  

以上をまとめると、今回のプログラムは次のような簡潔なものになる。
[Code5]  (実行結果 図16)
if (!i_INITIALIZED)
{
    Cannon[0].lastShootFrame = 8;
    Cannon[0].shootInterval = 15;
    Cannon[0].initShootDirection = new Vector2(0, 1);
    Cannon[0].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[0].shellSpeed = 0.40f;
    
    Cannon[1].lastShootFrame = 12;
    Cannon[1].shootInterval = 18;
    Cannon[1].initShootDirection = new Vector2(0, 1);
    Cannon[1].initShootPosition = new Vector2(0.0f, 1.75f);
    Cannon[1].shellSpeed = 0.40f;
    
    i_shootObjects = Cannon;
    
    i_INITIALIZED = true;
}

i_frameCount++;

EnemyMotion5();
EShellMotion();


1行目の初期化ブロックの内容は上記のBeta5と同じであるが、15行目においてインスタンス変数 i_shootObjects に Cannonがセットされている。上でも述べたが i_shootObjects はプログラム実行中にShellを発射するオブジェクトの配列であり、ここでShellを発射するオブジェクトを設定しているわけである。
EnemyMotion5()EShellMotion() は上で述べたものであり、このプログラムの実行結果は先ほどのBeta5と同じである (図16)。


# Code6
今回はさらに Shellを発射するオブジェクトを増やすが、それでもわずかな変更によって簡単に実装できることを以下で確認しよう。
今回使われるオブジェクトは、4つの Cannon 及び 2つの Battery である。1つのBatteryが2つのCannonを子オブジェクトとして持っている (具体的な階層構造は図17参照)。
そして、それらのオブジェクトが行う運動は前回と同じであり、2つのCannonが親オブジェクトであるBattery上で単振動をするというものである。前回は1組の Battery、Cannonの運動であったが、このプログラムでは2組の Battery、Cannonによる運動になっている。具体的には、図18のような運動である。

図17 階層構造
図18 実行結果 (Shell発射なし)

今回のオブジェクトの運動は EnemyMotion6() に以下のように記述されている (このコードだけを実行した結果が図18である)。
[EnemyMotion6()]
void EnemyMotion6()
{
    // Battery[0] & Cannon[0],[1]
    THMatrix3x3 rotBattery0 = TH2DMath.GetRotation3x3(-125);        
    THMatrix3x3 traBattery0 = TH2DMath.GetTranslation3x3(-8, 4);    
    THMatrix3x3 localBattery0 = traBattery0 * rotBattery0;
    
    i_shm += 2.0f;
    float x = 3.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
    THMatrix3x3 localCannon0 = TH2DMath.GetTranslation3x3(x - 2, 0);
    THMatrix3x3 localCannon1 = TH2DMath.GetTranslation3x3(x + 2, 0);
    
    THMatrix3x3 worldCannon0 = localBattery0 * localCannon0;
    THMatrix3x3 worldCannon1 = localBattery0 * localCannon1;
    THMatrix3x3 worldBattery0 = localBattery0;
    
    Cannon[0].SetMatrix(worldCannon0);
    Cannon[1].SetMatrix(worldCannon1);
    Battery[0].SetMatrix(worldBattery0);
    
    // Battery[1] & Cannon[2],[3]
    THMatrix3x3 rotBattery1 = TH2DMath.GetRotation3x3(125);        
    THMatrix3x3 traBattery1 = TH2DMath.GetTranslation3x3(8, 4);    
    THMatrix3x3 localBattery1 = traBattery1 * rotBattery1;
    
    THMatrix3x3 localCannon2 = TH2DMath.GetTranslation3x3(x - 2, 0);
    THMatrix3x3 localCannon3 = TH2DMath.GetTranslation3x3(x + 2, 0);
    
    THMatrix3x3 worldCannon2 = localBattery1 * localCannon2;
    THMatrix3x3 worldCannon3 = localBattery1 * localCannon3;
    THMatrix3x3 worldBattery1 = localBattery1;
    
    Cannon[2].SetMatrix(worldCannon2);
    Cannon[3].SetMatrix(worldCannon3);
    Battery[1].SetMatrix(worldBattery1);
}

実行結果(図18)からわかるように、同じ単振動を左右に分かれて行っているだけである。4行目から19行目までが左側のBattery、Cannonの運動であり、これは前回の EnemyMotion5() の内容と同じものである。22行目以降が右側のBattery、Cannonの運動であるが、違いは右側のBatteryの回転角度、移動先が、左側のBatteryのものと異なっているのみである。

今回のプログラムを以下に示す。
[Code6]  (実行結果 図19)
if (!i_INITIALIZED)
{
    for (int i = 0; i < Cannon.Length; i++)
    {
        Cannon[i].lastShootFrame = 8 + i * 4;
        Cannon[i].shootInterval = 15 + i * 3;
        Cannon[i].initShootDirection = new Vector2(0, 1);
        Cannon[i].initShootPosition = new Vector2(0.0f, 1.75f);
        Cannon[i].shellSpeed = 0.40f;
    }

    i_shootObjects = Cannon;

    i_INITIALIZED = true;
}

i_frameCount++;

EnemyMotion6();
EShellMotion();


図19 Code6 実行結果
プログラムの構成が前回のCode5とほとんど同じであることがわかるであろう。初期化ブロックでは、今回のプログラムで使われる4つのCannonから発射されるShellに関する設定を行っている。Shellが最初に発射されるフレームに関係するデータ lastShootFrame やShellの発射間隔を表す shootInterval を、4つのCannonで適当に値を変えて設定しているので実行結果(図19)にみられるように、Shellは4つのCannonからバラバラのタイミングで発射されている。

オブジェクトの運動をもっと複雑にする場合でも、変更する箇所は EnemyMotion() の中身だけでよい。どのような運動であっても、そのときCannonがいる地点から、そのときCannonが向いている方向へ指定のタイミングでShellが発射されることになる。Shellの発射される間隔や、Shellのスピードなどの変更は初期化ブロックの設定値を変えるだけで実現される。












© 2020-2024 Redpoll's 60 (All rights reserved)