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

C++で浮動小数点の誤差を考慮して等価比較する

<cfloat>ヘッダ中にあるDBL_EPSILONを使えば誤差も考慮して2つのdouble値の等価比較ができる。

float型ならFLT_EPSILONを使えばよい。

#include <cmath>
#include <cfloat>

//double型のaとbを誤差考慮して比較する
bool NearlyEqual(double a, double b)
{
    return abs(a - b) < DBL_EPSILON;
}

int main()
{
    double a,b;

    if(NearlyEqual(a,b))
    {
        return 0;
    }

    return 1;
}

追記(11/07) 故あってUE4の数学ライブラリ読んでたら似たような関数があったので、それに習って書き換えてみる。

struct Math
{

    static constexpr bool NearlyEqual(const float a, const float b, const float err = FLT_EPSILON)
    {
        return Abs<float>(a - b) <= err;
    }

    static constexpr bool NearlyEqual(const double a, const double b, const float err = DBL_EPSILON)
    {
        return Abs<double>(a - b) <= err;
    }

    template<typename T>
    static constexpr inline T Abs(const T a)
    {
        return (a >= (T)0) ? a : -a;
    }
};

Unity2018でTestRunnerを使うための参考リンク

現在Unityでゲームを作ってる。

PlantUMLで設計書から作り始めてるんだし、折角ならテストコードも書いてみたいよなー、とTest Runnerを試してみたのだが、Unity2018でTestRunnerの導入方法が大幅に変わっていて、困ったので参考になったサイトをメモ。

(plantUMLについては以前書いた)

ai-gaminglife.hatenablog.com

Unity2018以降でのTest Runnerの導入方法

ntgame.wpblog.jp

大体このサイト読めば解決すると思う。

Test Runnerの使い方

www.slideshare.net

  • そもそもテストってなんやねんってところから解説している。若干古い(2015年)情報であることは注意。

qiita.com

  • 初めてテストを書く時に参考になる。TestRunnerを使うメリットが分かる。

tsubakit1.hateblo.jp

  • Unityテスト完全に理解した勉強会のスライド集。2018年の情報で(記事執筆時点では)新しい情報なのが嬉しい。

実際使ってみて

  • 一々MonoBehaviorでnewしてAttachさせてDebug.Log()させる手間が省けるのは良い。

  • 設計がカッチリ決めてからの開発には向いているが、場当たり的な開発には向いてなそう。(テスト自体そう)

ほんの少しだけラクに、C++でAtCoderに参加するバッチファイル作った

先々週のABCからAtCoderを始めた。

前回参加時はたまたまTwitterを見ていた時にコンテスト開催中だということに気づき、折角だしやるか、と衝動的に参加したため、VS2017で書いて、動くかどうかは実際提出してみて試す、なんて非効率な事をしていた。

折角やるならちゃんと環境整えて、ローカルでもテストできるようにしたいよなー、ということで比較的簡単に環境構築できるバッチファイルを作った。

※と言ってもただディレクトリ作るだけの奴だけど......

前準備

torus711.hatenablog.com

このサイトを参考に最新のgcc導入まで済ます。

エディタには今回はVSCodeを採用。codeコマンドを叩いてVSCodeが起動すればOK。

バッチファイル作成

適当なフォルダでメモ帳を開いて、名前を "atcoder-init.bat"とする。

以下をコピペしてbatファイルに貼り付ける。(後から編集したい時は右クリック→編集を選択。左クリックするとバッチファイルが実行されてしまう)

cd /d [保存したいディレクトリ、自分で決める]
mkdir %1
cd %1
mkdir A
mkdir B
mkdir C
mkdir D
cd A
type nul > input.txt
type nul > Main.cpp
cd ..
cd B
type nul > input.txt
type nul > Main.cpp
cd ..
cd C
type nul > input.txt
type nul > Main.cpp
cd ..
cd D
type nul > input.txt
type nul > Main.cpp
cd ..

PATHを通す

(Windows10なら)スタートメニューで「環境変数」と入力すれば「環境変数を編集」メニューが出てくるのでそれを選択。(Win10、なんでコントロールパネル深いところに隠したんだろうか……)

出てきたウインドウの上側、ユーザー環境変数で、Pathを選択してから編集をクリック。

新規ボタンをクリックし、先程バッチファイルを保存したパスをコピペして保存する。

使い方

コマンドプロンプト(Not PowerShell)で

> atcoder-init [引数1]

を叩くと引数1に選択したディレクトリ以下にA~D問題を解くための作業ディレクトリが作成される。

A問題を解きたい時は

> code A

を入れればVSCodeが起動し、標準入力用のinput.txtと実際にコードを書くMain.cppを編集できる。input.txtには問題ページからサンプル入力をコピペしてはっつければいい。

Main.cppを編集し終えたら

> g++ A/Main.cpp

コンパイルコンパイルが通ったら、

> a.exe < A/input.txt

で実行する。

まとめ

ごくごく単純なバッチファイルだが、以前よりは確実にラクにテストできるようになった。世の中にはサンプル入力をスクレイピングして自動で拾ってくるようなのもあるらしいが、そんな技術はないので、仕方ない。

もっとラクなのがあったら教えてほしい。

水色までは頑張りたいなあ。

C# if文なしにプロパティで数値に上限/下限を設定する

月とか時間とか、数値に上限下限を設定したいとき、素直に実装するならif文使ってこんな感じにやる。

int month;
void setMonth(int value)
{
    if(value <= 1){
        month = 1;
    }else if(value >= 12){
        month = 12;
    }else{
        month = value;
    }
}

これじゃあやってることに対してコードが冗長すぎ。

解決策何かしらあるだろうなーと思って調べてみたら、Mathf.Min()やMathf.Max()を使えばできるらしいことが判明。折角のC#なのでプロパティを使ってもっとスマートに書いてみる。

public class Month
{
    //packing Field
    private int monthNum = 1;
    public int MonthNum
    {
        get { return monthNum; }
        set { this.monthNum = Mathf.Min(12, Mathf.Max(1, value)); }
    }
}

シェーダー芸やってる人やある程度プログラム書いてる人に取っちゃ当然の書き方なのかもしれないが、知らなかったので自分用にメモ。

まあClamp使えばいいんですが。

VSCode+PlantUMLでクラス設計図が簡単に作れる

 ブログ執筆を始め、ずっとエディタはAtomを使っていたのだが、Gitの設定をいじっていたらパッケージインストールで認証エラーが出るようになった。解決の仕方も分からなかったので、この機会だしVSCodeに移行してみた。

 こんな感じで記事を書いてる。

f:id:ai_gaminglife:20181011000348p:plain

 動作も軽快、UIもわかりやすく、Markdown PreviewCSSがちょっと見辛い以外は不満はない。


 で、本題。これまで私は何本かゲームを作ってきたが、どれも開発期間は長くて1ヶ月。行き当たりばったりでクラスを作り、結果相互参照連発しコンパイル時間が伸びる酷い設計のプログラムを何度も書いた。

 現在作り始めているUnityゲームは相当大規模になりそうで、こんな行き当たりばったり設計しててはあらゆる機能を持ってしまう(所謂)神クラスが誕生してしまいかねない。

 じゃあどうするかっていうと、UMLを使ったクラス図を作って事前に設計したくなる。しかし、以前まではWordかdraw.ioでテキストボックスをちまちま配置し、コネクタ同士を矢印で繋げて、コメント書いて……みたいな方法で作っていた。見た目は凝れるが、あまりに面倒。大規模だと流石にやってられない。

 PlantUMLを使えばそんな面倒な作業からおさらばできる。

plantuml.com

 簡易な文法ながら高機能で、複雑なクラス図を自動で作成、描画できる。

 PlantUMLを使いはじめて2時間位でこれくらいは作れてしまった。

 PlantUMLの優れているところはVSCodeに専用の拡張機能があることで、これを使うことでサイドタブにクラス図を描画させながら快適なVSCode環境で書くことができる。

 導入には以下のサイトが参考になるだろう。

注意

 PlantUMLはコメントを書きたい時は、

' 1行コメントはこれ
/*
複数行のコメントは
こんな感じ
*/

とすれば良い。

台風で停電した。

台風25号で約一日停電した。

人生ではじめて経験した命の危険を感じた台風。幸いなことに近所で窓が割れるなどの大きな被害は起きなかったが、電気が止まったことで断水も発生し、シャワーもしばらく浴びられなかった。この記事を書いてる時間でもまだ復旧していない地区もあり、考え過ぎってくらいに事前に備えておくのが本当に大切だなと感じた。

以下に実際停電中に役に経ったものを紹介する。

モバイルバッテリー

考えるまでもなく超役に立つ品。私はAnkerの10000mAhくらいのものを使っていて、停電するかもと思い台風上陸前から充電していた。

まあ、

Switch版イース8徹夜攻略に使ってしまったんですが。

(大学が停電していないっていう情報を入手していたのでもしなくなったら大学行けばいいとこんな無茶をした。決して真似をしてはいけない)

iPad

最近買ったのでスマホよりバッテリー持ちも良く、読書もできるので重宝した。普段使うスマホ以外にサブ機持っておくのは本当に便利だなーとこの時程感じることはなかった。

現金

停電中電子マネーは使えないので。21号の時電子マネーしか持ってなくて何も支払いが出来ない増田が話題になっていたが、やはり万が一のため1万程度は現金で持っておいたほうが良いと思う。

キングブレード(ペンライト)

今回のMVP。一応懐中電灯は持っていたが暗くて使えたもんじゃなく。キングブレードは本を読めるくらいに明るく、そして軽く持ち運びにも便利。停電中買い物に出掛けたときも重宝した。

f:id:ai_gaminglife:20181002221248j:plain

使ってたキンブレ。色も切り替えられて超便利。

こんな使い方もできる。