20200125のC#に関する記事は12件です。

ラムダ式プロパティは結果をキャッシュしない

using System;

namespace SampleLambdaProperty
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            var v = new Sample();
            var u = v.Property;
            // 二度目は何も変わっていないが,メソッドは再び呼ばれている.
            var u2 = v.Property;
        }
    }

    class Sample
    {
        public int Property => GetValue();

        private int GetValue()
        {
            Console.WriteLine("Method Called!");
            return 2;
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VisualStudioのソースコードスニペット用拡張機能作成Tips

はじめに

VisualStudioのスニペットではできない入力時の日時を入れたりすることが拡張機能ならできるようになります.
その拡張機能を作るうえでここだけサンプルから変えれば簡単なものが作れるといった内容を記載していきます.
以降は以下サイトの手順が完了しているものとします.
https://docs.microsoft.com/ja-jp/visualstudio/extensibility/walkthrough-displaying-statement-completion?view=vs-2017
※ 手順通りに行いusingの追加を行ってもエラーが残っている場合は参照に追加したもののバージョンに誤りがないか確認してみてください.時々同一名称の別バージョンのものを追加してしまっており,エラーとなってしまうことがあったりします.

Tips

対象ファイル指定

TestCompletionSourceProviderクラスとTestCompletionHandlerProviderクラスの[ContentType()]に対象を記載。

 [ContentType("TypeScript")]

挿入したい文字列の生成を行う箇所

TestCompletionSourceクラスの以下メソッド内

void ICompletionSource.AugmentCompletionSession(ICompletionSession session, IList<CompletionSet> completionSets)

現在のエディタ内のテキスト取得

文字を入力した際のエディタ内のテキストを取得できます.

m_textBuffer.CurrentSnapshot.GetText()

現在の文字挿入位置取得

(session.TextView.Caret.Position.BufferPosition - 1).Position

受付可能な文字の変更

TestCompletionCommandHandlerクラスのExecメソッド内の以下条件にて、10進の数字か文字のみ受け付けられるようになっています。
例えば「/」を使用したい場合はこの条件を書き換えることで使用できるようになります.

if (!typedChar.Equals(char.MinValue) && char.IsLetterOrDigit(typedChar))

設定より挿入する文字列を切り替える

以下チュートリアルを参考に設定項目を追加し,その値を使用する.
細かい話は気が向いたら書くかもしれません……
https://docs.microsoft.com/ja-jp/visualstudio/extensibility/extending-user-settings-and-options?view=vs-2017

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

Unity3D/C#で簡単なリバーシ(オセロ)を作る!!~Part2~

はいどうも〜ミ◯クボーイです〜!!
あ〜ありがとうございます!!え〜今、AWSのアカウントをいただきましたけどもね〜。
こんなんなんぼあってもいいですからね〜。(突然の茶番)

はじめに

本記事は前回の「Unity3D/C#で簡単なリバーシ(オセロ)を作る!!~Part1~」 (https://qiita.com/t-o2mt/items/40e4bca24011dd88d8a7) の続きとなります。

前回は盤と石を作成し、ゲームスタート時に石を初期配置するところまで説明しました。
今回はタップして石を置く処理と、引っくり返す処理を説明します。

タップして石を置く

    private int x;
    private int z;//タップした座標
    private eStoneState turn = eStoneState.BLACK;//ターン。最初は黒

    void Update()
    {
        PutStone();

        for (int i = 0; i < squareZ; i++)
        {
            for (int j = 0; j < squareX; j++)
            {
                // 石の状態を確認
                stoneManagers[i, j].SetState(stoneState[i, j]);
            }
        }
    }
    //タップで石を置く処理
    public void PutStone(){
        if (Input.GetMouseButtonDown(0))
        {
            //マウスのポジションを取得してRayに代入
            Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);

            //マウスのポジションからRayを投げて何かに当たったらhitに入れる
            if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out RaycastHit hit, 100))
            {
                //x,zの値を取得
                x = (int)hit.collider.gameObject.transform.position.x;
                z = (int)hit.collider.gameObject.transform.position.z;

                if (0 <= x && x < squareX && 0 <= z && z < squareZ &&
                    stoneState[z, x] == eStoneState.EMPTY && Turn(false) > 0)
                {
                    stoneState[z, x] = turn;
                    Turn(true);
                    turn = ((turn == eStoneState.BLACK) ? eStoneState.WHITE : eStoneState.BLACK);
                }
            }
        }
    }

"StageManager"に上記のコードを追加します。
Raycastの関数で得た座標をhitに入れ、xとz分け、stoneState[z, x]がeStoneState.EMPTYならturnでWHITEかBLACKに変え、Part1で記述したSetState()で石を表示させます。
これで、タップしたマスに石を置くことができます。

石を引っくり返す

    class TurnableStone {
        public int turnZ;
        public int turnX;
        public TurnableStone(int z, int x)
        {
            turnZ = z;
            turnX = x;
        }
    }//ひっくり返すことができる駒の位置

    int[] TURN_CHECK_X = new int[] { -1, -1, 0, 1, 1, 1, 0, -1 };
    int[] TURN_CHECK_Z = new int[] { 0, 1, 1, 1, 0, -1, -1, -1 };//石の隣8方向

    //ひっくり返す石を取得する処理
        int Turn(bool isTurn)
    {
        // 相手の石の色
        eStoneState enemyColor = ((turn == eStoneState.BLACK) ? eStoneState.WHITE : eStoneState.BLACK);

        bool isTurnable = false;// ひっくり返すことができるかどうか
        List<TurnableStone> turnableStoneList = new List<TurnableStone>();//ひっくり返す石のリスト
        int count = 0;
        int turnCount = 0;

        int plusX = 0, plusZ = 0;
        for (int i = 0; i < TURN_CHECK_X.Length; i++)
        {
            int _x = x;
            int _z = z;

            plusX = TURN_CHECK_X[i];
            plusZ = TURN_CHECK_Z[i];
            isTurnable = false;
            turnableStoneList.Clear();
            while (true)
            {
                _x += plusX;
                _z += plusZ;
                if (!(0 <= _x && _x < squareX && 0 <= _z && _z < squareZ))
                {
                    break;
                }
                if (stoneState[_z, _x] == enemyColor)
                {
                    // ひっくり返す対象
                    turnableStoneList.Add(new TurnableStone(_z, _x));
                }
                else if (stoneState[_z, _x] == turn)
                {
                    // ひっくり返すことができる
                    isTurnable = true;
                    break;
                }
                else
                {
                    break;
                }
            }

            //ひっくり返す処理
            if (isTurnable)
            {
                count += turnableStoneList.Count;
                if (isTurn)
                {
                    for (int j = 0; j < turnableStoneList.Count; j++)
                    {
                        TurnableStone ts = turnableStoneList[j];
                        stoneState[ts.turnZ, ts.turnX] = turn;
                        turnCount++;
                    }
                }
            }
        }
        return count;
    }

"StageManager"に上記のコードを追加します。Turnはint型でreturnさせています。
TURN_CHECKで隣接する全方位の石を確認し、引っくり返すことができるものだけをturnableStoneListに格納していき、for文でturnを繰り返し行えば引っくり返しが行えます。

駆け足ですが、これでようやくオセロの超最低限の機能をつけ終わることができました!!8888888888

最後に

bc3930ec879f30243e4f692fa5679611.png

Part1,2で説明した機能に【ランダムに石を置く敵AI】【枚数チェック】【制限時間】【勝利判定】を加えたものが上の画像です。やっぱり2Dよりきれい!!人は3Dより2D派だけど、ゲームはやっぱり3Dに限るね〜。

これにしっかりした敵AIさえつけることができればできれば、老若男女が白熱して楽しむことができる国民的ボードゲームアプリの完成です!!

ご拝読いただき、ありがとうございました。

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

.Net Frameworkと.Net Coreの違い、そして.Net 5

.Net ほにゃらら について

よく聞く割に何となくしか理解できていませんでした。
CoreとかFrameworkって言うのがあるんですよね。

少し調べたのでめちゃザックリかつ簡単にまとめておきます。
※あまり詳しく無いので間違えた解釈をしているなどあれば教えてください!

きーわーど
.Net Framework
.NET Core
.NET 5

.NET Frameworkとは

MicroSoftが開発したWindows向けのフレームワーク。
元々はこっちだけだった。
昔から作られていたモノはFramework。

.NET Coreとは

クロスプラットフォームに対応するために後から生まれた。
Windows、Linux、macOS向けのフレームワーク。

.Netフレームワークの完全上位互換のように見える。
けど色々と問題があった。

FrameworkにあるAPIのうち一部しかCoreの方にはなかった。

Frameworkの方ではゴチャゴチャした処理も簡単に記述できていたけど、
Coreでは対応していないから実装をベタ書きすることになったりする…

現時点ではバージョン3.1まで出ており、だいぶ充実している。

正直今から新規開発するなら.NET Coreでいい。

下記のページでMicrosoftの賢そうな人が
もうFrameWorkからCoreへの移植結構やったしもうやめるわ
的なこといってる。
Announcement: .NET Core 3.0 concludes the .NET Framework API porting project

.NET 5とは

FrameworkやらCoreについて書いていたけど2020年11月に.Net 5って言うのがリリースされるとのこと。
1つに統合されるみたい。
これでFrameworkとかCoreといった単語とおさらばできるかな。できたらいいなぁ。
特に不具合なくバージョンをあげれることを願います。

おわり。

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

C# GUIからバックグラウンドで実行したプロセスを管理する

はじめに

C# GUIアプリケーションからPythonスクリプトを実行する ではPythonスクリプトを実行する際にモードダルダイアログにし、実行が終わるまで他の操作はできない状況であった。しかし、あるプロセスをバックグラウンドで実行開始し、その後呼び出し元の画面にもどって別の操作をしたり、別のプロセスを実行できると便利だ。また実行中の全てのプロセスを一覧表示し、実行状況を確認したり、キャンセル等の操作も行えるとさらに便利だ。これを行うプロトタイプを作成してみた。

実現イメージ

実現したいことを図にするとこんな感じになる。

image.png

  • プロセス実行画面でプロセスの実行を開始すると、その画面は閉じることができ、プロセスはバックグラウンドで実行される。その後プロセス管理画面でプロセスの状況(終わったかどうか等)を確認できる。
  • 途中で詳細な実行状況を見たい場合は、プロセス管理画面で「Detail」ボタンをクリックすると、そのプロセスの進捗状況をプロセス詳細画面でいつでも参照できる。
  • 複数のプロセスの実行を同時に見ることができる。

実現方法

ソースはhttps://github.com/kimisyo/PythonParallelExecutor で公開している。
C# GUIアプリケーションからPythonスクリプトを実行する からの変更したポイントは以下の通りである。

  • プロセスを実行する際に指定するプロセスクラスのOnExit, OutputDataReceived, ErrorDataReceivedのイベントハンドラを親のフォームで一括管理する。
  • 親フォームでは実行されたプロセスの情報(プロセスクラス)を保持しておき、DataGridViewのDataSourceと連動させておく。また、OutputDataReceived,ErrorDataReceivedイベントハンドラに実行中の各プロセスからの標準入出力データが送られてくるので、これをプロセス別に保持しておく。
  • プロセスの実行詳細画面を表示する際は、これまでにそのプロセスがOutputDataReceived,ErrorDataReceivedで受け取った内容を標準入出力のエリアに、初期表示する。
  • そして肝になる部分が、OutputDataReceived, ErrorDataReceivedイベントハンドラにおいて標準入出力データを受け取る度に全子画面にイベント発行する点だ。子画面のイベントハンドラはそれをうけとって進捗状況にデータを表示する。もちろん、子画面がいつも存在するとはかぎらないため、プロセス実行詳細画面を表示する際にイベントとイベントハンドラを紐づけ、プロセス実行詳細画面を閉じる際にそれを解除している。コードでいうと、PythonCommandExecutorForm.csのthis.parentForm.MyProgressEvent += handler;, this.parentForm.MyProgressEvent -= this.handler;のように記載しているところになる。

おわりに

今まで避けてきたイベントの仕組みについて理解が深まった。イベントの仕組みを理解すると、非同期に各画面部品が連動しあうアプリケーションが簡単に作れそうな気がする。

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

C# GUIからバックグラウンドでプロセスを実行し管理する

はじめに

C# GUIアプリケーションからPythonスクリプトを実行する ではPythonスクリプトの実行が終わるまで他の操作はできない状況であった。しかし、もっと複雑なアプリケーションを考えた場合、あるPytnon等のプロセスをバックグラウンドで実行開始し、その後呼び出し元の画面にもどって別の操作をしたり、別のプロセスを実行できると便利だ。また実行中の全てのプロセスを一覧表示し、実行状況を確認したり、キャンセル等の操作も行えるとさらに便利だ。これを行うプロトタイプを作成してみた。

実現イメージ

実現したいことを図にするとこんな感じになる。

image.png

  • プロセス実行画面でプロセスの実行を開始すると、その画面は閉じることができ、プロセスはバックグラウンドで実行される。その後プロセス管理画面でプロセスの状況(終わったかどうか等)を確認できる。
  • 途中で詳細な実行状況を見たい場合は、プロセス管理画面で「Detail」ボタンをクリックすると、そのプロセスの進捗状況をプロセス詳細画面でいつでも参照できる。
  • 複数のプロセスの進捗状況を、複数のプロセス詳細画面で同時に見ることができる。

実現方法

ソースはhttps://github.com/kimisyo/PythonParallelExecutor で公開している。名前はPythonがついているが、コマンドラインで呼び出せるものであれば容易に修正は可能である。
C# GUIアプリケーションからPythonスクリプトを実行する からの変更したポイントは以下の通りである。

  • プロセスを実行する際に指定するプロセスクラスのExited, OutputDataReceived, ErrorDataReceivedイベントに対し、親のフォームのイベントハンドラを紐づけるよう変更する。
  • 親フォームでは実行されたプロセスの情報(プロセスクラス)を保持しておき、DataGridViewのDataSourceと連動させておく。また、OutputDataReceived,ErrorDataReceivedイベントハンドラに実行中の各プロセスからの標準出力データが送られてくるので、これをプロセス別に保持しておく。
  • プロセスの実行詳細画面を表示する際は、それまでにOutputDataReceived, ErrorDataReceivedで受け取った内容を標準出力のエリアに初期表示する。
  • そして肝になる部分が、OutputDataReceived, ErrorDataReceivedイベントハンドラにおいて標準入出力データを受け取る度に全子画面にイベント発行する点だ。子画面のイベントハンドラはそれをうけとってテキストエリアに進捗状況(標準出力のデータ)を表示する。もちろん、子画面がいつも存在するとはかぎらないため、プロセス実行詳細画面を表示する際にイベントとイベントハンドラを紐づけ、プロセス実行詳細画面を閉じる際にそれを解除している。コードでいうと、PythonCommandExecutorForm.csのthis.parentForm.MyProgressEvent += handler;, this.parentForm.MyProgressEvent -= this.handler;のように記載しているところになる。

おわりに

今まで避けてきたイベントの仕組みについて理解が深まった。イベントの仕組みを理解すると、各画面部品が非同期に連動するの独立性の高いアプリケーションが簡単に作れそうな気がする。

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

[Unity初心者Tips]Prefabは要るか?GameObjectを動的に生成する

初心者向けの本などで学習していると、何かをSceneに配置するときにはPrefabをあらかじめ用意してからInstantiateを用いることが多いですが、それは要らないときもある、という話です。

コンストラクタから作るGameObject

GameObject型はコンストラクタが在り、それを実行することで動的に生成することが可能です。

GameObject hogeGameObject = new GameObject("Hoge");

参考:GameObject-GameObject - Unity スクリプトリファレンス
https://docs.unity3d.com/ja/2017.4/ScriptReference/GameObject-ctor.html

新たにGameObjectをSceneに配置可能です。必要に応じてAddComponentすればほしい機能を持ったGameObjectも簡単にメモリ上に展開できます。

無駄を減らせる?注意してみよう

  • これを利用することで予めPrefabで用意しInstantiateが要らなくなる
  • 無駄に常駐させるGameObjectを減らせる可能性がある

こうした利点があるので、「これは一時的に存在すれば済む」というものには積極的に利用する検討をしてみましょう。頻繁に生成や削除をする場合は、コストがかさむのでお勧めしません、適宜、パフォーマンスをどうしたらよくなるか、考えながら作りましょう。

Sceneに在るGameObjectをコピーする方法

prefabを用いない方法には他に、Scene内のGameObjectを参照してInstantiateで複製するという方法もあります。

GameObject origin = GameObject.Find("Hoge"); //元になるHogeを探す
GameObject hogeGameObject = new GameObject(origin); //Hogeを複製

この場合は予めSceneに存在しないと出来ないので、何か在るものを増やしたいときには有効です。あまり頻繁にやると、newと同じくらいのコストがかさむので注意は必要です。

Prefabが要るかの使い分け

Prefabを用いた場合、以下の特徴があります。

  • ファイルが分れるので別の人に作業分担してもらいマージしやすい
  • AddComponentで追加がたくさんある場合コストが下がる
  • エディタでコンポーネントの操作をするのでゲームデザイナーにもいじるのが優しい
  • Scene内の何かの参照は持てないので動的にやりくりする仕組みは必要(GameObjectのコンストラクタを用いた後の処理と似たものが要る)

こうしたことを踏まえて、考えていきましょう。データの分け方にはSceneで分ける方法もあるので、チームでどうしたいのか、更新するとしたらどのAssetの単位でやったらいいのか、なども考えると良いですね。

参照などの話はこちらも参考になります。
【Unity】動的に増やすGameObjectは別にPrefabでなくとも良い
http://tsubakit1.hateblo.jp/entry/2018/03/01/005236

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

[Unity初心者Tips]Prefabなしもアリ!GameObjectを動的に生成する

初心者向けの本などで学習していると、何かをSceneに配置するときにはPrefabをあらかじめ用意してからInstantiateを用いることが多いですが、それは要らないときもある、という話です。

コンストラクタから作るGameObject

GameObject型はコンストラクタが在り、それを実行することで動的に生成することが可能です。

GameObject hogeGameObject = new GameObject("Hoge");

参考:GameObject-GameObject - Unity スクリプトリファレンス
https://docs.unity3d.com/ja/2017.4/ScriptReference/GameObject-ctor.html

新たにGameObjectをSceneに配置可能です。必要に応じてAddComponentすればほしい機能を持ったGameObjectも簡単にメモリ上に展開できます。

無駄を減らせる?注意してみよう

  • これを利用することで予めPrefabで用意しInstantiateが要らなくなる
  • 無駄に常駐させるGameObjectを減らせる可能性がある

こうした利点があるので、「これは一時的に存在すれば済む」というものには積極的に利用する検討をしてみましょう。頻繁に生成や削除をする場合は、コストがかさむのでお勧めしません、適宜、パフォーマンスをどうしたらよくなるか、考えながら作りましょう。

Sceneに在るGameObjectをコピーする方法

prefabを用いない方法には他に、Scene内のGameObjectを参照してInstantiateで複製するという方法もあります。

GameObject origin = GameObject.Find("Hoge"); //元になるHogeを探す
GameObject hogeGameObject = new GameObject(origin); //Hogeを複製

この場合は予めSceneに存在しないと出来ないので、何か在るものを増やしたいときには有効です。あまり頻繁にやると、newと同じくらいのコストがかさむので注意は必要です。

Prefabが要るかの使い分け

Prefabを用いた場合、以下の特徴があります。

  • ファイルが分れるので別の人に作業分担してもらいマージしやすい
  • AddComponentで追加がたくさんある場合コストが下がる
  • エディタでコンポーネントの操作をするのでゲームデザイナーにもいじるのが優しい
  • Scene内の何かの参照は持てないので動的にやりくりする仕組みは必要(GameObjectのコンストラクタを用いた後の処理と似たものが要る)

こうしたことを踏まえて、考えていきましょう。データの分け方にはSceneで分ける方法もあるので、チームでどうしたいのか、更新するとしたらどのAssetの単位でやったらいいのか、なども考えると良いですね。

参照などの話はこちらも参考になります。
【Unity】動的に増やすGameObjectは別にPrefabでなくとも良い
http://tsubakit1.hateblo.jp/entry/2018/03/01/005236

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

C# GUIからPythonのコードを実行する

はじめに

C# GUIアプリケーションからPythonスクリプトを実行する の延長で、GUIから直接入力したpythonのソースコードを実行できるようにしたという話。

作ったもの

画面は以下の通りだ。画面ではnumpyにより計算を実行した結果を表示している。
image.png

また文法が違っている場合は以下のようにエラーが表示される。
image.png

ソース

ユーザが入力したPythonのコードをpythonスクリプトとして一時フォルダに保存し、それをpytnon.exeに実行させるようにしただけで、技術的に目新しいことは全くない。

ただ、その場で自由にpythonのコードを入力し、実行できるのは楽しいものだ。

ふと浮かんだ応用例として、どうしても見られたくないPythonコードを配布する場合に使えるのではないか。

最後にソースを記載しておく。解説はC# GUIアプリケーションからPythonスクリプトを実行する を参考にしてほしい。

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace PythonExecutor
{

    public partial class Form1 : Form
    {
        private Process currentProcess;
        private StringBuilder outStringBuilder = new StringBuilder();
        private int readCount = 0;
        private Boolean isCanceled = false;
        private String pythonFileName = "temporaryPythonFile.py";

        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Textboxに文字列追加
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void AppendText(String data, Boolean console)
        {
            textBox1.AppendText(data);
            if (console)
            {
                textBox1.AppendText("\r\n");
                Console.WriteLine(data);
            }
        }

        /// <summary>
        /// 実行ボタンクリック時の動作
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {

            if (Directory.Exists(this.textBox3.Text.Trim()))
            {
                // workフォルダにpythonファイルに書き込む
                String path = this.textBox3.Text.Trim() + System.IO.Path.DirectorySeparatorChar + this.pythonFileName;

                using (StreamWriter writer = new StreamWriter(path))
                {
                    StringReader strReader = new StringReader(this.textBox4.Text.Trim());
                    while (true)
                    {
                        String aLine = strReader.ReadLine();
                        if (aLine != null)
                        {
                            writer.WriteLine(aLine);

                        }
                        else
                        {
                            break;
                        }
                    }
                    writer.Close();
                }

                // 前処理
                button1.Enabled = false;
                button2.Enabled = true;
                isCanceled = false;
                readCount = 0;
                outStringBuilder.Clear();
                this.Invoke((MethodInvoker)(() => this.textBox1.Clear()));

                // 実行
                RunCommandLineAsync(path);
            }
            else
            {
                this.Invoke((MethodInvoker)(() => MessageBox.Show("Workingフォルダが無効です")));
            }

        }

        /// <summary>
        /// コマンド実行処理本体
        /// </summary>
        public void RunCommandLineAsync(String pythonScriptPath)
        {

            ProcessStartInfo psInfo = new ProcessStartInfo();
            psInfo.FileName = this.textBox2.Text.Trim();
            psInfo.WorkingDirectory = this.textBox3.Text.Trim();
            // psInfo.Arguments = this.textBox4.Text.Trim();
            psInfo.Arguments = pythonScriptPath;

            psInfo.CreateNoWindow = true;
            psInfo.UseShellExecute = false;
            psInfo.RedirectStandardInput = true;
            psInfo.RedirectStandardOutput = true;
            psInfo.RedirectStandardError = true;

            Process p = Process.Start(psInfo);
            p.EnableRaisingEvents = true;
            p.Exited += onExited;
            p.OutputDataReceived += p_OutputDataReceived;
            p.ErrorDataReceived += p_ErrorDataReceived;

            p.Start();


            //非同期で出力とエラーの読み取りを開始
            p.BeginOutputReadLine();
            p.BeginErrorReadLine();

            currentProcess = p;
        }

        void onExited(object sender, EventArgs e)
        {
            int exitCode;

            if (currentProcess != null)
            {
                currentProcess.WaitForExit();

                // 吐き出されずに残っているデータの吐き出し
                this.Invoke((MethodInvoker)(() => AppendText(outStringBuilder.ToString(), false)));
                outStringBuilder.Clear();

                exitCode = currentProcess.ExitCode;
                currentProcess.CancelOutputRead();
                currentProcess.CancelErrorRead();
                currentProcess.Close();
                currentProcess.Dispose();
                currentProcess = null;

                // pythonファイルを削除する
                String pythonFilepath = this.textBox3.Text.Trim() + System.IO.Path.DirectorySeparatorChar + this.pythonFileName;
                if (File.Exists(pythonFilepath)) ;
                {
                    File.Delete(pythonFilepath);
                }

                this.Invoke((MethodInvoker)(() => this.button1.Enabled = true));
                this.Invoke((MethodInvoker)(() => this.button2.Enabled=false));

                if (isCanceled)
                {
                    // 完了メッセージ
                    this.Invoke((MethodInvoker)(() => MessageBox.Show("処理をキャンセルしました")));
                }
                else
                {
                    if (exitCode == 0)
                    {
                        // 完了メッセージ
                        this.Invoke((MethodInvoker)(() => MessageBox.Show("処理が完了しました")));
                    }
                    else
                    {
                        // 完了メッセージ
                        this.Invoke((MethodInvoker)(() => MessageBox.Show("エラーが発生しました")));
                    }
                }
            }
        }

        /// <summary>
        /// 標準出力データを受け取った時の処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void p_OutputDataReceived(object sender,
            System.Diagnostics.DataReceivedEventArgs e)
        {
            processMessage(sender, e);
        }

        /// <summary>
        /// 標準エラーを受け取った時の処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void p_ErrorDataReceived(object sender,
            System.Diagnostics.DataReceivedEventArgs e)
        {
            processMessage(sender, e);
        }

        /// <summary>
        /// CommandLineプログラムのデータを受け取りTextBoxに吐き出す
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void processMessage(object sender, System.Diagnostics.DataReceivedEventArgs e)
        {
            if (e != null && e.Data != null && e.Data.Length > 0)
            {
                outStringBuilder.Append(e.Data + "\r\n");
            }
            readCount++;
            // まとまったタイミングで吐き出し
            if (readCount % 5 == 0)
            {
                this.Invoke((MethodInvoker)(() => AppendText(outStringBuilder.ToString(), false)));
                outStringBuilder.Clear();
                // スレッドを占有しないようスリープを入れる
                if (readCount % 1000 == 0)
                {
                    Thread.Sleep(100);
                }
            }
        }

        /// <summary>
        /// キャンセルボタンクリック時の動作
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button2_Click(object sender, EventArgs e)
        {
            if (currentProcess != null)
            {
                try
                {
                    currentProcess.Kill();
                    isCanceled = true;
                }
                catch (Exception e2)
                {
                    Console.WriteLine(e2);
                }
            }
        }

        private void button3_Click(object sender, EventArgs e)
        {
            // Pythonコード部分のクリア
            this.textBox4.Clear();
            // 標準出力エリアのクリア
            this.textBox1.Clear();
        }
        private void textBox1_TextChanged(object sender, EventArgs e)
        {

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

C# DataGridView BindingList使用時に自動的に表示される列を制御する

背景

BindingListに自分で作ったクラスを登録し、DataGridViewのdatasourceにセットするとDataGridViewにデータが反映され便利である。ただ、自分で作ったクラスのデータのうち、一部しかデータグリッドに乗せたくない場面もある。その時にわざわざデータグリッド表示用のクラスと、それ以外のデータも管理するクラスを別々に作成するのは管理上手間がかかる。このため、1つのクラスだけでデータグリッドに表示させる/させないを制御する方法を調べたのでメモっておく。

方法

クラスのpublicなプロパティとして用意したデータのみがDataGirdViewに表示されるという性質を利用し、以下のように欺けばよい。

 /// データグリッド表示用のプロセスデータクラス
    public class ProcessData
    {
        // データグリッドで自動で見せる列
        public int processId { get; set; }
        public string Arguments { get; set; }
        public DateTime startTime { get; set; }
        public DateTime endTime { get; set; }
        public String exitCode { get; set; }
        public String status { get; set; }


        // データグリッドで自動で見せない列
        private Process process;
        private String inputString;
        private StringBuilder outputStringBuilder;
        private string workingDirectory;
        private PythonCommandExecutorForm detailForm;

        private string fileName;

        public ProcessData()
        {
            this.outputStringBuilder = new StringBuilder();
        }

        public void setProcess(Process process)
        {
            this.process = process;
        }
        public Process getProcess()
        {
            return this.process;
        }
        public void setOutputStringBuilder(StringBuilder outputStringBuilder)
        {
            this.outputStringBuilder = outputStringBuilder;
        }
        public StringBuilder getOutputStringBuilder()
        {
            return this.outputStringBuilder;
        }
        public void setWorkingDirectory(String workingDirectory)
        {
            this.workingDirectory = workingDirectory;
        }
        public String getWorkingDirectory()
        {
            return this.workingDirectory;
        }
        public void setFileName(String fileName)
        {
            this.fileName = fileName;
        }
        public String getFileName()
        {
            return this.fileName;
        }
        public String getInputString()
        {
            return this.inputString;
        }
        public void setInputString(String inputString)
        {
            this.inputString = inputString;
        }
        public void setDetailForm(PythonCommandExecutorForm detailForm)
        {
            this.detailForm = detailForm;
        }
        public PythonCommandExecutorForm getDetailForm()
        {
            return this.detailForm;
        }

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

C# DataGridView BindingListで自動的に表示される列を制御する

背景

BindingListに自分で作ったクラスを登録し、DataGridViewのdatasourceにセットするとDataGridViewにデータが反映され便利である。ただ、自分で作ったクラスのデータのうち、一部しかデータグリッドに乗せたくない場面もある。その時にわざわざデータグリッド表示用のクラスと、それ以外のデータも管理するクラスを別々に作成するのは管理上手間がかかる。このため、1つのクラスだけでデータグリッドに表示させる/させないを制御する方法を調べたのでメモっておく。

方法

クラスのpublicなプロパティとして用意したデータのみがDataGirdViewに表示されるという性質を利用し、以下のように欺けばよい。

 /// データグリッド表示用のプロセスデータクラス
    public class ProcessData
    {
        // データグリッドで自動で見せる列
        public int processId { get; set; }
        public string Arguments { get; set; }
        public DateTime startTime { get; set; }
        public DateTime endTime { get; set; }
        public String exitCode { get; set; }
        public String status { get; set; }


        // データグリッドで自動で見せない列
        private Process process;
        private String inputString;
        private StringBuilder outputStringBuilder;
        private string workingDirectory;
        private PythonCommandExecutorForm detailForm;

        private string fileName;

        public ProcessData()
        {
            this.outputStringBuilder = new StringBuilder();
        }

        public void setProcess(Process process)
        {
            this.process = process;
        }
        public Process getProcess()
        {
            return this.process;
        }
        public void setOutputStringBuilder(StringBuilder outputStringBuilder)
        {
            this.outputStringBuilder = outputStringBuilder;
        }
        public StringBuilder getOutputStringBuilder()
        {
            return this.outputStringBuilder;
        }
        public void setWorkingDirectory(String workingDirectory)
        {
            this.workingDirectory = workingDirectory;
        }
        public String getWorkingDirectory()
        {
            return this.workingDirectory;
        }
        public void setFileName(String fileName)
        {
            this.fileName = fileName;
        }
        public String getFileName()
        {
            return this.fileName;
        }
        public String getInputString()
        {
            return this.inputString;
        }
        public void setInputString(String inputString)
        {
            this.inputString = inputString;
        }
        public void setDetailForm(PythonCommandExecutorForm detailForm)
        {
            this.detailForm = detailForm;
        }
        public PythonCommandExecutorForm getDetailForm()
        {
            return this.detailForm;
        }

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

ArduinoとPCであらゆるSwitchソフトの操作を自動化できるようにしてみた

概要

先日、こちらこちらでNintendo Switchの「ポケットモンスター ソード/シールド」(以下、ポケモン剣盾)の操作を自動化する方法が公開されました。

これらはいずれも、ArduinoをSwitchにコントローラとして認識させる方法を取っています。ですので理屈上はあらゆるSwitch用ゲームの自動化が可能ですが、公開されている方法はいずれもポケモン剣盾の自動化に特化した内容です。

そこでユーザーが任意のマクロを構築することで、あらゆるSwitch用ゲームの操作を手軽に自動化できる、汎用型のPCソフトを作成してみました。

使用例

使用したマクロ
PressA.json
{
  "Commands": {
    "$type": "Command.ICommand[], Common",
    "$values": [
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 1000
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 1000
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 5
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 5
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 500
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 3
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 3
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 300
      }
    ]
  },
  "Description": null
}

使用したマクロ
KONAMICommand.json
{
  "Commands": {
    "$type": "Command.ICommand[], Common",
    "$values": [
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 18
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 18
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 2
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 2
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 2
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 2
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 3
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 3
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 3
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 3
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 1
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 1
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 0
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 0
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 1
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 1
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 0
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 0
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 5
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 5
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },https://twitter.com/twostarsmco/status/1220714002399014912
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 18
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 18
      }
    ]
  },
  "Description": null
}

使用したマクロ
PressA.json
{
  "Commands": {
    "$type": "Command.ICommand[], Common",
    "$values": [
      {
        "$type": "Command.OperateStick, Common",
        "TargetXAngle": 160,
        "TargetYAngle": 0,
        "TargetControlID": 13
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 500
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 5
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 100
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 5
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 900
      }
    ]
  },
  "Description": null
}

リポジトリ

https://github.com/twostarsmco/NSAutomation

開発の動機

・GitHubを本格的に使ってみたい
・C#、.NETの新しめの機能や構文を、周りに気兼ねせず試してみたい
・自動テスト、DIパターンなどのプラクティスを試してみたい

使ったもの

Nintendo Switch
Windowsパソコン
Arduino Leonardoまたは互換ボード
筆者が使用したものはこちらです。
FT232モジュール
筆者が使用したものはこちらです。電源と信号電圧はArduino側から取っています。
USBケーブルx2
FT232とパソコンを繋ぐもの、ArduinoとSwitchを繋ぐものがそれぞれ必要です。

使用方法

  1. こちらからNSAutomationWin.zipをダウンロードする
  2. こちらの記事の方法で、Switch⇔Arduino Leonardo⇔FT232⇔PCを接続する
  3. ArduinoがSwitchのコントローラとして認識された状態で、NSAutomationWin.exeを起動する
  4. FT232が接続されたCOMポートを、ドロップダウンメニューで選択する
  5. 画面の表にマクロを記述し、Runボタンを押す
  6. マクロが実行される
  7. マクロをファイルに保存して、後で読み込んだり、他のユーザーと共有したりする

マクロでできること

  • ボタンの操作
    ボタンと、左右スティックの押し込みを押す(PRESS)/離す(RELEASE)操作ができます。明示的に離すまで、押し込んだ状態が続きます。

  • スティックの操作
    左右のスティックを、縦横それぞれ256段階へ傾けることができます。一度傾けるとその状態をキープするので、終わったらニュートラルポジションの(128, 128)へ明示的に戻す必要があります。

  • 待機する
    次の行のコマンドの実行を、指定したミリ秒だけ遅らせます。あまりに速くコマンドを実行させるとSwitchが反応できないので、適当に遅らせてあげてください1
    内部的には、暫定的にTask.Delayを使った実装としています。WindowsではMultimediaTimerを使うと最高の時間精度を実現できると聞いた事があるので、将来的にはこちらに切り替えたいですね。めざせTASbot

学んだこと

工夫したところ

マクロを編集、保存、読み込みできるようにした

汎用性を求める上で、これらの機能は必須。

マクロのループ実行と中断をできるようにした

「一連のボタン操作を繰り返し入力する」使用方法が多いと想定しています。

ソフトの内部構造を徹底的に疎結合にした

ソフトの構成を

  • 制御コマンドの共通データモデル、、共通インタフェース
  • コマンドを解釈して実行するバックエンド
  • ユーザインタフェース

の3つのプロジェクトに分けました。将来的に、例えば特定のゲームソフトに特化した自動化ソフトを作るだとか、Windows以外のOSで同様のソフトを作る時などに流用が効くはずです(いつになるやら分かりませんが)。
スマホから、PCと同じマクロファイルを読んでSwitchを制御できたら楽しそうですね。あるいはSwitchの動画キャプチャと画像認識、強化学習を絡めて、任意のSwitchタイトルを自動強化学習……なんて事が出来ると面白いと思いませんか?

また開発中は、バックエンドをモックに差し替えることでUIのデバッグを通信処理と切り離して行うことができます。多少オーバーエンジニアリング気味かもしれませんが、デバッグはかなり楽でした。PC外部との通信を行うソフトなら、小さなモノでもこのように疎結合にするメリットはあると感じました。

苦労したところ

共通の親を継承した複数の子クラスをJSONへSerialize、Deserializeする処理

3種類のコマンドをそれぞれ、共通の親クラスを持つ別のクラスとして定義しています。各コマンドのパラメータを静的に型付けされた形で利用したかったためです。……が、マクロはこれらのコマンドのインスタンスを「共通の親クラスの配列」として保持しているので、マクロをデフォルトの設定でJSONにSerializeすると、どの子クラスのインスタンスだったのかという情報が失われてしまい、Deserializeが不可能になります。

今回はJSON.NETの設定に型情報を出力するものがあったので、それを利用しました。

Macro.cs
public string ToJSON(JsonSerializerSettings settings = null)
{
    settings = settings ?? new JsonSerializerSettings() {
             Formatting = Formatting.Indented };

    settings.TypeNameHandling = TypeNameHandling.Auto;
    return JsonConvert.SerializeObject(this, settings);
}

public static Macro FromJSON(string json)
{
    return JsonConvert.DeserializeObject<Macro>(
        json, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto });
}

ただこれをそのまま使うと、こんな感じで"$type"が入ってJSONの可読性が若干犠牲になるのが悩ましい所。

PressA.json
{
  "Commands": {
    "$type": "Command.ICommand[], Common",
    "$values": [
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 0,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 50
      },
      {
        "$type": "Command.OperateButton, Common",
        "TargetState": 1,
        "TargetControlID": 4
      },
      {
        "$type": "Command.Wait, Common",
        "WaitTime": 950
      }
    ]
  },
  "Description": null
}

こういうユースケースではどうするのがいいんでしょうかね。

今後の課題

  • 例外処理にたぶん穴が残ってるので塞ぐ
  • 多言語対応
  • PC側のUIから、スティックを256段階で操作できるようにする
  • PCのキーボードでボタンを操作できるようにする
  • 60FPS単位の、正確なタイミングでの自動操作を可能にする

注意

このPCソフトですが、Switchのコントローラで可能な操作は大半が実行可能です。悪意あるマクロや、動作確認が不十分なマクロを不用意に実行すると

  • ゲーム内の貴重なアイテムを消費させられる
  • セーブデータを消去される
  • オンラインショップでアイテムを購入させられる

ような事もあり得ます。
また電子工作のミスが、最悪の場合Switchの物理的故障に繋がる可能性もあります。十分にご注意ください。これらのハード・ソフトに発生した不具合については責任を負いかねますので、了承の上でご利用下さい。


  1. ソフトにもよります。ポケモン剣盾の場合は100ms程度必要です。ファミコンのグラディウスでは50msでも安定しました。 

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