20200629のC#に関する記事は18件です。

[C#/WPF] ユーザーがログイン/ログアウト、PCをシャットダウン等したときになにかしたい

もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4f

やりたいこと

アプリ起動中に、下記のような操作でユーザーがシャットダウンやログオフなどをしたとき、またその逆のログインしなおしたとき等に、アプリ側でそれを知って、しかるべき処理をしたい。(何かのサーバーやデバイスと通信するようなアプリであれば、一旦通信を終わらせたり、逆に再開したりしたい)

image.png
image.png

やり方

SystemEventsにある各種イベントを利用する。
↓この辺を使う。
image.png
MS資料

実験コード

https://github.com/tera1707/WPF-/tree/master/038_PowerMode_SessionSwitch

実験コード抜粋.cs
// 何のイベントハンドラが動いたのか、またイベントのArgの中身を表示する。
private void root_Loaded(object sender, RoutedEventArgs e)
{
    SystemEvents.SessionSwitch          += ((sender, e) => { AddLog("SessionSwitch       :" + e.Reason.ToString()); });
    SystemEvents.SessionEnding          += ((sender, e) => { AddLog("SessionEnding       :" + e.Reason.ToString()); });
    SystemEvents.SessionEnded           += ((sender, e) => { AddLog("SessionEnded        :" + e.Reason.ToString()); });
    SystemEvents.PowerModeChanged       += ((sender, e) => { AddLog("PowerModeChanged    :" + e.Mode.ToString()); });
    SystemEvents.EventsThreadShutdown   += ((sender, e) => { AddLog("EventsThreadShutdown:" + e.ToString()); });
}

実験アプリ仕様:
アプリを起動したまま、シャットダウンやらログオフやらをすると、画面表示&exeと同じ階層にログを出力してくれる仕組み。

ロック、再度サインイン

ロックして、再度サインインしたらどうなるか?
image.png

結果
SessionSwitchイベントで、e.ReasonSessionLockSessionUnlockがきた。
image.png

他のユーザー切り替えして、戻ってくる

他のユーザーに一旦切り替えて、また自分のユーザーに戻ってくる(サインインしなおす)
(=FUS(Fast User Switching)→あるユーザーがログインしたまま、別のユーザーもログインしてるイメージ)
image.png

結果
SessionSwitchイベントで、下記の順番で来た。(下から順番に)

image.png
※今回は自分のPCで直接ユーザー切り替え操作を行ったが、もしこれがリモートデスクトップでの接続だったら、ConsoleConnectConsoleDisconnectが、RemoteConnectRemoteDisconnectになるっぽい。

サインアウト

サインアウトする。
image.png

結果
SessinEndingLogoffと、そのあとにSessinIndedLogoffが来た。
image.png

スリープ、復帰

スリープさせる。
image.png

結果
下記の順で来た。(下から順番に)
image.png

シャットダウン

シャットダウンする。
image.png

結果
SessionEndingSystemShutdownのあとに、SessionEndedSystemShutdownが来た。
image.png

参考

SystemEvents.PowerModeChanged イベント
https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.win32.systemevents.powermodechanged?view=dotnet-plat-ext-3.1

上記ページの左側に、イベントが一覧になってるので、そこから必要なイベントに飛べる。
image.png

イベントの引数を見て、詳細な情報(例えば、SessionSwitchであれば、suspendなのか、resumeなのか、など)を知る必要があるが、そのへんは、同じページの下の方に、〇〇Argsへのリンクが載ってるので、そこから飛んで、引数を調べればいい。
image.png

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

Blazor WebAssemblyを試してみる

他の方の書かれたQiita記事を拝見していて、ぼちぼちBlazor WebAssemblyも実用レベルになりつつあるのかなと思いましたので、試してみました。

準備

.NET Core 3.1 SDKをインストール

ここからダウンロードしてインストールします。

テンプレートからアプリを作成する

以下のようにdotnetコマンドで作成します。

Blazor WebAssemblyのテンプレート最新をインストール

dotnet new --install Microsoft.AspNetCore.Components.WebAssembly.Templates::3.2.0

アプリ作成

dotnet new blazorwasm -o dotnetcore-blazorwasm-sample

追加のサンプルコードを書く

何かしら動きのあるサンプルを作りたかったので、今回は、/testPageにアクセスしたら1秒更新の時計を表示するページを追加してみます。

以下のコードを/Pages/TestPage.razorとして追加します。

TestPage.razor
@using System.Timers
@implements IDisposable
@page "/testPage"

<h1>Test Page</h1>

<p>@message</p>

<p>Now Time: [@nowTime]</p>

@code {
    private String message = "This is test page.";
    private String nowTime = DateTime.Now.ToString();
    private Timer timer;
    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, e) =>
        {
            nowTime = DateTime.Now.ToString();
            StateHasChanged();
        };
        timer.Start();
    }

    public void Dispose() {
        timer.Stop();
        timer.Dispose();
    }
}

ページ生成時にtimer = new Timer(1000);で1秒間隔のタイマーを作り、満了するごとに現在時刻をnowTimeに設定するようにします。
ポイントはStateHasChanged();を呼んでいるところで、これを呼ばないとビューの更新が必要なことが通知されず、表示が変わりません。

ページ破棄時にDispose()を呼んでもらうために@implements IDisposableしています。
Dispose()内ではタイマーを止めてリソースを破棄しています。

実行

dotnet run で実行します。
ブラウザでhttp://localhost:5000にアクセスして確認します。

結果

dotnetcore-blazorwasp-sample_1.png

上記のように時計が表示されて1秒毎に更新されます。

最後に

ちょっと起動時のLoading...表示が長かったりするかなとは思いましたが、こういった少し動きのあるページがJavaScript書かずにブラウザ上で動いているのはなかなか面白いですね。

なお、今回のサンプルコード全体はこちらのGitHubリポジトリで参照できるようにしてありますので、興味あればご覧くださいませ。

では。

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

【Unity】アウトゲームでの設計でMV(R)Pパターンを採用しました【設計】

0.前提と推奨するターゲット

前提

・実際に長期運用を行ったわけではないため、メリット、デメリットが自己解釈になる部分があります。

ターゲット

UniRxをある程度、理解している方
・MVPパターンなどのアーキテクチャパターンをある程度、理解している方
uGUIに触れたことがある方

1.MV(R)Pパターンとは

UniRx開発者の@neue ccさんが提唱しているMVPパターンとUniRxを組み合わせたUIアーキテクチャパターンです。最近のソシャゲ設計では、割とスタンダードなのかなと思います。

MVP.png

上記画像の②ViewとPresenter間の入力検知や、③ModelとPresenter間の値の変更検知をUniRxで執り行います。

2.アウトゲームでのMV(R)Pパターン

2.1 画面を階層構造で構築しました

OutGameImageArchitecture0.png

画面は階層構造で構築しました。
Scene(Class) -> (Canvas) -> Window(Class) -> Screen(Class)
SceneがWindowを管理し、WindowがScreenを管理すると言うルールを決めました。

とても参考になった記事
もしあなたがアウトゲーム(UI)を つくることになったら with Unity
Web出身のUnityエンジニアによる大規模ゲームの基盤設計

OutGameArchitecture1.png

OutGameScene内にGachaWindowやCharacterWindowなどがあり、GachaWindowにはGachaTopScreenやGachaResultScreenなどがあります。

2.2 画面単位でMVPを構築しました

OutGameArchitecture2.png

SceneやWindow、Screenと言った画面単位でMVPを構築します。
参考になる記事はこちらです。UniRx開発者さんの記事です。
neue cc / UniRx 4.8 - 軽量イベントフックとuGUI連携によるデータバインディング

2.3 ViewとPresenterの要素数と関係

MVPでもViewとPresenterのそれぞれの数にもだいたい2パターン考えられ、ViewとPresenterが1:1のものViewとPresenterが複数:1のものがあります。
今回はSceneごと、WindowごとなどにMVPが構成されているので、Viewは、下記のようにHomeWindowViewクラスの中に、ボタンやテキストがまとめられています。

OutGameArchitecture3.png

通常のMV(R)Pでの1:1のもの、複数:1のもののそれぞれのメリット、デメリットがあります。下記のスライドはとても参考になります。

UniRxでMV(R)Pパターンをやってみた

今回はViewが画面単位でまとめられているため、上記サイトで記載されているメリット、デメリットに合致しないことが複数あり、自分なりの今回の実装特有のメリット、デメリットを以下に書きます。

メリット
・Viewが画面単位でまとめられていることで、View要素の取得がしやすい(紐付けがしやすい)。
・画面単位でまとめられているため、ざっと見やすいと言う感覚
・ルールが決まっているため、画面単位でのコードが追いやすい。

デメリット
Viewが画面単位でまとめられていることで、小さいテストがしにくかったり、要素数の変更がしにくい可能性あり。

3.実装

シンプルな実装を例に一画面を解説します。
ボタンをクリックすると、テキストにクリック回数が反映されます。
この画面を仮にHomeWindowとします。

AnyConv.com__画面収録 2020-06-29 19.56.20.gif

スクリーンショット 2020-06-29 20.05.17.png

WindowやScreen単位でプレハブ化し、生成や破棄などを行うことで、画面遷移を実現します。

View

HomeWindowView.cs
public class HomeWindowView : MonoBehaviour
{
    [SerializeField] Button m_Button;
    // ButtonをPropertyとして外部に公開
    public Button m_ButtonProp => m_Button;

    // クリック回数を表示するテキスト
    [SerializeField] Text m_ClickCountText;

    // クリック回数を表示するテキストの描画を更新する
    public void SetCount(string count){
        m_ClickCountText.text = count;
    }
}

Viewには、描画に関する処理を限定的に書きます。
ここでは、uGUIであるButtonを用意します。Presenter側でSubscribeすることで、通知を検知することができます。また、外部からクリック回数を変更できるように、SetCount関数を用意します。
ボタンなどのuGUI以外での通知を送りたい場合は、Subjectなどを用いています。
[SerializeField]など、インスペクタでぽちぽち入れるのがめんどくさい、忘れる可能性がある場合は、コードで取得するようにした方が良いです。

Model

HomeWindowModel.cs
// 追加する
using UniRx;

public class HomeWindowModel
{

    // クリック回数を保持する
    private ReactiveProperty<int> m_ClickCount =  new ReactiveProperty<int>();
    public IReadOnlyReactiveProperty<int> m_ClickCountProp => m_ClickCount;

    // クリック回数の更新
    public void UpdateClickCount(){
        m_ClickCount.Value++;
    }

}

Modelは、ビジネスロジックや、データの取得を担います。
今回はクリック回数とクリック回数を変更する関数を用意しています。
クリック回数はReactivePropertyで、外部公開用に読み取り専用のIReadOnlyReactivePropertyを用いています。
バインディング周りがすっきりと書けるので、ありがたいです。
また、Modelは、DBからのリソースの確保や計算などが複雑化し場合は、それぞれでさらに分割することを推奨します。

Presenter

HomeWindowPresenter.cs
// 追加する
using UniRx;

public class HomeWindowPresenter : MonoBehaviour
{
    // view
    private HomeWindowView m_HomeWindowView;
    // model
    private HomeWindowModel m_HomeWindowModel;

    private void Awake() {
        Initialized();    
    }

    // インスタンス生成時に呼ばれる初期化関数
    private void Initialized() {
        m_HomeWindowView = GetComponent<HomeWindowView>();
        m_HomeWindowModel = new HomeWindowModel();

        SetEvent();
        Bind();
    }

    // Viewからの通知
    private void SetEvent(){
        // ボタンのクリック通知を監視
        m_HomeWindowView.m_ButtonProp.OnClickAsObservable().Subscribe(_ => m_HomeWindowModel.UpdateClickCount());

    }

    // Modelからの通知
    private void Bind(){
        m_HomeWindowModel.m_ClickCountProp.Subscribe(x =>m_HomeWindowView.SetCount(x.ToString()));
    }

}

Presenterは、ViewとModelの橋渡しを担い、他を担うとしても簡単な値の変換程度に修めます。
SetEvent関数は、Viewであるボタンのクリック通知を監視し、Modelの変更を行います。
Bind関数は、Modelの値の変更を監視し、Viewであるテキストの変更を行います。
また、今回は書いてませんが、Presenterも階層構造のために必要になる経路の選択も複雑化した場合は、Model同様に、さらに分割することを推奨します。

4.まとめ

コードの可読性や保守性に合わせて、どの設計を選択するかを決めるべきで、これが正しいと言うことはないと思います。

個人的には、ルールを決めることが大切だと思います。

今後は、VIPERアーキテクチャClean Architectureにも、触れていこうと思います。(気になった方は是非調べてみてください)

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

Xamarin.FormsでAndroid向けTwitterアプリケーションを開発したときに困ったこと ~WebView編~

N.Mです.

最近はAndroid用のアプリケーション,"TLExtension"を作っていました.AndroidでTwitter用のブラウザを見つつ,タブ操作で簡単に今まで作ってきたTwitter用ツールも利用できるアプリケーションです.自分用に作ったので,アプリ自体の公開はしていませんが,ひな型部分のソースコードはGitHubにも公開しました.

TLExtension

このアプリはXamarin.Formsを利用して作っているのですが,今回の記事はこれを作る際に困ったこととその解決策をまとめたものとなっております.
(主にAndroidアプリをXamarin.Formsで開発する際に,こんな機能はどう作ればいいのかといった内容になります。)

結局,Xamarin.Formsにあるプラットフォーム共通の機能じゃ足りないので,Xamarin.Androidでの実装が必要になるわけですが...

長くなってしまったので,2回に分けてお送りします.今回はWebView周りの話です.

WebView内に表示されたHTML取得

参考:https://github.com/xamarin/xamarin-forms-samples/tree/master/CustomRenderers/HybridWebView/Droid

CoreTweetでREST APIの認証を取得する際に,過去の記事でも触れたように,PIN番号の入力を自動化しようとすると,HTMLからPIN番号を抽出し,C#側で使えるようにする必要があります.また,Twitter内でのページ遷移(ホーム画面から個々のツイート画面への遷移など)では,WebView.Sourceに格納されるURLが変化しないためか,Navigatingなどのイベントも反応しません.TwitterのページのHTMLソースにはそのページのURLがあるので,これを取得すれば,遷移したかどうかがわかります.こんな感じで,なにかとC#側でHTMLを取得する必要があります.これを実現するには以下のような手順でjavascriptとC#を連携できるようにする必要があります.

Xamarin.Forms.WebViewクラスを継承して,自作のWebViewクラスを作ります.(自分のリポジトリでは,TLExtensionWebViewというクラス名にしています.)

② Android側のプロジェクトでWebViewRendererクラスを作成します.(Xamarin.Forms.Platform.Android.WebViewRenderer,自分のリポジトリではTLExtensionWebViweRendererというクラス名にしています.)このWebViewRendererと1で作ったWebViewを以下のようにして連携させます.このように連携すると,WebViewRender.Elementに連携したWebViewが格納されます.

//以下の文章をWebViewRenderクラスを宣言しているところの直前に記述します.
[assembly: ExportRenderer(typeof(TLExtensionWebView), typeof(TLExtensionWebViewRenderer))]

③ javascriptから呼び出せるメソッドを格納したJSBridgeクラス(Java.Lang.Objectを継承)を作ります.コード自体はリポジトリ内のJSBridge.csを参考にしてほしいですが,クラス内のメソッドの前に以下のように記述することで,javascript内でC#のそのメソッドを呼び出せるようになります.Exportの中に書いた文字列が,javascript内でのメソッド名になります.

//以下の文章をjavascriptで呼び出したい,JSBridgeのメソッドの直前に記述します.
[JavascriptInterface]
[Export("invokeAction")]

WebViewRendererクラスのOnElementChangedメソッドに以下を追加します.

if (e.OldElement != null)
{
    Control.RemoveJavascriptInterface("jsBridge");
}
if (e.NewElement != null)
{    
    Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
}

⑤ これで,Xamarin.Forms.WebViewEvalメソッドやAndroid.Webkit.WebViewEvaluateJavascriptメソッドにjavascript: jsBridge.invokeAction();を渡すと,JSBridgeクラスのメソッドが呼び出されます(jsBridgeは④のAddJavascriptInterfaceで登録した名前,invokeActionは③のExportに登録した名前にです.).JSBridgeクラスのメソッドの引数として文字列を渡せるようにしておけば,javascript: jsBridge.invokeAction(document.documentElement.outerHTML);で,そのページのHTMLをC#に取得することができます.

Xamarin.FormsWebViewでこの取得したHTMLを使用するならば,WebViewRendererJSBridgeからデータを受け取り,WebViewRenderer.Elementにそのデータを渡します.

動画の全画面表示

参考:https://github.com/mhaggag/XFAndroidFullScreenWebView

デフォルトだと,Twitterの動画で全画面表示を押しても全画面になりません(全画面表示になったという通知がXamarin.Forms側までいかないためだと思っています).全画面表示になったかは,Xamarin.Forms.Platform.Android.FormsWebChromeClientクラスを使えば検知できるので,ここからXamarin.Forms側まで通知を伝えていきます.

① フルスクリーンに入った時のイベント用のEventArgsを作ります.(自分のリポジトリではTLExtensionWebChromeClient.csにあります.)

public class EnterFullScreenRequestedEventArgs : EventArgs
{
    public View View { get; }
    public EnterFullScreenRequestedEventArgs(View view)
    {
        View = view;
    }
}

FormsWebChromeClientクラスを作ります(自分のリポジトリではTLExtensionWebChromeClient).全画面表示になるとOnShowCustomViewが,全画面表示が終了するとOnHideCustomViewが呼び出されます.

public class TLExtensionWebChromeClient : FormsWebChromeClient
{
    public event EventHandler<EnterFullScreenRequestedEventArgs> EnterFullScreenRequested;

    public event EventHandler ExitFullScreenRequested;

    public override void OnHideCustomView()
    {
        ExitFullScreenRequested?.Invoke(this, EventArgs.Empty);
    }

    public override void OnShowCustomView(View view, ICustomViewCallback callback)
    {
        EnterFullScreenRequested?.Invoke(this, new EnterFullScreenRequestedEventArgs(view));
    }
}

Xamarin.Forms側のWebViewクラスに以下のフルスクリーンになった時,解除されたときのアクションを追加します.

public static readonly BindableProperty EnterFullScreenCmmandProperty =
            BindableProperty.Create(
            propertyName: "EnterFullScreenCommand",
            returnType: typeof(ICommand),
            declaringType: typeof(TLExtensionWebView),
            defaultValue: new Command(async (view) => await DefaultEnterAsync((View)view))
            );

public ICommand EnterFullScreenCommand
{
    get => (ICommand)GetValue(EnterFullScreenCmmandProperty);
    set => SetValue(EnterFullScreenCmmandProperty, value);
}

public static readonly BindableProperty ExitFullScreenCmmandProperty =
            BindableProperty.Create(
            propertyName: "ExitFullScreenCommand",
            returnType: typeof(ICommand),
            declaringType: typeof(TLExtensionWebView),
            defaultValue: new Command(async (view) => await DefaultExitAsync())
            );

public ICommand ExitFullScreenCommand
{
    get => (ICommand)GetValue(ExitFullScreenCmmandProperty);
    set => SetValue(ExitFullScreenCmmandProperty, value);
}

//フルスクリーンを実現するためのメソッド
private static async Task DefaultEnterAsync(View view)
{
    var page = new ContentPage
    {
        Content = view
    };
    await Application.Current.MainPage.Navigation.PushModalAsync(page);
}

private static async Task DefaultExitAsync()
{
    await Application.Current.MainPage.Navigation.PopModalAsync();
}
//フルスクリーンを実現するためのメソッド ここまで

WebViewRendererクラスに②で作ったFormsWebChromeClientを登録します.FormsWebChromeClientで全画面表示の通知が発生したら,③で実装したDefaultEnterAsyncDefaultExitAsyncが呼び出されるように,処理を繋げます.(以下はフルスクリーン対応に必要な部分のみ抜き出しています.)

public class TLExtensionWebViewRenderer : WebViewRenderer
{
    private TLExtensionWebView _webView;
    private TLExtensionWebChromeClient webClient;

    public TLExtensionWebViewRenderer(Context context) : base(context)
    {
        webClient = new TLExtensionWebChromeClient();
        webClient.EnterFullScreenRequested += OnEnterFullScreenRequested;
        webClient.ExitFullScreenRequested += OnExitFullScreenRequested;
    }

    protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
        {
        base.OnElementChanged(e);

        if (e.NewElement != null)
        {
            Control.SetWebChromeClient(webClient);
        }
        _webView = (TLExtensionWebView)e.NewElement;            
    }

    //フルスクリーンにするための追加メソッド
    protected override FormsWebChromeClient GetFormsWebChromeClient()
    {
        return webClient;
    }

    private void OnEnterFullScreenRequested(object sender, EnterFullScreenRequestedEventArgs eventArgs)
    {
        if (_webView.EnterFullScreenCommand != null && _webView.EnterFullScreenCommand.CanExecute(null))
        {
            _webView.EnterFullScreenCommand.Execute(eventArgs.View.ToView());
        }
    }

    private void OnExitFullScreenRequested(object sender, EventArgs eventArgs)
    {
        if (_webView.ExitFullScreenCommand != null && _webView.ExitFullScreenCommand.CanExecute(null))
        {
            _webView.ExitFullScreenCommand.Execute(null);
        }
    }
}

全画面表示になるときは

TLExtensionWebChromeClient.OnShowCustomViewTLExtensionWebViewRenderer.OnEnterFullScreenRequstedTLExtensionWebView.DefaultEnterAsyncの順で

全画面表示が解除されるときは

TLExtensionWebChromeClient.OnHideCustomViewTLExtensionWebViewRenderer.OnExitFullScreenRequstedTLExtensionWebView.DefaultExitAsyncの順で

呼び出され,Xamarin.Forms側まで通知が行くようになり,全画面表示に対応できるようになります.

ソフトウェアキーボード表示時のWebViewの縮小

参考:https://qiita.com/amay077/items/6fcdec829a96bc604532

デフォルトのWebViewでTwitterのリプライをしようとすると,ソフトウェアキーボードが邪魔で自分が書いているツイートが見えなくなります.

ひとまず,Xamarin.Android側のMainActivityOnCreateメソッドに以下を入れれば解決します.

//appにはMainActivityでロードするAppクラスのオブジェクトを入れておく.

app.On<Xamarin.Forms.PlatformConfiguration.Android>().
    UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize);

しかし,検索で文字を打ち,サジェストを選択したときに,キーボードは閉じるがWebViewが縮んだままになるケースがありました.この場合は後述するリロードに,いったんWebViewを非表示にしてから,表示する処理を加えると,1,2回のリロードでもとに戻るようになりました.(自分用のアプリなので,ひとまずこれで妥協.)

Twitter外のページに遷移したときなどに,Chromeなどの別ブラウザで開く

参考:https://itblogdsi.blog.fc2.com/blog-entry-171.html

WebViewで遷移する際に,Chromeなどの別ブラウザで開く際には,Xamarin.Android側で別ブラウザを開くためのIntentを呼び出す必要があるみたいです.上記の記事が分かりやすいので,詳細はそちらをご覧ください.

Xamarin.Android側のOnPageStartedで,遷移開始時に処理をする場合は,OnPageStartedは2回呼ばれることがあります.最初のOnPageStartedでブラウザを開き,OnPageFinishedが呼ばれるまではブラウザを開かないようにする必要もあるようです.(参考:https://qiita.com/kikuchy/items/d0f5599a883e5e9350df

また,ページ遷移する際にWebView側もページ遷移しているので,WebView.GoBack()で戻らないと,遷移先のページがWebViewに表示されたままになってしまいます.

WebViewへのjavascript実行

WebView.Evalでjavascriptのスクリプトを入れれば,実行できますが,どうやら1つの文しか処理できないようです.複数文を処理する場合は,それらを1つの関数にまとめて一度WebView.Evalで登録した後,2回目のWebView.Evalでその関数を呼び出す必要があるみたいです.

また,HTMLが更新されると登録されていた関数も消えてしまうのと,Twitterではページ表示後もHTMLの更新が頻繁に発生するため,javascriptの関数を使用するタイミングで,その都度WebView.Evalで関数を登録する必要があります.

WebViewのリロード

最近のXamarin.Forms.WebViewにはreloadメソッドがあるみたいですが,自分が使用していたXamarin.Formsが古かったためか(バージョンは3.0.0.561731),reloadメソッドはありませんでした.

この場合は,WebViewと連携しているWebViewRendererControl.Reload()を呼ぶ必要があります.リロードボタンを作る際はXamarin.Forms.WebView側にAction型の変数を用意しておき,WebViewRendererでその変数にControl.Reload()を呼ぶアクションを登録する必要があります.リロードボタンのイベントで登録したアクションを発動します.

//WebViewRednererのOnElementChanged内
//reloadActionはWebView側の変数

_webView = (TLExtensionWebView)e.NewElement;
_webView.reloadAction = new Action(() => { Control.Reload(); });

WebViewの履歴削除

REST APIの認証が完了してから,認証ページに戻らないように,WebViewの履歴を消す処理も実装しました.しかし,履歴削除のメソッドは現在のXamarin.Forms.WebViewにも内容です.リロードの時と同じように,WebViewと連携しているWebViewRendererControl.ClearHistory()を呼ぶ必要があります.

(おまけ)TabbedPageのスライド禁止

Twitterでは,画像表示時にその画像が複数枚ある場合は,横スライドで画像を切り替えます.Xamarin.Forms.TabbedPageを使っている場合,TabbedPageのタブ切替えのための横スライドと重なり,デフォルトだと正常に画像を切り替えることができなくなります.

TabbedPageの横スライドによるタブ切替えを禁止するには,以下を呼び出します.

//thisTabbedPageはTabbedPageのオブジェクト
//falseをtrueにすると,スライドによるタブ切替えができるようになる.

thisTabbedPage.On<Xamarin.Forms.PlatformConfiguration.Android>().SetIsSwipePagingEnabled(false);

まとめ

Xamarin.Formsで開発してても,WebViewに少し複雑な機能を追加しようとするとAndroid側の実装が必要になってしまうようです.Xamarin.Forms.WebViewと連携しているXamarin.Forms.Platform.Android.WebViewRendererから,Android固有の実装を追加する場合が多いので,webViewを用いるAndroidアプリケーションを開発する場合は,このWebViewRendererにお世話になる機会も多くなるかと思います.

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

ダミープリンタサーバ(Bullzip PDF Printer)

帳票(ActiveReport(ActiveReportは単独でPDFの出力機能があるがプログラムの修正が必要になる)やCrystal Reports)で作成したものをPDFで印刷したい場合、サーバがAWSやAzure等のクラウドにあり、社内のプリンタに出力できない場合、このソフトをサーバにインストールしてBullzipのプリンタ(通常のプリンタとして認識される)に印刷指示をするだけでPDFに出力できるようになる。

image.png

はじめに

基本は無料版を使っても問題ないが、印刷すると印刷したPDFにBullzipの広告が表示されるので、開発やテストにだけ利用するだけにして、本番で利用する場合にはライセンスの購入を検討した方がよい。
無料版も有料版もソフトは同じで、ライセンスファイルを登録しないか登録するかの違いだけ。
無料版のライセンスの利用範囲についても書いてあるのでご確認ください。

インストール方法

オンラインでインストールしてください。インターネットが繋がらないオフラインの場合には下記のURLの内容をみて必要はモジュールをインストールしてください。

Offline Installation
必要モジュール (PDF Printer setup、Ghostscript Lite setup、PDF Power Tool setup、Xpdf setup)

インストール方法
Microsoft Windows Server 2016、2012、2008 R2、2008、2003、Windows 10、8.1、8、7、XP

設定方法

ダイアログを表示させずに印刷を設定させる方法
image.png

image.png

image.png

印刷テスト

image.png

image.png

ライセンス

Free コミュティ版
https://www.bullzip.com/products/pdf/download.php

The free community edition version is still available. 
It plays an important role in the BullZip philosophy 
where everybody should be able to afford the software. 
You can use it if you are in a small company with less than 10 installations or 
you want to use the software for personal projects.

無料のコミュニティ版はまだ利用可能です。
BullZipは、誰もがソフトウェアを購入できるようにするという
BullZipの理念の中で重要な役割を果たしています。
インストール数が10以下の小さな会社や、
個人的なプロジェクトで使用したい場合にお使いいただけます。

ライセンスの認証方法

ライセンスを購入すると、ライセンスファイルが添付されたメールが届きます。
添付ファイルの名前は、license.xmlなので、添付されたlicense.xmlをBullzipのインストールフォルダに置きます。Windowsであれば、C:\Program Files\Bullzip\PDF Printerのフォルダに

ライセンスのインストール
image.png

エディションによる違い

https://www.bullzip.com/products/pdf/features.php

Community

試用版のメッセージがPDFのフッタ、プロパティに表示される。
https://www.bullzip.com/products/pdf/trial.php
image.png

image.png

Standard

  • 印刷を既存のドキュメントと結合する
  • 名前と設定が異なる複数のPDFプリンターをインストールする
  • オフラインインストールをサポート

Professional

  • ターミナルサーバー、RDP、またはCitrixで実行
  • PDFパスワードで強力な暗号化を使用する
  • 高解像度の背景に印刷する
  • 画像圧縮を無効にする

Expert

プロフェッショナル機能に加えて、次のこともできます。

  • 暗号化されたPDFドキュメントを新しいPDFドキュメントに印刷する
  • PDFドキュメントの代わりにMicrosoft Wordドキュメントを作成する
  • 作成したPDFを別のプリンターで印刷する
  • HTTPSまたはSFTPを使用してPDFをサーバーに自動的にアップロードする

エラーの対処

※エラーがあったら都度記入していく。

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

【Unity(C#),PUN2】OculusQuestのハンドトラッキング同期実装

デモ

まずはデモです。
SyncHand2.gif

やってることは見たまんまの位置同期ですが、
ハンドトラッキングの実装はOculusIntegration内に存在するOVR系のコンポーネントを
理解する必要があり、私のレベルでは非常に面倒でした。

しかし、一度理解してしまえば使い回すだけなので、
同じ苦労をする人が一人でも減るようにメモしておきます。

バージョン情報

Unity2019.3.10f1
Oculus Integration 1.49
PUN2 Version 2.19.1

ハンドトラッキングの流れ

まずはOculusIntegration内に存在するOVR系のコンポーネントがどのような役割を持ち、
どのような流れでハンドトラッキングを行っているかを理解していきます。

簡単に言うと下記です。
①手を認識
②ボーンとなるオブジェクトを生成
③手のメッシュを作成
④手のメッシュにボーンを設定
⑤生成したボーンを認識した手の関節の座標に合わせる

おおざっぱに理解したにすぎないので、間違いがあったらコメントください。


OVRHandが手を認識

OVRHandが手を認識しているというのは詳細を言うと少し誤った表現です。

もう少し正確に言うと、
OVRHandが認識した手のデータを受け取って様々なクラスに
インタフェース経由でデータを渡している
という説明になるかと思います。

もっと辿っていくとOVRPluginというクラスが存在しており、
デバイスが手を認識した際のデータを
C#で利用できるようにするラッパークラスとしての役割を担っています。

OVRSkeletonがボーンとなるオブジェクトを生成

OVRSkeletonのコードを見ていくと手のボーンを生成するコードを見つけました。

OVRSkeleton内のボーン生成箇所
virtual protected void InitializeBones(OVRPlugin.Skeleton skeleton)
{
    _bones = new List<OVRBone>(new OVRBone[skeleton.NumBones]);
    Bones = _bones.AsReadOnly();

    if (!_bonesGO)
    {
        _bonesGO = new GameObject("Bones");
        _bonesGO.transform.SetParent(transform, false);
        _bonesGO.transform.localPosition = Vector3.zero;
        _bonesGO.transform.localRotation = Quaternion.identity;
    }

    // pre-populate bones list before attempting to apply bone hierarchy
    for (int i = 0; i < skeleton.NumBones; ++i)
    {
        BoneId id = (OVRSkeleton.BoneId)skeleton.Bones[i].Id;
        short parentIdx = skeleton.Bones[i].ParentBoneIndex;
        Vector3 pos = skeleton.Bones[i].Pose.Position.FromFlippedXVector3f();
        Quaternion rot = skeleton.Bones[i].Pose.Orientation.FromFlippedXQuatf();

        var boneGO = new GameObject(id.ToString());
        boneGO.transform.localPosition = pos;
        boneGO.transform.localRotation = rot;
        _bones[i] = new OVRBone(id, parentIdx, boneGO.transform);
    }

    for (int i = 0; i < skeleton.NumBones; ++i)
    {
        if (((OVRPlugin.BoneId)skeleton.Bones[i].ParentBoneIndex) == OVRPlugin.BoneId.Invalid)
        {
            _bones[i].Transform.SetParent(_bonesGO.transform, false);
        }
        else
        {
            _bones[i].Transform.SetParent(_bones[_bones[i].ParentBoneIndex].Transform, false);
        }
    }
}

この処理によって、動的にボーンとなるオブジェクトが生成されます。
OQBones.PNG

このBonesの子階層にあるオブジェクトはEditor上で確認すると
手の動きに追従して回転しているのが確認できます。
(InspectorでShould Update Boneにチェックを入れた場合)

【参考リンク】:【Unity】Oculus Link使ってEditor上でデバッグ


OVRMeshが手のメッシュを生成

OVRMeshのコード内で手のメッシュを生成しています。
BoneWeightの設定も行っています。

OVRMesh内の手のメッシュ生成箇所
private void Initialize(MeshType meshType)
{
    _mesh = new Mesh();

    var ovrpMesh = new OVRPlugin.Mesh();
    if (OVRPlugin.GetMesh((OVRPlugin.MeshType)_meshType, out ovrpMesh))
    {
        var vertices = new Vector3[ovrpMesh.NumVertices];
        for (int i = 0; i < ovrpMesh.NumVertices; ++i)
        {
            vertices[i] = ovrpMesh.VertexPositions[i].FromFlippedXVector3f();
        }
        _mesh.vertices = vertices;

        var uv = new Vector2[ovrpMesh.NumVertices];
        for (int i = 0; i < ovrpMesh.NumVertices; ++i)
        {
            uv[i] = new Vector2(ovrpMesh.VertexUV0[i].x, -ovrpMesh.VertexUV0[i].y);
        }
        _mesh.uv = uv;

        var triangles = new int[ovrpMesh.NumIndices];
        for (int i = 0; i < ovrpMesh.NumIndices; ++i)
        {
            triangles[i] = ovrpMesh.Indices[ovrpMesh.NumIndices - i - 1];
        }
        _mesh.triangles = triangles;

        var normals = new Vector3[ovrpMesh.NumVertices];
        for (int i = 0; i < ovrpMesh.NumVertices; ++i)
        {
            normals[i] = ovrpMesh.VertexNormals[i].FromFlippedXVector3f();
        }
        _mesh.normals = normals;

        var boneWeights = new BoneWeight[ovrpMesh.NumVertices];
        for (int i = 0; i < ovrpMesh.NumVertices; ++i)
        {
            var currentBlendWeight = ovrpMesh.BlendWeights[i];
            var currentBlendIndices = ovrpMesh.BlendIndices[i];

            boneWeights[i].boneIndex0 = (int)currentBlendIndices.x;
            boneWeights[i].weight0 = currentBlendWeight.x;
            boneWeights[i].boneIndex1 = (int)currentBlendIndices.y;
            boneWeights[i].weight1 = currentBlendWeight.y;
            boneWeights[i].boneIndex2 = (int)currentBlendIndices.z;
            boneWeights[i].weight2 = currentBlendWeight.z;
            boneWeights[i].boneIndex3 = (int)currentBlendIndices.w;
            boneWeights[i].weight3 = currentBlendWeight.w;
        }
        _mesh.boneWeights = boneWeights;

        IsInitialized = true;
    }
}

OVRMeshは生成したメッシュをSkinnedMeshrendererに設定します。
PlayModeを押すとMeshが動的に生成、設定されているのがわかります。
SyncHandForQiita1.gif


OVRMeshRendererが手のメッシュにボーンを設定

先ほど生成したMeshにボーンを設定します。
正確に言うと、SkinnedMeshrendererの持つMeshのボーン情報に設定します。

OVRMeshRenderer内のボーンを設定箇所
private void Initialize()
{
    _skinnedMeshRenderer = GetComponent<SkinnedMeshRenderer>();
    if (!_skinnedMeshRenderer)
    {
        _skinnedMeshRenderer = gameObject.AddComponent<SkinnedMeshRenderer>();
    }

    if (_ovrMesh != null && _ovrSkeleton != null)
    {
        if (_ovrMesh.IsInitialized && _ovrSkeleton.IsInitialized)
        {
            _skinnedMeshRenderer.sharedMesh = _ovrMesh.Mesh;
            _originalMaterial = _skinnedMeshRenderer.sharedMaterial;

            int numSkinnableBones = _ovrSkeleton.GetCurrentNumSkinnableBones();
            var bindPoses = new Matrix4x4[numSkinnableBones];
            var bones = new Transform[numSkinnableBones];
            var localToWorldMatrix = transform.localToWorldMatrix;
            for (int i = 0; i < numSkinnableBones && i < _ovrSkeleton.Bones.Count; ++i)
            {
                bones[i] = _ovrSkeleton.Bones[i].Transform;
                bindPoses[i] = _ovrSkeleton.BindPoses[i].Transform.worldToLocalMatrix * localToWorldMatrix;
            }
            _ovrMesh.Mesh.bindposes = bindPoses;
            _skinnedMeshRenderer.bones = bones;
            _skinnedMeshRenderer.updateWhenOffscreen = true;
#if UNITY_EDITOR
            _ovrSkeleton.ShouldUpdateBonePoses = true;
#endif
            IsInitialized = true;
        }
    }
}

このコード内ではメッシュのデフォルトの位置となるbindposesも設定しています。
Meshの持つボーンのデフォルトの位置を設定することで、
ボーンの移動した値とデフォルト値の差分から計算が可能になるそうです。

OVRSkeletonが生成したボーンを認識した手の関節の座標に合わせる

最後に再度OVRSkeletonの登場です。

自身で生成したBonesを認識した手の関節情報にそれぞれ追従させます。

OVRSkeleton内の生成したボーンを認識した手の関節の座標に合わせる箇所
void Update()
{
    //~省略~

    var data = _dataProvider.GetSkeletonPoseData();

    IsDataValid = data.IsDataValid;
    if (data.IsDataValid)
    {
        IsDataHighConfidence = data.IsDataHighConfidence;

        if (_updateRootPose)
        {
            transform.localPosition = data.RootPose.Position.FromFlippedZVector3f();
            transform.localRotation = data.RootPose.Orientation.FromFlippedZQuatf();
        }

        if (_updateRootScale)
        {
            transform.localScale = new Vector3(data.RootScale, data.RootScale, data.RootScale);
        }

        for (var i = 0; i < _bones.Count; ++i)
        {
            if (_bones[i].Transform != null)
            {
                _bones[i].Transform.localRotation = data.BoneRotations[i].FromFlippedXQuatf();
                if (_bones[i].Id == BoneId.Hand_WristRoot)
                {
                    _bones[i].Transform.localRotation *= wristFixupRotation;
                }
            }
        }
    }
}

同期実装の流れ

ハンドトラッキングの一連の流れが明らかになったので、
いよいよ同期処理を考えていきます。

オブジェクト同期と呼ばれる手法を用います。
流れとしては下記イメージです。


①各クライアントが手の位置情報を保持

VRGroup1.png


②手の見た目のみの役割を持つオブジェクトを用意し、各自の手の位置情報に追従させる

VRGroup2.png


③手の見た目のみの役割を持つオブジェクトを双方のクライアントに生成

VRGroup3.png


④お互いの手の位置情報を送り合い、生成した手の位置情報を更新

VRGroup4.png

おおざっぱではありますが、こんな感じです。

ハンドトラッキングでの同期方法

ここまでの理解でもかなり骨が折れましたが、本当に大変なのはここからでした。

一連の流れを見ればわかりますが、
手の見た目のみの役割を果たす同期用オブジェクト
用意する必要があります。

やり方としては、2つの選択肢があります。
1つは、事前にボーンとなるオブジェクト及び、手のメッシュを用意することです。

実際に下記の海外勢のサンプルでは、この手法を用いていました。
【参考リンク】:SpeakGeek-Normcore-Quest-Hand-Tracking

(サンプルではPUN2とは別のNormcoreというライブラリを使用して同期の実装を行っています)

下記GIFのように、あらかじめ同期するオブジェクトの中に
Bone及びBindPoseのオブジェクトがびっしりと用意されています。
SyncHandForQiita2.gif

ボーンの役割を担うオブジェクトはBindPoseも合わせると片手だけで全部で48個あります。
しかも、それぞれの座標が生成時に(0,0,0)ではないデフォルト値を持つので
自前で事前に用意するとなると、
生成時のすべての値を48×2回メモして一つずつ手打ち、、、もしくは
プレイモードで生成されたオブジェクトをそのまま保存できるスクリプトを用意する、、、
などなかなかの手間となります。

さらに、プロジェクトを跨いで利用する際には
毎回オブジェクトをインポートする必要があるので少々効率が悪いです。

そこで、もう一つのやり方として、
OVR系コンポーネントと同様に手の見た目のオブジェクトを動的に生成する方法を用います。

しかし、既存のOVR系コンポーネントは切り離すことが困難な蜜結合な状態になっています。
ですので、手の見た目の役割を果たすオブジェクトを生成する処理を
OVR系コンポーネントから拝借して自前で用意する必要がありました。

同期実装のコード

ここから実装の核心となるコードの説明です。
先ほどの同期実装の流れと合わせて見ていきます。


①各クライアントが手の位置情報を保持

VRGroup1.png

この処理に関しては完全にOVR系コンポーネントに担ってもらいます。
OVRHandOVRSkeletonをアタッチしたオブジェクトを各手のAnchorの子階層に配置します。
OVRSyncHand.PNG


②手の見た目のみの役割を持つオブジェクトを用意し、各自の手の位置情報に追従させる

VRGroup2.png

この処理に関しては長くなるので
・手の見た目のみの役割を持つオブジェクトを用意
・各自の手の位置情報に追従

の二つに分けて説明していきます。


手の見た目のみの役割を持つオブジェクトを用意

メッシュの生成に関してはOVRMeshをそのまま利用します。
生成するオブジェクトにSkinnedMeshrendererと共にアタッチしておきます。
SyncAvatarHand.PNG

ボーンの役割を担うオブジェクトの生成に関してはOVRSkeletonでの処理をほぼ丸パクリです。

/// <summary>
/// Bonesを生成
/// </summary>
/// <param name="skeleton">あらかじめ用意されたボーンの情報</param>
/// <param name="hand">左右どちらかの手</param>
private void InitializeBones(OVRPlugin.Skeleton skeleton, GameObject hand)
{
    _bones = new List<OVRBone>(new OVRBone[skeleton.NumBones]);

    GameObject _bonesGO = new GameObject("Bones");
    _bonesGO.transform.SetParent(hand.transform, false);
    _bonesGO.transform.localPosition = Vector3.zero;
    _bonesGO.transform.localRotation = Quaternion.identity;

    for (int i = 0; i < skeleton.NumBones; ++i)
    {
        OVRSkeleton.BoneId id = (OVRSkeleton.BoneId) skeleton.Bones[i].Id;
        short parentIdx = skeleton.Bones[i].ParentBoneIndex;
        Vector3 pos = skeleton.Bones[i].Pose.Position.FromFlippedXVector3f();
        Quaternion rot = skeleton.Bones[i].Pose.Orientation.FromFlippedXQuatf();

        GameObject boneGO = new GameObject(id.ToString());
        boneGO.transform.localPosition = pos;
        boneGO.transform.localRotation = rot;
        _bones[i] = new OVRBone(id, parentIdx, boneGO.transform);
    }

    for (int i = 0; i < skeleton.NumBones; ++i)
    {
        if (((OVRPlugin.BoneId) skeleton.Bones[i].ParentBoneIndex) == OVRPlugin.BoneId.Invalid)
        {
            _bones[i].Transform.SetParent(_bonesGO.transform, false);
        }
        else
        {
            _bones[i].Transform.SetParent(_bones[_bones[i].ParentBoneIndex].Transform, false);
        }
    }
}

次にMeshの生成を行います。
ついでにMesh、SkinnedMeshRendererにBindPose、Boneの登録もそれぞれ行います。

/// <summary>
/// 手のボーンのリストを作成
/// 後にOculusの持つボーン情報のリストと照らし合わせて値を更新するので順番に一工夫して作成
/// </summary>
/// <param name="hand">子にボーンを持っている手</param>
/// <param name="bones">空のリスト</param>
private void ReadyHand(GameObject hand, List<Transform> bones)
{
    //'Bones'と名の付くオブジェクトからリストを作成する
    foreach (Transform child in hand.transform)
    {
        _listOfChildren = new List<Transform>();
        GetChildRecursive(child.transform);

        //まずは指先以外のリストを作成
        List<Transform> fingerTips = new List<Transform>();
        foreach (Transform bone in _listOfChildren)
        {
            if (bone.name.Contains("Tip"))
            {
                fingerTips.Add(bone);
            }
            else
            {
                bones.Add(bone);
            }
        }

        //指先もリストに追加
        foreach (Transform bone in fingerTips)
        {
            bones.Add(bone);
        }
    }

    //動的に生成されるメッシュをSkinnedMeshRendererに反映
    SkinnedMeshRenderer skinMeshRenderer = hand.GetComponent<SkinnedMeshRenderer>();
    OVRMesh ovrMesh = hand.GetComponent<OVRMesh>();

    Matrix4x4[] bindPoses = new Matrix4x4[bones.Count];
    Matrix4x4 localToWorldMatrix = transform.localToWorldMatrix;
    for (int i = 0; i < bones.Count; ++i)
    {
        bindPoses[i] = bones[i].worldToLocalMatrix * localToWorldMatrix;
    }

    //Mesh、SkinnedMeshRendererにBindPose、Boneを反映
    ovrMesh.Mesh.bindposes = bindPoses;
    skinMeshRenderer.bones = bones.ToArray();
    skinMeshRenderer.sharedMesh = ovrMesh.Mesh;
}

/// <summary>
/// 子のオブジェクトのTransformを再帰的に全て取得
/// </summary>
/// <param name="obj">自身の子を全て取得したいルートオブジェクト</param>
private void GetChildRecursive(Transform obj)
{
    if (null == obj)
        return;

    foreach (Transform child in obj.transform)
    {
        if (null == child)
            continue;

        if (child != obj)
        {
            _listOfChildren.Add(child);
        }

        GetChildRecursive(child);
    }
}

Bonesの子階層、すなわち指のボーンとなるオブジェクトから謎のリストを作成している理由は
次の 各自の手の位置情報に追従 で説明します。


各自の手の位置情報に追従

先ほど作成した謎の順番整理を行ったリストですが、各自の手の位置情報に追従させる際に
利用する上で都合が良いです。

というのも、IOVRSkeletonDataProviderから渡ってきたボーン情報の順番が少し複雑だからです。
"Tip"と名の付く指先以外のボーンの位置情報以外が親指から順に列挙して送られてきたのち、
"Tip"と名の付く指先の情報が親指から順に送られてきます。

下記コードで見るとより理解しやすいと思います。

ミニサンプル(左手のみ)
[SerializeField] private GameObject _leftHandVisual;

private readonly List<Transform> _bonesL = new List<Transform>();
private List<Transform> _listOfChildren = new List<Transform>();
private Quaternion _wristFixupRotation

void Start()
{
    OVRSkeleton ovrSkeletonL = GameObject.Find("OVRHandL").GetComponent<OVRSkeleton>();
    OVRSkeleton.IOVRSkeletonDataProvider dataProviderL =
                ovrSkeletonL.GetComponent<OVRSkeleton.IOVRSkeletonDataProvider>();

    //ボーンの情報をC#で利用可能にするラッパークラス
    OVRPlugin.Skeleton skeleton = new OVRPlugin.Skeleton();

    //ボーンの元データを生成
    OVRPlugin.GetSkeleton((OVRPlugin.SkeletonType) dataProviderL.GetSkeletonType(), out skeleton);
    InitializeBones(skeleton, _leftHandVisual);

    //正しい順序で生成したボーンのリストを作成
    ReadyHand(_leftHandVisual, _bonesL);

    _wristFixupRotation = new Quaternion(0.0f, 1.0f, 0.0f, 0.0f);
}

void Update()
{
    //左手
    if (_dataL.IsDataValid && _dataL.IsDataHighConfidence)
    {
        //ルートのローカルポジションを適用
        _leftHandVisual.transform.localPosition = _dataL.RootPose.Position.FromFlippedZVector3f();
        _leftHandVisual.transform.localRotation = _dataL.RootPose.Orientation.FromFlippedZQuatf();

        _leftHandVisual.transform.localScale =
            new Vector3(_dataL.RootScale, _dataL.RootScale, _dataL.RootScale);

        //ボーンのリストに受け取った値を反映
        for (int i = 0; i < _bonesL.Count; ++i)
        {
            _bonesL[i].transform.localRotation = _dataL.BoneRotations[i].FromFlippedXQuatf();

            if (_bonesL[i].name == OVRSkeleton.BoneId.Hand_WristRoot.ToString())
            {
                _bonesL[i].transform.localRotation *= _wristFixupRotation;
            }
        }
    }
}

順番を整理したおかげで、for文を利用したボーンの情報をリストに順番通り取得してくる処理
が容易になっています。

ここまでの処理で
手の見た目のオブジェクト
だけを同期オブジェクトとして実装することが可能となりました。

③手の見た目のみの役割を持つオブジェクトを双方のクライアントに生成

VRGroup3.png

この処理に関しては非常に簡単です。

PhotonNetwork.Instantiateを使えばPUN2が自動で生成してくれます。

コードに落とし込むと下記です。

適当なオブジェクトにアタッチ
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

public class PunConnect : MonoBehaviourPunCallbacks
{
    [SerializeField] private GameObject _avatar;

    private const int _PLAYER_UPPER_LIMIT = 2;

    //ルームオプションのプロパティー
    private RoomOptions _roomOptions = new RoomOptions()
    {
        MaxPlayers = _PLAYER_UPPER_LIMIT, //人数制限
        IsOpen = true, //部屋に参加できるか
        IsVisible = true, //この部屋がロビーにリストされるか
    };

    private void Start()
    {
        //PhotonServerSettingsに設定した内容を使ってマスターサーバーへ接続する
        PhotonNetwork.ConnectUsingSettings();
    }

    //マスターサーバーへの接続が成功した時に呼ばれるコールバック
    public override void OnConnectedToMaster()
    {
        // "Test"という名前のルームに参加する(ルームが無ければ作成してから参加する)
        PhotonNetwork.JoinOrCreateRoom("Test", _roomOptions, TypedLobby.Default);
    }

    //部屋への接続が成功した時に呼ばれるコールバック
    public override void OnJoinedRoom()
    {
        //アバターを生成
        GameObject avatar = PhotonNetwork.Instantiate(
            _avatar.name,
            Vector3.zero, 
            Quaternion.identity);

        avatar.name = _avatar.name;
    }
}

_avatarはPrefabをアタッチする必要があり、そのPrefabはAssets/Photon/PhotonUnityNetworking/Resources
に配置する必要があります。

④お互いの手の位置情報を送り合い、生成した手の位置情報を更新

VRGroup4.png

最後に同期オブジェクトの位置情報を共有する実装です。

PUN2のIPunObservableを経由して送受信します。

ミニ同期処理サンプル(左手のみ)
/// <summary>
/// Transformをやり取りする
/// </summary>
/// <param name="stream">値のやり取りを可能にするストリーム</param>
/// <param name="info">タイムスタンプ等の細かい情報がやり取り可能</param>
void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
    //自身のクライアントから相手クライアントの同期オブジェクトに送る情報
    if (stream.IsWriting)
    {
        stream.SendNext(_leftHandVisual.transform.localPosition);
        stream.SendNext(_leftHandVisual.transform.localRotation);

        //ボーンのリストに受け取った値を反映
        for (var i = 0; i < _bonesL.Count; ++i)
        {
            stream.SendNext(_bonesL[i].transform.localRotation);
        }
    }
    //相手のクライアントから自身のクライアントの同期オブジェクトに送られてくる情報
    else
    {
        _leftHandVisual.transform.localPosition = (Vector3) stream.ReceiveNext();
        _leftHandVisual.transform.localRotation = (Quaternion) stream.ReceiveNext();

        //ボーンのリストに受け取った値を反映
        for (var i = 0; i < _bonesL.Count; ++i)
        {
            _bonesL[i].transform.localRotation = (Quaternion) stream.ReceiveNext();
        }
    }
}

これでようやく同期が完了しました。

最後に

ここまでの理解ですらかなりの時間を要しましたが、
最適化やUI/UXの面からまだまだ課題は多いです。

今後も引き続きハンドトラッキング含め、同期に関して
調査しようと思っています。

参考リンク

SkinnedMeshとBoneWeightについてメモ
PUN2で始めるオンラインゲーム開発入門【その5】

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

【ASP.NET】Angularチュートリアルのサーバーサイド側の作成【C#】

Angularチュートリアルで使うWEBAPI作成

Angular公式のチュートリアルの「Tour of Heroes」をやっていくと、6章でサーバーサイドと通信を行う部分があります。
チュートリアル通りにやるなら In-memory Web APIモジュール を使用して、サーバーサイドをシミュレートしますが、ここでは.NETで自作していきます。

環境

  • .NetFramework 4.7.2
  • VisualStudio2019 community
  • Angularチュートリアルの6章までやってあるAngularプロジェクト

Modelの用意

ヒーロー達を用意する必要があるので、クラスを作成

public class Hero
{
    public Hero(int id, string name)
    {
        this.id = id;
        this.name = name;
    }

    public int id { get; set; }
    public string name { get; set; }
}

初期値の用意

DBは使わないので、staticで用意します。
初回だけコンストラクタでListに追加して、初期表示のヒーローリストを作成します。

public class ValuesController : ApiController
{
    public static List<Hero> Heroes = new List<Hero>();
    public static string[] Names = {"Sato", "Suzuki", "Takahashi", "Tanaka", "Ito",
                             "Watabe", "Yamamoto", "Nakamura", "Kobayashi", "Kato"};

    // コンストラクター
    static ValuesController()
    {
        for (int i = 1; i <= 10; i++)
        {
            Heroes.Add(new Hero(i, Names[i - 1]));
        }
    }

    //成功200
    private HttpResponseMessage Response200()
    {
        HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK, "value");
        return response;
    }

    //エラー400
    private HttpResponseMessage Response400()
    {
        HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest, "value");
        return response;
    }

-各種アクション-

}

GET

GETは一覧取得/単体取得/検索の3種類のアクションを用意

// GET 一覧取得
public IEnumerable<Hero> Get()
{
    return Heroes;
}

// GET 詳細
public Hero Get(int id)
{
    try
    {
        return Heroes[id - 1];
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return null;
    }
}

// GET 部分一致検索(大小区別なし)
public IEnumerable<Hero> Get(string name)
{
    return Heroes.Where(m => m.name.ToUpper().Contains( name.ToUpper() ));
}


POST

IDの最大値を取得して、ヒーローリストに追加しています。

// POST 追加
public Hero Post([FromBody] Hero hero)
{
    int maxId = Heroes.Max(m => m.id) + 1;
    Heroes.Add(new Hero(maxId, hero.name));
    return new Hero(maxId, hero.name);
}

PUT

IDで一致するヒーローの名前を変更する。

// PUT 更新
public HttpResponseMessage Put([FromBody] Hero hero)
{
    try
    {
        Heroes.Where(m => m.id == hero.id).FirstOrDefault().name = hero.name;
        return Response200();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return Response400();
    }
}

DELETE

IDが一致するヒーローをリストから削除します。

// DELETE 削除
public HttpResponseMessage Delete(int id)
{
    try
    {
        Heroes.Remove(Heroes.Where(m => m.id == id).FirstOrDefault());
        return Response200();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return Response400();
    }
}

とりあえずCRUDはこれで動作します。
 

その他

ポートが違うとクロスオリジンで怒られるので、コントローラーに EnableCors属性 をつけておく。

[EnableCors(origins: "http://localhost:4200", headers: "*", methods: "*")]

 
循環参照で怒られる?ので Global.asax に下記を追加
参考:Web APIで応答をシリアル化できませんでした

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
GlobalConfiguration.Configuration.Formatters.Remove(GlobalConfiguration.Configuration.Formatters.XmlFormatter);

こんな感じです。
API作成は.NetFrameworkも.NetCoreもあまり変わらない気がします。

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

【ASP.NET WEB API】AngularチュートリアルのAPI側の作成【C#】

Angularチュートリアルで使うWEBAPI作成

Angular公式のチュートリアルの「Tour of Heroes」をやっていくと、6章でサーバーサイドと通信を行う部分があります。
チュートリアル通りにやるなら In-memory Web APIモジュール を使用して、サーバーサイドをシミュレートしますが、ここでは.NETで自作していきます。

環境

  • .NetFramework 4.7.2
  • VisualStudio2019 community
  • Angularチュートリアルの6章までやってあるAngularプロジェクト

Modelの用意

ヒーロー達を用意する必要があるので、クラスを作成

public class Hero
{
    public Hero(int id, string name)
    {
        this.id = id;
        this.name = name;
    }

    public int id { get; set; }
    public string name { get; set; }
}

初期値の用意

DBは使わないので、staticで用意します。
初回だけコンストラクタでListに追加して、初期表示のヒーローリストを作成します。

public class ValuesController : ApiController
{
    public static List<Hero> Heroes = new List<Hero>();
    public static string[] Names = {"Sato", "Suzuki", "Takahashi", "Tanaka", "Ito",
                             "Watabe", "Yamamoto", "Nakamura", "Kobayashi", "Kato"};

    // コンストラクター
    static ValuesController()
    {
        for (int i = 1; i <= 10; i++)
        {
            Heroes.Add(new Hero(i, Names[i - 1]));
        }
    }

    //成功200
    private HttpResponseMessage Response200()
    {
        HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK, "value");
        return response;
    }

    //エラー400
    private HttpResponseMessage Response400()
    {
        HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest, "value");
        return response;
    }

-各種アクション-

}

GET

GETは一覧取得/単体取得/検索の3種類のアクションを用意

// GET 一覧取得
public IEnumerable<Hero> Get()
{
    return Heroes;
}

// GET 詳細
public Hero Get(int id)
{
    try
    {
        return Heroes[id - 1];
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return null;
    }
}

// GET 部分一致検索(大小区別なし)
public IEnumerable<Hero> Get(string name)
{
    return Heroes.Where(m => m.name.ToUpper().Contains( name.ToUpper() ));
}


POST

IDの最大値を取得して、ヒーローリストに追加しています。

// POST 追加
public Hero Post([FromBody] Hero hero)
{
    int maxId = Heroes.Max(m => m.id) + 1;
    Heroes.Add(new Hero(maxId, hero.name));
    return new Hero(maxId, hero.name);
}

PUT

IDで一致するヒーローの名前を変更する。

// PUT 更新
public HttpResponseMessage Put([FromBody] Hero hero)
{
    try
    {
        Heroes.Where(m => m.id == hero.id).FirstOrDefault().name = hero.name;
        return Response200();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return Response400();
    }
}

DELETE

IDが一致するヒーローをリストから削除します。

// DELETE 削除
public HttpResponseMessage Delete(int id)
{
    try
    {
        Heroes.Remove(Heroes.Where(m => m.id == id).FirstOrDefault());
        return Response200();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return Response400();
    }
}

とりあえずCRUDはこれで動作します。
 

その他

ポートが違うとクロスオリジンで怒られるので、コントローラーに EnableCors属性 をつけておく。

[EnableCors(origins: "http://localhost:4200", headers: "*", methods: "*")]

 
循環参照で怒られる?ので Global.asax に下記を追加
参考:Web APIで応答をシリアル化できませんでした

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
GlobalConfiguration.Configuration.Formatters.Remove(GlobalConfiguration.Configuration.Formatters.XmlFormatter);

こんな感じです。
API作成は.NetFrameworkも.NetCoreもあまり変わらない気がします。

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

順列 C#

順列 C#

using System;
using System.Collections.Generic;

namespace Permutation
{
    class Program
    {
        static void Main(string[] args)
        {
            new Program();
        }

        public Program()
        {
            List<int> data = new List<int> { 1, 2, 3, 4 };
            List<List<int>> perm = GetPermutation(data);
            foreach (List<int> p in perm) {
                Console.WriteLine(string.Join(",", p));
            }
        }

        private List<List<int>> GetPermutation(List<int> data)
        {
            List<List<int>> perm = new List<List<int>>();
            if (data.Count == 0) {
                perm.Add(data);
                return perm;
            }

            for (int i = 0; i < data.Count; i++) {
                int head = data[i];

                List<int> dataCopy = new List<int>(data);
                dataCopy.RemoveAt(i);
                List<List<int>> childPerm = GetPermutation(dataCopy);
                foreach (List<int> child in childPerm) {
                    child.Insert(0, head);
                    perm.Add(child);
                }
            }
            return perm;
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LINQ All() の挙動について

LINQにはラムダで条件式を渡し、要素全てがその条件を満たしているかを判定してboolを返す、
All()という便利なメソッドがあります。

List<int> list1 = new List<int> { 1, 2, 3 };

if (list1.All(n => n > 0)) // ← 要素が全て0より大きいのでtrue
{
    Console.WriteLine("正の整数です");
}

// 出力: 正の整数です

空のリストに対してこのメソッドを実行すると、直感とは異なる挙動になります。
(「全てが満たす」だからfalseになると思っていた)

List<int> list2 = new List<int>();

if (list2.All(n => n > 0)) // ← true
{
    Console.WriteLine("正の整数です");
}

// 出力: 正の整数です

All()の実装を見てみると、リストの要素が空の場合は必ずtrueが返るようになっていました。
Any()等を組み合わせて判定するのが安全そうです。

// https://github.com/microsoft/referencesource/blob/master/System.Core/System/Linq/Enumerable.cs#L1305

public static bool All<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
    if (source == null) throw Error.ArgumentNull("source");
    if (predicate == null) throw Error.ArgumentNull("predicate");
    foreach (TSource element in source) { // ← sourceの要素が0なのでループが回らない
        if (!predicate(element)) return false;
    }
    return true; // ← ので、必ずtrueが返る
}
List<int> list2 = new List<int>();

if (list2.Any() && list2.All(n => n > 0)) // ← false
{
    Console.WriteLine("正の整数です");
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityにおけるInvokeとCoroutineの精度比較

環境

windows10
Unity 2019.3.15f1

指定時間後に動作する処理を書きたい

⇒高精度を求めるならCoroutine一択!※
理由は下記の記事になります。

※2020/06/30 FPSに依存するようです。高精度ではなく、Updateと同じ精度と考えるのが良さそうです。

InvokeとCoroutine

「Unity 指定時間後」で調べるとこの二つが出てきます。

【Unity】スクリプトの処理の実行タイミングを操作する
↑こちらの記事がわかりやすいです。

精度はどうなの?

「BPM200の16分音符を鳴らしたい=音を出してから75ms後に音を止めたい」
これが私にとっての課題です。InvokeもしくはCoroutineはこの課題を解決してくれるのでしょうか。

測定をする

測定にはStopwatchクラスを使用します。今回の測定を行うには十分に高精度です。
どちらも「スペースキー」が押されるとタイマースタート、
一定時間後にタイマーストップをするプログラムです。

Invokeのテストソース

Invoke(xxx, Time); の第二引数で時間を決めます。

InvokeTest.cs
public class InvokeTimerTest : MonoBehaviour
{
    [SerializeField, Range(0.01f, 1.0f)] float Time;
    List<long> results = new List<long>();

    Stopwatch sw = new Stopwatch();

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            sw.Start(); //計測開始
            Invoke("TestInvoke", Time);
        }
    }

    void TestInvoke()
    {
        sw.Stop();
        UnityEngine.Debug.Log($"TestInvoke()[{results.Count + 1}]:{sw.ElapsedMilliseconds}ms");
        results.Add(sw.ElapsedMilliseconds);
        sw.Reset();
    }
}

Coroutineのテストソース

yield return new WaitForSecondsRealtime(Time);の第一引数で時間を決めます。

Coroutine.cs
public class CoroutineTimerTest : MonoBehaviour
{
    [SerializeField, Range(0.01f, 1.0f)] float Time;
    List<long> results = new List<long>();
    Stopwatch sw = new Stopwatch();

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            sw.Start(); //計測開始
            StartCoroutine(TestCoroutine());
        }
    }

    IEnumerator TestCoroutine()
    {
        yield return new WaitForSecondsRealtime(Time);
        sw.Stop();
        UnityEngine.Debug.Log($"TestCoroutine()[{results.Count + 1}]:{sw.ElapsedMilliseconds}ms");
        results.Add(sw.ElapsedMilliseconds);
        sw.Reset();
    }
}

結果

Timeの値を変えて、各条件で100回実行した結果になります。

条件1.Time=100msの場合

最大値 最小値 平均 標準偏差
Invoke 104 83 93.79 6.02
Coroutine 108 100 102.50 1.67

条件2.Time=50msの場合

最大値 最小値 平均 標準偏差
Invoke 57 34 44.28 7.06
Coroutine 57 52 55.8 0.89

条件3.Time=10msの場合

最大値 最小値 平均 標準偏差
Invoke 18 0 4.03 5.13
Coroutine 25 10 17.54 2.24

条件4.Time=500msの場合

最大値 最小値 平均 標準偏差
Invoke 506 480 494.23 6.28
Coroutine 509 500 503.58 2.97

考察

1. 誤差

 Invokeの誤差は、+に8ms, -に20msが最大です。
 Coroutineの誤差は、+に15ms, -に0msが最大です。
 偏差を比べてもCoroutineの方が小さい傾向にあります。
 1ms単位とは言いませんが、±10ms程度のタイマーとしては有効ではないでしょうか。

2. 最小値

 Coroutineでは最小値が設定値を下回る事がありません。タイマーの時間が過ぎたら実行するプログラムと考えると納得がいきます。しかし、Invokeの最小値は設定値を下回っています。おおよそ60Hzの1フレーム分くらい早めに判定される事があるようです。(理由はよくわからないので、詳しい方補足をお願いします。)

3. Timeの限界値

 Time=10msではInvokeの最小値が0になる、Coroutineの誤差も最大+15ms(つまり、目標値の2.5倍)となり、正確な値が取れているとは言い難い状況です。Coroutineの方が精度が良いとはいえ、Time=50msでも約15%の誤差がありますので、過信は禁物です。どちらの誤差も総時間によらない固定値にも思えますので、長時間になればなるほど誤差率は下がりそうです。

まとめ

 Invokeでもシビアじゃない環境では十分に使える精度で動作しますが、Coroutineの方が精度よく引数も使えると利点があります。私の課題「75ms音を出す」は「誤差+10ms以内」で達成出来そうです。「この精度で十分かどうか」、それはもちろん使う用途によりますが、このデータが判断の手助けになれば幸いです。

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

C#のみを使って、今ソーシャルゲームアプリを作るとしたら

はじめに

現在進行形でC#のみを使って個人でソシャゲ作りを試しているyoship1639です。
本記事はQiita夏祭り2020「〇〇(言語)のみを使って、今△△(アプリ)を作るとしたら」のテーマに沿った内容となっています。

近年のソーシャルゲーム界隈は多様化が進んでクライアントサイドだけではなくサーバーサイドもあらゆる言語やフレームワークが試みられていますが、クライアントもサーバーも統一の言語で構成されているのはほとんどないかと思われます。言語にはその言語の得意分野があると思うので。

しかし、今まさに私が開発中の環境が好きな言語で開発しやすいという理由でクライアントもサーバーもC#で構成した作りになっているので、どのような構成でどうすれば最低限のソシャゲの基盤が作れるかを、解説が長くなり過ぎないようにまとめることが出来ればと思います。

三部構成で、クライアント実装、サーバー実装、AWS EC2へのデプロイまで解説できればと思っています。
Let's、C#のみでソシャゲを作ろう!

ソシャゲの概要

内容に入る前に、ソシャゲがどの様な流れで動作するのかを軽く説明します。

ソシャゲは基本的にクライアント(スマホ端末)とサーバーとのやり取りで動いています。サーバーが動いていないとクライアントは基本動作しません。これはクライアント側で不正にデータの書き換えをされると運営が困るからです。

サーバー側は大体以下の様なAPI機能を備えています。

  • アプリバージョン判定
  • マスターデータ・アセットバンドル更新判定
  • ログイン (セッション管理)
  • アカウント作成
  • クエスト開始・終了
  • ガチャ
  • etc...

挙げたらきりがないくらいにはサーバーにはやらなければならない仕事があります。それだけクライアントとサーバーは適所で通信しています。こうすることで、例えばクライアントのデータが紛失したとしてもサーバーから復元することが出来ますし、クライアント側で不正があったらサーバー側で検知してBANすることもできますし、ユーザーのアプリ上での動向から問い合わせにも対応することができるようになりますし、課金周りのレシート検証もサーバー側で正確に行えるので、課金したのに石が反映されないみたいな場面でも補填対応することが出来るようになります。基本ユーザにとっても運営にとってもメリットしかないです。

近年バックエンドはBaas(PlayFab、Firebase、GameSparks、GS2など)が鎬を削っており態々バックエンドを自前で準備しなくてもBaasを使うという手段がありますが、ドキュメントが英語のみだったり痒いところに手が届かなかったりと一長一短なので、どうしてもサーバーサイドを触りたくないという訳ではないのであれば個人的にはまだ自前で準備したほうが良いかな感はあります。

ソシャゲの動作の最初の流れとしては以下の様になります。

  1. アプリバージョンを検証
  2. ログイン (ログインできなかったらアカウント作成)
  3. 更新データ確認 (アセバン、マスターデータ)
  4. 以降アプリによって色々

今回は最低限の基盤だけ考えるので、2番の「アカウント作成」と「ログイン」機能を作りたいと思います。

構成の全体像

今回作るサンプルは、C#のみで構成するソシャゲの最低限の基盤で以下の構成となっています。

クライアント:C#(Unity2019.X)
サーバー:C#(.NetCore3.1)
デプロイ:AWS EC2(Amazon Linux 2)
サーバー <--> クライアント:MagicOnion(HTTP/2, gRPC)

構成図.png

クライアントは皆大好きUnity、サーバーはプラットフォーム関係なく動かせる.NetCore、デプロイはEC2、クライアントとサーバーのやり取りは巷で噂のMagicOnion(gRPCのC#ラッパー+α)です。最低限の構成であれば全部無料で準備できます。

本来であれば、DB用意したり、直じゃなくDockerコンテナでデプロイとかすべきですが、本記事から内容が逸れそうなので簡単な構成にしています。

まずは、クライアントサイドから作ってみます。

クライアントサイド

クライアントサイドはエンジンとしてUnity2019.Xを使います。言語は当然C#です。

実装手順としては以下の通りとなります。

① MagicOnion, MessagePack, grpc をUnityにインポートする
② Serviceを定義
③ NetworkManagerを実装
④ ログインテストコードを実装

① MagicOnion, MessagePack, grpc をUnityにインポートする

まず、MagicOnion、MessagePack、grpcをUnityにインポートします。サーバーと通信するのに必要なものです。
これらを簡単に説明すると、

  • MagicOnion: リアルタイム/API通信フレームワーク。gRPCをC#で使いやすいようにラップしたイメージ。
  • MessagePack: 高効率のバイナリ形式のシリアライズフォーマット。JSONよりすごいやつ。MagicOnionに必要。
  • grpc: googleが作ったRPCフレームワーク。MagicOnionの中身はこれ。

となっています。

なんでMagicOnionを使うかというと、以下のメリットがあるからです。

  • HTTP/2の恩恵を受け、かつ通信データが高効率で圧縮されるため通信が早い。
  • インターフェースベースの通信が実現されるのでデータフォーマットを考えなくていい。
  • エンドポイントやAPIスキーマを考えなくていい。
  • APIだけでなくリアルタイム通信としても使える。

使うには十分すぎるメリットではないかと思います。MagicOnionの詳細は解説しないので、各自調べていただければと思います。

まず、MagicOnionをインポートします。
https://github.com/Cysharp/MagicOnion/releases

こちらのリリースページにある「MagicOnion.Client.Unity.unitypackage」をダウンロードしUnityにインポートしてください。色々足りないと怒られますが気にせず次へいきます。

次に、MessagePackをインポートします。
https://github.com/neuecc/MessagePack-CSharp/releases

こちらのリリースページにある「MessagePack.Unity.XXXXX.unitypackage」をダウンロードしUnityにD&Dしてください。最新のリリースで問題なく動作するはずです。

この時、Pluginsフォルダ内のdllが既に取り込まれているよと警告されるので、Pluginsフォルダのチェックを外してインポートしてください。

qiita2020_001.png

最後に、grpcをインポートします。
https://packages.grpc.io/

こちらのページの最新のコミットのBuild IDをクリックし、C#欄にある「grpc_unity_package.XXXXX-dev.zip」をダウンロード、解凍します。
解凍すると「Plugins」フォルダがあるはずなので、Pluginsフォルダの中身をUnityのAssets/Pluginsフォルダに入れてインポートします。

qiita2020_002.png

それでもまだ怒られると思うので、エラーを解決していきます。

  • System.Buffersが被っているので、どちらかを削除
  • System.Memoryが被っているので、どちらかを削除
  • System.Runtime.CompilerServices.Unsafeが被っているので、どちらかを削除
  • unsafeコードが許可されていないぞ☆って怒られるのでunsafeコードを許可

これでエラーは出なくなるはずです。

② Serviceを定義

諸々インポートが完了したらServiceを定義します。ServiceとはWebAPIと同様のものと考えていただければと思います。
ソーシャルゲームは基本的に特定の動作ごとにサーバーにAPIを投げてそのレスポンスを基にクライアントを動かします。

本来、API定義を考える場合「https://〇〇〇〇/create_account」みたいなエンドポイントやらスキーマやらを考えなくてはいけませんが、MagicOnionの場合はインターフェース定義自体がそれに当たります。これメチャクチャ便利です。

アカウント作成とログインの機能は、以下の様に定義できます。

IAccountService.cs
using MagicOnion;

// アカウント周りのサービスを定義するインターフェース
public interface IAccountService : IService<IAccountService>
{
    // アカウント作成
    UnaryResult<(string userId, string password)> CreateAccount();

    // ログイン
    UnaryResult<string> Login(string userId, string password);
}

CreateAccountはサーバー側で作成されたユーザIDとパスワードを返し、Loginは引数にユーザIDとパスワードを入力するとログイン中であるセッション情報(string)を返します。

クライアントはIAccountServiceだけを知っていればいいので、IAccountServiceの実態はサーバー側で実装します。

③ NetworkManagerを実装

Serviceの定義が終わったら実際にサーバーと通信する処理を担当するNetworkManagerを実装します。
クライアントはこのNetworkManagerを使ってサーバーとのやり取りをします。

NetworkManager.cs
using System;
using System.Threading.Tasks;
using Grpc.Core;
using MagicOnion.Client;
using UnityEngine;

public class NetworkManager : MonoBehaviour
{
    [SerializeField] private string applicationHost = "localhost";
    [SerializeField] private int applicationPort = 12345;

    private IAccountService accountService;
    private string session;

    void Start()
    {
        var channel = new Channel(applicationHost, applicationPort, ChannelCredentials.Insecure);
        accountService = MagicOnionClient.Create<IAccountService>(channel);
    }

    // アカウント作成
    public async Task<(string userId, string password)> CreateAccount()
    {
        try
        {
            // サーバーにアカウント作成を要求、レスポンスは作成されたユーザIDとパスワード
            return await accountService.CreateAccount();
        }
        catch (Exception e)
        {
            Debug.Log(e);
            return (null, null);
        }
    }

    // ログイン
    public async Task<bool> Login(string userId, string password)
    {
        try
        {
            // ユーザIDとパスワードをサーバーに投げてログイン、レスポンスはセッション情報
            session = await accountService.Login(userId, password);
            return session != null;
        }
        catch (Exception e)
        {
            Debug.Log(e);
            session = null;
            return false;
        }
    }
}

applicationHostはlocalhostにしてありますが、後でデプロイ先のエンドポイントに切り替えます。
セキュリティの関係からsslにすべきですが、今回は割愛です。

④ ログインテストコードを実装

実際にログインのテストコードを記述してみます。
処理内容はとても単純で、まずローカルに保存してあるユーザー情報(ユーザーID、パスワード)を読み込みます。ユーザー情報そのものがなかったらアカウントを作成し作成されたユーザー情報を保存します。次に、ユーザー情報を元にログインし、通った時と通らなかった時で処理を分けるという形です。

LoginTest.cs
using System.IO;
using MessagePack;
using UnityEngine;

[MessagePackObject]
public class UserData
{
    [Key(0)]
    public string userId;
    [Key(1)]
    public string password;
}

public class LoginTest : MonoBehaviour
{
    async void Start()
    {
        // ネットワークマネージャ取得
        var network = GetComponent<NetworkManager>();

        // 保存してあるユーザーデータ情報を読み込み
        UserData userData = null;
        try
        {
            userData = MessagePackSerializer.Deserialize<UserData>(File.ReadAllBytes(Application.persistentDataPath + "/userData.dat"));
        }
        catch { }

        // ユーザーデータが存在しなかったらアカウント作成
        if (userData == null)
        {
            Debug.Log("アカウント作成開始");
            var res = await network.CreateAccount();
            if (res.userId == null || res.password == null)
            {
                // TODO: アカウント作成失敗時の処理
                Debug.LogWarning("アカウント作成失敗。。。");
                return;
            }
            userData = new UserData();
            userData.userId = res.userId;
            userData.password = res.password;

            // ユーザー情報保存(※本来は暗号化等する事!)
            var data = MessagePackSerializer.Serialize(userData);
            File.WriteAllBytes(Application.persistentDataPath + "/userData.dat", data);
            Debug.Log("アカウント作成成功");
        }

        // ログイン
        Debug.Log("ログイン中...");
        var loginResult = await network.Login(userData.userId, userData.password);
        if (!loginResult)
        {
            // TODO: ログイン失敗時の処理
            Debug.LogWarning("ログイン失敗。。。");
            return;
        }

        // TODO: ログインが通った後の処理
        Debug.Log("ログイン成功!");
    }
}

本来ならばもっと厳密にログイン処理を行うべきですが、今回はテストなので超単純に作っています。
ここを通ればログインに成功したことになるので、後はクライアント側は煮るなり焼くなりするだけです。

次に、サーバーサイドの実装に移ります。

サーバーサイド

サーバーサイドはフレームワークとして.NetCore3.1を使います。言語は当然C#です。
.NetFrameworkを使ってしまうとデプロイ周りで苦労することになるので、サーバーサイドC#は.NetCoreを使ってください。

サーバーの実装手順としては以下の様になります。

① プロジェクトの準備、MagicOnionのインストール
② Mainプログラムの記述
③ AccountServiceの実装
④ ローカル環境で動作確認

① プロジェクトの準備、MagicOnionのインストール

まず、プロジェクトを作成します。プロジェクトは「コンソール アプリ(.NET Core)」を選択してください。プロジェクト名は何でもいいです。私はとりあえず「Qiita2020TestServer」にしました。
qiita2020_003.png

プロジェクトの作成が終わったら、クライアントとの通信に必要なコンポーネントをNuget経由でインストールします。
プロジェクトのコンテキストメニューの「Nuget パッケージの管理(N)...」からMagicOnion.Hostingをインストールします。バージョンは最新の安定板で大丈夫です。
qiita2020_004.png

一応、Unityで使われている型をサーバーでも扱えるようにMessagePack.UnityShimsもインストールしておきます。
image.png

これでサーバーサイドに必要なコンポーネントがインストールできました。

② Mainプログラムの記述

サーバーを起動するMainプログラムを記述します。
やっていることはとても単純で、ログ出力先をコンソールに指定し、ホストとポートを指定して起動しているだけです。

Program.cs
using Grpc.Core;
using MagicOnion.Hosting;
using MagicOnion.Server;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;

namespace Qiita2020TestServer
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // コンソールにログ出力するように設定
            GrpcEnvironment.SetLogger(new Grpc.Core.Logging.ConsoleLogger());

            // MagicOnionを使ってホスト作成、起動
            await MagicOnionHost.CreateDefaultBuilder()
                .UseMagicOnion(
                    new MagicOnionOptions(isReturnExceptionStackTraceInErrorDetail: true),
                    new ServerPort("0.0.0.0", 12345, ServerCredentials.Insecure))
                .RunConsoleAsync();
        }
    }
}

一応これだけでもサーバーを起動することはできます。デバッグ実行すると以下の様な画面が出るはずです。
qiita2020_006.png

これだけでは何の機能もない張りぼてサーバーなので、クライアント側で実装した「アカウント作成」と「ログイン」機能を実装していきます。

③ AccountServiceの実装

サーバー側のアカウント作成とログイン機能の実装をします。
クライアントで定義したIAccountService.csが必要なので、予め丸々コピーしておいてください。(本来は、submodule等用いてソースコードの共有をすることをお勧めします。)

アカウント作成は、ランダムなハッシュ値を用います。ユーザーIDは20桁、パスワードは12桁にしておきます。ログインは作成されたユーザーIDとパスワードを検証し、一致したら以降のAPIを呼び出すことが出来るセッションを返します。セッションも一先ずランダムな20桁のハッシュ値を返します。

AccountService.cs
using MagicOnion;
using MagicOnion.Server;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace Qiita2020TestServer
{
    class AccountService : ServiceBase<IAccountService>, IAccountService
    {
        // セッション情報管理(本来はRedis等用いる事!)
        private static Dictionary<string, (string userId, DateTime expireAt)> sessions = new Dictionary<string, (string userId, DateTime expireAt)>();

        private static object lockObject = new object();

        // アカウント作成
        public async UnaryResult<(string userId, string password)> CreateAccount()
        {
            Logger.Info("CreateAccount Request");

            var userId = GenerateHash(20);
            var password = GenerateHash(12);

            // アカウント情報を仮でファイルに保存(本来はDBに入れる事!)
            try
            {
                if (!Directory.Exists("accounts")) Directory.CreateDirectory("accounts");
                File.WriteAllText("accounts/" + userId, password);
            }
            catch (Exception e)
            {
                Logger.Error(e, "CreateAccount Error");
                return (null, null);
            }

            Logger.Info($"CreateAccount UserId:{userId}, Password:{password}");

            return (userId, password);
        }

        // ログイン
        public async UnaryResult<string> Login(string userId, string password)
        {
            Logger.Info("Login Request");
            try
            {
                // アカウントがない
                if (!File.Exists("accounts/" + userId)) return null;

                // パスワードが一致しない
                if (File.ReadAllText("accounts/" + userId) != password)
                {
                    Logger.Warning("Login failed: " + (userId, password));
                    return null;
                };
            }
            catch (Exception e)
            {
                Logger.Error(e, "Login Error");
                return null;
            }

            // セッション情報作成
            var session = GenerateHash(20);

            lock (lockObject)
            {
                // 一先ず1日有効なセッションを保存
                sessions[session] = (userId, DateTime.UtcNow.AddDays(1));
            }

            Logger.Info("【" + userId + "】Login succeeded!");
            // セッションを返す
            return session;
        }

        // 指定の長さのランダムハッシュ値を取得
        private static string GenerateHash(int length)
        {
            return Sha256(Guid.NewGuid().ToString("N")).Substring(0, length).ToLower();
        }

        // Sha256ハッシュ
        private static string Sha256(string str)
        {
            var input = Encoding.ASCII.GetBytes(str);
            var sha = new SHA256CryptoServiceProvider();
            var sha256 = sha.ComputeHash(input);

            var sb = new StringBuilder();
            for (int i = 0; i < sha256.Length; i++)
            {
                sb.Append(string.Format("{0:X2}", sha256[i]));
            }

            return sb.ToString();
        }
    }
}

これで、アカウント作成とログイン機能を備えたサーバープログラムが整いました。

④ ローカル環境で動作確認

ここまでで、ローカル環境で動作確認をすることが出来るようになったので、確認してみます。
サーバーをデバッグ実行してローカルサーバーを立ち上げ、クライアントをデバッグ実行します。
問題がなければクライアントは以下の様に表示されるはずです。
qiita2020_007.png
サーバー側は以下の様に表示されます。
qiita2020_008.png
ローカルで問題なく動作できていることが確認できました。
最後に、実際にクラウド上にデプロイして確認してみたいと思います。

デプロイ

ローカル環境で問題なく動作させることが確認できれば、本来はデプロイまでは頑張らなくてもいいですが、せっかくなのでEC2へのデプロイまでやってみたいと思います。CI/CDやDockerコンテナでもよかったのですが解説が逸れそうなので直デプロイします。

手順としては、以下の様になります。
① awsでEC2インスタンスを用意、起動する
② ターミナルでEC2インスタンスにログイン
③ .NetCore3.1をインストール
④ サーバープロジェクトを配置、実行
⑤ クライアント動作確認

① awsでEC2インスタンスを用意、起動する

最初にAWSコンソールにサインインします。アカウントを持ってない人は作ってください。
qiita2020_010.png

サインインしたらEC2を選択します。EC2は仮想サーバーみたいなものだと思ってください。
qiita2020_009.png

左側の「キーペア」を選択します。
qiita2020_020.png

右上の「キーペアを作成」をクリックします。
qiita2020_021.png

名前を「Qiita2020TestServer」(名前は何でもいいです)にして、「キーペアを作成」をクリックします。
qiita2020_022.png

キーペアが作成されppkファイルがダウンロードされます。
このキーは後で作るインスタンスへのログインに必要なので、大切に保管しましょう。

次に、左側の「インスタンス」を選択します。
qiita2020_011.png

「インスタンスの作成」をクリックします。
qiita2020_012.png

何のインスタンスを作るか聞かれるので、「Amazon Linux 2 AMI」を選択してください。
qiita2020_013.png

どのスペックの仮想マシンを立ち上げるか聞かれるので、無料で使える「t2.micro」を選択し、「次のステップ:インスタンスの詳細と設定」をクリック。
qiita2020_014.png

いろんな設定項目がありますが、ここでは「自動割り当てパブリックIP」を「有効」にします。
有効にしたら「次のステップ:ストレージの追加」へ。
qiita2020_015.png

ストレージは30GBまで無料らしいので一先ず30GBに設定し「次のステップ:タグの追加」へ。
qiita2020_016.png

タグの追加を押し、キーに「Name」、値に「Qiita2020TestServer」と入力します。(値はわかれば何でもいいです)
入力したら「次のステップ:セキュリティグループの設定」へ。
qiita2020_017.png

「ルールの追加」を押し、以下の様に入力し12345ポートを解放します。
分かりやすいように説明も入れておきましょう。(※画像は日本語ですが、日本語の説明ではインスタンスが作成できなかったので、英語で入力してください!)
入力したら「確認と作成」をクリック。
qiita2020_018.png

確認画面で「すべてのIPからインスタンスにアクセスできるよ、いいの?」と警告されますが気にせず「起動」を押します。
qiita2020_019.png

インスタンスに安全に接続するためのキーペアを選びます。先ほど作成した「Qiita2020TestServer」キーペアを選んで、「インスタンスの作成」をクリックします。
qiita2020_024.png

インスタンス作成中の画面が表示されるので、右下の「インスタンスの表示」をクリックしてください。
すると、作成されたインスタンス一覧が表示されます。問題がなければインスタンスはそのまま起動します。
qiita2020_026.png

これで、インスタンスの準備は整いました。
作成したインスタンスの「IPv4パブリックIP」はターミナル接続先なので控えておいてください。

② ターミナルでEC2インスタンスにログイン

EC2インスタンスにログインします。
ターミナルソフトは何でもいいですが、私は「TeraTerm」を使って解説します。

TeraTermを起動したらホストに先ほど作成したインスタンスのパブリックIPを入力してOKをクリック。
qiita2020_033.png

SSH認証が必要なので、ユーザ名に「ec2-user」、認証方式には最初の方に作成したキーペアの秘密鍵「Qiira2020TestServer.ppk」を指定します。
qiita2020_034.png

問題なくSSH接続できたら以下の様に表示されます。ここからはいつものターミナルです。
qiita2020_035.png

③ .NetCore3.1をインストール

必要なパッケージをインストールします。今回は.NetCoreを動かすためのランタイム「.NetCoreRuntime」をインストールすればOKです。

まずはパッケージ更新
$ sudo yum update

Microsoftパッケージリポジトリを追加。
$ sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm

.NetCore3.1SDKインストール
$ sudo yum install dotnet-sdk-3.1

.NetCore3.1ランタイムインストール
$ sudo yum install dotnet-runtime-3.1

これでEC2上で.NetCore3.1プロジェクトが実行できるようになりました。

④ サーバープロジェクトを配置、実行

サーバープロジェクトを配置します。配置方法は何でもいいですが個人的にはgitを使うのが一番楽です(余力のある方はGithubActions等を使ったCI/CDをお勧めします。)。サーバープロジェクトをgithubでリモート管理し、git cloneでそのまま配置します。こうすると何が良いかというと、サーバープロジェクトの更新が入った時にgit pullするだけで更新できます。
ただ今回はプロジェクトをgithubに配置しないので、おとなしくsftpで配置します。sftpの解説はしません。各自良い感じにサーバープロジェクトを根こそぎ持ってきてください。

配置場所はec2-userフォルダ内に「Qiita2020TestServer」を作ります。
$ mkdir Qiita2020TestServer
この中にプロジェクトを配置します。

配置したら、Qiita2020TestServer.csprojがあるフォルダまで移動します。
.Netはcsprojをそのままdotnet runで実行することが出来るので、実行してみます。

$ dotnet run

これで以下の様にEC2上でサーバーが立ち上がりました。
直に立ち上げるとSSHを終了した段階でサーバープログラムが止まってしまうのでscreenを使うと色々捗ります。
qiita2020_036.png

最後にクライアントからEC2上にアクセスできるか確認します。

⑤ クライアント動作確認

クライアントのapplicationHostがlocalhostのままなので、NetworkManagerのこの部分をEC2インスタンスのパブリックIPに置き換えてください。
qiita2020_037.png

置き換えて、クライアントをデバッグ実行します。そして、以下の表示が出たらEC2との接続に成功です!
qiita2020_007.png

これで、C#のみで作ったソシャゲの最低限の基盤ができました。
ここからマスターデータやアセバン管理、クエスト処理やガチャ処理等を生やしていけば、C#のみで作るソシャゲの出来上がりです。お疲れまでした。

おわりに

いかがでしょうか、意外と簡単にソシャゲの最低限の基盤を作ることができたのではないかと思います。もちろん本物はこれだけじゃ済まないボリュームですが、基礎を捉えるのは大きな前進になるのではないかと思います。

今回は省きまくりましたが、セキュリティだけはしっかり設定してください。ソシャゲはユーザの大事な情報を管理するので、ガバガバ設定では当然許されません。最低でも、ユーザーデータの暗号化、SSL、サーバー監視はしっかりするように!

皆様はもちろんC#信者だと思うので、その熱意をUnityだけではなくそのままサーバーサイドにも向けてみてはいかがでしょうか。私もまだまだサーバーサイドは勉強中なので偉そうなことは言えませんが、クライアントサイドとはまた別の面白さがあるので、やりがいはいっぱいあるかと思います。

頑張れば、一人でもソシャゲが作れる時代です。

最後まで読んでいただき、ありがとうございました。

参考資料

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

C#のみを使って、変更前後の差分表示のできる、ファイル名変更ソフトを作った。

概要

2020年にもなってファイル名変更ソフトを新しく作りました。名前はFileRenamerDiffです。
この記事では、作った動機とソフトの機能紹介をします。
C#/.NET Core/WPFで作成しました。使用した技術などは別の記事にする予定です。
C#のみと書きつつ、XAMLも使っていますが、ビルド時にC#になっていますので、良しとしておきます。

image.png

なぜ作ったか

ファイル名変更ソフトを使ったことのあるかたは多いと思います。私自身も今までは別のソフトウェアを使っていました。
ただ、その中でいくつかの不満があり、それに対応したソフトウェアが見つかりませんでした。
具体的には以下のようなものです。

  • 変更前後での差分表示:ファイル名が長い場合にどこが変わったかの確認が大変だった
  • 重複判定:プレビューで確認してから、ファイル名変更時にファイル名重複で処理が中断するとツライ
  • 設定パターン数の制限:削除&置換パターンの数に制限があると何回も設定を変えて処理する必要がある。
  • デザイン:ユーティリティソフトにありがちな灰色ばっかの画面は好きになれない

そこで、これらの不満を解消したソフトを自分で新しく作ることにしました。

機能紹介

基本的な使い方は以下のとおりです。

  1. ファイル名を変更したいフォルダをドラッグアンドドロップまたはダイアログで指定
  2. ファイル名の削除・置換パターンを指定
  3. プレビューで変更内容を確認
  4. 実際にファイル名を保存

変更前後での差分表示

ファイル名変更前後での差分をGitのDiffっぽく表示しました。ソフト名通りにこれが一番作りたかった機能です。

screenshot2.png

制限数なしの正規表現による削除&置換パターン

制限数なしで削除&置換パターンを作れます。正規表現も使えます。

image.png

変更後のファイル名重複判定

プレビュー時点でファイル名の重複を判定して、警告してくれる機能です。
重複があるままだと、保存できません。

screenshot3.png

その他の機能

Light/Darkテーマ切り替え

設定でLightテーマ、Darkテーマを切り替えできます。
screenshot_Dark-Light.png

任意のファイルをエクスプローラーで開く機能

重複判定時などに、このファイル名だけ手直ししたい、といった時にそのファイルのディレクトリをエクスプローラーで開く機能です。
image.png

よく使うパターン集

削除・置換でよく使うパターン集です。これはまだ数が少ないので、これから増やしていきます。

image.png

ファイル・フォルダをリネーム対象にするか選択可能

ファイルだけorフォルダだけリネームしたい時があるので選択できるようにしました。

拡張子での除外判定

iniファイルなど、特定の拡張子をリネーム対象から除外する機能です。

変更あり・重複ありのみ表示

変更のあるファイル名だけ・重複のあるファイル名だけ、に表示をしぼりこむ機能です。

image.png

多言語対応

英語・日本語・中国語・ロシア語・ドイツ語に対応しています。
基本はDeepLです。
英語と中国語はなんとなく分かるから、ドイツ語とロシア語は知り合いに聞ける人がいるから選びました。

Microsoftストアでの公開

一度やってみたかった。思ったよりも簡単でした。この方法などは後日記事にします。

ダウンロード

ソフトウェアは以下の場所から入手できます。

Microsoft Store (Windows 10 のみ)

Microsoft Store

Portable version (Windows 7以降)

GitHub RELEASEの最新版からどうぞ.

  • FileRenamerDiff_app_win-x64~ は x64(64bit) OS用
  • FileRenamerDiff_app_win-x86~ は x86(32bit) OS用

まとめ

自分が欲しかったソフトを作ってみました。よかったら使ってみてください。
使ってみた感想、バグや機能要望などがあれば、ここのコメント欄やGitHubのIssueに書いてくれると嬉しいです。

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

シグモイド関数

private void button_Click(object sender, EventArgs e)
{
    var x = np_arange(-5.0, 5.0, 0.1);
    var y = sigmoid(x);

    var plt = new Chart();
    plt_plot(plt, x, y);
    plt_xlim(plt, -6.0, 6.0);
    plt_ylim(plt, -0.1, 1.1);
    plt_show(plt);
}

private U[] np_arange<T, U>(T start, T end, U step) where T : struct where U : struct
{
    var start_ = Convert.ToDouble(start);
    var end_ = Convert.ToDouble(end);
    var step_ = Convert.ToDouble(step);

    return Enumerable.Range(0, (int)((end_ - start_) / step_ + 1)).Select(x => (U)(dynamic)(start_ + x * step_)).ToArray();
}

private double[] sigmoid<T>(T[] x) where T : struct
{
    return x.Select(n => 1 / (1 + Math.Exp(-Convert.ToDouble(n)))).ToArray();
}

private void plt_plot<T, U>(Chart plt, T[] x, U[] y) where T : struct where U : struct
{
    var x_ = x.Select(n => Convert.ToDouble(n)).ToArray();
    var y_ = y.Select(n => Convert.ToDouble(n)).ToArray();

    plt.Size = new System.Drawing.Size(500, 350);

    plt.ChartAreas.Add("area");
    plt.Legends.Add("legend");
    plt.Series.Add("line");

    plt.ChartAreas["area"].AxisX.MajorGrid.Enabled = false;
    plt.ChartAreas["area"].AxisX.LabelStyle.Format = "0.0";
    plt.ChartAreas["area"].AxisX.Minimum = x_.First();
    plt.ChartAreas["area"].AxisX.Maximum = x_.Last();

    plt.ChartAreas["area"].AxisY.MajorGrid.Enabled = false;
    plt.ChartAreas["area"].AxisY.LabelStyle.Format = "0.0";
    plt.ChartAreas["area"].AxisY.Minimum = y_.First();
    plt.ChartAreas["area"].AxisY.Maximum = y_.Last();

    plt.Series["line"].ChartType = SeriesChartType.Line;

    foreach (var n in x.Zip(y, Tuple.Create))
    {
        plt.Series["line"].Points.AddXY(n.Item1, n.Item2);
    }
}

private void plt_xlim(Chart plt, double minimum, double maximum)
{
    plt.ChartAreas["area"].AxisX.IntervalOffset = 0.0;
    plt.ChartAreas["area"].AxisX.Minimum = minimum;
    plt.ChartAreas["area"].AxisX.Maximum = maximum;
    plt.ChartAreas["area"].AxisX.Interval = 2;
}

private void plt_ylim(Chart plt, double minimum, double maximum)
{
    plt.ChartAreas["area"].AxisY.IntervalOffset = 0.1;
    plt.ChartAreas["area"].AxisY.Minimum = minimum;
    plt.ChartAreas["area"].AxisY.Maximum = maximum;
    plt.ChartAreas["area"].AxisY.Interval = 0.2;
}

private void plt_show(Chart plt)
{
    Bitmap bitmap = new Bitmap(plt.Width, plt.Height);
    plt.DrawToBitmap(bitmap, new Rectangle(System.Drawing.Point.Empty, plt.Size));
    Cv2.ImShow("plot", BitmapConverter.ToMat(bitmap));
}

000.png

参考文献:
斎藤康毅(2016)『ゼロから作るDeep Learning』オライリー・ジャパン

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

Unity で REST API を呼ぶ (そして Json を parse して Class に変換する)

やること

https://xxxxx.com/xxxxx にアクセスすると下記のような Json を返す API を、

{
    "items": [
        {
            "user_name": "post",
            "points": {
                "of_distance": 5,
                "of_coin": 5
            },
            "total_point": 10,
            "timestamp": 1592761117
        },
        {
            "user_name": "post",
            "points": {
                "of_distance": 5,
                "of_coin": 5
            },
            "total_point": 10,
            "timestamp": 1592761117
        }
    ]
}

Unity から呼び出して、 同じ構造を持った Class へ Deserialize, Parse する。

ライブラリのインポート

UniRx

Unity で非同期処理のタイミング調整(?)が行えるライブラリ。
async/await が使えるようになる。

書き方によっては使わなくても良さそう。

導入方法

AssetStore から一式インポート

unirx.png

https://assetstore.unity.com/packages/tools/integration/unirx-reactive-extensions-for-unity-17276

Newtonsoft.Json

Json の Deserialize, Parse をいい感じにやってくれる。

導入方法

https://qiita.com/kingyo222/items/11100e8f7be396b98453

この方法で NuGet を import して、 NuGet の機能で Newtonsoft.Json を取得。

nuget.png

すると、 Assets/Packages/Newtonsoft.Json.12.0.3 が取得できる。

実装

通信を行うクラス

UnityWebRequest を使って json を取得して、JsonConvert.DeserializeObject を使って Deserialize, Parse を行う。

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using System.Collections.Generic;
using Newtonsoft.Json;

public class HttpSample
{
    public string url;
    public Scores scores { get; private set; }

    public IEnumerator Get(System.IObserver<Scores> observer)
    {
        UnityWebRequest req = UnityWebRequest.Get(url);
        yield return req.SendWebRequest();
        if (req.isNetworkError)
        {
            Debug.Log(req.error);
        }
        else if (req.isHttpError)
        {
            Debug.Log(req.error);
        }
        else
        {
            // json が返る
            string result = req.downloadHandler.text;
            // Scores 型に変換
            this.scores = JsonConvert.DeserializeObject<Scores>();
            observer.OnNext(scores);
            observer.OnCompleted();
        }
    }

    // クラスに `[JsonObject("xxxxx")]` ,
    // クラスプロパティに `[JsonProperty("xxxxx")]` をつけると、
    // json parse 時のフォーマットを変更できる
    [JsonObject("scores")]
    public class Scores {
        [JsonProperty("items")]
        public List<Score> items;
        [JsonObject("score")]
        public class Score
        {
            [JsonProperty("user_name")]
            public string user_name;
            [JsonProperty("points")]
            public Points points;
            [JsonProperty("total_point")]
            public int total_point;
            [JsonProperty("timestamp")]
            public int timestamp;

            [JsonObject("points")]
            public class Points
            {
                [JsonProperty("of_distance")]
                public int of_distance;
                [JsonProperty("of_coin")]
                public int of_coin;
            }
        }
    }
}

呼び出すクラス

async/await を使って同期的に結果を取得し、取得したクラスを使っていく。

using UnityEngine;
using UniRx;

public class ScoresController : MonoBehaviour
{
    // UniRx を入れると async が書ける
    async void Start()
    {
        HttpSample scoreHttp = new HttpSample("https://xxxxxxxxxxxxxxx.com/api");
     // await を使うことで、結果を同期的に扱える。
        HttpSample.Scores result = await Observable.FromCoroutine<HttpSample.Scores>(observer => scoreHttp.Get(observer));
        Debug.Log(result)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#の標準入力された数値を半角スペースで区切る方法を教えてください。

タイトルの通りです
このサイトにもいろいろ書かれているのを見ましたが
やり方がたくさんあったり何年も前のコードだったりと
結局どれが一番使いやすくて無難なやり方なんですか?

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

C#の開発環境を整える(Visual Studio 2019)

はじめに

C#プログラミングを開始するため、環境づくりを説明します。

目次

1.Visual Studio 2019 Community Editionのインストール
2.C#でHello Worldを実行
3.NuGet関係のパッケージをインストール
4.インテリセンスを日本語化

1.Visual Studio 2019 Community Editionのインストール

インストールファイルをダウンロード

MicrosoftのサイトよりCommunity Editionの実行ファイルをダウンロードします。

https://visualstudio.microsoft.com/ja/downloads/

download.png

インストーラーで.NETデスクトップ開発を選択

.NETデスクトップ開発を選択後、ダウンロードおよびインストールしていきます。
Microsoftのアカウントでのログインを要求されますので、新規作成か既存のIDでログインしてください。

CSinstall.png

2.C#でHello Worldを実行

新規プロジェクトを作成

Visual Studio 2019を起動し、C#プログラムを実行する環境を作成してください。

2020-06-28_23h46_26.png

HelloWorldプログラムを実行

新規プロジェクトを作成すると、HelloWorldプログラムが自動生成されます。
ビルド後、実行できるか確認してください。

HelloWorld.cs
using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

実行エラー発生

自動生成のHelloWorldプログラムを実行しようとしたとき、必ずエラーが発生しました。
ネットを調べるとNuGet関係のパッケージがインストールされていないことをが問題のようです。

資産ファイル 'xxx\obj\project.assets.json' が見つかりません。NuGet パッケージの復元を>実行して、このファイルを生成してください。

3.NuGet関係のパッケージをインストール

Visual Studio installerを起動し、個別コンポーネントから、
NuGetのターゲットおよびビルドタスクをインストールします。
インストール完了後、HelloWorldプログラムが実行できることを確認してください。

nuget.png

4.インテリセンスを日本語化

Visual Studio 2019では関数の説明が英語のままで内容が分かりづらいです。
Microsoftのサイトにローカライズされたファイルがあるのでインストールします。
その際、ダウンロードしたファイルは.Net core Release 3.1.xです。

 https://docs.microsoft.com/ja-jp/dotnet/core/install/localized-intellisense

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

C#でHTTPサーバを作る(REST/Grapevine)

一言でいうと、秒で開発用のHttpサーバを立てます。

課題が多いC#のHTTPサーバ

アプリのテスト用にC#でHttpリクエスト(REST)に答えるサーバをお試しに立ち上げたいとして、思ったよりも難しいことに気づきます。

  • ローカルテスト用、実機?環境の対抗機としてPCと外部機器で1対1で単体/結合テスト用のMocサーバを立ち上げたい
  • できればexeスタンドアローンでサーバを立ち上げたい(apache/IIS立ち上げができない環境)
  • VisualStudioではASP.NET MVCを使用することになるが、基本的にIIS依存。IISに依存しない方法については多分調べる時間の方が長い。(気づいたらASP.NET MVCの歴史を勉強されられてた。勘弁してほしい)
  • HttpListenerでHTTPのレスポンスを書くのが一般的だが、URLの処理(ルーティング)は自分で全部実装しないといけない。例えば、"http://localhost/page/" というURLや引数に対する文字列処理をしないといけない。
  • URLの自前処理は非常にセキュリティやバグのリスクが高く、普通はやらない。

と言ったところを堂々巡りします。
そこで、Grapevineというライブラリを見つけたので紹介したいと思います。.NET的にNuGetで入れてチョイチョイで立ち上がるので安心。

Grapevine

git: https://github.com/sukona/Grapevine
Getting started: https://sukona.github.io/Grapevine/en/

LINQPad上で早速立ち上げてみる

LINQPadでF4→Add NuGetから"Grapevine"を検索して"The embeddable~"という長ったらしいのがGrapevineライブラリ本体です。もしLINQPadのNuGet機能が使用できない場合、GrapevineのサイトからDllを入手してF4→Browse...より参照してください。
image.png
次にF4→Additinal Namespace Importsタブから名前空間を登録します。C# のUsingにあたるところになります。
とりあえず以下の名前空間を登録します。
Grapevine.Server
Grapevine.Server.Attributes
Grapevine.Interfaces.Server
Grapevine.Shared
image.png

あとはGetting started: https://sukona.github.io/Grapevine/en/ に沿って進めます。
一番最初のサンプルコードをLINQPadに貼り付けます。

最初のサンプル
using (var server = new RestServer())
{
    server.Start();
    Console.ReadLine();
    server.Stop();
}

LINQPadのLanguageがStatementsまたはC# Programになっていることを確認して、再生ボタンを押してください。
すると以下の画像のようになり、ReadLine()で入力待ちの状態になります。

image.png

この状態になれば"http://localhost:1234" のURLに接続できるようになります。
ポートやホスト名を変える場合はRestServerのコンストラクタで指定できます。
終了時は、LINQPad上ではコンソール入力用の黒い帯が出ているので、ここに何か入れると次の行に進んで正常終了できます。


(追記)
ReadLine()で待ち受けるとLINQPadのダンプがスレッド実行待ちになって止まるので、以下のような動かしっぱなしのコードの方が良いようです。止めるときはメニューのQuery→Cancel All threads and Resetsで止めます。

最初のサンプル
var server = new RestServer();
server.Start();

NuGetやサイトからDll取ってくる→ソースコピペ→実行の3ステップで立ち上げまで一瞬でした。
引き続きGetting startedを進めるのですが、クラスを書く場合はLINQPadのLanguageをC# Programに変更してください。
次に書いてあるLoggingについては、標準出力へのトレースログはそのままLINQPadへ出力されます。

Grapevineでのサイト(システム)構築

GrapevineではURLのルーティングはクラスやメソッドに属性をつけるだけで自動的にやってくれるので、完全にストレスフリーです。
Routingでヒットしたメソッドはdllの定義順に実行され、context.SendResponse()などで、レスポンスを返した時点で以降のRoutingは実行されません。また一度もレスポンスを返す処理が実行されなかった場合、ライブラリのエラーレスポンスとなります。(私の環境では文字化けした怪しめの出力になりました)

(実際に簡単なサイトを構築した例は別途掲載予定。)

さいごに

GrapevineはIISとSystem.Webを駆逐してやると謳っているだけあって、使用感がシンプルで好感が持てます。
LINQPadを使用して、秒でスクラッチコードを書いて動かせるので素晴らしい。

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