20200906のC#に関する記事は7件です。

【Unity】AddExplosionForceサンプル

実装動画

AddExplosionForce (float explosionForce, Vector3 explosionPosition, float explosionRadius, float upwardsModifier= 0.0f, ForceMode mode= ForceMode.Force));

とりあえず以下のパラメータで設定。

  • explosionForce = 100
  • explosionRadius = 20
  • upWardsModifier = 3.0f(球体的な爆発をシミュレートするため、見かけ上の位置を調整する。)
  • mode = Impulse

実装

cubeオブジェクト生成

爆発を受ける対象物をStart時に自動生成。
特殊なことはやらず、愚直にforで回して生成する。

CreateCube.cs
using UnityEngine;

public class CreateCube : MonoBehaviour
{
    public GameObject cube;
    public int interval = 2;

    private int startPosition = -30;
    private float startYPosition = 0.5f;
    private int finishPosition = 30;
    private float finishYPosition = 10.5f;

    void Start()
    {
        for (int z = startPosition; z <= finishPosition; z+=interval)
        {
            for (float y = startYPosition; y < finishYPosition; y+=interval)
            {
                for (int x = startPosition; x <= finishPosition; x += interval)
                {
                    Instantiate(cube, new Vector3(x, y, z), Quaternion.identity);
                }
            }
        }
    }
}

AddExplosionForceの実装

こちらののっぴの備忘録さんの記事を参考に作成。

Blast.cs
using UnityEngine;

public class Blast : MonoBehaviour
{
    public float power = 100f;
    public float radius = 20.0f;
    public float upwardsModifier = 3.0f;

    Ray ray;
    RaycastHit hit;

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            //クリック点にRayを飛ばす
            ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray, out hit, 100f))
            {
                //クリックした所から半径20mの範囲のColliderを取得
                Collider[] collider = Physics.OverlapSphere(hit.point, radius);
                foreach (Collider cube in collider)
                {
                    //範囲内のオブジェクトのRigidbodyに爆破の力を作用させる
                    if (cube.GetComponent<Rigidbody>())
                    {
                        cube.GetComponent<Rigidbody>().
                            AddExplosionForce(power, hit.point, radius, upwardsModifier, ForceMode.Impulse);
                    }
                }
            }
        }
    }
}

終わり

  • 爆発をシミュレートすることが出来た。ただ爆発してモノを壊すだけのゲームを作成したい。
  • 注意点として、被爆発対象の物体にはrigidbodyを付けないとシミュレートできない。
  • 数値を変えた検証動画も作成したいなぁと思ってます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity講座 #4移動を操作する

はじめに

※Qiitaに#1~#3が無いのは仕様です

動画から見てくれていた方、お久しぶりです。
動画を見てくれる人も減ってきたし、自分も動画編集にかける時間がそれほど取れなくなってしまったので、記事で更新することになりました。
この記事はこれまでの動画を見てくれていた前提でやっているので、Qiitaから来てくれた方は以下の動画を見てくださると嬉しいです。(動画に対する指摘等あればyoutubeのコメント欄にてお願いします)
Unity講座#1 インストール
Unity講座#2 操作説明
Unity講座#3 プロジェクト作成

今回の講座では、前回作った処理を改良して、
・PCスペックに依存しない移動方法へ
・ボールが壁を抜けないような移動方法へ
・入力による操作に変更
といった内容をやっていこうと思います。

開発環境

Unity 2019.4.5f1
Windows10 pro

PCスペックに依存しない処理へ

Update関数は1フレームに一回呼ばれると説明しました。
1秒間にこの関数が呼ばれる回数は、実はPCの性能によって大きく変わってきます。
ゲームでは、1秒間で何枚も画像が切り替わっています。
要するにパラパラ漫画のようなものですね。
この切り替わり枚数が多いとなめらかに動いているように見えます。(逆に枚数が少ないと動作が飛び飛びに見えてしまいます)
deltatime.png

Gameタブにある、Statsのボタンを押してみましょう。
GraphicsのところにあるFPS(Frame Per Second)が、この一秒間に何枚画像を更新できるかの情報になっています。
コメント 2020-09-06 103303.png

自分の環境では、FPSが1764になっていますね。つまり、
transform.position += new Vector3(0,0,1.0f);
が1秒間に1800回近くも行われていたため、移動速度がめちゃくちゃ早かったわけです。
秒速1800メートルとかマッハ5くらいになるのかな?
前回の最後で0.02fに直して、やっと見える速度になったのはこれが原因です。
ゲームが完成したあと、プレイする人々のPCスペックはバラバラです。
自分のPCの環境ではスムーズに動く移動速度にしたところで、自分よりハイスペックな人にとっては移動が早すぎるし、低スペックな人にとっては移動が遅すぎることになります。
そうならないようにUnity側で対処方法が用意されています。

Time.deltaTime

公式リファレンス(タイムとフレームレートの管理)
このリンクにほぼほぼここで書いている説明が書かれています。
Time.deltaTimeというプロパティを使うことで、どのようなPCでも一定の速度で移動させることができます。
このプロパティの値は前のフレームから今のフレームまでの経過時間です。
大体FPSの逆数だと考えてもらえばわかりやすいですかね。
詳しくはこのリンクを見てもらうとわかりやすいと思います。
とりあえず、速度にかける分には問題ありません。
Time.deltaTimeが返すのは秒数なので、単位にs(秒)をかけることで、左辺と右辺の単位が揃うように使わなければいけません。

さて、いきなりこんなややこしい事言われてもわからん、となるでしょうし実際にどのように修正すればいいのかについてやっていきましょう。

PlayerMove.cs
public class PlayerMove : MonoBehaviour
{
    [SerializeField]private float moveSpeed = 0.02f;
    // Start is called before the first frame update
    //このオブジェクトが作られたときに行われる処理
    void Start()
    {

    }

    // Update is called once per frame
    //毎フレーム行われる処理
    void Update()
    {
        transform.position += new Vector3(0,0,1.0f) * moveSpeed;

    }
}

以上の内容は前回の処理内容をmoveSpeedの大きさを変更することで、移動速度を変更できるようにしただけのものです。

[SerializeField]private float moveSpeed = 0.02f;
ここは、float型の変数moveSpeedを宣言しています。
[SerializeField]の部分で、インスペクタ上に表示するようにしています。
publicで宣言してもインスペクタ上に表示できるようになりますが、前回解説したとおり、外部のクラスから数値をいじれてしまうようになるので、privateにした上で、インスペクタから変更できるようにしています。

一旦この状態でプレイしてみましょう。前回の動画の最後と同じ動きをしていれば問題ないです。
(フレームレートによって移動速度は違うと思います)
コメント 2020-09-06 112922.png

さて、このように、Playerのインスペクタ上にMoveSpeedが出現していますよね?
この大きさを変更してみましょう。
例えば0.02f→1.0fにしてみると、50倍の速度になります。(プレイしてみましょう)

注意が必要なのが、スクリプト上で初期化している数値よりも、インスペクタ上で指定している数値のほうが優先されることです。
スクリプト上でいくら変更しても速度は変わらないので気をつけましょう。

さて、Time.deltaTimeを使ってみましょう。

PlayerMove.cs
public class PlayerMove : MonoBehaviour
{
    [SerializeField]private float moveSpeed = 0.02f;
    // Start is called before the first frame update
    //このオブジェクトが作られたときに行われる処理
    void Start()
    {

    }

    // Update is called once per frame
    //毎フレーム行われる処理
    void Update()
    {
        transform.position += new Vector3(0, 0, 1.0f) * moveSpeed * Time.deltaTime;
    }
}
                      正面方向に        移動速度    1フレームの時間 
単位(m)               (方向ベクトル)         単位(m/s)   単位(s)
transform.position += new Vector3(0,0,1.0f) * moveSpeed * Time.deltaTime;

上記の通り、左辺と右辺の単位が揃っています。
Time.deltaTimeを使うときはとりあえず単位を書き出すようにしてみましょう。
この状態でプレイしてみると、一秒に0.02mというすごく遅い速度で動くようになります。
どのPCでも同じ速度で動くようになったので、こうなれば速度を小さくする必要はありません。
今回はとりあえずmoveSpeedを3にしておきましょう。

壁を抜けない移動

前回軽く解説しましたが、transform.positionを変更する移動方法はワープです。
つまり、物理的に移動してないということは、物理エンジンによる衝突判定などがうまく働かないということですね。
ということで物理演算に必要なコンポーネント、Rigidbodyについて話していきます。

Rigidbodyとは

Rigidbodyは、物理演算を行うためのコンポーネントです。
オブジェクトを作成したときにはColliderが自動的に追加されますが、これとはまた違うものです。
簡単に言ってしまうとColliderは接触判定を行うだけで、物理演算をして接触して跳ね返って...のような処理はしてくれません。
IsTriggerをTrueにするとイベントマスを踏んで処理を行う、といったようなときに使えます。

さて、Rigidbodyを使うことで、物理的に動かし、物理演算をしてもらうことができます。
ということでまずは前回作った処理を変更していきましょう。
コメント 2020-09-06 204303.png
上記のようにPlayerにRigidbodyを足してみましょう。
そうすると壁にぶつかって止まりますね。

RigidbodyとColliderの使い分けですが、当たり判定は欲しいが移動はしないものにはColliderを。
両方欲しいときにはRigidbodyを入れるといいでしょう。
まあこのまとめ方もかなり細かい説明を省いているので、それぞれUnity公式のリファレンスを確認するといいでしょう。
Rigidbody
Collider

入力による移動

さて、上記の状態だと壁にぶつかったあと、壁に向かって移動し続けて、横に移動したりしますね。
現段階では球は前に進むことしかできないので、自分で動かせるようにしてみましょう。

入力を受け取る際にはUnityEngine.Inputのメソッド群を使います。
せっかくなので、キーバインドを自由にする方法で行います。
スマブラやマイクラなどをやっている方だとよく分かると思うのですが、できるならばボタンの割り当てを自分の好きにいじることができる方がいいですよね?

「Fキーでメニュー画面表示」のように実装することもできますが、拡張性が低いです。
マルチプラットフォームにも対応しているゲームエンジンなので、いちいちデバイスに合わせてif文で分岐させるなんて言うのは無理がありますよね。
つまり、「Menuボタンが押されたらメニュー画面表示」、「MenuボタンにFキーを割り当て」といった二段構えで設定することで、実装を楽にすることができます。
マルチプラットフォームの例で言うと、PS4では「MenuボタンにOptionボタンを割り当て」、Switchでは「Menuボタンに+ボタンを割り当て」という風にすればよくなり、プログラム自体を書き換える必要はなくなります。

左上にある、Edit→Project Settings→Input Managerを見てみましょう。
今回の入力で使うのは「Horizontal」と「Vertical」なので、とりあえずはここだけ見てみましょう。
コメント 2020-09-06 214322.png

ここでよく見てほしいのはNegative/Positive ButtonとAlt Negative/Positive Buttonです。
前者には左/右十字キーが割り当てられていて、後者にはa/dキーが割り当てられています。
つまり十字キーとWASDは同じ入力を意味するようにされています。
一番上のSizeの値を変えることで新しい入力を作ることができるので、自分でゲームを作った際に新しくボタンの役割を追加したい場合はぜひ活用してください。

さて、それでは実際の使い方を確認してみましょう。

PlayerMove.cs
public class PlayerMove : MonoBehaviour
{
    [SerializeField]private float moveSpeed = 3f;
    // Start is called before the first frame update
    //このオブジェクトが作られたときに行われる処理
    void Start()
    {

    }

    // Update is called once per frame
    //毎フレーム行われる処理
    void Update()
    {
        float hor = Input.GetAxis("Horizontal");
        float ver = Input.GetAxis("Vertical");
        Vector3 moveDir = new Vector3(hor, 0, ver);
        transform.position += moveDir * moveSpeed * Time.deltaTime;
    }
}

Input.GetAxis(<入力ソース名>);
は-1~1の値をスティックの倒し具合によって返します。
positive方向の入力を+、negative方向の入力を-と考えて返します。
コントローラーにスティックがあれば小数も扱えますが、PCのキーボードでは半押し不可能なので、
・-1(negative buttonを押している)
・1(positive buttonを押している)
・0(どちらのボタンも押していない)
以上のどれかになります。

入力ソース名には、先程Input Managerで確認した名前を入れましょう。
String型で入れないとエラー。Input Managerに存在しない名前を入れてもエラーになるのでタイプミスには注意しましょう。

横方向の入力(Horizontal)がx軸、縦方向の入力(Vertical)がz軸なので、その通りにmoveDirとして宣言してあげると、無事入力による移動が実装できます。
さっき確認したとおり、十字キーでもWASDでも同じ動きをしてくれることを確認してみてください。

今回は何も実装しませんが、Button系列の解説もしておきます。
Input.GetButton(<入力ソース名>);
Input.GetButtonDown(<入力ソース名>);
上記の2つは、入力ソースを軸ではなくボタンとして受け取ります。
2つの違いは何かというと、ButtonDownは押したフレームのみ動作し、Buttonは押している間ずっと動作します。

これだけじゃわかりにくいかと思うので実験してみましょう。

if (Input.GetButtonDown("Jump"))
{
    Debug.Log("Jump Button Pressed");
}

Update関数の最後尾にこの内容を付け足しましょう。
そして、ゲームをプレイしてスペースキーを押してみましょう。
コメント 2020-09-06 223154.png
Debug.Logはとても便利な関数です。
このようにConsoleに引数で渡した文字を表示させることができます。
変数の中身を表示させて正しい処理が行われているか確認したり、プロパティの中身を確認したりできます。
GetButtonDownなので、この場合は押して離すまでに1回しかログが出されません。

上記のプログラムをGetButton("Jump")に変更してみましょう。
コメント 2020-09-06 223713.png
すると、一瞬しか押していなくても複数回ログが出されます。
これは押されているフレームの間ずっとDebug.Logが実行されるためです。

ゲームで実装したい内容によっては使い分ける必要があるので、知っておきましょう。
また、サラッと説明してしまいましたが、Debug.Logは本当に重要なので覚えておきましょう。

最後に

これまで4回に渡ってUnityの解説をしてきましたが、いかがでしたでしょうか?
これで最低限は動かせるようになったのではないでしょうか。
とはいえ公式リファレンスを読めば基本的にはどの機能の使い方もわかるようになっています。
これからも、新しい機能を使ってみるときにはまず公式リファレンスを確認する癖をつけてください。
(そうは言うものの、いまいち画像が足りなかったり、英語しかなかったり、和訳がガバガバだったりと、公式だけじゃわからないときもしばしば...)

言ってしまうと、Unityでのゲーム制作は
Unity <やりたいこと>
でgoogle検索をすれば大抵は解決できてしまいます。
ただそれはソースコードをそのままコピペして解決してるだけです。
自分の講座では、なるべく何をしているかを説明していますが、説明を書いていない個人ブログなども検索に出てくることがあります。
みなさんには、なんで動いているのかよくわからないコードをコピペするだけの人にはなってほしくありません。
コピペで解決するのが悪いのではなく、動作内容を理解できていないのにコピペをして作ってしまうのをやめて欲しいということです。
(とはいえ物理演算を理解するために、物理と数学の勉強しろとは言いません)
Unityの関数の処理内容の解説をしているQiita記事などもあるので、なるべくたくさんの人の解説を見て、できるだけ処理内容を理解した上で作ってください。

当然ながらこの記事についても同様です。
なるべく色々調べて正確性を上げていますが、間違いがあるかもしれません。
この記事でわからなかったことは他の人の記事やサイトなどを確認してみてください。

参考

https://bardaxel.jp/archives/15

サークル公式Twitter
https://twitter.com/chuojoken?s=09

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ReactiveExtensions】任意の時間間隔で値を発行する2つの方法

ReactiveExtensionsには、等間隔で値を発行するObservable.Intervalファクトリメソッドはあるのですが、任意の間隔で値を発行するファクトリメソッドがありません。
いや、厳密にはあるのですが、使い方が複雑なのでここで紹介します。

任意の時間間隔で値を発行する2つの方法

方法1.Observable.Generateファクトリメソッドを使う

Observable.Generateファクトリメソッドの引数に、値の発行間隔を定義できるFunc<TSource, TimeSpan>型のtimeSelector引数があるので、任意の時間間隔で値を発行するIObservableを作り出すことができます。

Observable.Generateメソッドの定義と使い方

まずは、Observable.Generateメソッドの定義と基本的な使い方を説明します。

定義

一番簡単な定義は次のとおりです。

public static IObservable<TResult> Generate<TState, TResult> (
    TState initialState,                    //TStateの初期値を指定
    Func<TState, bool> condition,           //継続条件を指定
    Func<TState, TState> iterate,           //TStateの変化量を指定
    Func<TState, TResult> resultSelector)   //発光する値を指定

使用例

例えば、0〜4の値を発行するには次のようにします。

Observable.Generate(initialState: 0,            //初期値:0
                    condition: i => i < 5,      //継続条件:発行値が5より小さい
                    iterate: i => ++i,          //変化量:1ずつインクリメント
                    resultSelector: i => i)     //iをそのまま発行
          .Subscribe(i => Console.WriteLine(i),
                     () => Console.WriteLine("OnCompleted"));

まるでfor文のような引数の指定方法ですね。

実行結果

0
1
2
3
4
OnCompleted

Observable.Generateメソッドで値の発行間隔を指定する

本題はここからです。
Observable.Generateメソッドには、値の発行間隔を指定できるオーバーロードがあります。

値の発行間隔を指定できるオーバーロード定義

public static IObservable<TResult> Generate<TState, TResult> (
    TState initialState,                    //TStateの初期値を指定
    Func<TState, bool> condition,           //継続条件を指定
    Func<TState, TState> iterate,           //TStateの変化量を指定
    Func<TState, TResult> resultSelector,   //発光する値を指定
    Func<TState, TimeSpan> timeSelector)    //時間間隔を指定;

このtimeSelectorに発行間隔を定義したメソッドを指定できます。

使い方

どのように使うかですが、例えば発行する順番を定義したTimeSpanListを用意して、timeSelectorで値を順番に読んでいくように指定することで、Listに定義した間隔で値が発行されます。

class MainClass
{
    public static void Main(string[] args)
    {
        //発行間隔を定義
        List<TimeSpan> intervals = new List<TimeSpan>()
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(2),
            TimeSpan.FromSeconds(3),
            TimeSpan.FromSeconds(4),
            TimeSpan.FromSeconds(5)
        };

        Console.WriteLine($"{DateTime.Now} 値の発行を開始します");
        Observable.Generate(initialState: 0,
                            condition: n => n < intervals.Count,
                            iterate: n => ++n,
                            resultSelector: n => n,
                            timeSelector: n => intervals[n])
                  .Timestamp()
                  .Subscribe(val => Console.WriteLine($"{val.Timestamp.ToLocalTime().DateTime} 発行された値:{val.Value}"),
                              () => Console.WriteLine("値の発行が完了しました"));

        Console.Read();
    }
}

実行結果

2020/04/26 23:51:09 値の発行を開始します
2020/04/26 23:51:10 発行された値:1
2020/04/26 23:51:12 発行された値:2
2020/04/26 23:51:15 発行された値:3
2020/04/26 23:51:19 発行された値:4
2020/04/26 23:51:24 発行された値:5
値の発行が完了しました

1,2,3,4,5秒間隔で値が発行されています。

簡単に任意の時間間隔で値を発行するファクトリメソッドを作る

Observable.Generateファクトリメソッドは引数が多くて面倒なので、簡単に任意の時間間隔で値を発行できるファクトリメソッドを作ります。

public static IObservable<int> AnyInterval(IReadOnlyList<TimeSpan> intervals) =>
    Observable.Generate(initialState: 0,
                        condition: n => n < intervals.Count,
                        iterate: n => ++n,
                        resultSelector: n => n,
                        timeSelector: n => intervals[n]);

待機したい時間の順番を定義したTimeSpan型リストを渡すだけで使えます。
ほんとは引数はIEnumerable<TimeSpan>にしたかったのですが、内部でCountとインデクサを使っていたのでやめました。

実際に使ってみるとこんな感じです。

ObservableEx.AnyInterval(new List<TimeSpan>{TimeSpan.FromSeconds(1),
                                            TimeSpan.FromSeconds(2),
                                            TimeSpan.FromSeconds(3),
                                            TimeSpan.FromSeconds(4),
                                            TimeSpan.FromSeconds(5) })
            .Timestamp()
            .Subscribe(val => Console.WriteLine($"{val.Timestamp.ToLocalTime().DateTime} 発行された値:{val.Value}"),
                        () => Console.WriteLine("値の発行が完了しました"));

これだと値を5個発行して終わりですが、無限に発行したければRepeat()オペレータを挟めばループしてくれます。

方法2.Observable.FromAsyncファクトリメソッドを使う

Observable.FromAsyncファクトリメソッドはTaskを引数にとってTaskが終了したらその戻り値を流してくれます。
そのため、Task.Delay等で時間を調整してやれば任意の時間間隔で値を発行するIObservableを作り出すことが可能です。

例えば、以下のような感じですね。

Observable.FromAsync(() => Task.Run(async () =>
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    return Unit.Default;
}));

これだと1秒待ってUnitを発行します。
Unitを発行してOnCompletedしてしまうので、Repeatで無限に発行するようにすれば、ひとまず1秒間隔で値を発行するIObservableができます。

Observable.FromAsync(() => Task.Run(async () =>
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    return Unit.Default;
}))
.Repeat();

あとはどのようにTask.Delayの引数を変えるかですが、外部変数とかを使うしかないんですかね?
他になにかいい方法があれば教えて下さい。

List<TimeSpan> intervals = new List<TimeSpan>()
{
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(2),
    TimeSpan.FromSeconds(3),
    TimeSpan.FromSeconds(4),
    TimeSpan.FromSeconds(5)
};
int n = 0;

Observable.FromAsync(() => Task.Run(async () =>
{
    await Task.Delay(intervals[n]);
    return n++;
}))
.Repeat(intervals.Count).Timestamp()
.Subscribe(val => Console.WriteLine($"{val.Timestamp.ToLocalTime().DateTime} 発行された値:{val.Value}"),
            () => Console.WriteLine("値の発行が完了しました"));

この方法だと、あまりきれいではありませんが、特定の条件下で発行する間隔を変えるなんてことも可能です。

List<TimeSpan> intervals = new List<TimeSpan>()
{
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(2),
    TimeSpan.FromSeconds(3),
    TimeSpan.FromSeconds(4),
    TimeSpan.FromSeconds(5)
};
int n = 0;

Observable.FromAsync(() => Task.Run(async () =>
{
    await Task.Delay(intervals[n]);
    if(条件) await Task.Delay(~); //特定の条件下で発行間隔を増やす
    return n++;
}))
.Repeat(intervals.Count).Timestamp()
.Subscribe(val => Console.WriteLine($"{val.Timestamp.ToLocalTime().DateTime} 発行された値:{val.Value}"),
            () => Console.WriteLine("値の発行が完了しました"));

すごく汚いですし副作用も多そうなので積極的に使うべきではありませんが、どうしてもという場合は仮で使えそうです。

まとめ

  • 基本的にObservable.Generateを使う方法でやれば任意の時間間隔で値を発行できる
  • 柔軟に発行間隔を変えたい場合などはObservable.FromAsyncを使えば自由度は高い、ただし副作用も多そうで注意が必要

こんな感じですかね…。
最後雑な感じで終わってしまいましたがFromAsyncを使った方法は自分でも疑問なのでアドバイスなどあればコメントくださると嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#]競プロではfor文のカウンタ変数にvarを使わない方が良い話

はじめに

AtCoderで問題を解いていて、for文のカウンタ変数でvarと型で実行時間にかなり差が出て驚いたため記事にしました。

MSでは、for文のカウンタ変数にvarを使うことを認めていますし、私も業務上では使います。
var - C# Reference | Microsoft Docs

ただ、実行時間の制限がある競プロにおいてfor文のカウンタ変数にvarを使うと良くないかもしれません。
(競プロ初心者なので、間違っておりましたらご指摘ください)

TLEになってしまう問題

「AtCoder Begginner Contest 057 C-Digits in Multiplication」を解いていた時のことです。
提出するとTLEになってしまいましたが、他のACになっているコードを見ても解法には違いがありません。

ところが、よく目を凝らしてみると、for文にvarを使っている箇所に違いがあったため、for文のカウンタ変数の型をlongに変更してみました。
すると、ACで通ったのです。

実際に提出したソースコードで比べてみました。
ソースコードは、for文のカウンタ変数の型を変えただけです。
言語は、「C# (.NET Core 3.1.201)」です。

実行時間 (ms) ソースコード
varの場合 2206 提出 #16188260 - AtCoder Beginner Contest 057
longの場合 97 提出 #16518402 - AtCoder Beginner Contest 057

for文の変数の型をlongだけでこんなに実行時間に差が出るとは……。

まとめ

競プロでは、for文のカウンタ変数にvarを使わずきちんと型を書いた方が良さそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#]競プロではfor文のカウンタ変数にvarを使ってTLEになってハマった話

はじめに

for文のカウンタ変数にvarを使って実行時間がTLEになった理由がvarが原因ではないとコメント頂きました。
初心者のハマりどころとして追記しました。(9/6 19:30)

AtCoderで問題を解いていて、for文のカウンタ変数でvarと型で実行時間にかなり差が出て驚いたため記事にしました。

MSでは、for文のカウンタ変数にvarを使うことを認めていますし、私も業務上では使います。
var - C# Reference | Microsoft Docs

ただ、実行時間の制限がある競プロにおいてfor文のカウンタ変数にvarを使うと良くないかもしれません。
(競プロ初心者なので、間違っておりましたらご指摘ください)

TLEになってしまう問題

「AtCoder Begginner Contest 057 C-Digits in Multiplication」を解いていた時のことです。
提出するとTLEになってしまいましたが、他のACになっているコードを見ても解法には違いがありません。

ところが、よく目を凝らしてみると、for文にvarを使っている箇所に違いがあったため、for文のカウンタ変数の型をlongに変更してみました。
すると、ACで通ったのです。

実際に提出したソースコードで比べてみました。
ソースコードは、for文のカウンタ変数の型を変えただけです。
言語は、「C# (.NET Core 3.1.201)」です。

実行時間 (ms) ソースコード
varの場合 2206 提出 #16188260 - AtCoder Beginner Contest 057
longの場合 97 提出 #16518402 - AtCoder Beginner Contest 057

for文の変数の型をlongだけでこんなに実行時間に差が出るとは……。

原因

varは、型推論ですのでlongにしたい場合はサフィックスをつけて型を明示しなければいけませんでした。

ソースコードを見て頂ければ分かるのですが、
varの場合

for (var i = 1; i * i <= N ; i++)

と記載していました。

MSのドキュメントでは、型の決定を以下のように記載しています。

整数リテラルの型は、そのサフィックスによって次のように決まります。
サフィックスがないリテラルの型は、int、uint、long、ulong の型のうちその値を表すことができる最初のものになります。

整数数値型 - C# リファレンス | Microsoft Docs

varの場合だとiの初期値は1と小さな値ですので、intとして型が決まります。

Nについての制約が以下になっていますので、

1 ≦ N ≦ 10^{10}

intの最大値、2,147,483,647を超えてしまい、オーバーフローしてしまいました。
オーバーフローをした状態でインクリメントすると、マイナスになります。

試しに以下のソースコードを実行するとiは、-2147483648になります。
正の最大値にインクリメントすると負の最大値になってしまいます。

int i = int.MaxValue;
i++;

オーバーフローしてしまったせいで、forで無限ループが発生してしまったためTLEになってしまいました。

よって、ソースコードのfor文を以下に直すと無事ACになりました。

for (var i = 1L; i * i <= N ; i++)

しかもvar + サフィックスLの方が若干早い。

実行時間 (ms) ソースコード
var + サフィックスLの場合 93 提出 #16539970 - AtCoder Beginner Contest 057
varの場合 2206 提出 #16188260 - AtCoder Beginner Contest 057
longの場合 97 提出 #16518402 - AtCoder Beginner Contest 057

まとめ

競プロでは、for文のカウンタ変数にvarを使うときは型を意識しよう。
競プロなど時間制限がある場合、慌てているとサフィックスやキャストを忘れる可能性もあるためきちんと型を書いてもいいのかなと思いました。
競プロでは、for文のカウンタ変数にvarを使わずきちんと型を書いた方が良さそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tyeの構成ファイルの tye.yaml のスキーマ定義を見てみる

マイクロサービス開発ツールであるTyeはYamlファイルで構成を定義します。ファイル名は tye.yaml です。

今回はこの tye.yaml のスキーマについて学びます。

tye.yaml のスキーマはGitHubで管理されており、誰でも閲覧することができます。

https://github.com/dotnet/tye/blob/master/docs/reference/schema.md

Visual Studio Codeで tye.yaml を書く準備

こちらのドキュメントに沿って環境を整えることで、Visual Studio Codeで tye.yaml を書く際にインテリセンスが表示されるようになります。

[https://github.com/dotnet/tye/blob/master/src/schema/README.md]

  1. Visual Studio CodeにYaml拡張機能をインストールする
  2. Visual Studio Codeの設定画面を起動する (Ctrl+,)
  3. 「Yaml: Schemas」の設定を探し、settings.jsonを編集する
  4. "yaml.schemas"にTyeのスキーマファイルを指定する
{
  "yaml.schemas": {
    "https://raw.githubusercontent.com/dotnet/tye/master/src/schema/tye-schema.json": [
      "tye.yaml"
    ]
  }
}

スキーマの定義

過去記事のサンプルアプリケーションで使用している tye.yaml を例に説明します。

https://github.com/tsubakimoto/project-tye-sample/blob/master/tye.yaml

name: microservice
registry: tsubakimoto
services:
- name: backend
  project: backend\backend.csproj
- name: frontend
  project: frontend\frontend.csproj
- name: redis
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"
- name: redis-cli
  image: redis
  args: "redis-cli -h redis MONITOR"

name プロパティ

Tyeでのアプリケーションの名前となります。このプロパティが使われることはほとんどないですが、Kubernetesにデプロイした場合はラベル名に使用されます。

このプロパティが指定されない場合は tye.yaml のあるディレクトリ名(小文字)が使用されます。

registry プロパティ

コンテナーレジストリの名前を指定します。DockerHubもしくはAzure Container Registryの名前を指定することができます。

ここで指定したコンテナーレジストリに、イメージやタグがプッシュされます。

namespace プロパティ

Kubernetesへのデプロイ時に、Kubernetesの名前空間(namespace)として使用されます。

以前のサンプルアプリケーションでは namespace プロパティを使用していないので、後日検証してみようと思います。

services プロパティ

アプリケーションを構成するサービスを指定します。少なくとも1つは必要です。

services プロパティでは以下のプロパティを定義することができます。

name プロパティ

サービス名となり、DNS名として利用可能な文字種である必要があります。

  • 最大63文字
  • 英数字または「-」のみ
  • 英数字で始まる
  • 英数字で終わる

project プロパティ

.csproj ファイル、もしくは .fsproj ファイルのパスを指定します。ファイルパスは tye.yaml からの相対パスです。

このプロパティに指定されたプロジェクトは、ローカルでのビルドや実行の対象となり、パッケージ化の対象となります。

image プロパティ

Dockerイメージを使用する場合にイメージ名とタグを指定します。

上記のサンプルの image: redis は、redis というイメージの latest タグとなります。

bindings プロパティ

ドキュメントには「サービスによって公開されるバインディングのリスト」とあります。

該当のサービスが公開(実行)されるときのプロトコルに関する設定です。

  • port プロパティ
    • ポートバインディングに使用するポート番号。
  • connectionString
    • サービスにバインドされる接続文字列。参照元のサービス側でどのように解釈されるかはサービスディスカバリという仕組みがあるみたいです。

args プロパティ

サービスの起動時に使用するコマンドライン引数となります。


他にも https://github.com/dotnet/tye/blob/master/docs/reference/schema.md には、スキーマ定義が記述されているため気になるものは後日検証してみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ML-Agentsのサンプルの動かし方

概要

ML-Agentのサンプルモデルの学習方法と動かし方についてまとめた記事です。大まかな流れだけのメモだと思ってください。Anacondaやpipなど使用します。これらの設定方法は各自行ってください。細かい環境などは前回の記事を参考にしてください。

環境

  • macOS Catalina 10.15.5
  • Anaconda 4.8.3
  • python 3.8.3
  • pip 20.1.1

3DBallモデル

今回はML-Agentsのサンプル内にある3DBallというモデルを使用し、学習方法などを説明していきます。3DBallはボックスの上に乗っているボールを落とさないようにするモデルのことです。

前回の記事の続きです。

左下の Assets/ML-Agents/Examples/3DBall/Scenes を開き、赤い印をつけたモデルをダブルクリックします。下の画像のようになると思います。
スクリーンショット 2020-09-05 21.40.10.png
赤く印つけた再生ボタンを押すと下の画像のようになります。サンプルのモデルはすでに学習済みなので、ボールを落とさないように動いていることが確認できます。
qiita_gif.gif
次からモデルの学習方法について説明していきます。

学習する環境を整える

まずはじめにモデルを学習させるAnacondaの仮想環境を作成します。
ML-Agentsではpython 3.6.1 以降が必要です。今回はpython 3.8.3 を使用します。

$ conda create -n mlagents python=3.8.3 anaconda
$ conda activate mlagents

次にpythonパッケージの ml-agents、 ml-agents-env をインストールします。
前回ダウンロードした ml-agents-release_6 に移動して以下のコマンドを実行します。

cd ml-agents-envs
pip3 install -e ./
cd ..
cd ml-agents
pip3 install -e ./
cd ..

これでモデルを学習させる環境が整いました。

モデルを学習させる

次に3DBallを学習させていきます。
やることは以下の3つです。

  • ハイパーパラメータを設定する。
  • Anacondaの仮想環境でプログラムを実行する。
  • unityのモデルを実行する。

ハイパーパラメータを設定する

ml-agents-release_6/config で学習アルゴリズムやパラメータを設定します。
ppo、sacというファイルがあります。それぞれ強化学習のアルゴリズムになります。

  • PPO : Proximal Policy Optimization
  • SAC : Soft Actor-Critic

とりあえずppoを使用します。このファイル内にppoのパラメータを設定するYAMLファイルがあります。YAMLファイルを作成することでハイパーパラメータを自由に設定することができます。3DBallのパラメータはすでに3DBall.yamlに設定されているのでそのまま使用します。

Anacondaの仮想環境でプログラムを実行する

学習させるpythonスクリプトを実行します。さっき作成した仮想環境で ml-agents-release_6/ml-agents に移動し以下のコマンドを実行してください。

mlagents-learn ../config/ppo/3DBall.yaml --run-id=3DBall --train

--run-id はなんでもいいです。実行すると次の画像のようにunityのロゴが出てきます。
スクリーンショット 2020-09-06 2.24.42.png

unityのモデルを実行する

次にunity上でモデルを実行します。実行ボタンを押すだけでいいです。学習が進むと以下の画像のようになります。
スクリーンショット 2020-09-06 3.15.52.png
「Mean Reward」は平均報酬です。3DBallは最大値が100になっています。16万ステップあたりから最大になっていることがわかります。「Std of ReWard」は報酬の標準偏差です。小さくなるほど良いです。20万ステップあたりで学習仕切れていることがわかります。

1万ステップと15万ステップの挙動を以下に示します。ボールを落とさないように学習できていることがわかります。

1万ステップ
10000step.gif
15万ステップ
15000step.gif

学習後のモデル

学習したモデルは ml-agents/results にさっき指定した --run-id の名前のファイルができています。そのファイルの中の 3DBall.nn が学習済みのモデルになります。新しく学習したモデルを使用する場合はunityの Assets/ML-Agents/Example/3DBall/TFModels に 3DBall.nn を追加し、Agent の Behavior Parameters の Model で 3DBall.nn を選択してください。

まとめ

ML-Agentsのサンプルモデルの動かし方について記事を書きました。ML-Agentsでの学習方法は基本的にこの流れになります。間違っていることがあるかもしれないので参考程度にしてください。また公式にドキュメントがあります。

次回はカートポールのモデルを作成し、学習させる記事を書きたいと思います。

参考にさせてもらったサイト

Unity ML-Agents 0.15.0 のチュートリアル(1)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む