- 投稿日:2020-06-29T23:52:52+09:00
[C#/WPF] ユーザーがログイン/ログアウト、PCをシャットダウン等したときになにかしたい
もくじ
⇒https://qiita.com/tera1707/items/4fda73d86eded283ec4fやりたいこと
アプリ起動中に、下記のような操作でユーザーがシャットダウンやログオフなどをしたとき、またその逆のログインしなおしたとき等に、アプリ側でそれを知って、しかるべき処理をしたい。(何かのサーバーやデバイスと通信するようなアプリであれば、一旦通信を終わらせたり、逆に再開したりしたい)
やり方
SystemEvents
にある各種イベントを利用する。
↓この辺を使う。
→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と同じ階層にログを出力してくれる仕組み。ロック、再度サインイン
結果
SessionSwitch
イベントで、e.Reason
がSessionLock
→SessionUnlock
がきた。
他のユーザー切り替えして、戻ってくる
他のユーザーに一旦切り替えて、また自分のユーザーに戻ってくる(サインインしなおす)
(=FUS(Fast User Switching)→あるユーザーがログインしたまま、別のユーザーもログインしてるイメージ)
結果
SessionSwitch
イベントで、下記の順番で来た。(下から順番に)
※今回は自分のPCで直接ユーザー切り替え操作を行ったが、もしこれがリモートデスクトップでの接続だったら、ConsoleConnect
とConsoleDisconnect
が、RemoteConnect
とRemoteDisconnect
になるっぽい。サインアウト
結果
SessinEnding
のLogoff
と、そのあとにSessinInded
のLogoff
が来た。
スリープ、復帰
シャットダウン
結果
SessionEnding
のSystemShutdown
のあとに、SessionEnded
のSystemShutdown
が来た。
参考
SystemEvents.PowerModeChanged イベント
https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.win32.systemevents.powermodechanged?view=dotnet-plat-ext-3.1上記ページの左側に、イベントが一覧になってるので、そこから必要なイベントに飛べる。
イベントの引数を見て、詳細な情報(例えば、SessionSwitchであれば、suspendなのか、resumeなのか、など)を知る必要があるが、そのへんは、同じページの下の方に、
〇〇Args
へのリンクが載ってるので、そこから飛んで、引数を調べればいい。
- 投稿日:2020-06-29T23:11:32+09:00
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
にアクセスして確認します。結果
上記のように時計が表示されて1秒毎に更新されます。
最後に
ちょっと起動時の
Loading...
表示が長かったりするかなとは思いましたが、こういった少し動きのあるページがJavaScript書かずにブラウザ上で動いているのはなかなか面白いですね。なお、今回のサンプルコード全体はこちらのGitHubリポジトリで参照できるようにしてありますので、興味あればご覧くださいませ。
では。
- 投稿日:2020-06-29T23:00:12+09:00
【Unity】アウトゲームでの設計でMV(R)Pパターンを採用しました【設計】
0.前提と推奨するターゲット
前提
・実際に長期運用を行ったわけではないため、メリット、デメリットが自己解釈になる部分があります。
ターゲット
・
UniRx
をある程度、理解している方
・MVPパターンなどのアーキテクチャパターン
をある程度、理解している方
・uGUI
に触れたことがある方1.MV(R)Pパターンとは
UniRx開発者の@neue ccさんが提唱している
MVPパターンとUniRxを組み合わせたUIアーキテクチャパターン
です。最近のソシャゲ設計では、割とスタンダードなのかなと思います。上記画像の②ViewとPresenter間の入力検知や、③ModelとPresenter間の値の変更検知をUniRxで執り行います。
2.アウトゲームでのMV(R)Pパターン
2.1 画面を階層構造で構築しました
画面は階層構造で構築しました。
Scene(Class) -> (Canvas) -> Window(Class) -> Screen(Class)
SceneがWindowを管理し、WindowがScreenを管理すると言うルールを決めました。とても参考になった記事
もしあなたがアウトゲーム(UI)を つくることになったら with Unity
Web出身のUnityエンジニアによる大規模ゲームの基盤設計OutGameScene内にGachaWindowやCharacterWindowなどがあり、GachaWindowにはGachaTopScreenやGachaResultScreenなどがあります。
2.2 画面単位でMVPを構築しました
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クラスの中に、ボタンやテキストがまとめられています。通常のMV(R)Pでの1:1のもの、複数:1のもののそれぞれのメリット、デメリットがあります。下記のスライドはとても参考になります。
今回はViewが画面単位でまとめられているため、上記サイトで記載されているメリット、デメリットに合致しないことが複数あり、自分なりの今回の実装特有のメリット、デメリットを以下に書きます。
メリット
・Viewが画面単位でまとめられていることで、View要素の取得がしやすい(紐付けがしやすい)。
・画面単位でまとめられているため、ざっと見やすいと言う感覚
・ルールが決まっているため、画面単位でのコードが追いやすい。
デメリット
Viewが画面単位でまとめられていることで、小さいテストがしにくかったり、要素数の変更がしにくい可能性あり。3.実装
シンプルな実装を例に一画面を解説します。
ボタンをクリックすると、テキストにクリック回数が反映されます。
この画面を仮にHomeWindowとします。WindowやScreen単位でプレハブ化し、生成や破棄などを行うことで、画面遷移を実現します。
View
HomeWindowView.cspublic 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
にも、触れていこうと思います。(気になった方は是非調べてみてください)
- 投稿日:2020-06-29T20:00:29+09:00
Xamarin.FormsでAndroid向けTwitterアプリケーションを開発したときに困ったこと ~WebView編~
N.Mです.
最近はAndroid用のアプリケーション,"TLExtension"を作っていました.AndroidでTwitter用のブラウザを見つつ,タブ操作で簡単に今まで作ってきたTwitter用ツールも利用できるアプリケーションです.自分用に作ったので,アプリ自体の公開はしていませんが,ひな型部分のソースコードはGitHubにも公開しました.
このアプリは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.WebView
のEval
メソッドやAndroid.Webkit.WebView
のEvaluateJavascript
メソッドにjavascript: jsBridge.invokeAction();
を渡すと,JSBridge
クラスのメソッドが呼び出されます(jsBridge
は④のAddJavascriptInterface
で登録した名前,invokeAction
は③のExport
に登録した名前にです.).JSBridge
クラスのメソッドの引数として文字列を渡せるようにしておけば,javascript: jsBridge.invokeAction(document.documentElement.outerHTML);
で,そのページのHTMLをC#に取得することができます.⑥
Xamarin.Forms
のWebView
でこの取得したHTMLを使用するならば,WebViewRenderer
でJSBridge
からデータを受け取り,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
で全画面表示の通知が発生したら,③で実装したDefaultEnterAsync
やDefaultExitAsync
が呼び出されるように,処理を繋げます.(以下はフルスクリーン対応に必要な部分のみ抜き出しています.)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.OnShowCustomView
→TLExtensionWebViewRenderer.OnEnterFullScreenRequsted
→TLExtensionWebView.DefaultEnterAsync
の順で全画面表示が解除されるときは
TLExtensionWebChromeClient.OnHideCustomView
→TLExtensionWebViewRenderer.OnExitFullScreenRequsted
→TLExtensionWebView.DefaultExitAsync
の順で呼び出され,
Xamarin.Forms
側まで通知が行くようになり,全画面表示に対応できるようになります.ソフトウェアキーボード表示時のWebViewの縮小
参考:https://qiita.com/amay077/items/6fcdec829a96bc604532
デフォルトのWebViewでTwitterのリプライをしようとすると,ソフトウェアキーボードが邪魔で自分が書いているツイートが見えなくなります.
ひとまず,
Xamarin.Android
側のMainActivity
のOnCreate
メソッドに以下を入れれば解決します.//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
と連携しているWebViewRenderer
のControl.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
と連携しているWebViewRenderer
のControl.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
にお世話になる機会も多くなるかと思います.
- 投稿日:2020-06-29T19:51:28+09:00
ダミープリンタサーバ(Bullzip PDF Printer)
帳票(ActiveReport(ActiveReportは単独でPDFの出力機能があるがプログラムの修正が必要になる)やCrystal Reports)で作成したものをPDFで印刷したい場合、サーバがAWSやAzure等のクラウドにあり、社内のプリンタに出力できない場合、このソフトをサーバにインストールしてBullzipのプリンタ(通常のプリンタとして認識される)に印刷指示をするだけでPDFに出力できるようになる。
はじめに
基本は無料版を使っても問題ないが、印刷すると印刷した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設定方法
印刷テスト
ライセンス
Free コミュティ版
https://www.bullzip.com/products/pdf/download.phpThe 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のフォルダにエディションによる違い
https://www.bullzip.com/products/pdf/features.php
Community
試用版のメッセージがPDFのフッタ、プロパティに表示される。
https://www.bullzip.com/products/pdf/trial.php
Standard
- 印刷を既存のドキュメントと結合する
- 名前と設定が異なる複数のPDFプリンターをインストールする
- オフラインインストールをサポート
Professional
- ターミナルサーバー、RDP、またはCitrixで実行
- PDFパスワードで強力な暗号化を使用する
- 高解像度の背景に印刷する
- 画像圧縮を無効にする
Expert
プロフェッショナル機能に加えて、次のこともできます。
- 暗号化されたPDFドキュメントを新しいPDFドキュメントに印刷する
- PDFドキュメントの代わりにMicrosoft Wordドキュメントを作成する
- 作成したPDFを別のプリンターで印刷する
- HTTPSまたはSFTPを使用してPDFをサーバーに自動的にアップロードする
エラーの対処
※エラーがあったら都度記入していく。
- 投稿日:2020-06-29T18:26:40+09:00
【Unity(C#),PUN2】OculusQuestのハンドトラッキング同期実装
デモ
やってることは見たまんまの位置同期ですが、
ハンドトラッキングの実装は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); } } }この処理によって、動的にボーンとなるオブジェクトが生成されます。
この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が動的に生成、設定されているのがわかります。
④
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; } } } } }同期実装の流れ
ハンドトラッキングの一連の流れが明らかになったので、
いよいよ同期処理を考えていきます。オブジェクト同期と呼ばれる手法を用います。
流れとしては下記イメージです。
①各クライアントが手の位置情報を保持
②手の見た目のみの役割を持つオブジェクトを用意し、各自の手の位置情報に追従させる
③手の見た目のみの役割を持つオブジェクトを双方のクライアントに生成
④お互いの手の位置情報を送り合い、生成した手の位置情報を更新
おおざっぱではありますが、こんな感じです。
ハンドトラッキングでの同期方法
ここまでの理解でもかなり骨が折れましたが、本当に大変なのはここからでした。
一連の流れを見ればわかりますが、
手の見た目のみの役割を果たす同期用オブジェクトを
用意する必要があります。やり方としては、2つの選択肢があります。
1つは、事前にボーンとなるオブジェクト及び、手のメッシュを用意することです。実際に下記の海外勢のサンプルでは、この手法を用いていました。
【参考リンク】:SpeakGeek-Normcore-Quest-Hand-Tracking(サンプルではPUN2とは別のNormcoreというライブラリを使用して同期の実装を行っています)
下記GIFのように、あらかじめ同期するオブジェクトの中に
Bone及びBindPoseのオブジェクトがびっしりと用意されています。
ボーンの役割を担うオブジェクトはBindPoseも合わせると片手だけで全部で48個あります。
しかも、それぞれの座標が生成時に(0,0,0)ではないデフォルト値を持つので
自前で事前に用意するとなると、
生成時のすべての値を48×2回メモして一つずつ手打ち、、、もしくは
プレイモードで生成されたオブジェクトをそのまま保存できるスクリプトを用意する、、、
などなかなかの手間となります。さらに、プロジェクトを跨いで利用する際には
毎回オブジェクトをインポートする必要があるので少々効率が悪いです。そこで、もう一つのやり方として、
OVR系コンポーネントと同様に手の見た目のオブジェクトを動的に生成する方法を用います。しかし、既存のOVR系コンポーネントは切り離すことが困難な蜜結合な状態になっています。
ですので、手の見た目の役割を果たすオブジェクトを生成する処理を
OVR系コンポーネントから拝借して自前で用意する必要がありました。同期実装のコード
ここから実装の核心となるコードの説明です。
先ほどの同期実装の流れ
と合わせて見ていきます。
①各クライアントが手の位置情報を保持
この処理に関しては完全にOVR系コンポーネントに担ってもらいます。
OVRHand
とOVRSkeleton
をアタッチしたオブジェクトを各手のAnchorの子階層に配置します。
②手の見た目のみの役割を持つオブジェクトを用意し、各自の手の位置情報に追従させる
この処理に関しては長くなるので
・手の見た目のみの役割を持つオブジェクトを用意
・各自の手の位置情報に追従
の二つに分けて説明していきます。
手の見た目のみの役割を持つオブジェクトを用意
メッシュの生成に関しては
OVRMesh
をそのまま利用します。
生成するオブジェクトにSkinnedMeshrendererと共にアタッチしておきます。
ボーンの役割を担うオブジェクトの生成に関しては
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文を利用したボーンの情報をリストに順番通り取得してくる処理
が容易になっています。ここまでの処理で
手の見た目のオブジェクト
だけを同期オブジェクトとして実装することが可能となりました。③手の見た目のみの役割を持つオブジェクトを双方のクライアントに生成
この処理に関しては非常に簡単です。
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
に配置する必要があります。④お互いの手の位置情報を送り合い、生成した手の位置情報を更新
最後に同期オブジェクトの位置情報を共有する実装です。
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の面からまだまだ課題は多いです。今後も引き続きハンドトラッキング含め、同期に関して
調査しようと思っています。参考リンク
- 投稿日:2020-06-29T16:48:28+09:00
【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もあまり変わらない気がします。
- 投稿日:2020-06-29T16:48:28+09:00
【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もあまり変わらない気がします。
- 投稿日:2020-06-29T15:23:26+09:00
順列 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; } } }
- 投稿日:2020-06-29T14:59:48+09:00
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("正の整数です"); }
- 投稿日:2020-06-29T14:38:11+09:00
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.cspublic 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.cspublic 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以内」で達成出来そうです。「この精度で十分かどうか」、それはもちろん使う用途によりますが、このデータが判断の手助けになれば幸いです。
- 投稿日:2020-06-29T08:10:08+09:00
C#のみを使って、今ソーシャルゲームアプリを作るとしたら
はじめに
現在進行形でC#のみを使って個人でソシャゲ作りを試しているyoship1639です。
本記事はQiita夏祭り2020「〇〇(言語)のみを使って、今△△(アプリ)を作るとしたら」のテーマに沿った内容となっています。近年のソーシャルゲーム界隈は多様化が進んでクライアントサイドだけではなくサーバーサイドもあらゆる言語やフレームワークが試みられていますが、クライアントもサーバーも統一の言語で構成されているのはほとんどないかと思われます。言語にはその言語の得意分野があると思うので。
しかし、今まさに私が開発中の環境が好きな言語で開発しやすいという理由でクライアントもサーバーもC#で構成した作りになっているので、どのような構成でどうすれば最低限のソシャゲの基盤が作れるかを、解説が長くなり過ぎないようにまとめることが出来ればと思います。
三部構成で、クライアント実装、サーバー実装、AWS EC2へのデプロイまで解説できればと思っています。
Let's、C#のみでソシャゲを作ろう!ソシャゲの概要
内容に入る前に、ソシャゲがどの様な流れで動作するのかを軽く説明します。
ソシャゲは基本的にクライアント(スマホ端末)とサーバーとのやり取りで動いています。サーバーが動いていないとクライアントは基本動作しません。これはクライアント側で不正にデータの書き換えをされると運営が困るからです。
サーバー側は大体以下の様なAPI機能を備えています。
- アプリバージョン判定
- マスターデータ・アセットバンドル更新判定
- ログイン (セッション管理)
- アカウント作成
- クエスト開始・終了
- ガチャ
- etc...
挙げたらきりがないくらいにはサーバーにはやらなければならない仕事があります。それだけクライアントとサーバーは適所で通信しています。こうすることで、例えばクライアントのデータが紛失したとしてもサーバーから復元することが出来ますし、クライアント側で不正があったらサーバー側で検知してBANすることもできますし、ユーザーのアプリ上での動向から問い合わせにも対応することができるようになりますし、課金周りのレシート検証もサーバー側で正確に行えるので、課金したのに石が反映されないみたいな場面でも補填対応することが出来るようになります。基本ユーザにとっても運営にとってもメリットしかないです。
近年バックエンドはBaas(PlayFab、Firebase、GameSparks、GS2など)が鎬を削っており態々バックエンドを自前で準備しなくてもBaasを使うという手段がありますが、ドキュメントが英語のみだったり痒いところに手が届かなかったりと一長一短なので、どうしてもサーバーサイドを触りたくないという訳ではないのであれば個人的にはまだ自前で準備したほうが良いかな感はあります。
ソシャゲの動作の最初の流れとしては以下の様になります。
- アプリバージョンを検証
- ログイン (ログインできなかったらアカウント作成)
- 更新データ確認 (アセバン、マスターデータ)
- 以降アプリによって色々
今回は最低限の基盤だけ考えるので、2番の「アカウント作成」と「ログイン」機能を作りたいと思います。
構成の全体像
今回作るサンプルは、C#のみで構成するソシャゲの最低限の基盤で以下の構成となっています。
クライアント:C#(Unity2019.X)
サーバー:C#(.NetCore3.1)
デプロイ:AWS EC2(Amazon Linux 2)
サーバー <--> クライアント:MagicOnion(HTTP/2, gRPC)クライアントは皆大好き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フォルダのチェックを外してインポートしてください。
最後に、grpcをインポートします。
https://packages.grpc.io/こちらのページの最新のコミットのBuild IDをクリックし、C#欄にある「grpc_unity_package.XXXXX-dev.zip」をダウンロード、解凍します。
解凍すると「Plugins」フォルダがあるはずなので、Pluginsフォルダの中身をUnityのAssets/Pluginsフォルダに入れてインポートします。それでもまだ怒られると思うので、エラーを解決していきます。
- System.Buffersが被っているので、どちらかを削除
- System.Memoryが被っているので、どちらかを削除
- System.Runtime.CompilerServices.Unsafeが被っているので、どちらかを削除
- unsafeコードが許可されていないぞ☆って怒られるのでunsafeコードを許可
これでエラーは出なくなるはずです。
② Serviceを定義
諸々インポートが完了したらServiceを定義します。ServiceとはWebAPIと同様のものと考えていただければと思います。
ソーシャルゲームは基本的に特定の動作ごとにサーバーにAPIを投げてそのレスポンスを基にクライアントを動かします。本来、API定義を考える場合「https://〇〇〇〇/create_account」みたいなエンドポイントやらスキーマやらを考えなくてはいけませんが、MagicOnionの場合はインターフェース定義自体がそれに当たります。これメチャクチャ便利です。
アカウント作成とログインの機能は、以下の様に定義できます。
IAccountService.csusing 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.csusing 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.csusing 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」にしました。
プロジェクトの作成が終わったら、クライアントとの通信に必要なコンポーネントをNuget経由でインストールします。
プロジェクトのコンテキストメニューの「Nuget パッケージの管理(N)...」からMagicOnion.Hosting
をインストールします。バージョンは最新の安定板で大丈夫です。
一応、Unityで使われている型をサーバーでも扱えるように
MessagePack.UnityShims
もインストールしておきます。
これでサーバーサイドに必要なコンポーネントがインストールできました。
② Mainプログラムの記述
サーバーを起動するMainプログラムを記述します。
やっていることはとても単純で、ログ出力先をコンソールに指定し、ホストとポートを指定して起動しているだけです。Program.csusing 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(); } } }一応これだけでもサーバーを起動することはできます。デバッグ実行すると以下の様な画面が出るはずです。
これだけでは何の機能もない張りぼてサーバーなので、クライアント側で実装した「アカウント作成」と「ログイン」機能を実装していきます。
③ AccountServiceの実装
サーバー側のアカウント作成とログイン機能の実装をします。
クライアントで定義したIAccountService.csが必要なので、予め丸々コピーしておいてください。(本来は、submodule等用いてソースコードの共有をすることをお勧めします。)アカウント作成は、ランダムなハッシュ値を用います。ユーザーIDは20桁、パスワードは12桁にしておきます。ログインは作成されたユーザーIDとパスワードを検証し、一致したら以降のAPIを呼び出すことが出来るセッションを返します。セッションも一先ずランダムな20桁のハッシュ値を返します。
AccountService.csusing 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(); } } }これで、アカウント作成とログイン機能を備えたサーバープログラムが整いました。
④ ローカル環境で動作確認
ここまでで、ローカル環境で動作確認をすることが出来るようになったので、確認してみます。
サーバーをデバッグ実行してローカルサーバーを立ち上げ、クライアントをデバッグ実行します。
問題がなければクライアントは以下の様に表示されるはずです。
サーバー側は以下の様に表示されます。
ローカルで問題なく動作できていることが確認できました。
最後に、実際にクラウド上にデプロイして確認してみたいと思います。デプロイ
ローカル環境で問題なく動作させることが確認できれば、本来はデプロイまでは頑張らなくてもいいですが、せっかくなのでEC2へのデプロイまでやってみたいと思います。CI/CDやDockerコンテナでもよかったのですが解説が逸れそうなので直デプロイします。
手順としては、以下の様になります。
① awsでEC2インスタンスを用意、起動する
② ターミナルでEC2インスタンスにログイン
③ .NetCore3.1をインストール
④ サーバープロジェクトを配置、実行
⑤ クライアント動作確認① awsでEC2インスタンスを用意、起動する
最初にAWSコンソールにサインインします。アカウントを持ってない人は作ってください。
サインインしたらEC2を選択します。EC2は仮想サーバーみたいなものだと思ってください。
名前を「Qiita2020TestServer」(名前は何でもいいです)にして、「キーペアを作成」をクリックします。
キーペアが作成されppkファイルがダウンロードされます。
このキーは後で作るインスタンスへのログインに必要なので、大切に保管しましょう。何のインスタンスを作るか聞かれるので、「Amazon Linux 2 AMI」を選択してください。
どのスペックの仮想マシンを立ち上げるか聞かれるので、無料で使える「t2.micro」を選択し、「次のステップ:インスタンスの詳細と設定」をクリック。
いろんな設定項目がありますが、ここでは「自動割り当てパブリックIP」を「有効」にします。
有効にしたら「次のステップ:ストレージの追加」へ。
ストレージは30GBまで無料らしいので一先ず30GBに設定し「次のステップ:タグの追加」へ。
タグの追加を押し、キーに「Name」、値に「Qiita2020TestServer」と入力します。(値はわかれば何でもいいです)
入力したら「次のステップ:セキュリティグループの設定」へ。
「ルールの追加」を押し、以下の様に入力し12345ポートを解放します。
分かりやすいように説明も入れておきましょう。(※画像は日本語ですが、日本語の説明ではインスタンスが作成できなかったので、英語で入力してください!)
入力したら「確認と作成」をクリック。
確認画面で「すべてのIPからインスタンスにアクセスできるよ、いいの?」と警告されますが気にせず「起動」を押します。
インスタンスに安全に接続するためのキーペアを選びます。先ほど作成した「Qiita2020TestServer」キーペアを選んで、「インスタンスの作成」をクリックします。
インスタンス作成中の画面が表示されるので、右下の「インスタンスの表示」をクリックしてください。
すると、作成されたインスタンス一覧が表示されます。問題がなければインスタンスはそのまま起動します。
これで、インスタンスの準備は整いました。
作成したインスタンスの「IPv4パブリックIP」はターミナル接続先なので控えておいてください。② ターミナルでEC2インスタンスにログイン
EC2インスタンスにログインします。
ターミナルソフトは何でもいいですが、私は「TeraTerm」を使って解説します。TeraTermを起動したらホストに先ほど作成したインスタンスのパブリックIPを入力してOKをクリック。
SSH認証が必要なので、ユーザ名に「ec2-user」、認証方式には最初の方に作成したキーペアの秘密鍵「Qiira2020TestServer.ppk」を指定します。
問題なくSSH接続できたら以下の様に表示されます。ここからはいつものターミナルです。
③ .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
を使うと色々捗ります。
最後にクライアントからEC2上にアクセスできるか確認します。
⑤ クライアント動作確認
クライアントのapplicationHostが
localhost
のままなので、NetworkManagerのこの部分をEC2インスタンスのパブリックIPに置き換えてください。
置き換えて、クライアントをデバッグ実行します。そして、以下の表示が出たらEC2との接続に成功です!
これで、C#のみで作ったソシャゲの最低限の基盤ができました。
ここからマスターデータやアセバン管理、クエスト処理やガチャ処理等を生やしていけば、C#のみで作るソシャゲの出来上がりです。お疲れまでした。おわりに
いかがでしょうか、意外と簡単にソシャゲの最低限の基盤を作ることができたのではないかと思います。もちろん本物はこれだけじゃ済まないボリュームですが、基礎を捉えるのは大きな前進になるのではないかと思います。
今回は省きまくりましたが、セキュリティだけはしっかり設定してください。ソシャゲはユーザの大事な情報を管理するので、ガバガバ設定では当然許されません。最低でも、ユーザーデータの暗号化、SSL、サーバー監視はしっかりするように!
皆様はもちろんC#信者だと思うので、その熱意をUnityだけではなくそのままサーバーサイドにも向けてみてはいかがでしょうか。私もまだまだサーバーサイドは勉強中なので偉そうなことは言えませんが、クライアントサイドとはまた別の面白さがあるので、やりがいはいっぱいあるかと思います。
頑張れば、一人でもソシャゲが作れる時代です。
最後まで読んでいただき、ありがとうございました。
参考資料
- 投稿日:2020-06-29T07:12:45+09:00
C#のみを使って、変更前後の差分表示のできる、ファイル名変更ソフトを作った。
概要
2020年にもなってファイル名変更ソフトを新しく作りました。名前はFileRenamerDiffです。
この記事では、作った動機とソフトの機能紹介をします。
C#/.NET Core/WPFで作成しました。使用した技術などは別の記事にする予定です。
C#のみと書きつつ、XAMLも使っていますが、ビルド時にC#になっていますので、良しとしておきます。なぜ作ったか
ファイル名変更ソフトを使ったことのあるかたは多いと思います。私自身も今までは別のソフトウェアを使っていました。
ただ、その中でいくつかの不満があり、それに対応したソフトウェアが見つかりませんでした。
具体的には以下のようなものです。
- 変更前後での差分表示:ファイル名が長い場合にどこが変わったかの確認が大変だった
- 重複判定:プレビューで確認してから、ファイル名変更時にファイル名重複で処理が中断するとツライ
- 設定パターン数の制限:削除&置換パターンの数に制限があると何回も設定を変えて処理する必要がある。
- デザイン:ユーティリティソフトにありがちな灰色ばっかの画面は好きになれない
そこで、これらの不満を解消したソフトを自分で新しく作ることにしました。
機能紹介
基本的な使い方は以下のとおりです。
- ファイル名を変更したいフォルダをドラッグアンドドロップまたはダイアログで指定
- ファイル名の削除・置換パターンを指定
- プレビューで変更内容を確認
- 実際にファイル名を保存
変更前後での差分表示
ファイル名変更前後での差分をGitのDiffっぽく表示しました。ソフト名通りにこれが一番作りたかった機能です。
制限数なしの正規表現による削除&置換パターン
制限数なしで削除&置換パターンを作れます。正規表現も使えます。
変更後のファイル名重複判定
プレビュー時点でファイル名の重複を判定して、警告してくれる機能です。
重複があるままだと、保存できません。その他の機能
Light/Darkテーマ切り替え
任意のファイルをエクスプローラーで開く機能
重複判定時などに、このファイル名だけ手直ししたい、といった時にそのファイルのディレクトリをエクスプローラーで開く機能です。
よく使うパターン集
削除・置換でよく使うパターン集です。これはまだ数が少ないので、これから増やしていきます。
ファイル・フォルダをリネーム対象にするか選択可能
ファイルだけorフォルダだけリネームしたい時があるので選択できるようにしました。
拡張子での除外判定
iniファイルなど、特定の拡張子をリネーム対象から除外する機能です。
変更あり・重複ありのみ表示
変更のあるファイル名だけ・重複のあるファイル名だけ、に表示をしぼりこむ機能です。
多言語対応
英語・日本語・中国語・ロシア語・ドイツ語に対応しています。
基本はDeepLです。
英語と中国語はなんとなく分かるから、ドイツ語とロシア語は知り合いに聞ける人がいるから選びました。Microsoftストアでの公開
一度やってみたかった。思ったよりも簡単でした。この方法などは後日記事にします。
ダウンロード
ソフトウェアは以下の場所から入手できます。
Microsoft Store (Windows 10 のみ)
Portable version (Windows 7以降)
FileRenamerDiff_app_win-x64~
は x64(64bit) OS用FileRenamerDiff_app_win-x86~
は x86(32bit) OS用まとめ
自分が欲しかったソフトを作ってみました。よかったら使ってみてください。
使ってみた感想、バグや機能要望などがあれば、ここのコメント欄やGitHubのIssueに書いてくれると嬉しいです。
- 投稿日:2020-06-29T06:36:32+09:00
シグモイド関数
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)); }参考文献:
斎藤康毅(2016)『ゼロから作るDeep Learning』オライリー・ジャパン
- 投稿日:2020-06-29T02:13:05+09:00
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 から一式インポート
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 を取得。
すると、 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) } }
- 投稿日:2020-06-29T01:37:50+09:00
C#の標準入力された数値を半角スペースで区切る方法を教えてください。
タイトルの通りです
このサイトにもいろいろ書かれているのを見ましたが
やり方がたくさんあったり何年も前のコードだったりと
結局どれが一番使いやすくて無難なやり方なんですか?
- 投稿日:2020-06-29T00:17:44+09:00
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/
インストーラーで.NETデスクトップ開発を選択
.NETデスクトップ開発を選択後、ダウンロードおよびインストールしていきます。
Microsoftのアカウントでのログインを要求されますので、新規作成か既存のIDでログインしてください。2.C#でHello Worldを実行
新規プロジェクトを作成
Visual Studio 2019を起動し、C#プログラムを実行する環境を作成してください。
HelloWorldプログラムを実行
新規プロジェクトを作成すると、HelloWorldプログラムが自動生成されます。
ビルド後、実行できるか確認してください。HelloWorld.csusing 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プログラムが実行できることを確認してください。4.インテリセンスを日本語化
Visual Studio 2019では関数の説明が英語のままで内容が分かりづらいです。
Microsoftのサイトにローカライズされたファイルがあるのでインストールします。
その際、ダウンロードしたファイルは.Net core Release 3.1.xです。https://docs.microsoft.com/ja-jp/dotnet/core/install/localized-intellisense
- 投稿日:2020-06-29T00:15:36+09:00
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...より参照してください。
次にF4→Additinal Namespace Importsタブから名前空間を登録します。C# のUsingにあたるところになります。
とりあえず以下の名前空間を登録します。
Grapevine.Server
Grapevine.Server.Attributes
Grapevine.Interfaces.Server
Grapevine.Shared
あとは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()で入力待ちの状態になります。この状態になれば"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を使用して、秒でスクラッチコードを書いて動かせるので素晴らしい。