プログラミング演習Ⅲ(2023)

【WPF練習14】壁崩し(5/9)

プロジェクトタイプC# WPFアプリケーション※
プロジェクト名T10b
ソリューション名PET10
ターゲットフレームワーク.NET 6.0 (長期的なサポート)

※ 「WPFアプリ(.NET Framework)」ではないので注意せよ!

注意
  • 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.
    • 先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと.
    • プロジェクトの作成作業については準備を参照せよ.

14-5. 衝突判定と衝突応答

次にボールがブロックやプレイヤーにぶつかったときに跳ね返る処理を作ろう.このためにまず線分同士の 交差判定を行うメソッドが必要である.MainWindowクラスに_に示す二つのメソッドを追記しよう.

perp()メソッドは与えられたベクトルに垂直なベクトルをつくるヘルパーメソッド, TestSegmentsIntersection()メソッドは二つの線分の交差を判定するメソッドである. これらのメソッドではコードを簡潔にするために,これまでの授業で説明していない文法要素が いくつか含まれている.詳しくは以下のページを参考にするとよいだろう.

なおTestSegmentsIntersection()メソッドで用いられている交差判定の数理については, ここでは説明が長くなるため省略する.詳しくは以下の書籍を参考にするとよいだろう.

MainWindow.xaml.csの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 与えられたベクトルに垂直なベクトルをつくるヘルパーメソッド
private static Vector perp(Vector v) => new Vector(v.Y, -v.X);

// 線分同士の交差判定を行うメソッド
private static Vector? TestSegmentsIntersection((Vector, Vector) seg1, (Vector, Vector) seg2) 
{
    // 参考: Christer Ericson(著), 中村達也(訳),
    //       ゲームプログラミングのためのリアルタイム衝突判定,
    //       pp.151-153, ボーンデジタル(2008)            

    // just alias
    Vector A = seg1.Item1;
    Vector B = seg1.Item2;
    Vector C = seg2.Item1;
    Vector D = seg2.Item2;
    Vector n = perp(D - C);

    // 線分2の方向から見て右側からの進入は無視する.
    Vector dir1 = n;
    dir1.Normalize();
    Vector dir2 = B - A;
    dir2.Normalize();
    if (dir1 * dir2 >= 0) return null;

    // 線分1と線分2が平行な場合は交差なし.
    double determinant = n * (B - A);
    if (determinant == 0) return null;

    // 線分1の始点より後ろに交点が見つかった場合は交差なし.
    double t = (n * (C - A)) / determinant;
    if (t < 0) return null;

    // 交点位置を計算する.
    Vector p = A + t * (B - A);

    // 線分1の境界ボックス
    double seg1MinX = Math.Min(A.X, B.X);
    double seg1MaxX = Math.Max(A.X, B.X);
    double seg1MinY = Math.Min(A.Y, B.Y);
    double seg1MaxY = Math.Max(A.Y, B.Y);

    // 線分2の境界ボックス
    double seg2MinX = Math.Min(C.X, D.X);
    double seg2MaxX = Math.Max(C.X, D.X);
    double seg2MinY = Math.Min(C.Y, D.Y);
    double seg2MaxY = Math.Max(C.Y, D.Y);

    // 計算した交点位置が,線分1と線分2の両方の
    // 境界ボックスに包含されてる場合は交差あり.
    if (   seg1MinX <= p.X && p.X <= seg1MaxX
        && seg1MinY <= p.Y && p.Y <= seg1MaxY
        && seg2MinX <= p.X && p.X <= seg2MaxX
        && seg2MinY <= p.Y && p.Y <= seg2MaxY)
        return p;
    else
        return null;
}

このTestSegmentsIntersection()メソッドを使って,ボールの元の位置(ballXballY)と 次の位置(nextBallXnextBallY)とが作る線分(以下この線分を変位線分とよぶ)が, ブロックやプレイヤーを表す四角形の四辺を跨ぐかどうかを判定することで衝突判定を行うことができる.

このためにはcanvas1に含まれているすべての四角形(Rectangle)をチェックする必要があるが, 今回は単純にすべてのブロックを逐次チェックする方法を用いることにする.本来であれば変位線分から 遠い四角形はチェックを省略することが可能であるが,ブロックはせいぜい数百個程度であるため 常にすべてをチェックしてもパフォーマンス上の問題にはなりにくいため,このような単純な方法を採用している.

MainWindowクラスに_に示すQueryBySegment()メソッドを追記する.

MainWindow.xaml.csの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 
// canvas1に存在している四角形(= Rectangle )のうち,引数として渡した線分に交差する四角形を検索するメソッド.
// そのような四角形は複数あり得るので,最も近いものを返す.
//
// 戻り値は,交差を発見した四角形,交点位置,その点における法線方向を表すタプルである.
// 交差が見つからない場合は null を返す.
//
private (Rectangle, Vector, Vector)? QueryBySegment((Vector, Vector) query) 
{    
    Rectangle nearestRect = null;                 // 最も近い四角形を格納するための変数
    double nearestDist = double.PositiveInfinity; // その四角形までの距離
    Vector intersectionPoint;                     // 交点位置
    Vector intersectionNormal;                    // 交点における法線方向

    // canvas1 に含まれるすべての四角形をチェックする.
    foreach (Rectangle rect in GetRectangles())
    {
        double rectX = Canvas.GetLeft(rect); // 四角形の左上座標を
        double rectY = Canvas.GetTop(rect);  // 取得する.

        // この四角形を構成する四辺の線分を作成する.
        (Vector, Vector)[] segments = new[] 
        {
            (new Vector(rectX,              rectY),               new Vector(rectX + rect.Width, rectY)),
            (new Vector(rectX + rect.Width, rectY),               new Vector(rectX + rect.Width, rectY + rect.Height)),
            (new Vector(rectX + rect.Width, rectY + rect.Height), new Vector(rectX,              rectY + rect.Height)),
            (new Vector(rectX,              rectY + rect.Height), new Vector(rectX,              rectY)),
        };

        // ↑の線分を逐次チェックする.
        foreach (var seg in segments) 
        {
            // 引数で与えられた線分と辺の交差をチェックする.
            Vector? p = TestSegmentsIntersection(query, seg);
            
            if (p.HasValue) // 交差がある場合
            {
                // 引数で与えられた線分の始点から交点位置までの距離を計算する    
                double dist = (query.Item1 - p.Value).Length; 
                                
                if (dist < nearestDist) // より近い交点が見つかった場合,
                {
                    // 最も近い四角形などの変数を更新する.
                    nearestRect = rect;
                    nearestDist = dist;
                    intersectionPoint = p.Value;
                    intersectionNormal = perp(seg.Item2 - seg.Item1);
                    intersectionNormal.Normalize();
                }// if
            }// if
        }// foreach
    }// foreach

    // 交差のある四角形が見つかっている場合は,それと交点位置や法線方向を戻り値として返す.
    if (nearestRect != null)
        return (nearestRect, intersectionPoint, intersectionNormal);
    else
        return null;
}

このQueryBySegment()メソッドによって任意の線分と交差を持つ四角形を検索できるようになったので, タイマー処理(Timer_Tick()メソッド)でボールとブロックやプレイヤーとの交差判定を行い, 衝突時の応答処理を実装する.Timer_Tick()メソッドに_に示す内容を追記する.

MainWindow.xaml.csの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private void Timer_Tick(object sender, EventArgs e)
{
    const double EPSILON = 1 / 256.0;

    double ballX = Canvas.GetLeft(ball) + ball.Width / 2;
    double ballY = Canvas.GetTop(ball) + ball.Height / 2;

    double nextBallX = ballX + speedX;
    double nextBallY = ballY + speedY;

    if (nextBallX < 0 || canvas1.Width <= nextBallX) 
    {
        speedX *= -1;
        ballX = Math.Clamp(nextBallX, 0, canvas1.Width) + EPSILON * speedX;
        nextBallX = ballX + speedX;
    }// if

    if (nextBallY < 0 || canvas1.Height <= nextBallY) 
    {
        speedY *= -1;
        ballY = Math.Clamp(nextBallY, 0, canvas1.Height) + EPSILON * speedY;
        nextBallY = ballY + speedX;
    }// if

    // 変位線分を作る.
    var query = (new Vector(ballX, ballY), new Vector(nextBallX, nextBallY));

    // ↑と交差を持つ四角形を検索する.
    var result = QueryBySegment(query);
    
    if (result.HasValue) // 交差を持つ四角形が見つかった場合
    {
        // 戻り値のタプルを個別の変数にばらす.
        (Rectangle rect, Vector point, Vector normal) = result.Value;

        // 現在の速度ベクトルの正反射方向のベクトルを作成する.
        Vector currSpeed = new Vector(speedX, speedY); // 現在の速度ベクトル
        
        Vector newSpeed = 2 * (normal * -currSpeed) * normal + currSpeed; // 速度の正反射ベクトル 

        speedX = newSpeed.X; // ↑を speedX,
        speedY = newSpeed.Y; // speedY に設定する.

        // 交差位置を交点から少しずらす
        point += EPSILON * newSpeed;
        nextBallX = point.X;
        nextBallY = point.Y;

        // 見つかった四角形がブロックである場合,
        // それを canvas1 から取り除く.
        if ((string)rect.Tag == "Bricks")
            canvas1.Children.Remove(rect);
    }// if

    Canvas.SetLeft(ball, nextBallX - ball.Width / 2);
    Canvas.SetTop(ball, nextBallY - ball.Height / 2);
}

ここまで書けたら起動してみよう._に示すようにブロックにボールがぶつかると そのブロックが消滅して,かつボールが反射することが分かるだろう.またプレイヤーにぶつかった場合も 反射することが分かるはずである.

これでおおむね壁崩しとしての動作はできたが,ゲームの終了処理を作っていないため, ボールが画面下に落下しても相変わらず反射し続けてしまうし,またすべてのブロックを破壊した あともボールは動き続けてしまう.次節ではこの終了処理を実装してみよう.

作業結果
Last updated on 2024-01-09
Published on 2024-01-09

Powered by Hugo. Theme by TechDoc. Designed by Thingsym.