Siv3D で自動操縦AIを作成してみた
はじめに
本記事は、Siv3D Advent Calendar 2019 の 15 日目です。
Siv3D を触ってみたので、感想とか、こう実装してみた、という話を書きます。
本当は機械学習導入したかったんですけどね…
もの
— shonen (@shonen9th) 2019年12月15日
ソースはここ
面倒くさくなって実装も全部ヘッダに書いてます
TL;DR
ゲームよりもゲームの仕組みを作って遊びたいC++17使いにとって Siv3D は超強力
Siv3D 実装
道路
PathFileld.h
に書いてあります。
方針はこんな感じです。
Array<Vec2>
で閉路を書きます。- 閉路を元に肉付けします。直線部分を長方形で、角を円で生成して、
Polygon
にappend
していきます。
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:もっと高度な事をやりたかったのに