Redpoll's 60
 Home / 3Dプログラミング入門 / 第2章 $§$2-17
第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-17 衝突判定 5


Shellを使った衝突判定は本節が最後であり、今までの内容を総合したものになっている。
本節では、キー操作によって動かすPlaneを「ユーザー」、シーン内のPlane以外のオブジェクトに対しては「敵」という表現を用いて議論を進める。
2-15節の最後に Planeの運動(ユーザー側の運動)を UserMotion() としてメソッド化した。そして、前節ではプログラムの見通しをよくするために敵側の運動、敵が発射するShellの運動 をそれぞれ EnemyMotion()EShellMotion() としてメソッド化したが、本節では まずユーザーが発射するShellの運動 を UShellMotion() としてメソッド化する。
さらに 2つの衝突判定処理、すなわち、ユーザーの発射したShellと敵オブジェクトとの衝突判定、敵の発射したShellとユーザーとの衝突判定 をメソッド化する (この2つの処理は最終的には1つのメソッドに統合される)。そして、これらのメソッドを使用して本節ではユーザーと敵オブジェクトの間での打ち合いを実装する。
なお、本節におけるプログラムでは、ユーザーが発射するShellは「UShell」、敵オブジェクトが発射するShellは「EShell」という名前の配列で使われている (両者ともに THShell2D型の配列)。


# Code1
以下のメソッドは前節で作成した EShellMotion() であるが、その内容は敵オブジェクト(Cannon)が発射するShellの運動に関するものである。
[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) > 30 || Mathf.Abs(newShellPos.y) > 18)
            {
                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);
            }
        }
    }
} 

前節では複数のCannonが一定間隔で自動的にShellを発射するものであったが、あるフレームで Shellが発射されるかどうかはプログラム4行目の foreachブロックにおいて決定される。List型のローカル変数 curShooters は、Shellを発射するオブジェクトが複数である場合を考慮して用意されているものであった。

以下に示すメソッド UShellMotion() は、ユーザー(Plane)が発射する Shellの運動に関するものであるが、その内容は基本的には上記の EShellMotion() と同じである。
[UShellMotion()]
void UShellMotion()  
{
    bool bShoot = false;
    if (Input.GetKey(KeyCode.A))
    {
        if (i_frameCount >= Plane.lastShootFrame + Plane.shootInterval)
        {
            Plane.lastShootFrame = i_frameCount;
            bShoot = true;
        }
    }

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

            shell.SetPosition(newShellPos);
        }
        else
        {
            if (bShoot)
            {
                THMatrix3x3 M = Plane.GetMatrix();

                Vector2 startPos = M * Plane.initShootPosition;
                Vector2 shootDir = (M * Plane.initShootDirection).normalized;

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

                shell.SetPosition(startPos);

                bShoot = false;
            }
        }
    }
} 

図1 Code1 実行結果
図2 Planeの初期状態におけるShell発射開始位置、発射方向
このメソッドには特に目新しい処理はない。EShellMotion() との違いは、Shell発射の決定が 4行目の Aキーが押されているかどうかの判定に変わっていることと、foreachループの中で使われているShellが EShell ではなく UShell になっていること、及び Shellを発射するオブジェクトが複数ではなく、Planeのみなので List型の curShooters ではなく、bool型の bShoot が使われていることぐらいである (curShootersbShootに関しては前節を参照)。

UserMotion()UShellMotion() によって、ユーザー側の運動及びShell発射の記述は次のように簡潔なものになる。

[Code1]  (実行結果 図1)
if (!i_INITIALIZED)
{
    Plane.lastShootFrame = 0;
    Plane.shootInterval = 3;
    Plane.initShootDirection = new Vector2(0, 1);
    Plane.initShootPosition = new Vector2(0.0f, 2.1f);
    Plane.shellSpeed = 0.80f;

    i_INITIALIZED = true;
}

i_frameCount++;

UserMotion();
UShellMotion();



# Code2
次に、ユーザーの発射するShellと敵オブジェクトとの間の衝突判定を実装する。

以下のプログラムは 2-14節 Code5 の衝突判定部分を抜粋したものである。
[2-14節 Code5]
// Collision Test 
Vector2 shell_pos = OneShell.GetPosition();

foreach(var obj in i_collisionObjects)
{
    bool bColl = CollisionTest_AllTypes(obj, shell_pos);
    if (bColl)
    {
        Hit(obj);
        break;
    }
}

このプログラムは連射ではないので、ユーザー(Plane)の発射するShellは1つだけであり、画面内には同時に1つのShellしか存在しない。したがって、72行目のforeach文は その1つのShellと衝突判定対象のすべてのオブジェクトとの衝突判定を1つ1つ順番に行っていくものである。つまり、ただ1つのShellが衝突判定対象のオブジェクトのどれかと衝突しているかを調べる処理となっている (4行目の i_collisionObjects は衝突判定対象となるオブジェクトがセットされている配列である)。
しかし、今回はユーザーが発射するShellは連射によって発射されるので、画面内には同時に複数のShellが存在することになる。したがって、今回の衝突判定では 複数のShellの1つ1つについて、衝突判定対象のオブジェクトのどれかと衝突しているかを調べる必要がある。

以下のプログラムはユーザーの発射したShellと敵オブジェクトとの衝突判定を行うものであり、Shellは連射によって発射された複数個のShellであることを想定している。
[衝突判定 : UShell vs Enemy]
// UShell vs Enemy
foreach (var shell in UShell)
{
    if (!shell.active) { continue; }

    Vector2 shell_pos = shell.GetPosition();

    foreach(var obj in i_collisionObjects)
    {
        bool bColl = CollisionTest_AllTypes(obj, shell_pos);
        if (bColl)
        {
            Hit(obj);

            shell.SetPosition(-1000, 0);
            shell.active = false;
        }
    }
} 

先程のプログラムが1つのShellと衝突判定対象のすべてのオブジェクトとの衝突判定であったのに対し、このプログラムは画面内に存在するすべてのShellと衝突判定対象のすべてのオブジェクトとの衝突判定を行うものである。最初のforeach文ではユーザー側のShell配列であるUSehllを1つ1つ調べて、そのShellがactiveであれば(画面内に存在していれば)、第2のforeach文において衝突判定対象のすべてのオブジェクトとの衝突判定処理に移動するが、activeでないShellの場合はそのShellに関しての衝突判定処理は行われない。
Shellがオブジェクトと衝突した際には11行目のifブロックに処理が移る。2-14節や2-15節では Shellがオブジェクトに衝突した際には、オブジェクトの色を一時的に赤くするために Hit(..)メソッドを実行するだけであったが、上のプログラムでは Hit(..)メソッド(13行目)を実行した後に15行目、16行目において
    shell.SetPosition(-1000, 0);
    shell.active = false;
という処理が行われる。
これは、Shellがオブジェクトに衝突した時点で、そこから先に進ませずに画面外に移動させることを目的としている。画面外に移動したShellは activeでないShellなので、activeプロパティには false をセットする。

次に、敵オブジェクトの発射するShellとユーザーとの間における衝突判定プログラムを以下に示す。
[衝突判定 : EShell vs User]
// EShell vs User(Plane)
foreach (var shell in EShell)
{
    if (!shell.active) { continue; }

    Vector2 shell_pos = shell.GetPosition();
    bool bColl = CollisionTest_AllTypes(Plane, shell_pos);
    if (bColl)
    {
        Hit(Plane);

        shell.SetPosition(-1000, 0);
        shell.active = false;
    }
} 

このプログラムでは敵オブジェクト側のShell配列であるEShellを1つ1つ調べて、そのShellがactiveであれば衝突判定処理を行うものである。このプログラムでの衝突判定の対象は Planeのみであるから、上の「UShell vs Enemy」のプログラムと異なり、衝突判定部分には第2のforeach文が使われていない。その他の点については上のプログラムと同様である。

今見てきた2つのプログラム「UShell vs Enemy」、「EShell vs User」は、その内容自体は同じであり、前者がShell配列と複数のオブジェクトとの間における衝突判定であるのに対し、後者はShell配列と1つのオブジェクトとの間における衝突判定となっている。両者の違いは、使用するShell配列及び衝突判定の対象となるオブジェクトが複数か1つかといったものでしかない。
そこで、この2つのプログラムを以下のメソッドに統合する。
[void CollisionTest(..)]
void CollisionTest(THShell2D[] shell_array, THAlphaObject2D[] target_array)
{
    foreach(var shell in shell_array)
    {
        if (!shell.active) { continue; }

        Vector2 shell_pos = shell.GetPosition();

        foreach(var target in target_array)
        {
            bool bColl = CollisionTest_AllTypes(target, shell_pos);
            if (bColl)
            {
                Hit(target);

                shell.SetPosition(-1000, 0);
                shell.active = false;
            }
        } 
    } 
}

このようにメソッド化しておくことで、たとえば上記の「UShell vs Enemy」、「EShell vs User」は次のような記述で済ませることができる。
// UShell vs Enemy
CollisionTest(UShell, i_collisionObjects);
// EShell vs User
CollisionTest(EShell, new THAlphaObject2D[] { Plane });

11行目の CollisionTest_AllTypes(..) は全ての種類の衝突判定に対応させるための入口的な役割のメソッドであるが、具体的な内容は以下のとおり。
[CollisionTest_AllTypes(..)]
bool CollisionTest_AllTypes(THAlphaObject2D obj, Vector2 shell_pos)
{
    THMatrix3x3 mtx;
    Vector2 P;

    bool bColl = false;
    switch (obj.GetType())
    {
        case THAlphaObject2D.DISK :
            Vector2 C = obj.GetPosition();
            float r = obj.GetDiskScale() * obj.initRadius;
            bColl = CollisionTest_Point_Disk(shell_pos, C, r);
            break;

        case THAlphaObject2D.RECT :
            mtx = obj.GetMatrix();
            P = mtx.inverse * TH2DMath.ToVector3(shell_pos);
            bColl = CollisionTest_Point_AABB(P, obj.initRectBounds);
            break;

        case THAlphaObject2D.MULTI_RECT : 
            mtx = obj.GetMatrix();
            P = mtx.inverse * TH2DMath.ToVector3(shell_pos);
            bColl = CollisionTest_Point_MultiAABB(P, obj.initRectBounds);
            break;

        case THAlphaObject2D.TRIMESH :
            mtx = obj.GetMatrix();
            P = mtx.inverse * TH2DMath.ToVector3(shell_pos);
            bColl = CollisionTest_Point_TriMesh(P, obj.initTriangleCoords);
            break;
    }

    return bColl;
}

2D空間における衝突判定のオブジェクトの種類は最終的には4種類で、円盤(DISK)、長方形(RECT)、一般形状(三角形の集合 ; TRIMESH)については既に見てきた。上記の21行目の MULTI_RECT は、オブジェクトを複数の長方形の集合として扱うものであるが、これについては後ほど解説する。

では、以上の準備をもとにしてユーザーと敵オブジェクトとの間における打ち合いを実装する。
最初のプログラムで敵オブジェクトとして使われるのは、前節で使用した4つのCannonである。それら4つのCannonの運動についても前節のCode6で使用した EnemyMotion6() を使うが、ここでは EnemyMotion2() という名前で使用される。
[EnemyMotion2()  (前節のEnemyMotion6)]
void EnemyMotion2()
{
    // 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);
}

また、ユーザー側オブジェクト Plane の運動は先程のCode1で使用した UserMotion() を使う。ユーザーが発射するShellの運動 及び 敵オブジェクトが発射するShellの運動については、上記の UShellMotion()EShellMotion() を使用する。
なお、初期化ブロックにおけるユーザー側オブジェクト(Plane)の設定データはCode1と同じであり、敵側オブジェクト(4つのCannon)の設定データの内容も前節Code6と同じである。

実際のプログラムを以下に示す。
[Code2]  (実行結果 図3)
if (!i_INITIALIZED)
{
    Plane.lastShootFrame = 0;
    Plane.shootInterval = 3;
    Plane.initShootDirection = new Vector2(0, 1);
    Plane.initShootPosition = new Vector2(0.0f, 2.1f);
    Plane.shellSpeed = 0.80f;
    
    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_collisionObjects = Cannon;
    
    i_INITIALIZED = true;
}

i_frameCount++;

UserMotion();
UShellMotion();

EnemyMotion2();
EShellMotion();

// UShell vs Enemy
CollisionTest(UShell, i_collisionObjects);

// EShell vs User
CollisionTest(EShell, new THAlphaObject2D[] { Plane });


図3 Code2 実行結果
図4 Cannonの衝突判定用モデル。4つの長方形のいずれかと衝突したかどうかで判定する。

Cannonとの衝突判定については、一般のオブジェクトの場合のように Cannonを三角形の集合として衝突判定を行うこともできるが、ここではより効率的な方法を採っている。それは Cannonに独自の衝突判定用モデルを用意して、衝突判定はその衝突判定用モデルに対して行うようにするものである。Cannonの衝突判定用モデルは図4に示されるように4個の長方形で構成されており、衝突判定はこの4個の長方形のいずれかと Shellが衝突したかどうかで判定される。Shellと長方形との衝突判定は 2-13節、2-14節で扱ってきたものと同様である。2-13節、2-14節では1つの長方形オブジェクトは1つの長方形で構成されていたが、ここでは1つのCannonは複数の長方形で構成されているという点で違いはあるが、衝突判定の記述自体に大きな違いはない。
以下は上記の CollisionTest_AllTypes(..) の一部を抜粋したものである。
..
..
    case THAlphaObject2D.RECT :
        mtx = obj.GetMatrix();
        P = mtx.inverse * TH2DMath.ToVector3(shell_pos);
        bColl = CollisionTest_Point_AABB(P, obj.initRectBounds);
        break;

    case THAlphaObject2D.MULTI_RECT : 
        mtx = obj.GetMatrix();
        P = mtx.inverse * TH2DMath.ToVector3(shell_pos);
        bColl = CollisionTest_Point_MultiAABB(P, obj.initRectBounds);
        break;
..
..

15行目の case はオブジェクトの種類が RECT である場合の衝突判定処理であり、21行目の case はオブジェクトの種類が MULTI_RECT である場合の衝突判定処理である。その内容は両者ほとんど同じである。違いは、衝突判定用のメソッドが CollisionTest_Point_AABB(..) であるか CollisionTest_Point_MultiAABB(..) であるかという点のみであるが、この2つのメソッドも以下に示されるように違いはほとんどない。

[CollisionTest_Point_AABB(..)]
bool CollisionTest_Point_AABB(Vector2 P, Vector2[] minmax)
{
    Vector2 min = minmax[0];
    Vector2 max = minmax[1];

    if (min.x <= P.x && P.x <= max.x &&
        min.y <= P.y && P.y <= max.y)
    {
        return true;
    }

    return false;
}

[CollisionTest_Point_MultiAABB(..)]
bool CollisionTest_Point_MultiAABB(Vector2 P, Vector2[] minmax)
{
    for (int idx = 0; idx < minmax.Length; idx += 2)
    {
        Vector2 min = minmax[idx];
        Vector2 max = minmax[idx + 1];

        if (min.x <= P.x && P.x <= max.x &&
            min.y <= P.y && P.y <= max.y)
        {
            return true;
        }
    }

    return false;
}

第2引数の minmax は、長方形の左下隅、右上隅の頂点の座標配列である。CollisionTest_Point_AABB(..) の場合は衝突判定の対象となる長方形は1つなので minmax の要素数は常に 2 であるが、CollisionTest_Point_MultiAABB(..) の場合は衝突判定の対象となる長方形が複数個となるので minmax の要素数は一定ではない。たとえば、今回のCannonの場合は、その衝突判定用モデルは4個の長方形で構成されているので、minmax の要素数は 8 である。具体的には、図4に示されるように4個の長方形の左下隅、右上隅の頂点座標がセットされている (CollisionTest_Point_AABB(..) は1つの長方形の場合を独立させているに過ぎない。これを CollisionTest_Point_MultiAABB(..) に統合することもできる。その方がより簡潔であろう)。


# Code3
では次に、敵オブジェクトを変えた場合について見てみよう。
今回の敵オブジェクトは 2-15節で使用した一般形状(三角形の集合)のオブジェクトである Starと Letterである。

図5 Star 初期状態
図6 Letter 初期状態

Star及び Letterの初期状態における Shell発射開始位置、及び発射方向は下図のとおり。

図7 Starの初期状態におけるShell発射開始位置、発射方向
図8 Letterの初期状態におけるShell発射開始位置、発射方向

敵オブジェクトが変わってもプログラムの内容自体は以下に示されるようにほとんど変わらない。今回の打ち合いのプログラムは次のようになる。
[Code3]  (実行結果 図9)
if (!i_INITIALIZED)
{
    Plane.lastShootFrame = 0;
    Plane.shootInterval = 3;
    Plane.initShootDirection = new Vector2(0, 1);
    Plane.initShootPosition = new Vector2(0.0f, 2.1f);
    Plane.shellSpeed = 0.80f;
    
    Star.lastShootFrame = 5;
    Star.shootInterval = 20;
    Star.initShootPosition = new Vector2(3.04f, 0.89f);
    Star.initShootDirection = Star.initShootPosition.normalized;
    Star.shellSpeed = 0.25f;
    
    Letter.lastShootFrame = 10;
    Letter.shootInterval = 18;
    Letter.initShootPosition = new Vector2(-2.14f, -2.61f);
    Letter.initShootDirection = new Vector2(0, -1);
    Letter.shellSpeed = 0.30f;
    
    i_shootObjects = new THAlphaObject2D[] { Star, Letter };
    i_collisionObjects = i_shootObjects;
    
    i_INITIALIZED = true;
}

i_frameCount++;

UserMotion();
UShellMotion();

EnemyMotion3();
EShellMotion();

// UShell vs Enemy
CollisionTest(UShell, i_collisionObjects);
// EShell vs User
CollisionTest(EShell, new THAlphaObject2D[] { Plane });


図9 Code3 実行結果
実際に Code1からの変更点は、初期化ブロックにおける各敵オブジェクトの設定データ (初期状態での発射開始位置、発射方向など)、さらに初期化ブロックにおいて、Shellを発射するオブジェクト及び 衝突判定の対象オブジェクトとしてインスタンス変数 i_shootObjectsi_collisionObjects に Star、Letter をセットしている。そして、今回の敵オブジェクトの運動が EnemyMotion3() に定義されているという点ぐらいである。
プログラム内で使っている UserMotion()UShellMotion()EShellMotion()CollisionTest(..) などのメソッドの内容はもちろんCode1のものと同じである。

今回の敵オブジェクトの運動は指定位置において、ある角度からある角度までの回転を繰り返すものである。具体的には、Starは $-35$°から$-5$°までの回転を繰り返し、Letterは $-40$°から$0$°までの回転を繰り返している。それらの一定範囲の往復運動は単振動によって実装されており、以下のプログラムにおける i_shm は単振動の計算で使用する角度を表すインスタンス変数である。
[EnemyMotion3()]
i_shm += 2.0f;
float deg = 15.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad) - 20.0f;    // [-35, -5]
THMatrix3x3 R = TH2DMath.GetRotation3x3(deg);    
THMatrix3x3 T = TH2DMath.GetTranslation3x3(-8.9f, -3.0f);
THMatrix3x3 M = T * R;
Star.SetMatrix(M);

deg = 20.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad) - 20.0f;    // [-40, 0]
R = TH2DMath.GetRotation3x3(deg);    
T = TH2DMath.GetTranslation3x3(8.5f, 5.0f);    
M = T * R;
Letter.SetMatrix(M);


何度か述べたが、あるオブジェクトからShellを発射させるためには、初期化ブロックにおいてそのオブジェクトのShell発射に関する各データ設定、及び そのオブジェクトを「Shellを発射するオブジェクト」としてインスタンス変数 i_shootObjects へ登録。そして、そのオブジェクトの運動を EnemyMotion() へ記述する。以上の手続きによって、プログラム実行中にそのオブジェクトの指定の位置からShellが一定の間隔で発射されるようになるのである。
たとえば、上記で使用した Starの発射開始位置、及び発射方向を下図10のように変更するとしよう (Star上部の赤い部分から Shellが発射されるようにする)。

図10
図11

その場合でも初期化ブロックは以下のように設定すればよい。
if (!i_INITIALIZED)
{
    ...
    Star.initShootPosition = new Vector2(0.0f, 3.2f);
    Star.initShootDirection = new Vector2(0, 1);
    ...
}

この初期化ブロックにおいて Starに設定されるデータで、Code2から変更されている箇所は、発射開始位置、発射方向を表すプロパティ initShootPositioninitShootDirection だけである。実行結果(図11)に示されるように、Star上部の赤い部分から Shellが発射されるようになる (ただし、ここでは Starのみを表示し、Starの運動もCode2とは違うものにしている)。つまり、このような変更を行う場合でも 初期化ブロックと EnemyMotion() だけを書き換えればよく、それ以外のメソッドは不変なのである。


# Code4
Code2とCode3では敵オブジェクトを変えて、ユーザーと敵オブジェクトの間での打ち合いを実装した。2つのプログラムの内容はほとんど同じであり、敵オブジェクトの変更によって必要となるのは、プログラムにおいては以下の点であった。

(1)  Shellを発射するオブジェクトの発射開始位置、発射方向などの各データ設定 (初期化ブロックにて設定)
(2)  Shellを発射するオブジェクト、衝突判定の対象となるオブジェクトをインスタンス変数 i_shootObjectsi_collisionObjects へセット (初期化ブロックにて設定)
(3)  敵オブジェクトの運動の記述 (EnemyMotion()に記述)

つまり、この3点をプログラムで変更すれば、どのようなオブジェクトでもShellの打ち合いの相手とすることができるわけである。以降のプログラムでは、このことをやや複雑なオブジェクトを使って確認してみよう。
図12は以降のプログラムで使用する敵オブジェクト Battleship であり、図13はその階層構造である (一部省略してある)。

図12 Battleship (すべてのオブジェクトを一体化した状態)
図13 Battleship 階層構造

Battleshipは以下の5種類のオブジェクトから構成されており、以下の図はそれらのオブジェクトの初期状態である。図14の Body は一番上の階層の親オブジェクトであり、図15の HeadCannonは直接Body上に置かれ、図16の Propellerは同じものが3個あり、これらもまた直接Body上に置かれる。
図17の SideBatteryは同じものが4個あり、これらも直接Body上に置かれる。図18の SideCannonは同じものが8個あり、2つずつ1組になって1つのSideBattery上に置かれる。
オブジェクトの階層は上図13に示されるように、Bodyの直接の子オブジェクトとして HeadCannon、4個のSideBattery、3個のPropellerがあり、4個のSideBatteryのそれぞれに2個のSideCannonが子オブジェクトとして配置される。

  • 図14 Body 初期状態
  • 図15 HeadCannon 初期状態
  • 図16 Propeller 初期状態

図17 SideBattery 初期状態
図18 SideCannon 初期状態

まず始めに、1つのSideBattery上において、子オブジェクトの2つのSideCannonが一定の範囲を回転するプログラムを作成する。簡単のため、1つのSideBattery、2つのSideCannon 及び Body以外は非表示にしてある。
[Beta4A]  (実行結果 図19)
i_shm += 2.0f;
float rdg0 = 30.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
THMatrix3x3 leftTra = TH2DMath.GetTranslation3x3(c_attachPos_SideCannonL);
THMatrix3x3 rightTra = TH2DMath.GetTranslation3x3(c_attachPos_SideCannonR);
THMatrix3x3 Rot = TH2DMath.GetRotation3x3(rdg0);
THMatrix3x3 localSideCannon0 = leftTra * Rot;
THMatrix3x3 localSideCannon1 = rightTra * Rot;

THMatrix3x3 T = TH2DMath.GetTranslation3x3(c_attachPos_SideBattery[0]);
THMatrix3x3 R = TH2DMath.GetRotation3x3(c_deg_SideBattery[0]);
THMatrix3x3 localSideBattery0 = T * R;

THMatrix3x3 localBody = THMatrix3x3.identity;

THMatrix3x3 worldSideCannon0 = localBody * localSideBattery0 * localSideCannon0;
THMatrix3x3 worldSideCannon1 = localBody * localSideBattery0 * localSideCannon1;
THMatrix3x3 worldSideBattery0 = localBody * localSideBattery0;
THMatrix3x3 worldBody = localBody;

SideCannon[0].SetMatrix(worldSideCannon0);
SideCannon[1].SetMatrix(worldSideCannon1);
SideBattery[0].SetMatrix(worldSideBattery0);
Body.SetMatrix(worldBody);


図19 Beta4A 実行結果
実行結果(図19)に示されるように、このプログラムは SideBatteryの向きを変えた状態でアタッチポジションに移動させ、移動後のSideBattery上において2つのSideCannonを一定の範囲で回転させるものである。1行目から7行目では2つのSideCannonがSideBattery上でのそれぞれのアタッチポジションにおいて $-30$°から$30$°の範囲を往復する変換行列 localSideCannon0localSideCannon1 を求めている (3、4行目の c_attachPos_SideCannon# は2つのSideCannonのアタッチポジションを表す定数である)。11行目の変換行列 localSideBattery0 の内容は、SideBatteryをある角度回転させて、さらに Body上のアタッチポジションに移動させるという処理をこの順で実行するものである。SideBatteryの回転角度及び移動量は2つの定数 c_deg_SideBattery[#]c_attachPos_SideBattery[#] の値を使っている。また、Bodyはここでは動かさないので実行される行列はidentity行列である。

続いて、HeadCannonの運動を追加する。運動の内容自体は単に、アタッチポジションにおいて一定の範囲を回転するだけのものである。
[Beta4B (= EnemyMotion4)]  (実行結果 図20)
i_shm += 2.0f;
float rdg0 = 30.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
THMatrix3x3 leftTra = TH2DMath.GetTranslation3x3(c_attachPos_SideCannonL);
THMatrix3x3 rightTra = TH2DMath.GetTranslation3x3(c_attachPos_SideCannonR);
THMatrix3x3 Rot = TH2DMath.GetRotation3x3(rdg0);
THMatrix3x3 localSideCannon0 = leftTra * Rot;
THMatrix3x3 localSideCannon1 = rightTra * Rot;

THMatrix3x3 T = TH2DMath.GetTranslation3x3(c_attachPos_SideBattery[0]);
THMatrix3x3 R = TH2DMath.GetRotation3x3(c_deg_SideBattery[0]);
THMatrix3x3 localSideBattery0 = T * R;

THMatrix3x3 localBody = THMatrix3x3.identity;

THMatrix3x3 worldSideCannon0 = localBody * localSideBattery0 * localSideCannon0;
THMatrix3x3 worldSideCannon1 = localBody * localSideBattery0 * localSideCannon1;
THMatrix3x3 worldSideBattery0 = localBody * localSideBattery0;
THMatrix3x3 worldBody = localBody;

SideCannon[0].SetMatrix(worldSideCannon0);
SideCannon[1].SetMatrix(worldSideCannon1);
SideBattery[0].SetMatrix(worldSideBattery0);
Body.SetMatrix(worldBody);

T = TH2DMath.GetTranslation3x3(c_attachPos_HeadCannon);
R = Rot;
THMatrix3x3 localHeadCannon = T * R;
THMatrix3x3 worldHeadCannon = localBody * localHeadCannon;
HeadCannon.SetMatrix(worldHeadCannon);


このプログラムは上記のBeta4Aと23行目までは同じであり、25行目以降の HeadCannonの運動が追加されているだけである。実行結果(図20)に示されるように、HeadCannonはBodyの先端において、SideCannonと同じく $-30$°から$30$°の範囲を往復している (c_attachPos_HeadCannonは HeadCannonのアタッチポジションを表す定数であり、これによって Bodyの先端に移動する)。

では、HeadCannonと2つのSideCannonからのShellの発射を実装しよう。先程述べたように、オブジェクトからShellを発射させるために必要となるのは初期化ブロックにおける設定と EnemyMotion()での運動の記述である。今回は上記のプログラムBeta4Bをオブジェクトの運動、すなわち EnemyMotion() の内容とする。したがって、EnemyMotion() の記述の方はすでに行われているので、あとは初期化ブロックにおいてShell発射に関する各種データを設定すればよい。

実際のプログラムは次のようになる。
[Code4]  (実行結果 図21)
if (!i_INITIALIZED)
{
    // HeadCannon
    HeadCannon.lastShootFrame = 40;
    HeadCannon.shootInterval = 5;
    HeadCannon.initShootDirection = new Vector2(1, 0);
    HeadCannon.initShootPosition = new Vector2(1.95f, 0.0f);
    HeadCannon.shellSpeed = 0.54f;
    
    // SideCannon 
    SideCannon[0].lastShootFrame = 6;
    SideCannon[0].shootInterval = 16;
    SideCannon[0].initShootDirection = new Vector2(0, -1);
    SideCannon[0].initShootPosition = new Vector2(0.0f, -2.45f);
    SideCannon[0].shellSpeed = 0.40f;
    
    SideCannon[1].lastShootFrame = 12;
    SideCannon[1].shootInterval = 16;
    SideCannon[1].initShootDirection = new Vector2(0, -1);
    SideCannon[1].initShootPosition = new Vector2(0.0f, -2.45f);
    SideCannon[1].shellSpeed = 0.40f;
    
    i_shootObjects = new THAlphaObject2D[] { SideCannon[0], SideCannon[1], HeadCannon };
    
    i_INITIALIZED = true;
}

i_frameCount++;

EnemyMotion4();
EShellMotion();


1行目の初期化ブロックでは、HeadCannon及び 2つのSideCannonのShell発射に関する各種データを設定し、そしてインスタンス変数 i_shootObjects に Shellを発射するオブジェクトとしてその3つのオブジェクトを登録している。
EnemyMotion4() は上述したものであり、EShellMotion() は今までのものと同じである。
実行結果(図21)に見られるように、確かに HeadCannon及び 2つのSideCannonから Shellが発射されるようになっている。

図20 Beta4B 実行結果 (EnemyMotion4)
図21 Code4 実行結果

今回 Shellを発射するオブジェクトは2つのSideCannonと1つのHeadCannonである。SideCannonは初期状態でy軸マイナス方向を向いており、初期状態における Shellの発射開始位置及び 発射方向は図22に示されるようにy軸マイナス側にある。HeadCannonは初期状態でx軸プラス方向を向いており、初期状態における Shellの発射開始位置及び 発射方向は図23に示されるようにx軸プラス側にある。
以下の図に示される発射開始位置、発射方向は上のプログラムの初期化ブロックで initShootPositioninitShootDirection に設定されている値と同じである。

図22 SideCannonの初期状態におけるShell発射開始位置、発射方向
図23 HeadCannonの初期状態におけるShell発射開始位置、発射方向


# Code5
Code4では SideBattery、SideCannon は1組だけであったが、今回は4組の SideBattery、SideCannon 及び HeadCannon による Shellの発射を実装する。Shellを発射するオブジェクトの数が増えてもプログラムで変更する点は、初期化ブロックにおける設定とオブジェクトの運動の記述だけである。
まずは オブジェクトの運動を記述するが、その内容はCode4と同様である。Code4では、SideBattery上において2つのSideCannonが一定の範囲を回転するものであったが、そこでは SideBattery、SideCannonの組は1組だけであった。ここでは4組の SideBattery、SideCannonがそれぞれ指定の位置において、Code4と同じ運動を行っている。

具体的には、次のとおり。
[Beta5 (= EnemyMotion5)]  (実行結果 図24)
// local matrix : SideCannon
i_shm += 2.0f; 
float deg0 = 30.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
float deg1 = 30.0f * Mathf.Sin((i_shm + 120) * Mathf.Deg2Rad);
float deg2 = 30.0f * Mathf.Sin((i_shm + 180) * Mathf.Deg2Rad);
float deg3 = 30.0f * Mathf.Sin((i_shm + 300) * Mathf.Deg2Rad);
THMatrix3x3[] Rot = 
{
    TH2DMath.GetRotation3x3(deg0), TH2DMath.GetRotation3x3(deg1),
    TH2DMath.GetRotation3x3(deg2), TH2DMath.GetRotation3x3(deg3)
};
THMatrix3x3 leftTra = TH2DMath.GetTranslation3x3(c_attachPos_SideCannonL);
THMatrix3x3 rightTra = TH2DMath.GetTranslation3x3(c_attachPos_SideCannonR);
THMatrix3x3[] localSideCannon = new THMatrix3x3[SideCannon.Length];
for (int idx = 0; idx < SideCannon.Length; idx += 2)
{
    localSideCannon[idx]     = leftTra * Rot[idx / 2];
    localSideCannon[idx + 1] = rightTra * Rot[idx / 2];
}

// local matrix : SideBattery
THMatrix3x3 T, R;
THMatrix3x3[] localSideBattery = new THMatrix3x3[SideBattery.Length];
for (int i = 0; i < SideBattery.Length; i++)
{
    R = TH2DMath.GetRotation3x3(c_deg_SideBattery[i]);
    T = TH2DMath.GetTranslation3x3(c_attachPos_SideBattery[i]);
    localSideBattery[i] = T * R;
}

// local matrix : HeadCannon
T = TH2DMath.GetTranslation3x3(c_attachPos_HeadCannon);
R = Rot[0];
THMatrix3x3 localHeadCannon = T * R;

// local matrix : Body
float degBody = 10.0f * Mathf.Sin(i_shm * Mathf.Deg2Rad);
THMatrix3x3 localBody = TH2DMath.GetRotation3x3(degBody);


// world matrix : SideCannon
for (int i = 0; i < SideCannon.Length; i++)
{
    THMatrix3x3 worldSideCannon = localBody * localSideBattery[i / 2] * localSideCannon[i];
    SideCannon[i].SetMatrix(worldSideCannon);
}

// world matrix : SideBattery
for (int i = 0; i < SideBattery.Length; i++)
{
    THMatrix3x3 worldSideBattery = localBody * localSideBattery[i];
    SideBattery[i].SetMatrix(worldSideBattery);
}

// world matrix : HeadCannon
THMatrix3x3 worldHeadCannon = localBody * localHeadCannon;
HeadCannon.SetMatrix(worldHeadCannon);

// world matrix : Body
THMatrix3x3 worldBody = localBody;
Body.SetMatrix(worldBody);


一見複雑に見えるが上でも述べたように、ここで記述されている内容は、4組の SideBattery、SideCannonのそれぞれに指定位置において、Code4と同じ運動を行わせるものである。実行結果(図24)に示されるように、Body上に配置された4つのSideBattery上のそれぞれで、2つのSideCannonが一定の範囲を往復している (2~19行目 ; $-30$°から$30$°の間の往復であり、4組のSideCannonは往復のタイミングを少しずつずらしてある)。HeadCannonの運動も同様に一定範囲の往復である (32~34行目 ; $-30$°から$30$°の間の往復)。Bodyは前回のプログラムでは何も運動はしていなかったが、今回は $-10$°から $10$°の範囲で回転を繰り返している (37~38行目)。
また、プログラム中の c_attachPos_##c_deg_## などの定数は上記 Beta4A、Beta4B で解説したものと役割は同じである。

では、今作成した Beta5 を今回のオブジェクトの運動としてメソッド EnemyMotion5() に独立させる。さらに、Shell発射に関する設定のために初期化ブロックを追加し、今回用意された4組(合計8個)のSideCannonとHeadCannonからのShell発射を実装する。

[Code5]  (実行結果 図25)
if (!i_INITIALIZED)
{
    // HeadCannon
    HeadCannon.lastShootFrame = 40;
    HeadCannon.shootInterval = 5;
    HeadCannon.initShootDirection = new Vector2(1, 0);
    HeadCannon.initShootPosition = new Vector2(1.95f, 0.0f);
    HeadCannon.shellSpeed = 0.54f;
    
    // SideCannon (8個)
    for (int i = 0; i < SideCannon.Length; i++)
    {
        SideCannon[i].lastShootFrame = 6 * i;
        SideCannon[i].shootInterval = 16;
        SideCannon[i].initShootDirection = new Vector2(0, -1);
        SideCannon[i].initShootPosition = new Vector2(0.0f, -2.45f);
        SideCannon[i].shellSpeed = 0.40f;
    }
    
    
    i_shootObjects = new THAlphaObject2D[]
    {
        SideCannon[0], SideCannon[1], SideCannon[2], SideCannon[3],
        SideCannon[4], SideCannon[5], SideCannon[6], SideCannon[7], 
        HeadCannon
    };
    
    i_INITIALIZED = true;
}

i_frameCount++;

EnemyMotion5();
EShellMotion();


Code4からの変更点は、オブジェクトの運動を除けば初期化ブロックにおける設定において SideCannonの個数がここでは8個になっているので、その分の記述が増えているだけである。今回の設定では各SideCannonから Shellが最初に発射されるのは $6$フレームずつずらして行われるが(13行目)、Shell発射間隔は各SideCannonで同じであり、$16$フレームおきの発射となる(14行)。

図24 Beta5 実行結果 (EnemyMotion5)
図25 Code5 実行結果


# Code6
では最後に Battleship との打ち合いを実装する。
Battleshipの運動は本節前半で見てきたオブジェクトと比べて複雑ではあるが、プログラムの構成自体は特別変わるところはない。

[Code6]  (実行結果 図26)
if (!i_INITIALIZED)
{
    // Plane
    Plane.lastShootFrame = 0;
    Plane.shootInterval = 3;
    Plane.initShootDirection = new Vector2(0, 1);
    Plane.initShootPosition = new Vector2(0.0f, 2.1f);
    Plane.shellSpeed = 0.80f;
    
    // HeadCannon
    HeadCannon.lastShootFrame = 40;
    HeadCannon.shootInterval = 5;
    HeadCannon.initShootDirection = new Vector2(1, 0);
    HeadCannon.initShootPosition = new Vector2(1.95f, 0.0f);
    HeadCannon.shellSpeed = 0.54f;
    
    // SideCannon (8個)
    for (int i = 0; i < SideCannon.Length; i++)
    {
        SideCannon[i].lastShootFrame = 6 * i;
        SideCannon[i].shootInterval = 16;
        SideCannon[i].initShootDirection = new Vector2(0, -1);
        SideCannon[i].initShootPosition = new Vector2(0.0f, -2.45f);
        SideCannon[i].shellSpeed = 0.40f;
    }
    
    i_shootObjects = new THAlphaObject2D[]
    {
        SideCannon[0], SideCannon[1], SideCannon[2], SideCannon[3],
        SideCannon[4], SideCannon[5], SideCannon[6], SideCannon[7],
        HeadCannon
    };
    
    i_collisionObjects = new THAlphaObject2D[1] { Body };
    
    i_INITIALIZED = true;
}

i_frameCount++;

UserMotion();
UShellMotion();

EnemyMotion6();
EShellMotion();

// UShell vs Enemy
CollisionTest(UShell, i_collisionObjects);
// EShell vs User
CollisionTest(EShell, new THAlphaObject2D[] { Plane });


図26 Code6 実行結果
初期化ブロックにおける各オブジェクトのShell発射に関する設定は、今までのプログラムで見てきたものと同じである。
敵側のShell発射オブジェクトはCode5と同じく1つのHeadCannonと8個のSideCannonであり、インスタンス変数 i_shootObjects にセットされるオブジェクトはそれら計9個のオブジェクトとなる (27~32行目)。
敵側において衝突判定の対象となるオブジェクトは Body(図[14])のみなので、衝突判定対象のオブジェクトをセットするインスタンス変数 i_collisionObjects には Bodyのみがセットされている (35行目)。

Code4の解説の最初の方で以下の3点をプログラムで変更すれば、どのようなオブジェクトでもShellの打ち合いの相手とすることができると述べた。

(1)  Shellを発射するオブジェクトの発射開始位置、発射方向などの各データ設定 (初期化ブロックにて設定)
(2)  Shellを発射するオブジェクト、衝突判定の対象となるオブジェクトをインスタンス変数 i_shootObjectsi_collisionObjects へセット (初期化ブロックにて設定)
(3)  敵オブジェクトの運動の記述 (EnemyMotion()に記述)

実際、本節の Code2、Code3 及びこの Code6 を比較すると、(使われている敵オブジェクトが異なる点を除けば)その違いは上記の3点だけであることがわかる。今回の Battleshipのように比較的複雑な運動をするオブジェクトとのShell打ち合いを実装する場合でも、必要な変更は上記の3点のみなのである。

なお、敵オブジェクトBattleshipの運動であるが、プログラムでは EnemyMotion6() に記述されている。しかし、その運動はランダムに算出される目的地点への移動を繰り返すだけであり、特に汎用性があるわけでもなく、また 今回のプログラムは、どのようなオブジェクトでも上で示した3つの変更をプログラムに行えば、打ち合いの実装ができることを確認することが目的であるから、敵オブジェクトの運動 EnemyMotion6() についての解説は省略する。












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