Gaming Life

一日24時間、ゲームは10時間

OpenSiv3D C++ 反射ベクトルを計算して玉の反射を実現する1

OpenSiv3Dのリファレンスにはブロック崩しのサンプルがある。

https://scrapbox.io/Siv3D/%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E3%81%8F%E3%81%9A%E3%81%97

このサンプルでは玉が壁 or ブロックに衝突した時、玉の速度ベクトルのX成分もしくはY成分を反転させることで反射を実現している。

しかし、この方法では斜め壁に対応できていない。

というわけで壁に衝突した時に反射ベクトルを計算して玉を反射させるプログラムを書いた。

f:id:ai_gaminglife:20181110013644g:plain

コード

# include <Siv3D.hpp> // OpenSiv3D v0.2.5

Vec2 calcReflectVec(const Vec2& direction, const Vec2& normal)
{
    auto L = -direction.normalized();
    auto LdotNx2 = 2.0 * L.dot(normal);

    return (LdotNx2 * normal - L).normalized();
}

Vec2 calcLineNormal(const Line& line)
{
    auto dirVec = Vec2(line.begin.x - line.end.x, line.begin.y - line.end.y);
    auto normal = Vec2(-dirVec.y, dirVec.x);

    return normal.normalize();
}

class Ball
{

public:
    Ball() = default;

    Ball(Vec2 pos, Vec2 v)
        :circle(pos, 8)
        ,vec(v.normalize())
    {}

    ~Ball() = default;

    void update()
    {
        circle.moveBy(SPEED * vec.normalize());
    }

    void draw()
    {
        circle.draw();
    }

    Circle getCircle() const
    {
        return circle;
    }

    void bound(const Vec2& normal)
    {
        auto n = (vec.dot(normal) >= 0) ? normal : -normal;

        vec = calcReflectVec(vec, n);
    }

private:

    Circle circle;
    const double SPEED = 8.0;
    Vec2 vec;

};

void Main()
{
    Array<Line> lines;
    lines.push_back(Line(0, 0, 0, Window::Height()));
    lines.push_back(Line(Window::Width(), 0, Window::Width(), Window::Height()));
    lines.push_back(Line(0, 0, Window::Width(), 0));
    lines.push_back(Line(0, Window::Height(), Window::Width(), Window::Height()));
    lines.push_back(Line(550, 250, 420, 420));
    lines.push_back(Line(0, 150, 220, 200));

    Ball ball(Window::Center(), Vec2(-0.7, 0.5));

    while (System::Update())
    {
        ball.update();

        for (const auto& line : lines)
        {
            if (line.intersects(ball.getCircle()))
            {
                ball.bound(calcLineNormal(line));
            }
        }

        ball.draw();
        
        for (const auto& line : lines) 
        {
            line.draw();
        }
    }
}

方針

f:id:ai_gaminglife:20181110013833p:plain

-L: 玉の速度ベクトル

N : 壁の法線ベクトル

R : 壁に反射した時の玉の反射ベクトル

-LとNがわかれば以下の式でRを求めることができる。

\vec{R}=2(\vec{L} \cdot \vec{N})\vec{N}-\vec{L}

玉のクラスを作成する

class Ball
{
    public:
    Ball() = default;

    Ball(Vec2 pos, Vec2 v)
        :circle(pos, 8)
        ,vec(v.normalize())
    {}

    ~Ball() = default;

    void update()
    {
        circle.moveBy(SPEED * vec.normalize());
    }

    void draw()
    {
        circle.draw();
    }

    Circle getCircle() const
    {
        return circle;
    }

    void bound(const Vec2& normal)
    {

    }

private:

    Circle circle;
    const double SPEED = 8.0;
    Vec2 vec;

};

bound()は後で実装する。

壁の法線ベクトルを求める

今回、壁はSiv3DのLineクラスを利用する。

壁の端点の座標を減算し、壁の方向ベクトルtを求めることができる。

\vec{t} = (t_x, t_y)

tと壁の法線ベクトルnは内積を取ると0になる。

\vec{t} \cdot \vec{n}=t_x \times n_x + t_y \times n_y = 0

ここからnは二通りの解が得られる。

\vec{n} = (-t_y,t_x) \vec{n} = (t_y,-t_x)

なぜ法線が二種類得られるかというと、壁には表裏があるから。

Vec2 calcLineNormal(const Line& line)
{
    auto dirVec = Vec2(line.begin.x - line.end.x, line.begin.y - line.end.y);
    auto normal = Vec2(-dirVec.y, dirVec.x);

    return normal.normalize();
}

ここで、図をもう一度見返してみる。

f:id:ai_gaminglife:20181110013833p:plain

正しい法線nは、玉の方向ベクトルと鈍角をなす。鈍角をなす2つのベクトルは内積を取ると負の値を取るので、以下の処理を挟んで正しい法線ベクトルを得る必要がある。

auto n = (vec.dot(normal) >= 0) ? normal : -normal;

反射ベクトルを計算する

-L: 玉の速度ベクトル

N : 壁の法線ベクトル

R : 壁に反射した時の玉の反射ベクトル

とした時、

\vec{R}=2(\vec{L} \cdot \vec{N})\vec{N}-\vec{L}

なので、以下の関数で反射ベクトルを求めることができる。

Vec2 calcReflectVec(const Vec2& direction, const Vec2& normal)
{
    auto L = -direction.normalized();
    auto LdotNx2 = 2.0 * L.dot(normal);

    return (LdotNx2 * normal - L).normalized();
}

ここで求めた反射ベクトルをbound()関数で使用する。

void bound(const Vec2& normal)
{
    auto n = (vec.dot(normal) >= 0) ? normal : -normal;

    vec = calcReflectVec(vec, n);
}

壁を作成する

Siv3Dの動的配列Array<T>を利用してLineを格納する。

 Array<Line> lines;
    //ここからウィンドウ四隅の壁
    lines.push_back(Line(0, 0, 0, Window::Height()));
    lines.push_back(Line(Window::Width(), 0, Window::Width(), Window::Height()));
    lines.push_back(Line(0, 0, Window::Width(), 0));
    lines.push_back(Line(0, Window::Height(), Window::Width(), Window::Height()));
    //ここまでウィンドウ四隅の壁

    lines.push_back(Line(550, 250, 420, 420));
    lines.push_back(Line(0, 150, 220, 200));

玉と壁の当たり判定を取る

今回、壁と玉の当たり判定はintersects()関数を利用する。 壁をRange-based forでArray<Line>に格納した壁を取り出し、総当たりで当たり判定を取り、衝突していればbound()を呼び出す。

ball.update();
for (const auto& line : lines)
    {
        if (line.intersects(ball.getCircle()))
        {
            ball.bound(calcLineNormal(line));
        }
    }

描画する

draw()を呼び出して描画する。

ball.draw();
        
    for (const auto& line : lines) 
    {
        line.draw();
    }

結果

f:id:ai_gaminglife:20181110013644g:plain

未解決の問題

壁には厚みがあるので壁の横から衝突した時におかしな動きになってしまう。

まとめ

実装を始めた当初は数式を見てギョッとしたが、冷静に一つ一つ見ていくとそこまで難しいことはやっていない。未解決の問題だけはまだどうしようも出来ていないが。どなたか解決方法があったら教えてください。

参考サイト

qiita.com