shonen.hateblo.jp

やったこと,しらべたことを書く.

Siv3D で自動操縦AIを作成してみた

はじめに

本記事は、Siv3D Advent Calendar 2019 の 15 日目です。

Siv3D を触ってみたので、感想とか、こう実装してみた、という話を書きます。

本当は機械学習導入したかったんですけどね…

もの

ソースはここ

github.com

面倒くさくなって実装も全部ヘッダに書いてます

TL;DR

ゲームよりもゲームの仕組みを作って遊びたいC++17使いにとって Siv3D は超強力

Siv3D 実装

道路

PathFileld.h に書いてあります。 方針はこんな感じです。

  1. Array<Vec2> で閉路を書きます。
  2. 閉路を元に肉付けします。直線部分を長方形で、角を円で生成して、Polygonappend していきます。

Polygon.append があるおかげで、単純な図形をappend していくだけで良いため、非常に簡単な実装になっています。

    // Path を Array<Vec2> とみなす
    using Path = Array<Vec2>;

    // 道幅
    double width_ = 20;
    // 閉路
    Path path_;
    // 生成した道路を表す多面体
    Polygon polygon_;

    //

    // 線分 v1,v2 を中心とする幅widthの長方形を作成して返す
    static Polygon createRect(Vec2 v1, Vec2 v2, double width) {
        auto n12 = (v2 - v1).normalize();
        auto l12 = n12.yx(); l12.x = -l12.x;
        auto w2 = width / 2;
        return Polygon(Array<Vec2>{ v1 + w2 * l12, v1 - w2 * l12, v2 - w2 * l12, v2 + w2 * l12 }, {});
    }

    // path_から多面体を作成して polygon_ に格納する
    void generatePolygonFromPath() {
        // 空の多面体
        polygon_ = Polygon();
        // 直前の頂点
        auto lastv = path_.back();

        for (auto v : path_) {
            // 直線部分の道を追加
            polygon_.append(createRect(lastv, v, width_));
            // コーナー部分の道を追加
            polygon_.append(Circle(v, width_ / 2).asPolygon());
            // 直前の頂点を更新する
            lastv = v;
        }
    }

車のような何か

Siv3DCar.h に書いてあります。

ボディは⬇で代用しています。絵文字をベクタ画像代わりに使うツールって最近では一般的なのでしょうか…

setDamping を除いて、自動車にapplyForce しているだけです。
setDamping(0) して、減衰も applyForce で実装したほうが調整しやすいかも。

    // 初期化
    void initialize(P2World& world) {
        const MultiPolygon carPolygon = Emoji::CreateImage(U"⬇").alphaToPolygonsCentered().simplified(0.8).scale(0.04);
        body_ = P2Body{ world.createPolygons({0, 0}, carPolygon, P2Material(0.1, 0.0, 1.0)) };
        texture_ = Texture(Emoji(U"⬇"));

        body_.setDamping(1.4);
        body_.setAngularDamping(2);
    }
    // ループで1回だけ呼ぶ
    void apply(const IFieldPresenter& field) override {
        if (hasController()) ctrl_ = controller_->apply(*this, field);

        engine_ += CAccselEngine * ctrl_.accsel();
        engine_ *= CEngineDump;

        // 横滑りを抑える力
        auto side = body_.getVelocity().dot(-Vec2::UnitX().rotated(body_.getAngle()));
        body_.applyForce(0.05*side* Vec2::UnitX().rotated(body_.getAngle()));

        // 前輪が車に与える力
        body_.applyForce(
            Vec2{ 0, CEngineForceFront * engine_ }.rotate(body_.getAngle() + CSteerRear * ctrl_.steer()),
            Vec2{ 0, -0.5 }.rotate(body_.getAngle())
        );
        // 後輪が車に与える力
        body_.applyForce(
            Vec2{ 0, CEngineForceRear * engine_ }.rotate(body_.getAngle() + CSteerFront * ctrl_.steer()),
            Vec2{ 0, 0.5 }.rotate(body_.getAngle())
        );
    }
    // 描画
    void draw() const override {
        texture_.scaled(0.04).rotated(body_.getAngle()).drawAt(body_.getPos());
    }

自動操縦

動画ではスイスイ走っている割にコードはシンプルでした*1

左側のセンサーが道に多く接していれば左にハンドルを切り、右側のセンサーが道に多く接していれば、右にハンドルを切る実装です。
(減速せずヘアピンカーブを曲がれる挙動ってどうなの、という話はありますが)

    ControllerMessage apply(const ICarPresenter& car, const IFieldPresenter& field) override {
        double ap = 0;
        for (int i = 5; i <= 15; ++i) {
            double a = Math::Pi * i / 20;
            ap += Math::Cos(a) * car.getSensor(field, { Math::Cos(a) * 30, Math::Sin(a) * 30 });
        }
        return ControllerMessage{ 1, -ap };
    }

ControllerMessage(accsel, steer) は、アクセルをどの程度踏むか、ハンドルをどの程度切るか、を -1..1 の範囲で指定する構造体です

car.getSensor は、指定した車からの相対座標が道路の上であれば1、そうでなければ0を返します。
道路は Polygon なので、Polygon.contains を呼ぶだけです。

感想

Polygonがとても扱いやすい点はかなり助かっています。 STLも普通に使えるので、新たに覚える事も少なく、ロジックの実装に集中できました。

*1:もっと高度な事をやりたかったのに