- 投稿日:2019-12-14T23:37:05+09:00
Zenject を使ったプロジェクト運用
最近仕事のプロジェクトで Zenject を使うようになって、個人的に実運用をしていくのに有用そうなワークフローを確立しつつあるので、それを紹介します。基本的に UI をベースにしたゲームを作っているので、そのような書き方が多くなると思いますが、大体は普遍的にゲームに適用できるかと思います。ちなみに DOTS は範囲外とします(DI じゃなくて ECS を使おう!)。
Zenject の前に
Zenject に関する説明はネット上にころころあるので既に知っている方が多いかと思うのですが、必要最低限な前提を記述しておきます。
Zenject は、いわゆる DI(Dependency Injection)フレームワークと呼ばれるもので、DI を行うためのツールになっています。
DI は、オブジェクト指向で言われるSOLIDの「依存性逆転の原則」を実現するための手法です。例えば以下のような、モデルとビューの依存関係があるとします。(簡素化しています)
モデルがビューを所有し、モデルに変更があった場合ビューを直接呼び出しています。これはビューに変更に非常に弱い形になります。
SetHp
の引数が変われば破綻しますし、View
クラスが削除されても破綻します。これを解消するために、依存性を逆転させて変更への弱さを解消します。
View
クラスへの矢印が逆転し、モデルは間接的にView
クラスを参照するようになりました。これにより、ビューへの変更はIView
の実装を脅かさなければモデルに影響を与えませんし、モデルもビューの変更を気にすることなく実装を変更することができます。これが大まかに DI を行うメリットとその手法なのですが、実際には「誰が
View
を生成し、Model
に渡すのか?」という部分が欠落しています。それを達成するために、たとえば MVP や MVC、MVVM など様々な手法が開発されています。それを手助けするのが DI の仕組みです。なぜ Unity で DI したいのか
Unity で開発を行う場合、一番重要なパーツはシーンに配置されるゲームオブジェクトです。ゲームオブジェクトがなければ何も画面上に表示されません(DOTS を除く)。ですので、各スクリプトはゲームオブジェクトが存在する前提で動作しますし、また必要になるパーツも大体はゲームオブジェクトが付随しています。
しかし、ゲームオブジェクト自体が管理できないもの(するのが不便なもの)もあります。例えば主人公の体力などはゲームオブジェクトで保持してもいいかもしれませんが、ステージの数や出てくる敵、装備画面の説明文など、ゲームオブジェクトに付随させると大変になるデータも存在します。そのようなデータを MonoBehaviour に所持させ、DontDestroyOnLoad などを使って使い回したり、static な変数でどこかに保持したりしていると、不便な場面がやってきます。
例えば、アクションゲームを作っていて、主人公キャラを作っているとしましょう。Input を使って移動させたり、弾を発射させたり。いざ実装が終わって実行してみると、なぜか NullReferenceException だけがコンソールに表示され何も動きません。なぜか? static で保持しているプレイヤーの初期 HP が初期化されていませんでした。仕方ないので、Start メソッド内に
#if UNITY_EDITOR
で括った範囲を作り、そこに static 変数を初期化するメソッドを書きます。また実行してみますが、今度は Hp が -1 になっていて、すぐにプレイヤーが死んでしまいました。どうやら、DontDestroyOnLoad
な他のスクリプトで初期化が走っていて、-1 に設定してしまっているようです。更に#if UNITY_EDITOR
のブロックの中にStaticClass.IsInit = true;
と記述し、再度初期化が行われないようにしたのです。さて、これ動いたプログラムが実際のアプリケーションでちゃんと動作すると自信を持って言えるでしょうか? 私はこのような状況に陥ったとき、デプロイされたコードに対して非常に不安になります。このような状況をなるべく緩和してくれるのが、Zenject の仕組みになるわけです。
Let's Zenject
Zenject 自体の使い方については、他にいろいろ具体的で詳細な使用方法が記載されている記事にお任せして、こちらでは主に設計と、それを考えていく道筋を紹介したいと思います。
ただ、何も知らない状況の方が見ている可能性もありますので、ひとまず読み進められるよう簡単に使い方を説明しておきます。
Zenject では、以下のような流れでインジェクションを行っていきます。
- シーンに、SceneContext というコンポーネントのついたゲームオブジェクトを設置します(これはコンテキストメニューから行います)
- SceneContext に対して、
MonoInstaller
というクラスを継承したゲームオブジェクトを指定します。- 指定された
MonoInstaller
は、DiContainer
というクラスのインスタンスにバインド情報を登録します。- シーンがロードされる際、シーン内に存在するスクリプトの
[Inject]
属性がついているメンバ全てにインジェクションを行います。ざっくりとこんな感じの流れになります。
また、Zenject を使う際、SceneContext について以下の前提条件を知っておく必要があります。
- シーンごとに1つ設置できる。
- 親子関係が作れる
- 子のシーン(Additive なシーン)にのみインジェクトすることができる
SceneContext のこの仕様を利用して、インジェクションを行っていきます。
実践
以下のようなコードを、Zenject を使って開発しやすいコードにしていきましょう。
public class WeaponList : MonoBehaviour { [SerializeField] private ListDisplay list; private async void Start() { IEnumerable<Weapons> weapons = await ApiManager.GetWeaponList(); foreach(var weapon in weapons) { list.AddItem(new ListDisplayItem { Text = weapon.Name }); } } }このコードには、
ListDisplay
というリストを UI に表示するクラスと、武器データのモデルであるWeapons
クラス、API を管理し呼び出しを行うApiManager
リストがあります。これらを使用し、武器の一覧を表示する UI だとします。(API を使用するで、きっと所持アイテムリストとかでしょう)このコードの問題点は、
ApiManager.GetWeaponList
が確実に動作していることが前提になっているという点です。これは個人開発であれば、特に問題にはならないかもしれませんが、チーム開発の場合問題になる可能性があります。このコードは、以下のような条件でのみ成立するコードになっています。
1. API の開発が終了している
2. API を取得するコードの開発が終了している
3. API の仕様が変わっておらず、Name を取得できることが確定している2 と 3 に関しては、ギリギリ自分でなんとかできます。API は自分で実装すればいいですし、仕様が確定せずとも、仮のデータを Name に入れるような実装にしてしまえば一応動きます。ただ 1 はどうしようもありません。サーバが C# で実装されているならともかく、知らない言語で書かれている場合はかなり大変ですし、また実際に開発を行っているメンバーと軋轢を招く可能性もあります。このような状況を避けるために、
ApiManager
は自分でコントロールできる範囲内に収めるほうが利口だと思われます。まず、ApiManager はインターフェイス化してしまいましょう。
public interface IWeaponApi { UniTask<IEnumerable<Weapons>> GetWeaponList(); } public class WeaponList : MonoBehaviour { [SerializeField] private ListDisplay list; private IWeaponApi weaponApi; private async void Start() { IEnumerable<Weapons> weapons = await weaponApi.GetWeaponList(); foreach(var weapon in weapons) { list.AddItem(new ListDisplayItem { Text = weapon.Name }); } } }この状態だと、もちろん
Start
でぬるぽになってしまうので、Zenject を使いインジェクションを行います。
IWeaponApi
には、[Inject]
属性をつけてしまいましょう。[Inject] private IWeaponApi weaponApi;次に、MonoInstaller を実装します。(ついでに ApiManager もインスタンス化するように変更しました)
public class ApiInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<ApiManager>().To<IWeaponApi>().AsSingle(); } }これで、
WeaponList
クラスはApiManager
を参照するのではなくIWeaponApi
を参照するようになりました。ここまで来れば、以下のようなクラスを作成しpublic class DebugApiInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<DebugWeaponApi>().To<IWeaponApi>().AsSingle(); } private class DebugWeaponApi : IWeaponApi { public async UniTask<IEnumerable<Weapons>> GetWeaponList() { return new [] { new Weapon { Name = "マスターソード" } } } } }
ApiInstaller
の代わりに SceneContext に設定してあげればデバッグ用のデータになりました。運用ポイント① シーンツリーは簡単に開けるようにしよう
さて、ここまでの Zenject を使用したデータの切り替えは、多分 Zenject の入門的な記事を読めばどこにでも書いてあるのですが、これを実運用しようとすると結構面倒くさかったりします。
このコンテキストの切り替え、どう行うのがいいのでしょうか? 毎回設定を変えるごとにシーンを読み直しますか? 依存が多くなった場合、どうしましょう? シーンを正しい順番にロードするために手動で毎回フォルダビューからシーンをロードしているとめんどくさくなってきますよね。
現在私の携わっているプロジェクトには、シーンをロードするための拡張を行いました。コード自体は公開できませんが、イメージ図でご紹介いたします。
まず、ツールバーのような EditorWindow を作りましょう。そこには、以下のようなコードを書きます(C# 風ニセコードです)
interface IExtension string Name { get } Action Action { get } IEnumerable<IExtension> extensions = Assembly.LoadAllClassWithInterface<IExtension>(); foreach(var extension in extensions) if (GUI.Button(extension.Name)) extension.Action() class DebugWeaponSceneExtension : IExtension public string Name => "DebugWeaponScene" public Action Action => () => SceneManager.NewScene() var gameObject = new GameObject() var sceneContext = gameObject.AddComponent<SceneContext>() var installer = gameObject.AddComponent<DebugApiInstaller>() sceneContext.AddInstaller(installer) SceneManager.LoadScene("WeaponScene", Additive) class ProductWeaponSceneExtension : IExtension public string Name => "DebugWeaponScene" public Action Action => () => SceneManager.NewScene() var gameObject = new GameObject() var sceneContext = gameObject.AddComponent<SceneContext>() var installer = gameObject.AddComponent<ApiInstaller>() sceneContext.AddInstaller(installer) SceneManager.LoadScene("WeaponScene", Additive)こんな感じで、エディタのツールバーにあるボタンを押すと、必要な組み合わせのシーンができあがるようなものを作りました。
運用ポイント② 「マネジャークラス」は作ってもいい
最初は、何でもかんでもインターフェイスを噛ませていましたが、ある程度動作が安定している場合や(マスターのデータなど)、そもそも外部に影響されないようなものは、マネジャークラスをインジェクトしてもいいと思います。
例えば、シーン遷移を司るクラスは便利です。以下のようなシーンを作ります。
Scene -> DebugApiInstallerScene -> SceneManagerScene -> WeaponListSceneWeaponListScene は 上2つの SceneContext に依存します。あくまでインジェクションを使っているので、SceneManager は必要なくなったら破棄できます。
他にも背景を切り替える BackgroundManager なんかもよく作ってます。
最後に
DI や Zenject の説明にほとんど使い、運用の部分が結構短くなってしまいましたが、実は「シーンの組み合わせを簡単に開けるようにしよう」と「マネジャーはシングルトンじゃなきゃ作ってもいいんだよ」だけで済む話だったりします。個人的にこの方法を使ってからだいぶ安心してコードをデプロイできるようになりました。もちろん実装漏れは出たりするんですが、実装した部分に関しては自信をもてます。みんなで安定したコードを書きましょう!
参考
- 投稿日:2019-12-14T21:24:32+09:00
Azure PlayFab をサーバーサイド(ASP.NET Core Web API)から利用する
はじめに
この記事ではサーバーサイドから PlayFab を利用する方法を紹介します。
サーバーサイドの言語や FW は何でも良いのですが、今回は C# (ASP.NET Core) を使って Web API を用意してみます。
サンプルでは用意した Web API を通して PlayFab の TitleInternalData(クライアントでは扱うことができない内部データ)を取得して表示します。
対象読者は PlayFab やサーバーサイドの初心者の方です。
これらにある程度詳しい方には以下の 3 行の情報で事足りると思います。
- タイトルの SecretKey を発行する
- サーバーサイドで PlayFabSettings.staticSettings.DeveloperSecretKey に SecretKey をセットする
- サーバーサイドで PlayFabServerAPI を使って PlayFab へアクセスする
環境
- Windows 10
- .NET Core 3.1
- Visual Studio Community 2019 16.4.1
手順
1. タイトルの Secret Key を発行する
GameManager にログインして、自分のタイトルを開いて [歯車アイコン] を押します。
[名前] に任意の名前を入力して [シークレットキーを保存] を押します。
2. Web API を作成する
2.1. プロジェクトを作成する
Visual Studio を起動して [新しいプロジェクトの作成] を押します。
[web] を検索して [ASP.NET Core Web アプリケーション] を選択して [次へ] を押します。
任意の [プロジェクト名] [場所] [ソリューション名] を入力して [作成] を押します。
暫く待つとプロジェクトが作成されてこのような画面が表示されます。
2.2. NuGet から PlayFabAllSDK をインストールする
[参照] を押してから [PlayFab] を検索します。
[PlayFabAllSDK] が表示されたら選択してインストールします。
2.3. TitleInternalData を取得する API を作成する
ソリューションエクスプローラーで [Controllers] を右クリックし、コントローラーを追加します。
[読み取り/書き込みアクションがある API コントローラー] を選択して [追加] を押します。
TitleDataController.cs を開いて using を追加します。
using PlayFab; using PlayFab.ServerModels;TitleDataConrtoller.cs の最初の Get メソッドを下記の通りに書き換えます。
// GET: api/TitleData [HttpGet] public async Task<Dictionary<string, string>> Get() { var request = new GetTitleDataRequest(); var titleInternalData = await PlayFabServerAPI.GetTitleInternalDataAsync(request); return titleInternalData.Result.Data; }2.4. PlayFab の接続情報を設定する
Startup.cs を開いて using を追加します。
using PlayFab;Configure メソッドで PlayFab の接続情報(TitleID と SecretKey)を設定します。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // 下 2 行を追加 PlayFabSettings.staticSettings.TitleId = "ここに Title Id を入れます"; PlayFabSettings.staticSettings.DeveloperSecretKey = "ここに Secret Key を入れます"; // サンプルなので雑管理だけど許して欲しい if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }3. 動作確認してみる
GameManager で タイトル内部データに任意のデータを登録します。
ブラウザが起動したら /api/titledata へアクセスすると PlayFab から取得した TitleInternalData が表示されます。
あとがき
Unity への導入も簡単ですがサーバーサイドへの導入も簡単ですね。
ゲーム以外の Web アプリなどのバックエンドとして使っても良いかなと思いました。
- 投稿日:2019-12-14T19:30:38+09:00
obnizを動かすBlazor編
今年のテーマ
元々去年せっかくAzureを使ったし今年はCognitive Serviceを使ってobnizで何かをしようとしてました。
そして最終的にCognitive Serviceの前にBlazorを使ってJSを慣れべく使わないでロジックをC#でなんとか書きたいと足掻いた結果になります。問題点と考えたこと
1.そもそもCognitive Serviceを使ってなにをするかのアイディアが全く浮かばなかった。
2.自分がWed系の人ではないのでNode.jsとJavascriptまたはそのフレームワークをスラスラ使えない。
まず1点目について。obnizのネットにつながっていて、センサー・アクチュエータの操作がJSで出来てブラウザからの操作・可視化が簡単にできるという利点をうまく活かしたアイディアに苦戦しました。色々浮かんではそれobnizじゃなくてraspberry piでいいじゃんという状況になり中々いいアイデアが最後まで・・・。
2点目についてはやっぱり本職ではないのでそんなにガリガリかけないよなという思いが強く、何とか自分が得意なフィールドに落としてこられないだろうかと考え始めました。
そんな中全然関係ないところで知ったBlazorというフレームワーク?を使うとJS的な部分をC#に置き換えられるらしく、これ使えるんじゃないと思ったのが投稿1週間前だったのでもうこれで行こうと思い今回のテーマとしました。でもそもそもobnizってweb系の人がマイコンを触りやすいようにJSとかWeb系の技術を使えるのが売りの一つって昔見たような気がするのでやることは逆行している気も・・・。
ここから本題です。
Blazorのプロジェクト作成
まず環境は
OS:Windows10
開発環境:Visual Studio2019
です。Visual Studioの画面を開くとまずこの画面になると思います。
この中から右下の新しいプロジェクトの作成をクリックします。
次の画面ではBlazorアプリを探しだして選択してください。
右上のフィルターでWebにするとすぐ見つかります。
次の画面ではプロジェクトの作成になります。適当なプロジェクトの名にして右下の作成ボタンを押下します。
obnizのnpmの取り込み
初めここで時間がかかりました。Blazorのプロジェクトにどうやってnpmを取り込むのか。
まずソリューションエクスプローラー(右側のやつ)からプロジェクト(自分が名付けた名前になっていると思います)を右クリックで選択し「追加」→「新しい項目を選択します」
次の画面でnpm構成ファイル(下にスクロールすると出てくる)を選択してデフォルトの名前のままで右下の追加を押下してください。
最初の画面に戻って右のソリューションエクスプローラーに先程の名前のファイル(package.json)があるのでクリックして開きます。下記のコードをdevDependencies内に張り付けてください。(スクショの通りになるはず)
"obniz": "^3.0.0-beta.0"これで準備OKです。
Obzinを操作するためのページ作成
ソリューションエクスプローラー(右側のやつ)からPageフォルダーを右クリックし、「追加」→「新しい項目」を選択します
次の画面でRazorコンポーネントを選択し、名前を「Obniz_Component.razor」にして右下の追加をクリックします。
Obzinを操作するためのコードを書いていく
Obniz_Component.razor内のコードを全部消してから下記のコードをコピペします。
このコードは実際にobnizを操作するページのコードになります。頭の「@page」にページ名的なものを書きます。
@inject IJSRuntime JsRuntime;はJSのコードを読み込むためのメソッドを呼ぶための宣言みたいなものです。
あとは単純なHTMLのコードです。少し異なるのは@obniz_funcの部分ですね。これは「@code」の中のC#のメソッドを呼び出しています。
「@code」の中はC#のコードが書かれてます。中のメソッドはJSの関数を呼ぶものです。@page "/obniz_component" @inject IJSRuntime JsRuntime; <head> <style> #on { padding: 35px; } #off { padding: 35px; } </style> </head> <body> <div id="obniz-debug"></div> <h1>LED Switch</h1> <button id="on">ON</button> <button id="off">OFF</button><br /> <button @onclick="@obniz_func">接続</button><br /> </body> @code{ private void obniz_func() { JsRuntime.InvokeVoidAsync("obniz_led"); } }次は「_Host.cshtml」を右のソリューションエクスプローラーから選択してファイルを開きます。
17行目に下記のコードをコピペします。「ここにobnizの番号を入力」のところに番号を入れておくとスムーズにobnizにアクセスできます。
そのままでもあとで入力することになるので大丈夫です。
下記のコードは単純にobnizでLチカするときのコードそのままですね。<script src="https://obniz.io/js/jquery-3.2.1.min.js"></script> <script src="https://unpkg.com/obniz@latest/obniz.js"></script> <script> function obniz_led() { var obniz = new Obniz("ここにobnizの番号を入力"); obniz.onconnect = async function () { var led = obniz.wired("LED", { anode: 0, cathode: 1 }); $("#on").on("click", function () { led.on(); }); $("#off").on("click", function () { led.off(); }); }; } </script>次はSharedフォルダの中から「NavMenu.razor」のファイルを開いて27行目に下記のコードをコピペします。
このコードはobnizのページにアクセスするタブを追加するコードです。<li class="nav-item px-3"> <NavLink class="nav-link" href="obniz_component"> <span class="oi oi-list-rich" aria-hidden="true"></span> obniz_component </NavLink> </li>これで動かせるはずなので画面中央上の付近の「IIS Express」を押下して実行します。
ブラウザーが立ち上がったら右のタブからobniz_componentを選択すると下のスクショの画面が出てくるはずです。
接続を押すとobnizにつながります。
あとはLEDの0番ピンにLEDのアノード、1番ピンにLEDのカソードを接続し、ブラウザーのONを押下するとLEDが付きます。
あとがき
結局javascriptでobnizの制御コード書いてC#のコードでそれを呼び出してるだけじゃんと自分でも思ってますが、工夫すればちゃんとC#で書けるはずなのでこの記事を書いてる奴の問題です。(俺はここまでだ・・・。先に行け。)
少なくともフロント側をもうちょっとC#で書きたかったんですが、テーマ変更に伴う時間の関係でここまででした。
むしろ今回みたいにLチカするわけでなくobnizで大きなシステムを作ってみたいというときに使えると思います。Visual StudioなのでAzreuに簡単にデプロイできるし、C#で書いていたCognitive Service等既存のC#資産も利用できます。javascritpはよくわかららないけどC#なら書けるという
がobnizをもっと触ってくれることを願います。個人的にはそのうちLチカ以外のこともしてみたいと思います。雑な文章ですが最後まで読んでいただきありがとうございました。
- 投稿日:2019-12-14T19:30:38+09:00
obnizをC#を使って動かすための第1歩(Blazor編)
今年のテーマ
元々去年せっかくAzureを使ったし今年はCognitive Serviceを使ってobnizで何かをしようとしてました。
そして最終的にCognitive Serviceの前にBlazorを使ってJSを慣れべく使わないでロジックをC#でなんとか書きたいと足掻いた結果になります。問題点と考えたこと
1.そもそもCognitive Serviceを使ってなにをするかのアイディアが全く浮かばなかった。
2.自分がWed系の人ではないのでNode.jsとJavascriptまたはそのフレームワークをスラスラ使えない。
まず1点目について。obnizのネットにつながっていて、センサー・アクチュエータの操作がJSで出来てブラウザからの操作・可視化が簡単にできるという利点をうまく活かしたアイディアに苦戦しました。色々浮かんではそれobnizじゃなくてraspberry piでいいじゃんという状況になり中々いいアイデアが最後まで・・・。
2点目についてはやっぱり本職ではないのでそんなにガリガリかけないよなという思いが強く、何とか自分が得意なフィールドに落としてこられないだろうかと考え始めました。
そんな中全然関係ないところで知ったBlazorというフレームワーク?を使うとJS的な部分をC#に置き換えられるらしく、これ使えるんじゃないと思ったのが投稿1週間前だったのでもうこれで行こうと思い今回のテーマとしました。でもそもそもobnizってweb系の人がマイコンを触りやすいようにJSとかWeb系の技術を使えるのが売りの一つって昔見たような気がするのでやることは逆行している気も・・・。
ここから本題です。
Blazorのプロジェクト作成
まず環境は
OS:Windows10
開発環境:Visual Studio2019
です。Visual Studioの画面を開くとまずこの画面になると思います。
この中から右下の新しいプロジェクトの作成をクリックします。
次の画面ではBlazorアプリを探しだして選択してください。
右上のフィルターでWebにするとすぐ見つかります。
次の画面ではプロジェクトの作成になります。適当なプロジェクトの名にして右下の作成ボタンを押下します。
obnizのnpmの取り込み
初めここで時間がかかりました。Blazorのプロジェクトにどうやってnpmを取り込むのか。
まずソリューションエクスプローラー(右側のやつ)からプロジェクト(自分が名付けた名前になっていると思います)を右クリックで選択し「追加」→「新しい項目を選択します」
次の画面でnpm構成ファイル(下にスクロールすると出てくる)を選択してデフォルトの名前のままで右下の追加を押下してください。
最初の画面に戻って右のソリューションエクスプローラーに先程の名前のファイル(package.json)があるのでクリックして開きます。下記のコードをdevDependencies内に張り付けてください。(スクショの通りになるはず)
"obniz": "^3.0.0-beta.0"これで準備OKです。
Obzinを操作するためのページ作成
ソリューションエクスプローラー(右側のやつ)からPageフォルダーを右クリックし、「追加」→「新しい項目」を選択します
次の画面でRazorコンポーネントを選択し、名前を「Obniz_Component.razor」にして右下の追加をクリックします。
Obzinを操作するためのコードを書いていく
Obniz_Component.razor内のコードを全部消してから下記のコードをコピペします。
このコードは実際にobnizを操作するページのコードになります。頭の「@page」にページ名的なものを書きます。
@inject IJSRuntime JsRuntime;はJSのコードを読み込むためのメソッドを呼ぶための宣言みたいなものです。
あとは単純なHTMLのコードです。少し異なるのは@obniz_funcの部分ですね。これは「@code」の中のC#のメソッドを呼び出しています。
「@code」の中はC#のコードが書かれてます。中のメソッドはJSの関数を呼ぶものです。@page "/obniz_component" @inject IJSRuntime JsRuntime; <head> <style> #on { padding: 35px; } #off { padding: 35px; } </style> </head> <body> <div id="obniz-debug"></div> <h1>LED Switch</h1> <button id="on">ON</button> <button id="off">OFF</button><br /> <button @onclick="@obniz_func">接続</button><br /> </body> @code{ private void obniz_func() { JsRuntime.InvokeVoidAsync("obniz_led"); } }次は「_Host.cshtml」を右のソリューションエクスプローラーから選択してファイルを開きます。
17行目に下記のコードをコピペします。「ここにobnizの番号を入力」のところに番号を入れておくとスムーズにobnizにアクセスできます。
そのままでもあとで入力することになるので大丈夫です。
下記のコードは単純にobnizでLチカするときのコードそのままですね。<script src="https://obniz.io/js/jquery-3.2.1.min.js"></script> <script src="https://unpkg.com/obniz@latest/obniz.js"></script> <script> function obniz_led() { var obniz = new Obniz("ここにobnizの番号を入力"); obniz.onconnect = async function () { var led = obniz.wired("LED", { anode: 0, cathode: 1 }); $("#on").on("click", function () { led.on(); }); $("#off").on("click", function () { led.off(); }); }; } </script>次はSharedフォルダの中から「NavMenu.razor」のファイルを開いて27行目に下記のコードをコピペします。
このコードはobnizのページにアクセスするタブを追加するコードです。<li class="nav-item px-3"> <NavLink class="nav-link" href="obniz_component"> <span class="oi oi-list-rich" aria-hidden="true"></span> obniz_component </NavLink> </li>これで動かせるはずなので画面中央上の付近の「IIS Express」を押下して実行します。
ブラウザーが立ち上がったら右のタブからobniz_componentを選択すると下のスクショの画面が出てくるはずです。
接続を押すとobnizにつながります。
あとはLEDの0番ピンにLEDのアノード、1番ピンにLEDのカソードを接続し、ブラウザーのONを押下するとLEDが付きます。
あとがき
結局javascriptでobnizの制御コード書いてC#のコードでそれを呼び出してるだけじゃんと自分でも思ってますが、工夫すればちゃんとC#で書けるはずなのでこの記事を書いてる奴の問題です。(俺はここまでだ・・・。先に行け。)
少なくともフロント側をもうちょっとC#で書きたかったんですが、テーマ変更に伴う時間の関係でここまででした。
むしろ今回みたいにLチカするわけでなくobnizで大きなシステムを作ってみたいというときに使えると思います。Visual StudioなのでAzreuに簡単にデプロイできるし、C#で書いていたCognitive Service等既存のC#資産も利用できます。javascritpはよくわかららないけどC#なら書けるという
がobnizをもっと触ってくれることを願います。個人的にはそのうちLチカ以外のこともしてみたいと思います。雑な文章ですが最後まで読んでいただきありがとうございました。
- 投稿日:2019-12-14T19:28:23+09:00
C#foreachで自身の配列にデータを追加する
前に書いた記事の部分抜粋(C++)
main.cpp#include <iostream> #include <vector> using namespace std; void Print(vector<int> v) { for (auto& i : v) { cout << " " << i ; } cout << endl; } int main() { vector<int> vkeep{ 5, 5, 0, 5, 1 }; //5個 Print(v); cout << "--- rfor内で自身のコンテナ追加 ------rforの値がバグる--------------" << endl; for (auto& i : v) { cout << " " << i ; if (i == 5) v.emplace_back(123); //v.push_back(123); } cout << endl << "-----5は3つあるので3つ123が増えないといけないが、バグっている-----" << endl; Print(v); }実行結果、イテレータの位置がおかしくなり、エラーでなくバグる << 仕様らしい(エラーにしてほしいが・・・)
実行結果 :要素数も増えていなくて値も -572662307 という表示になる
--- rfor内で自身のコンテナ追加 ------rforの値がバグる-------------- 5 -572662307 -572662307 -572662307 -572662307これのC#バージョンを作る、ついでにラムダ記述方法も忘れたので確認
結論から言うと、foreach内での自身の配列(List)に要素数を増やすとエラーになる。
下記では、別配列(List)を用意して一旦こちらに追加して、まとめて追加(AddRange)しているmain.csusing System; using System.Collections.Generic; namespace ConsoleApp1 { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); List<int> v = new List<int>(){ 5, 5, 0, 5, 1 }; var vpp = new List<int>(); foreach (var a in v) { //if (a == 5) v.Add(123); //error if (a == 5) vpp.Add(123); } // ラムダで書いたバージョン //v.ForEach(a => { if (a == 5) v.Add(123); }); //error //v.ForEach(a => { if (a == 5) vpp.Add(123); }); v.AddRange(vpp); // v配列に appデータを増やす foreach(var a in v) { Console.Write(" {0}",a); } // ラムダで書いたバージョン //v.ForEach(a =>{ // Console.Write(" {0}",a); //}); Console.WriteLine(); } } }実行結果
Hello World! 5 5 0 5 1 123 123 123ちなみにエラーコメントの行のコメントを外してもビルドエラーは出ない。
C# ではforeach で自身配列の変更をしてはいけないとなっており、
実行するとエラーが foreach の行 ですぐ出る。C++では参照型を用い、rfor内の配列内の値の変更は可能だが、C#ではforeach 内ではそれすら許されない。
それは要素の増加に関しても同様のようだ。※VS2010時のC#の古いバージョンでは、foreachでの自身の値の変更はできて便利だった・・・
- 投稿日:2019-12-14T15:45:59+09:00
C# ジェネリック(ジェネリクス)
ジェネリック(ジェネリクス)
様々な型に対応する関数(やクラス)を定義することができます。
例えば、以下の様な関数があったとします。
Int32 score = 5; Int32 old_score = 3; public static Int32 GetHighestScore() { return (score > old_score) ? score : old_score; }万が一、
score
とold_score
がdouble
やfloat
であった場合、
以下の様に別で関数定義する必要があります。public static Int32 GetHighestScore() { return (score > old_score) ? score : old_score; } double score = 5; double old_score = 3; public static double GetHighestScoreA() { return (score > old_score) ? score : old_score; }といったように、複数のパターンを想定した関数を定義することで解決します。
しかしながら、これは悪い例です。
ハードコーディングですし、プログラムのメンテナンス性(保守性)も最悪です。
これを解決してくれるのが、ジェネリックです。今回の場合、以下の様に関数定義を行います。
static Int32 score = 5; static Int32 old_score = 3; public static unsafe T GetHighestScore<T>(T one, T two) where T : unmanaged, IComparable { if(one.CompareTo(two) > 0) { return one; } return two; }ここでいう、
where T : unmanaged
は、型がアンマネージド型であることを条件にするという意味です。アンマネージド型とは
sbyte
byte
、short
、int
、uint
、long
、
ulong
、char
、float
、double
、decimal
、bool
がアンマネージド型です。
アンマネージド型 (C# リファレンス)実例
実例として、
Marshal
のStructureToPtr
をメソッドとして簡易化してみます。
(※Marshal.StructureToPtr()
もジェネリック型として定義されていますね!)public struct myStruct { public int x; public int y; } static void Main(string[] args) { Console.Title = "ジェネリック"; myStruct ms = new myStruct(); var pStructure = StructureToPointer<myStruct>(ms); Console.WriteLine("構造体ポインタ: " + (Int32)(pStructure)); Console.ReadKey(); } //////////////////////////////////////////////////////////////////////// // 構造体のマーシャリング //////////////////////////////////////////////////////////////////////// public unsafe static IntPtr StructureToPointer<T>(T 構造体) where T : struct { var size = Marshal.SizeOf(構造体.GetType()); IntPtr pStructure = Marshal.AllocCoTaskMem(size); //アンマネージドメモリの確保 Marshal.StructureToPtr(構造体, pStructure, false); //確保した領域に構造体ポインタを展開 Marshal.DestroyStructure(pStructure, typeof(T));//後処理 Marshal.FreeCoTaskMem(pStructure);//確保したアンマネージドメモリの解放 return pStructure; }構造体から構造体ポインタへのマーシャリングを簡易化する関数です。
ここでは、where T : struct
とし、型がstruct
であることを条件とします。本来、マネージド型である(アンマネージド型ではない)ジェネリック関数では
sizeof()
を使用することができません。(sizeof()
がアンマネージド型にのみ対応している為)
なので、Marshal.Sizeof()
を使用します。
- 投稿日:2019-12-14T12:36:09+09:00
津波警報などが発表された際のピロピロ音を解析するまで
はじめに
津波警報などが発表された際にNHKなどでよく流される1ピロピロ音ですが、これは低い音と高い音がなっていて、その高い方の音が1、低い方の音が0になっていて現在発生している事象についての情報が送信されているらしいです。
世間一般ではFSK(周波数偏移変調)と呼ばれているらしいです。
仕様としては、
状態 周波数 0 640Hz 1 1024Hz で64bpsで送信されているようです。
この音自体はWikipedia先生によると緊急警報放送(EWS)を開始するための信号で、この音に反応して対応している機器を起動させたりする(していた)ようです2。
信号処理などの知識は全くありませんが、昔から実際にリアルタイムにビット列を見ながら解析してみたいと思っていたのもあり、この度チャレンジしてみました。大変でした。
完成したもの
ソース: https://github.com/ingen084/EwsDemodulator
開発言語はC#(.NET Core)でNAudioを使用してWasapiLoopbackCapture
でPCの鳴っている音を解析します。
解析部分は現状完全にNAudioに頼らない作りになりました。
WASAPIは音を鳴らしてるアプリケーションがない場合キャプチャデータが送られてこなくなるのですが、今回は考慮していません。動画(音量注意): https://streamable.com/4quqw
音源はWikipediaにあるものを使用しました。実際に作っている最中に困ったこと
YouTubeの動画の音声を使用してはいけない
実際の放送で動くか検証するために、Youtubeの動画を使用しようとしていましたが、音声の加工がされているため波形レベルでは全く異なる音声になっている動画がありました。3
初回の第2種開始信号がなんかおかしい…(EWS) pic.twitter.com/XtyQwJo85J
— ingen084 (@ingen084) December 3, 2019ニコニコ動画の音声を利用しました。
プレイヤーにも気をつけよう
これは当たり前というか、自分の落ち度なのですがPCの音をキャプチャしながらテストしたりする場合、プレイヤーの設定によっては波形などがカットされてしまったりするため注意しておきましょう。
今回はfoobar2000のSoundTouch DSPを有効にしていたのですが、(実際に再生速度を変更する設定は有効にしていない)メチャクチャな波形が出力されていました。4
試行
とりあえず区切ってFFT
私の知識では波形から周波数を特定するにはフーリエ変換という思い込みがあった5ので、そのまま波形を区切ってFFTにかけようと考えました。
64bpsで送られてくるということはサンプリングレートからサンプルの数が割り出せるので、そこから計算しようと考えました。(画像は波形を表してるつもり。)
ですが、まずきれいにサンプルを区切ることはできませんでした。
44100/64 = 689.0625 なので当たり前です。
また、無音検出のずれやそもそものサンプル数の少なさもあり成分が混ざり全くうまく判断することができませんでした6。Goertzelアルゴリズムを使用してみる
FFT以外の検出方法を探していると、DTMF7の検出などに使用されるGoertzelアルゴリズムというものを発見しました。
これはイケるかもと思い組み込んでみましたが、やはりサンプルがきれいに取れないということで精度は変わらず、散々でした。常に一定幅で区切ってみる
FFTでは精度の確保のために1ビット分(15ミリ秒ぐらい)をまとめて処理させる必要がありましたが、Goertzelの場合ある程度短くても問題ないだろうと無音区間が終わってからのサンプルを5msごとに区切った上で0か1を判断し、だいたい1ビットの長さになればそのビットを確定するというような手法にしてみました。
動画: https://streamable.com/bdtdw
音源はNHKFMの試験放送
精度はだいぶ改善され、体感では6~70%ぐらいにはなっていたと思います。しかし、この手法でもかなり問題点がありました。
一定間隔ごとで区切ってチェックしないといけないためビットを確定する際は1/64秒よりも少ないサンプル数で確定しないといけませんでした。
ですが高頻度で信号が変化する場合うまく捉えることができず、そのために確定するサンプル数を減らすと同じ値が4~5連続した際に検出数が増えてしまうことがありました。
解決のために1msで区切ったりしようとしましたが、さすがにアルゴリズムの検出の限界もあり精度を上げることはできませんでした8。ゼロクロッシング検出
もうだめだと思っていたのですが、よく考えてみれば単音なので波形を見るだけで周波数を特定することが可能ということがわかりました。(手書きなのでめっちゃ汚いのは許してください…)
実際に手法としてゼロクロッシング検出9という名前がついていたので、実装してみることにしました。
当時とりあえず実装してみた手法としてはこんな感じです。
- (実際に入力された波形を舐めながら)波形がマイナスからプラスに0と交差している点を探す。
- 前回交差した時間からの経過時間(サンプルのインデックス)とサンプリングレートから周波数を推定する。
- 50Hz以上ずれているようであればカウンタをリセット。
- 一定時間以上周波数が安定していれば周波数からビットの割り出す。
50Hzというのは640Hzあたりにおいてかなり大きな周波数の違いになってしまっていたと思います。
ですが1周期だけでの推定はかなりズレが大きく、実際に安定させるにはこれぐらいの遊びが必要でした。結果として精度はさらに向上し8~90%ぐらいにはなっていたと思います。
ですがいくつかの問題は克服できておらず、実際の80%ぐらいのサンプル数で確定しないといけない点など前回の問題点をそのまま引き継ぐ形となってしまいました。断定方法の改善
これが現在の方法です。
まずは無駄な点を排除していきました。
周波数ベースで計算するのも誤差が大きい上に640Hz or 1024Hz のものしか必要ないのでその周波数に必要なサンプル数を事前に計算しておきその2つの周波数に近いかだけチェックするようにしました。(3値化)
周波数が変わった際にもすぐに反応するわけではなくしばらく待ってから変更を確定するようにしました。
さらに次のビットが来るであろう時間を予測しその時間が経過後現在のビットとして確定するようにしました。流れとしては
生のサンプル -> 1周期の波形 -> 3値 -> 1ビットとなりました。
動画はこちら
(音量注意)解析してる感があってとてもいい
— ingen084 (@ingen084) December 4, 2019
2011/3/11に実際に放送されたデータを参考にしています pic.twitter.com/JBAYoU1a3Z精度はこの時点でほぼ100%になり、十分実用圏内に入りました。
パーサの作成
デコードが正常に動くようになったテンションでさらっと書いたのもあり、かなり適当に作りました。
EWSのフォーマットについては昭和60年に郵政省が告示している 無線設備規則第九条の三第五号の規定に基づく緊急警報信号の構成 に記載があります。
地域コードなどはあまりにも面倒だったので正規表現で一括変換しました。処理の流れ
パーサの主なステートは2つあり、前置符号解析とブロック解析です。
前置符号解析
- 最初の4ビットまで待って信号の種類を検出する
- 信号の種類が検出できたら種別に応じたブロック解析モードへ
ブロック解析
- 32ビット受信するまで待つ
- 先頭の16ビットが固定符号に一致しているかチェックする
- 処理中のブロックステージ・処理中のブロック数からパースする内容を決める
- 現在処理している信号の種類やブロックの数で終了させるかなどを決定する
注意点
年符号が昭和ベースなので西暦に変換する必要があります。
事前の設定等なしに違和感なく表示させるには±10年の中で一番近い年を選ぶ形になると思います。
なのでいま2011年のものを解析すると2021年と表示されます。ビット抜けした場合の対処などきれいに判定できるようにするにはかなり処理が大きくなると思うのですが、もっといい感じに処理できる方法があったらおしえてください…。
さいごに
正直なところEWSというシステム自体どこをどうターゲットにしたものなのかがわからないという事がわかります10。
わざわざ日付などの情報が入っていますが実際に現在の日付とチェックしている機器は無かったらしく、テレビ/ラジオの電源をつけるだけなら前置符号、固定符号、地域区分符号を受信した時点で起動(アラート)してよさそうです。全く知識がありませんでしたが最初の頃からするとかなり安定したものが完成できたのではないかと思います。
もっといい方法があるとかこいつアホみたいなことしてるなと思った人がいたらぜひ後学のためにも教えていただけると嬉しいです!
現在TV放送でその音がなるのはNHKだけという意味。 ↩
Wikipediaでも言及されている通り、現在はデジタル放送になっているためNHK以外のTV局をソースとするのであればPT3などで受信したTSを直接解析する形になる。 ↩
もしかすると元のデータ自体に問題があったのかもしれないので、YouTubeが悪いと決めつけるのは早計かもしれない。 ↩
本当に恐ろしいのはその状態の波形を普段から違和感なく聴いていた自分がいること ↩
FFTは周波数成分の取り出し(のはず)なので特定の単音の周波数が鳴っているか判断するのには適切ではない。 ↩
雑に使ってたのもあって検出率は10%ぐらいだった気がする… ↩
電話のピポパ音 ↩
自分の使い方が悪いだけかも。音量によって検出後の値が変わってしまう問題の解決もできていなかったので…。 ↩
波形が0と交差する点を見つけるため
ゼロクロッシング
と呼ばれている。らしい。 ↩休日等に試験放送がかぶると昼間からテレビでゲームしてる人などが急にチャンネル切り替わってびっくりしてる様子などが見受けられた。 ↩
- 投稿日:2019-12-14T11:49:04+09:00
Blazorでドラクエウォークのお土産近いところ調べるサイト作った話
Qiita Advent Calendar 2019 C# の記事です。C#でWEBアプリケーションが作成できるBlazorの良いところとか、ロードマップとか記載しようと思いましたが、ドラクエウォークでお土産近いところ調べたいなと思って、気づいたらコード書いてました!今は反省している。。。
何県人かばれてしまう。。
概要
現在地から、ドラクエウォークのお土産に近い順に表示します。 PCのブラウザはほとんど大丈夫だと思うのですが、ブラウザによっては、動かないかもです。 GeoLocation情報はブラウザから取得しており、
JavaScript
を使用しています。デバイスによってはGPSの精度にばらつきがありますので、必ずしも正確な距離が出ない場合があります。
BlazorはJavaScriptレス
をウリにしている感じですが、現状はJavaScript
とは切っても切れない関係ですね。Blazorの説明。HTMLにインラインでC#コーディング
Razor構文に慣れると非常に楽です。C#を使って、要素に対し直感的に扱うことができるので、この辺はすんなりと書けるようになると思います。また、
LINQ
もバシバシ使うことができます。Index.Razor@page "/" @inject HttpClient Http @using AspNetMonsters.Blazor.Geolocation @inject LocationService LocationService <h3>ドラクエウォーク 現在地から近いところTop20</h3> @*データがある場合、表示*@ @if(Datas.Any()) { @foreach (var d in Datas) { <SurveyPrompt Title="@(Pres.Where(p=>p.Id==d.Pre).First().Name)" Data="@d"/> } } @functions { Location location; private Omiyage[] Datas = new Omiyage[0]; private Pre[] Pres = new Pre[0]; protected async override Task OnInitializedAsync() { // 現在地取得 location = await LocationService.GetLocationAsync(); // お土産データ取得 Datas = await Http.GetJsonAsync<Omiyage[]>("data.json"); // 県名取得 Pres = await Http.GetJsonAsync<Pre[]>("pre.json"); //データのセット SetData(); } //データのセット void SetData() { // 現在地の取得 var lat1 = (double)location?.Latitude; var lon1 = (double)location?.Longitude; // 現在地との距離を計算 foreach(var data in Datas) { data.SetKyori(lat1, lon1); } // 近い順に20個まで取得 Datas = Datas.OrderBy(p => p.Kyori).Take(20).ToArray(); } }Blazorの説明。.Netのライブラリが豊富!
Blazor Client-Sideアプリケーションは、
WebAssembly
によりブラウザ上で実行されます。現時点ではプレビュー版ではあるのですが、ほぼ実用レベルに近いところまで行っているのではないでしょうか。もちろんブラウザでできること以外のことはできませんが、nugetパッケージは適用できるパッケージが多く、既存のノウハウを活かした開発ができるようになっています。2点間距離の計算も、一発。。これ、ブラウザ上で動いているわけですから、違和感さえ覚えます。
Model.cs/// <summary> /// 現在地との距離 /// </summary> public double Kyori { get; set; } /// <summary> /// 距離を算出する /// </summary> /// <param name="lat2">現在の緯度</param> /// <param name="lon2">現在の経度</param> public void SetKyori(double lat2,double lon2) { Kyori = new System.Device.Location.GeoCoordinate(Lat, Lon).GetDistanceTo(new System.Device.Location.GeoCoordinate(lat2, lon2)); }Blazorに興味を持ったら。。。
このアプリケーションの作成には、それほど時間をかけてません(位置情報の収集には時間がかかりましたが。。)Blazorに興味を持ったら、BlazorのQiita Advent Calendarにアクセスしてみてください。
- 投稿日:2019-12-14T11:37:30+09:00
C# - AnimateWindow (Win32API) を試してみた
タスクトレイから呼ぶフォーム用に、
「ひょこっ」とした感じでウィンドウを表示したかった。結果
スライドアニメーション(
AW_SLIDE
)はイマイチ。
・カクカク感がある
・タイトルバーが先に表示されてしまう(↓方向だけは視覚的にセーフ?)フェードアニメーション(
AW_BLEND
)は、まぁよさげ。
・ただし、時間設定を長め(1000msとか)にすると複数回フェードするような挙動をした。謎。サンプルコード(簡素版)
using System; using System.Drawing; using System.Windows.Forms; using System.Runtime.InteropServices; public class AnimateWindowTest: Form { Button btn; class NativeMethods { [Flags] public enum AnimateWindowFlags { AW_NONE = 0, AW_HOR_POSITIVE = 0x00000001, AW_HOR_NEGATIVE = 0x00000002, AW_VER_POSITIVE = 0x00000004, AW_VER_NEGATIVE = 0x00000008, AW_CENTER = 0x00000010, AW_HIDE = 0x00010000, AW_ACTIVATE = 0x00020000, AW_SLIDE = 0x00040000, AW_BLEND = 0x00080000 } [DllImport("user32.dll")] public static extern bool AnimateWindow(IntPtr hWnd, UInt32 dwTimeMSec, AnimateWindowFlags dwFlags); } AnimateWindowTest() { btn = new Button(); btn.Text = "New Window"; btn.Click += Btn_Click; Controls.Add(btn); ClientSize = new Size(200,100); } void Btn_Click(object sender, EventArgs e) { Form f = new Form(); f.Controls.Add(new Button(){Text="button",Dock=DockStyle.Bottom}); NativeMethods.AnimateWindow( f.Handle, 500, // delay tiem in [ms] NativeMethods.AnimateWindowFlags.AW_ACTIVATE | NativeMethods.AnimateWindowFlags.AW_SLIDE | NativeMethods.AnimateWindowFlags.AW_VER_NEGATIVE ); f.Show(); } [STAThread] static void Main(string[] args) { Application.Run(new AnimateWindowTest()); } }サンプルコード(子Formに処理を持たせる版)
using System; using System.Drawing; using System.Windows.Forms; using System.Runtime.InteropServices; public class AnimateWindowTest: Form { Button btn; AnimateWindowTest() { btn = new Button(); btn.Text = "New Window"; btn.Click += Btn_Click; Controls.Add(btn); ClientSize = new Size(200,100); } void Btn_Click(object sender, EventArgs e) { Form f = new AnimForm(); f.Show(); } [STAThread] static void Main(string[] args) { Application.Run(new AnimateWindowTest()); } } public class AnimForm:Form { class NativeMethods { [Flags] public enum AnimateWindowFlags { AW_NONE = 0, AW_HOR_POSITIVE = 0x00000001, AW_HOR_NEGATIVE = 0x00000002, AW_VER_POSITIVE = 0x00000004, AW_VER_NEGATIVE = 0x00000008, AW_CENTER = 0x00000010, AW_HIDE = 0x00010000, AW_ACTIVATE = 0x00020000, AW_SLIDE = 0x00040000, AW_BLEND = 0x00080000 } [DllImport("user32.dll")] public static extern bool AnimateWindow(IntPtr hWnd, UInt32 dwTimeMSec, AnimateWindowFlags dwFlags); public const int WM_SHOWWINDOW = 0x0018; } public AnimForm() { Controls.Add(new Button(){Text="button",Dock=DockStyle.Bottom}); } protected override void WndProc(ref Message m) { if (m.Msg == NativeMethods.WM_SHOWWINDOW) { //base.WndProc(ref m); if (m.LParam == IntPtr.Zero && m.WParam != IntPtr.Zero ) { // lParam == 0 : message by ShowWindow // mParam == 1 : being shown // mParam == 0 : being hidden NativeMethods.AnimateWindow( this.Handle, 100, // delay tiem in [ms] NativeMethods.AnimateWindowFlags.AW_ACTIVATE | NativeMethods.AnimateWindowFlags.AW_BLEND //AW_SLIDE // | NativeMethods.AnimateWindowFlags.AW_VER_NEGATIVE ); } else{ base.WndProc(ref m); } } else { base.WndProc(ref m); } } }参考サイト
- 投稿日:2019-12-14T10:12:00+09:00
C# - マルチディスプレイ(マルチスクリーン)における座標の取得について調べてみた - Windows10
調査内容
ウィンドウ(C#の
Form
)をスクリーンをまたぐ位置に置いてみて、クリックしたときに得られる座標を調べた。調査結果
実験してみたところ、
Form
はどちらか一方のScreenに所属する扱いのようである。
「左端をクリックしたらPrimaryスクリーン、右端をクリックしたらSecondaryスクリーン」というわけではない。座標については、スクリーンをまたいで連続しているようである。
調査に使ったプログラムのコード
using System; using System.Drawing; using System.Windows.Forms; class MultiScreenTest : Form { Button btn; MultiScreenTest() { Text = "Multi Screen Test"; ClientSize = new Size(500,400); btn = new Button(); btn.Text = "Show X,Y"; btn.Size = new Size(500,200); btn.MouseClick += Btn_MouseClick; Controls.Add(btn); } void Btn_MouseClick(object sender, MouseEventArgs e) { Point screenP = btn.PointToScreen(e.Location); Console.Write("Click: "); Console.Write(screenP.X); Console.Write(", "); Console.Write(screenP.Y); Screen screen = Screen.FromPoint(screenP); Console.Write(" Primary:"); Console.WriteLine(screen.Primary); } [STAThread] static void Main(string[] args) { Application.Run(new MultiScreenTest()); } }プログラムの実行結果
Click: 457, 251 Primary:True Click: 119, 122 Primary:True Click: 114, 114 Primary:True Click: 31, 49 Primary:True Click: 1878, 387 Primary:False Click: 1921, 390 Primary:False Click: 1958, 391 Primary:False Click: 1916, 410 Primary:False Click: 1881, 413 Primary:False Click: 1838, 401 Primary:False Click: 1153, 283 Primary:True Click: 1800, 378 Primary:False Click: 1730, 382 Primary:False Click: 1074, 237 Primary:True Click: 1099, 242 Primary:True Click: 1529, 341 Primary:True Click: 2239, 274 Primary:False Click: 2231, 322 Primary:False参考サイト
- 投稿日:2019-12-14T01:02:56+09:00
【Unity】 Oculus Quest のバッテリー情報を取得する
Oculus Quest でバッテリー残量を取得する方法
実装例
float hmd = SystemInfo.batteryLevel; byte left = OVRInput.GetControllerBatteryPercentRemaining(OVRInput.Controller.LTouch); byte right = OVRInput.GetControllerBatteryPercentRemaining(OVRInput.Controller.RTouch); Debug.LogFormat("Battery: HMD = {0}%, Left Controller = {1}%, Right Controller = {2}%", (int)(hmd * 100), left, right);HMDのバッテリー残量
- SystemInfo.batteryLevel で0~1のfloat値で取得できる
また、SystemInfo.batteryStatus を使えば、バッテリーの充電状況を取得できるコントローラのバッテリー残量
- OVRInput.GetControllerBatteryPercentRemaining で取得できる
引数にコントローラ種別を渡せば、バッテリー残量がパーセンテージ数値(0~100)で返ってくる。
※ SystemInfo.batteryLevelのように0~1のfloat値ではないことに注意参考