- 投稿日:2020-05-29T20:32:53+09:00
リフレクションでpublicでないコンストラクタやメソッドを持つクラスのメソッドを実行する
記事の目的
テストを作りたいけど関数のアクセス修飾子がprivateとかinternalだから単体テストを作れない。
↓
Reflectionでできるかも
↓
さすがにドンピシャなのはないけど色んなページを参考にしたらできた!
↓
今後のためにメモっておこう!コード
/* HogeProjectのHogeProject.FugaNamespace.PiyoClass.NyanMethodを実行させたい場合を考えます。 HogeProject →参照設定しているプロジェクト、かつHogeProjectを作成した時に自動でVisualStudioが作ってくれる名前空間。 FugaNamespace →PiyoClassの属すNamespace。 PiyoClass →クラス名。インスタンスがinternal PiyoClass(int input)で定義されている。 NyanMethod →internal int NyanMethod(str input)で定義されている関数。 ってことにしてください */ var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic; var instance = (HogeProject.FugaNamespace.PiyoClass)System.Activator.CreateInstance( typeof(HogeProject.FugaNamespace.PiyoClass) ,flags ,null ,new object[]{0} // インスタンスに引数を与えてる ,null); var method = instance.GetType().GetMethod("NyanMethod",flags); var result = method.invoke(instance, new object[]("hikisu~"));で、行けるはず。
参考
C#リフレクションTIPS 55連発|Qiita
How do I use reflection to invoke a private method?|StackOverflow
Activator.CreateInstanceを使って型情報からインスタンスを作成する|smdn:総武ソフトウェア推進所
- 投稿日:2020-05-29T19:21:02+09:00
UnityでStartCoroutineを実行した際のNullReferenceException対策
UnityでStartCoroutineメソッドを実行した際に、なぜかNullReferenceExceptionのエラーが発生して2時間ほどハマったので、その解決策をメモしておきます。
結論から言うと、Update()メソッド以外のメソッドからコルーチン処理を実行する(StartCoroutineメソッドを実行する)場合は、そのコルーチン処理を呼び出すためのオブジェクトをnewで直接生成してはならないようです。
StartCoroutineの使い方について解説している他の参考サイトなどでは、Update()メソッド内からStartCoroutineを実行している事例しか見当たらなかったため、今回のように他のメソッドから実行したい場合はどうすればよいのか悩みました。
まず、Updateメソッド内からコルーチン処理を呼び出す事例を載せておきます。
EventManager.cspublic class EventManager : MonoBehaviour { void Update() { //Updateメソッド内からコルーチン呼び出し(この場合は正常に実行できる) StartCoroutine("Event"); } //コルーチン関数"Event"を定義 IEnumerator Event() { Debug.Log("あいうえお"); //マウス左クリックを待つ処理 yield return new WaitUntil(() => Input.GetMouseButtonDown(0)); Debug.Log("かきくけこ"); } }出力結果を確認すると、正常に実行できていました。
次に、エラーが発生した事例を載せておきます。
EventManager.cspublic class EventManager : MonoBehaviour { public static void CallEvent(string eventName) { //EventManager のオブジェクトをnewで生成 EventManager eventManager = new EventManager(); //コルーチン呼び出し(この場合はエラーが発生する) eventManager.StartCoroutine("Event"); } //コルーチン関数"Event"を定義 IEnumerator Event() { Debug.Log("あいうえお"); //マウス左クリックを待つ処理 yield return new WaitUntil(() => Input.GetMouseButtonDown(0)); Debug.Log("かきくけこ"); } }今回は、他のクラスからCallEventメソッドを呼び出して"Event"というコルーチン処理を実行しようとしています。しかし、実行結果はNullReferenceExceptionが発生してしまいました。
MonoBehaviour(及びそれを継承したEventManagerクラス)をnewで直接生成しているのがエラーの原因です。ではどうするのかと言うと、newではなくAddComponentメソッドを使ってオブジェクトを生成します。そのサンプルを載せておきます。
EventManager.cspublic class EventManager : MonoBehaviour { public static void CallEvent(string eventName) { //AddComponentでオブジェクトを生成 EventManager eventManager = (new GameObject("適当なオブジェクト名")).AddComponent<EventManager>(); //コルーチン呼び出し(この場合は正常に実行できる) eventManager.StartCoroutine("Event"); } //コルーチン関数"Event"を定義 IEnumerator Event() { Debug.Log("あいうえお"); //マウス左クリックを待つ処理 yield return new WaitUntil(() => Input.GetMouseButtonDown(0)); Debug.Log("かきくけこ"); } }はい、これでエラーなく正常に実行できました。
- 投稿日:2020-05-29T16:19:08+09:00
PythonとC#でLチカ
PythonでLチカ
知識ゼロから作って学ぶIoT入門
Arduinoではありますがブレッドボードの説明やLチカ説明が載っており、1500円セールの時なら元はとれます。gpioのピンの一覧を表示する
基板のGPIOの番号を確認します。
$ gpio readallwiringpiライブラリー古い場合はエラーが出た場合
ライブラリーが古い場合はエラーが出ます
Oops - unable to determine board type... model: 17wiringpiを更新する
cd /tmp $ wget https://project-downloads.drogon.net/wiringpi-latest.deb $ sudo dpkg -i wiringpi-latest.deb再度
gpio readallgpioのPIN番号の表示
Physical = 連番 PIN
0v = GND
ターミナルに表示されます
左側に3.3Vの電流がそれぞれのPINに流れています
右側に5Vの電流がそれぞれのPINに流れています0vがGNDになります。GNDはマイナス
GNDについてはUmedyの動画で詳しく説明しています。
GPIについてUmedyの動画で体系的に学ぶことをお勧めします。GPIO.BOARD: 物理ピン番号(連番) GPIO.BCM: 役割ピン番号(broadcomが命名しているもの)GPIO番号はモデルごと違うのでPIN連番(BOARD)を使う
GPIO番号はモデルごと違う場合があるので、PIN番号で指定するGPIO.BOARD(連番)を使った方がよいみたいです。
l.py#!/usr/bin/env python # -*- coding: utf-8 -*- import RPi.GPIO as GPIO #RPi.GPIOモジュールをインポート from time import sleep #GPIO.setmode(GPIO.BCM) #GPIO.setmode(GPIO.BOARD) #連番を利用する GPIO.setmode(GPIO.BOARD) GPIO.setup(7, GPIO.OUT) try: while True: GPIO.output(7, GPIO.HIGH) sleep(0.5) GPIO.output(7, GPIO.LOW) sleep(0.5) except KeyboardInterrupt: # GPIO設定クリア GPIO.cleanup()$ python l.pay全体の流れ
PINのプラス(赤)からブレッドボードを通してPIN マイナス(黒)(GND)につなげる
ダイオードに電流が流れすぎないように抵抗を加えて調整する必要がある
パーツをまとめて購入(サンプルファイルCD付き)
最終的にこれらをC#に変換しマイクロソフトのGPIOライブラリを使って使おうと思います。ポイント
センサーパーツをAmazonでまとめて購入できる
体系的にGPIOを学んだほうがよい。UmedyにRaspberyの動画があった
Amazonの安いテスターを買って電流がちゃんと流れているか確認すると理解が早まる
パーツをまとめて購入セットにはサンプルプロジェクトファイルがあり、自走学習が可能と思われる次回 C#でLチカ
マイクロソフトがGPIOライブラリをリリースしてきたのでそれを使ってlチカをやってみます。
- 投稿日:2020-05-29T16:19:08+09:00
RaspberryPi PythonとC#でLチカ
PythonでLチカ
知識ゼロから作って学ぶIoT入門
Arduinoではありますがブレッドボードの説明やLチカ説明が載っており、1500円セールの時なら元はとれます。gpioのピンの一覧を表示する
基板のGPIOの番号を確認します。
$ gpio readallwiringpiライブラリー古い場合はエラーが出た場合
ライブラリーが古い場合はエラーが出ます
Oops - unable to determine board type... model: 17wiringpiを更新する
cd /tmp $ wget https://project-downloads.drogon.net/wiringpi-latest.deb $ sudo dpkg -i wiringpi-latest.deb再度
gpio readallgpioのPIN番号の表示
Physical = 連番 PIN
0v = GND
ターミナルに表示されます
左側に3.3Vの電流がそれぞれのPINに流れています
右側に5Vの電流がそれぞれのPINに流れています0vがGNDになります。GNDはマイナス
GNDについてはUmedyの動画で詳しく説明しています。
GPIについてUmedyの動画で体系的に学ぶことをお勧めします。GPIO.BOARD: 物理ピン番号(連番) GPIO.BCM: 役割ピン番号(broadcomが命名しているもの)GPIO番号はモデルごと違うのでPIN連番(BOARD)を使う
GPIO番号はモデルごと違う場合があるので、PIN番号で指定するGPIO.BOARD(連番)を使った方がよいみたいです。
l.py#!/usr/bin/env python # -*- coding: utf-8 -*- import RPi.GPIO as GPIO #RPi.GPIOモジュールをインポート from time import sleep #GPIO.setmode(GPIO.BCM) #GPIO.setmode(GPIO.BOARD) #連番を利用する GPIO.setmode(GPIO.BOARD) GPIO.setup(7, GPIO.OUT) try: while True: GPIO.output(7, GPIO.HIGH) sleep(0.5) GPIO.output(7, GPIO.LOW) sleep(0.5) except KeyboardInterrupt: # GPIO設定クリア GPIO.cleanup()$ python l.pay全体の流れ
PINのプラス(赤)からブレッドボードを通してPIN マイナス(黒)(GND)につなげる
ダイオードに電流が流れすぎないように抵抗を加えて調整する必要がある
パーツをまとめて購入(サンプルファイルCD付き)
最終的にこれらをC#に変換しマイクロソフトのGPIOライブラリを使って使おうと思います。追記
Sample File URL
OSOYOOのサイト サンプルファイルと動画がありましたポイント
センサーパーツをAmazonでまとめて購入できる
体系的にGPIOを学んだほうがよい。UmedyにRaspberyの動画があった
Amazonの安いテスターを買って電流がちゃんと流れているか確認すると理解が早まる
パーツをまとめて購入セットにはサンプルプロジェクトファイルがあり、自走学習が可能と思われる次回 C#でLチカ
マイクロソフトがGPIOライブラリをリリースしてきたのでそれを使ってlチカをやってみます。
- 投稿日:2020-05-29T15:34:23+09:00
c#:(多分)最短でGenericにCsvを読み書きするコードwith CsvHelper。
CsvHelperでいちいちMapをつくるのめんどくさい。
CsvHelperでジェネリックにCsvファイルを読み書きしたい!
そんなときはこれで解決
このコードで、クラスのプロパティの文字列に一致したHeaderを持つCsvファイルを読み込んだり書き込んだりしてくれます。
読み込み
ReadCsv.cspublic static IList<T> Read<T>(string filepath) { using (var reader = new StreamReader(filepath)) using (var csv = new CsvReader(reader, CultureInfo.CurrentCulture)) return csv.GetRecords<T>().ToList(); }書き込み
WriteCsv.cspublic static void Write<T>(IEnumerable<T> objs, string filepath) { using (var reader = new StreamWriter(filepath)) using (var csv = new CsvWriter(reader, CultureInfo.CurrentCulture)) csv.WriteRecords<T>(objs); }Appendix
ちなみにクラスに対応するHeaderがないときはエラーになってしまう。。。
そんなときは!MyClass.cspublic class MyClass { [Index(0)] public DateTime Date { get; set; } [Index(1)] public double Value { get; set; } }みたいな感じでAttributionをつければいい感じに読み取ってくれます。
まとめ
多分これが一番シンプルで汎用性が高い!と思ってる。
いままでのコードが激減するかも。
- 投稿日:2020-05-29T13:11:07+09:00
Linuxで.NETの開発をする
(An English version of this article is also available.)
筆者は複数のマシンを持っていますが、そのうちの一台はLinuxが入っています。デスクトップ機である関係上、これを開発に使うことも多いのですが、その環境に関して紹介していきたいと思います。
何ができるのか?
.NET Coreは複数のプラットフォーム対応がなされているため、開発に関してもほとんどWindowsと同じようなことができます。
可能なこと
筆者が可能であると確認しているのは以下のとおりです。
- コマンドラインツールの開発とデバッグ。
- ウェブアプリケーション(ASP.NET、Blazorなど)の開発とデバッグ。
- PowerShell (旧PowerShell Core)バイナリモジュールの開発とデバッグ(プロセスにアタッチしてのデバッグ)。
- .NET Core Tools(Entity Framework、ML.NETなど)の取得と実行。
- Unityを使用した開発。(以前はWindows、Macのみでしたが、Beta期間を経て、正式に利用できるようになりました。)
不可能(または困難)なこと
- WinFormやWPFなどのGUIアプリケーションの開発。
- .NET Frameworkをターゲットにする開発。
- Xamarinを使用した開発。(サポートされていないものの、厳密にはできないことはないようです。例えば、xamarin-androidはJenkinsから取り出してくることで、使用できるようです。)
環境の作り方
.NET Core
.NET Coreはほとんどのメジャーなディストリビューションで使用することができ、また各種パッケージも提供されていますが、おすすめしたいのは、シェルスクリプトを使用したインストールです。
シェルスクリプト経由のインストールを行うことで、複数のバージョンの.NET Coreランタイムやライブラリのインストールが容易になります。
例えば、LTS版をインストールする場合は、以下のような形です。
./dotnet-install.sh --channel LTS
尚、追加でインストールが必要なライブラリが必要になるかもしれません。一般的にあまり通常では入っていないと思われるものはlibicuです。
インストール先はユーザーが書き込める場所でなくとも構いません。追加のテンプレートやツールなどはそれぞれのユーザーの管理下に置かれます。
開発環境
Visual Studio Code
無料で開発環境を揃えたいのであれば、以下のような組み合わせがおすすめになります。
上記で、開発が開始できる環境にすることができます。
JetBrains Rider
開発環境にお金を出してもいい、という方は、JetBrains Riderを検討するもの一つの選択肢になります。JetBrains社はVisual Studio用のリファクタリングプラグインであるReSharperを開発している会社でもあり、高度なコーディング支援が提供されるため、快適に開発することができます。
RiderはRider単体、RiderとReSharper同梱版、全IDE製品を含んだAll Products Packが存在しています。詳しくは購入ページを参照して下さい。(筆者は他の言語のものも使用することがあるため、All Products Packを個人的に契約しています。)尚、Riderに限らず、JetBrains社のほとんどのIDEはWindowsなどの他の環境でも使用できるため、複数のOSで同じIDEを使用できるという点においても重宝しています。
また、NuGetパッケージ管理などもグラフィカルにできるため、C#を行う機会が多い、という方におすすめです。
その他
尚、OmniSharp(上記の拡張でも使用)を用いることで、Language Server Protocolに対応したお好みのエディタで開発することも可能です。この方法はエディタによって変わってくるのでここでは割愛します。
早速試してみる
プロジェクト作成
環境を揃えると開発を開始することができます。
プロジェクトを作成するのにはdotnetコマンドを使用します。
dotnet new console -o myapp
以下の用に、引数を入れずに実行すると使用できるテンプレートが表示されます。
dotnet newUsage: new [options] Options: -h, --help Displays help for this command. -l, --list Lists templates containing the specified name. If no name is specified, lists all templates. -n, --name The name for the output being created. If no name is specified, the name of the current directory is used. -o, --output Location to place the generated output. -i, --install Installs a source or a template pack. -u, --uninstall Uninstalls a source or a template pack. --nuget-source Specifies a NuGet source to use during install. --type Filters templates based on available types. Predefined values are "project", "item" or "other". --dry-run Displays a summary of what would happen if the given command line were run if it would result in a template creation. --force Forces content to be generated even if it would change existing files. -lang, --language Filters templates based on language and specifies the language of the template to create. --update-check Check the currently installed template packs for updates. --update-apply Check the currently installed template packs for update, and install the updates. Templates Short Name Language Tags ---------------------------------------------------------------------------------------------------------------------------------- Console Application console [C#], F#, VB Common/Console Class library classlib [C#], F#, VB Common/Library WPF Application wpf [C#] Common/WPF WPF Class library wpflib [C#] Common/WPF WPF Custom Control Library wpfcustomcontrollib [C#] Common/WPF WPF User Control Library wpfusercontrollib [C#] Common/WPF Windows Forms (WinForms) Application winforms [C#] Common/WinForms Windows Forms (WinForms) Class library winformslib [C#] Common/WinForms Worker Service worker [C#] Common/Worker/Web Unit Test Project mstest [C#], F#, VB Test/MSTest NUnit 3 Test Project nunit [C#], F#, VB Test/NUnit NUnit 3 Test Item nunit-test [C#], F#, VB Test/NUnit xUnit Test Project xunit [C#], F#, VB Test/xUnit Razor Component razorcomponent [C#] Web/ASP.NET Razor Page page [C#] Web/ASP.NET MVC ViewImports viewimports [C#] Web/ASP.NET MVC ViewStart viewstart [C#] Web/ASP.NET Blazor Server App blazorserver [C#] Web/Blazor Blazor WebAssembly App blazorwasm [C#] Web/Blazor/WebAssembly ASP.NET Core Empty web [C#], F# Web/Empty ASP.NET Core Web App (Model-View-Controller) mvc [C#], F# Web/MVC ASP.NET Core Web App webapp [C#] Web/MVC/Razor Pages ASP.NET Core with Angular angular [C#] Web/MVC/SPA ASP.NET Core with React.js react [C#] Web/MVC/SPA ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA Razor Class Library razorclasslib [C#] Web/Razor/Library/Razor Class Library ASP.NET Core Web API webapi [C#], F# Web/WebAPI ASP.NET Core gRPC Service grpc [C#] Web/gRPC dotnet gitignore file gitignore Config global.json file globaljson Config NuGet Config nugetconfig Config Dotnet local tool manifest file tool-manifest Config Web Config webconfig Config Solution File sln Solution Protocol Buffer File proto Web/gRPC足りないテンプレートに関してはオンライン取得して、インストールすることができます。
dotnet install hoge
これで、myappディレクトリ内にコンソールアプリケーションのボイラープレートコードが生成されますので、エディタを開き、このまま開発を開始できます。(Visual Studio Codeは初回起動にはC#プラグインの必要コンポーネントとダウンロードが行われるため、ネット環境が必要であり、また少し次回がかかるかもしれません。)
Riderを使用している場合は、専用ダイアログからプロジェクトを作成できます。
対応していないテンプレートもあるので、その場合はコマンドラインからプロジェクトを作成する形になります。
.NET Coreツール
dotnetコマンドを使用することで、プロジェクトの作成や操作を行う他.NET Coreツールという形で各種のツールもインストールすることができます。
例えば、PowerShellも.NETの一部としてインストールすることができ、その場合は以下のようになります。
dotnet tool install -g powershellこれは他にもML.NETツールなどをインストールしたい場合も同様です。
dotnet tool install -g mlnetデバッグ
Visual Studio Codeでデバッグすることも可能で、通常のVisual Studioのような使用感で行うことができます。
ただし、設定は
launch.json
などを書き換えていく必要があるため、少し慣れが必要かもしれません。以下、その例を示します。{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": ".NET Core Launch (console)", "type": "coreclr", "request": "launch", "WARNING01": "*********************************************************************************", "WARNING02": "The C# extension was unable to automatically decode projects in the current", "WARNING03": "workspace to create a runnable launch.json file. A template launch.json file has", "WARNING04": "been created as a placeholder.", "WARNING05": "", "WARNING06": "If OmniSharp is currently unable to load your project, you can attempt to resolve", "WARNING07": "this by restoring any missing project dependencies (example: run 'dotnet restore')", "WARNING08": "and by fixing any reported errors from building the projects in your workspace.", "WARNING09": "If this allows OmniSharp to now load your project then --", "WARNING10": " * Delete this file", "WARNING11": " * Open the Visual Studio Code command palette (View->Command Palette)", "WARNING12": " * run the command: '.NET: Generate Assets for Build and Debug'.", "WARNING13": "", "WARNING14": "If your project requires a more complex launch configuration, you may wish to delete", "WARNING15": "this configuration and pick a different template using the 'Add Configuration...'", "WARNING16": "button at the bottom of this file.", "WARNING17": "*********************************************************************************", "preLaunchTask": "build", "program": "${workspaceFolder}/bin/Debug/netcoreapp3.1/myapp.dll", "args": [], "cwd": "${workspaceFolder}", "console": "internalConsole", "stopAtEntry": false }, { "name": ".NET Core Attach", "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}" } ] }設定ファイル自体は自動生成する機能がありますが、プラットフォーム名(
netcoreapp3.1
)やアプリケーション名(myapp.dll
)などは各自設定する必要があります。Riderを使用する場合は、設定に関しては自動的に行ってくれる部分も多いのでかなり楽です。
最後に
.NET Core登場以前はMonoなどがあったものの、やはり.NETはWindowsの環境という性格が強かったのですが、.NET Core登場以後はツール周りも含めて、Linux環境下でも遜色ない開発ができるようになってきました。
特にデプロイ先がLinuxである場合など、Linuxで開発できると便利、という場面もあるかもしれません。そのような場合に、Linuxでも開発環境が作れるというのは大きな利点だと思います。
- 投稿日:2020-05-29T10:20:21+09:00
多人数リアルタイム通信ゲームの移動処理について考察する
はじめに
VRM使ったリアルタイム通信でなんか面白いこと出来ないか考えているyoship1639です。
リアルタイム通信は以前は低レイヤのAPIを使いやすいように自分でラップしたり、フラグメント化回避のために四苦八苦したり、サーバ~クライアント間の統一したインターフェースをどうやって実装するか悩んだり、シリアライズどうするか絶望したりしていましたが、最近はgRPCがでたり、それを.Netで使いやすいようにラップしたMagicOnionが登場したりで上記の悩みを全部吹き飛ばしてくれる規格やフレームワークが出てきていい時代になったな~と実感しています。
リアルタイム通信は上記の悩みが最初のボトルネックなので、クリアしたら今度はそれを如何に活用できるかが重要になります。例えばチャットアプリは何も考えずにクライアント->サーバ->全クライアントという様にメッセージを流せばいいですが、これが3Dリアルタイム通信ゲームだと話が変わります。
3Dリアルタイム通信ゲームでよくあるのはキャラクタの移動ですが、これをクライアント全てのキャラクタの移動をリアルタイムで同期するにはどうすればいいでしょうか。
今回は、Unityでリアルタイム通信を使った3Dキャラクタの移動処理を実装してみたら意外といい線行ったので、リアルタイム通信特有の問題をどのような考えを元に乗り越え、どの様な実装をしたのかをまとめられればと思います。
多人数リアルタイム通信
多人数のリアルタイム通信の移動処理は簡単にまとめると以下の形が基本形となります
【クライアント】
- 自キャラ(自クライアントのキャラクタ)を移動させる
- 一定間隔で自キャラの位置、回転をサーバに送信する
- 他キャラ(他クライアントのキャラクタ)の位置、回転情報をサーバから受け取り、他キャラのモデルに適用させる
【サーバ】
- クライアントからキャラクタの位置、回転を受け取る
- 一定間隔でクライアントに全キャラクタの位置、回転をブロードキャストする
通信処理を記述するなら当たり前のことを綴っているだけですが、実は上記の説明ではリアルタイム通信が抱える問題を密かに解決しています。それは扇問題(正確な名称かは不明)です。扇問題は本記事とは直接的には関係ないので、記事の最後に記載するので解決の詳細を知りたい方は番外編:扇問題の解決を見てください。
さて、他キャラをリアルタイムに同期するときに起こる問題はどの様なことでしょうか。
特有の問題
キャラクタがワープしながら移動する
何も考えずにサーバから受け取った位置、回転の情報を他キャラに反映させると、他キャラはワープしながら移動します。これはサーバから受け取る他キャラ情報がPCのリフレッシュレートよりも遅い時、または一定間隔で受け取れない時に発生します。これは安直にコーディングしたら起こる至極当然の現象なので理解できるかと思います。
分かりやすく仮にコーディングすると以下のようになります。
class CharacterInfo { public Vector3 pos; // キャラクタの位置 public Quaternion rot; // キャラクタの回転 } // サーバからキャラクタ情報を受け取る度に呼ばれる public void OnReceive(CharacterInfo info) { transform.position = info.pos; transform.rotation = info.rot; } void FixedUpdate() { // 特に何もしない }キャラクタが一定のスピードで移動しない①
次に、キャラクタの移動に補間処理を付け加えてみます。他キャラがワープするのを避けたい場合、前回受け取った他キャラ情報からキャラクタの位置、回転を予測しその位置に他キャラを配置する処理を施します。
class CharacterInfo { public Vector3 pos; public Quaternion rot; } private CharacterInfo prevInfo; // 前回の位置情報 private CharacterInfo nowInfo; // 位置情報 private float time; // 受け取った時点の時間 public void OnReceive(CharacterInfo info) { prevInfo = nowInfo; nowInfo = info; time = Time.time; } void FixedUpdate() { // timeを元にtransformを補間 var delta = Time.time - time; var rate = delta / Time.fixedDeltaTime; transform.position = Vector3.Lerp(prevInfo.pos, nowInfo.pos, rate); transform.rotation = Quaternion.Slerp(prevInfo.rot, nowInfo.rot, rate); }補間処理が記述できているので問題なさそうです。
さて、これがどの様な挙動をするかというと、他キャラは一定速度で移動せず、微妙にブツ切りにワープします。なぜそのようなことが起こるかというと、サーバから一定間隔で他キャラ情報を受け取っていないからです。この移動処理は一定間隔でサーバから他キャラ情報を受け取ることを前提にしていますが、それは回線やその他の問題で事実上不可能です。なので、一定間隔でサーバから情報を受け取れない事を前提に移動処理を記述しなければなりません。
キャラクタが一定のスピードで移動しない②
先の対応ではキャラクタが一定のスピードで移動してくれない事が分かりました。なので、線形補間を使った滑らかな移動を試してみます。キャラクタの現在位置から目的の位置までを毎フレーム差分時間を使って移動させるやり方です。移動処理だけでなくいろんな場面で滑らかな表現を可能にする手法なので行けそうな気がします。
class CharacterInfo { public Vector3 pos; public Quaternion rot; } [SerializeField] private float damp = 4.0f; //減衰 private CharacterInfo targetInfo; // 目的の位置情報 public void OnReceive(CharacterInfo info) { targetInfo = info; } void FixedUpdate() { transform.position = Vector3.Lerp(transform.position, targetInfo.pos, Time.fixedDeltaTime * damp); transform.rotation = Quaternion.Slerp(transform.rotation, targetInfo.rot, Time.fixedDeltaTime * damp); }この手法でキャラクタの動きがどの様に見えるかというと、一定速度で動かずガクガクした動きになります。damp(移動減衰値)を下げれば滑らかな動きに多少なりますが、キャラクタの本来の位置と現在位置がかなりずれるため、リアルタイム性が求められるゲームには全く向いていません。
↑ガクガクした動きの例(apngなのでブラウザによってはうまく表示されないかもしれません)色んな補間のやり方がありますが上記の手法ではどれもうまくいきませんでした。サーバからデータが送られてくる間隔が一定でない場合に他キャラをほとんど遅延なく滑らかに動かすにはどうすれば良いでしょうか。
考えた解決手法
上記の問題を解決するために考えた解決手法は、他キャラが位置情報を送信する時点の時間を同時に送り、サーバから受け取った他キャラ情報をリストに保持し自クライアントの時間と他キャラ情報の時間を比べて位置を補間するというやり方です。分かりやすく解説していきます。
① 他クライアントは送信時に位置情報だけでなく時間も同時に送る
他クライアントは通常位置情報をサーバに送信するだけですが、そこに送信時の時間も載せます。こうする事で、クライアントがサーバから受け取った他キャラ情報がサーバから受け取った時間ではなく他キャラの送信時点の時間になるので、一定時間で受け取らなくても問題にならなくなります。(MagicOnionのhubを使っていますが、ここでは詳細を割愛いたします)
class CharacterInfo { public Vector3 pos; public Quaternion rot; public float time; } IEnumerator UpdateCoroutine() { while(true) { // サーバにキャラクタの位置、回転、送信時のクライアント時間を送る var info = new CharacterInfo() { pos = player.transform.position, rot = player.transform.rotation, time = Time.time }; hub.UpdateAsync(info); // 0.04秒ごとにサーバに送信する想定 yield return new WaitForSeconds(0.04f); } }② サーバから受け取った他キャラ情報をリストに保持する
次に、自クライアントは他キャラ情報をリストに保持していきます。これは、ある一定期間の他キャラ情報が無いと補間が出来なくなるためです。
class CharacterInfo { public Vector3 pos; public Quaternion rot; public float time; } private List<CharacterInfo> infoList = new List<CharacterInfo>(); public void OnReceive(CharacterInfo info) { infoList.Add(info); }③ 自クライアントと他クライアントの時間差を考慮する
このままでは、自クライアントの時間と他クライアントの時間に差が出来てしまいうまく位置の補間が出来なくなります。なので、時間差を埋める処理を入れます。
class CharacterInfo { public Vector3 pos; public Quaternion rot; public float time; } private List<CharacterInfo> infoList = new List<CharacterInfo>(); private float fixedTime = 0.0f; // 他キャラ情報の補正時間 private float startTime = 0.0f; // 自クライアントの補正時間 private Vector3 targetPos; // 他キャラの目的の位置 void Start() { // 自クライアントの時間補正も考慮する startTime = Time.fixedTime; } public void OnReceive(CharacterInfo info) { // 最初に送られてきた他キャラ情報の時間を起点にする // こうする事で、送られてきた時点から経過時間を考える事が出来る if (fixedTime == 0.0f) fixedTime = info.time; info.time -= fixeTime; infoList.Add(info); }④ 時間から他キャラの位置を補完する
補正時間を考慮した記述で他キャラの位置を割り出します。
class CharacterInfo { public Vector3 pos; public Quaternion rot; public float time; } [SerializeField] private float moveDamp = 12.0f; // 移動減衰 private List<CharacterInfo> infoList = new List<CharacterInfo>(); private float fixedTime = 0.0f; // 他キャラ情報の補正時間 private float startTime = 0.0f; // 自クライアントの補正時間 private int infoListIdx = 0; // 参照するinfoListのindex void Start() { // 自クライアントの時間補正も考慮する startTime = Time.fixedTime; } public void OnReceive(CharacterInfo info) { // 最初に送られてきた他キャラ情報の時間を起点にする // こうする事で、送られてきた時点から経過時間を考える事が出来る if (fixedTime == 0.0f) fixedTime = info.time; info.time -= fixeTime; infoList.Add(info); } void FixedUpdate() { var t = Time.fixedTime - startTime; // 現在の時間 var i = infoListIdx; while (i < infoList.Count - 2) { // 一定期間内に他キャラがいる if (t >= infoList[i].time && t < infoList[i+1].time) { // 補間時間を割り出す var rate = Mathf.InverseLerp(infoList[i].time, infoList[i+1].time, t); // 目的位置を割り出す targetPos = Vector3.Lerp(infoList[i].pos, infoList[i+1].pos, rate); // 回転はここで指定 transform.rotation = Quaternion.Slerp(infoList[i].rot, infoList[i+1].rot, rate); // インデックス指定 infoListIdx = Mathf.Max(i - 1, 0); // 不要な他キャラ情報は削除 if (infoListIdx > 0) infoList.RemoveAt(0); break; } i++; } // 滑らかに移動するために固めの線形補間を使う transform.position = Vector3.Lerp(transform.position, targetPos, Time.fixedDeltaTime * moveDamp); }⑤ レイテンシを考慮する
しかしこのままでは、他クライアント->サーバ->自クライアントに情報が来るまでの遅延を考慮していないので、正しい動作にならない可能性があります。そこで、他クライアントから送られてくる情報の遅延の大きさに関わらずに位置を補正できる様に可変レイテンシを組み込みます。
可変レイテンシを組み込んで細かい調整をした最終的な形はこうなります。
class CharacterInfo { public Vector3 pos; public Quaternion rot; public float time; } [SerializeField] private float moveDamp = 12.0f; // 移動減衰 [SerializeField] private int interval = 2; // infoList参照の広さ private List<CharacterInfo> infoList = new List<CharacterInfo>(); private float fixedTime = 0.0f; // 他キャラ情報の補正時間 private float startTime = 0.0f; // 自クライアントの補正時間 private int infoListIdx = 0; // 参照するinfoListのindex private float latency = 0.0f; // 最初は遅延なしで考える void Start() { // 自クライアントの時間補正も考慮する startTime = Time.fixedTime; } public void OnReceive(CharacterInfo info) { // 最初に送られてきた他キャラ情報の時間を起点にする // こうする事で、送られてきた時点から経過時間を考える事が出来る if (fixedTime == 0.0f) fixedTime = info.time; info.time -= fixeTime; infoList.Add(info); } void FixedUpdate() { var t = Time.fixedTime - startTime - latency; // 現在の時間 var i = infoListIdx; while (i < infoList.Count - interval) { // 一定期間内に他キャラがいる var fromInfo = infoList[i]; var toInfo = infoList[i + interval - 1]; if (t >= fromInfo.time && t < toInfo.time) { // 補間時間を割り出す var rate = Mathf.InverseLerp(fromInfo.time, toInfo.time, t); // 目的位置を割り出す targetPos = Vector3.Lerp(fromInfo.pos, toInfo.pos, rate); // 回転はここで指定 transform.rotation = Quaternion.Slerp(fromInfo.rot, toInfo.rot, rate); // インデックス指定 infoListIdx = Mathf.Max(i - 1, 0); // 不要な他キャラ情報は削除 if (infoListIdx > 0) { infoList.RemoveAt(0); // 遅延なく参照できた場合は遅延を少し少なくする latency = Mathf.Max(latency - Time.fixedDeltaTime, 0.0f); } break; } i++; } // 上手く参照できなかった場合は遅延を大きくする if (i >= infoList.Count - interval) latency += Time.fixedDeltaTime; // 滑らかに移動するために固めの線形補間を使う transform.position = Vector3.Lerp(transform.position, targetPos, Time.fixedDeltaTime * moveDamp); }移動処理だけを重点的に記述しただけなのでこのままでは当然動きませんが、これをベースに動くようにしたら下図の様に他キャラが動くようになります。
いかがでしょう、自分でキャラを動かしている様に見えるくらいには滑らかに動かせているのではないでしょうか。
様々な手法を試して一番良い動きをしたのがこの手法でした。今回の手法ではキャラクタの動きを予測して移動させる(加速度や入力方向をサーバから受け取って処理する)という事はしていません、予測した動きをするとキャラクタがいきなり早くなったりワープせざる負えない可能性があるからです。もちろん予測した動きを記述したほうが遅延が無いように見せることできますが、そのコードを記述するのには少し時間がかかりそうなので、一先ず今回の手法を提案した次第です。
番外編:扇問題の解決
扇問題とは、クライアントサーバモデルでN個のクライアントの情報をリアルタイムに他のクライアントに反映させたい場合、安直にコーディングすると通信回数が$N * (N-1)$になるという問題です。つまり、クライアント数が増えれば増えるほど、通信回数が爆発的に増えるという事です。
詳しくは以下の記事を参照いただければ分かるかと思います
この問題、安直なコーディングをすると必ず発生してしまうのですが、意外と簡単に回避できます。
それは、サーバからクライアントにデータを送るタイミングを、クライアントからデータを受け取った時ではなく、サーバ主導で一定間隔で送るようにすればいいだけです。クライアントからデータを受け取る度にサーバが処理していたら処理負荷がクライアントの数に引っ張られてしまうので、それを回避するというのが重要となります。詳しく説明する前にまず、サーバがクライアントからデータを受け取ったタイミングで全クライアントにデータをブロードキャストする場合の通信回数の例を見てみます①。
クライアント数を$N = 100$、クライアントは秒間$T = 20$回データを送信すると仮定すると、1秒間にサーバからクライアントに向けて発生する通信回数は以下の通りとなります。
$N * (N - 1) * T = 100 * 99 * 20 = 198,000$
これでは、通信回数があまりにも多くて負荷が大きすぎるのは目に見えています。
次に、サーバ主導でクライアントにデータをブロードキャストするとどうなるか見てみます②。
先ほどと同じく、$N = 100$、クライアントは秒間$T = 20$回データを送信すると仮定します。そして、サーバは秒間20回全クライアントのデータをまとめて送信する様にします。すると、通信回数は以下の通りになります。
$N * T = 100 * 20 = 2,000$
198,000が2,000まで減りました。約1/100です。少し工夫するだけでこれだけ通信回数が変わりました。
気になるのは通信量です。実は①と②は通信量自体はほぼ変わりません。クライアントが送信するデータを$D = 28$byte(位置(float x 3) + 回転(float x 4))と仮定すると、①の1秒間の通信量は
99人に28byte送信する処理が100人分あり、それを20回行うので$(N * (N - 1) * D) * T = 100 * 99 * 28 * 20 = 5,544,000$
で約5.54MBです。
②の1秒間の通信量は
100人に100人分の28byte送信を20回行うので$N * (N * D) * T = 100 * 100 * 28 * 20 = 5,600,000$
でこちらは約5.6MBとなります。
通信量が変わらないとあまり意味がないと思うかもしれませんが、1 x 10000 と 100 x 100をプログラムが捌くのは明らかに後者の方が速いですし、更に今回はMagicOnionを使っている前提の話なので、内部のデータフォーマッタであるMessagePackによる送信データの圧縮が効きます。それを考えると、②の方が断然効率が良くなります。
キャラクタの位置回転等、秒間20回など高頻度で送信しなければならない場合は②の方が良いですが、例えばキャラクタがアイテムを拾うという処理データを通信するとなると、逆にこちらは①の方が良いです。なぜなら、送信頻度が高くなく連続で送らなくてもいいデータだからです。処理によって住み分けをしっかり行うことが大切になります。
まとめ
3Dリアルタイム通信の移動処理について、うまくいかなかったパターンと解決出来たパターンを紹介いたしました。
勿論、今回考察した移動処理の捌き方が正解というわけではありませんし、問題なく処理できるという保証もありません(実際サーバをクラウドにデプロイして確認したわけではないので…)。なので、1つのやり方としてこんな移動処理の捌き方があるんだという認識をしていただければと幸いです。この手の記事がほとんど見受けられなかったので、率先して記事にさせていただきました。今回の手法よりもより現実的で実用的な手法があったら是非参考にしたいのでご教授いただければと思います。
権利表記
以下のモデルをお借りして動作確認を行っています。
初音ミク(6)
https://3d.nicovideo.jp/works/td66256
著作: 如月z 様
https://3d.nicovideo.jp/users/56980238
- 投稿日:2020-05-29T09:25:39+09:00
C#のグローバル変数を実現する上で一番わかりやすかった記事って結局これだった
嘘をつきました、記事より、いただいたコメントの方が分かりやすかったです。
@albireoさんより紹介
public static partial class App { static public string Cnstr = ""; // グローバル変数の宣言 static public string ID= ""; public string NonStatic = "abc"; // エラーになる //頑張って分かるやりかた。 //1、「エラーになる」と「・・・の宣言」の違いを比較する。 //2、クラス名称を別例(もう一つの例)と比較する。普通のクラス宣言とも比較する。 //3、for文やif文などで条件分岐から値を取得する場合にもCnstr、つまりグローバル変数が使える便利。 // でもクラス内だけならクラスのグローバル宣言要らない可能背があるかも? } //外部クラスでグローバル変数をつかう。 private void Btn_Update_Click() { Cconsole.WriteLine(App.Cnstr); // App で宣言されたグローバル変数を利用 }@shiracamus さんより以下のコメントをいただきました。
一般的に「クラス変数」と呼ばれるものですね。
オブジェクト指向では、依存するクラスを減らす努力が必要です。
つまり、クラス間の依存をなくす。何故なくすかっていうと、何も考えずに文字を羅列すると
「複数のクラスに色々またがってるから管理が面倒。かつ危険」
「このクラスって何してるん?あれ?」ってなりそう
「引継ぎの時面倒」「作り直ししたくなる」
なんだけど。何でかぐぐる。これが分かりやすかった。と紹介しましたが・・・嘘でした。
https://w.atwiki.jp/cs00/pages/11.html
これ中身//グローバル変数を設定。 public partial class App : Application { static public string Cnstr = ""; // グローバル変数の宣言 static public string ID= ""; } //グローバル変数をつかう。 private void Btn_Update_Click() { App.Cnstr = "SQL"; // App で宣言されたグローバル変数を利用 }@shiracamusさんより、
「クラスの数を複数設けないように努力する必要が有る。」というアドバイスもあり
グローバル変数用のクラスを設けなくてもいいことがわかりました。
- 投稿日:2020-05-29T08:54:29+09:00
CefSharpでFlashを再生する方法
はじめに
CefShrapでFlashが使えるようにする方法がけっこう複雑だったので、まとめておきます。
pepflashplayer.dllのダウンロード
CefSharpではFlashがサポートされていないので、FlashのDLLファイルを用意する必要があります。
こちらからバージョン28.0.0.137のものをダウンロードしてください。
最新のDLLだと動かないので注意してください。ダウンロードしたらファイルを解凍し、pepflashplayer.dllを
プロジェクトフォルダ\bin\Debug
にコピーしておきます。Flashの設定
CefSharpではFlashがデフォルトで無効化されているので、パラメータで設定します。
CefSettings settings = new CefSettings(); // レンダリングを最適化 settings.SetOffScreenRenderingBestPerformanceArgs(); // Flashを有効化 settings.CefCommandLineArgs.Add("enable-npapi", "1"); settings.CefCommandLineArgs.Add("ppapi-flash-path", "pepflashplayer.dll"); settings.CefCommandLineArgs.Add("ppapi-flash-version", "28.0.0.137"); settings.CefCommandLineArgs.Add("debug-plugin-loading", "1"); settings.CefCommandLineArgs.Add("allow-outdated-plugins", "1"); settings.CefCommandLineArgs.Add("always-authorize-plugins", "1"); settings.CefCommandLineArgs.Add("disable-web-security", "1"); settings.CefCommandLineArgs.Remove("enable-system-flash"); settings.CefCommandLineArgs.Add("enable-system-flash", "1"); settings.CefCommandLineArgs.Add("plugin-policy", "allow"); settings.CefCommandLineArgs.Add("disable-plugins-discovery", "1"); Cef.Initialize(settings); // 自動再生を許可 var contx = Cef.GetGlobalRequestContext(); Cef.UIThreadTaskFactory.StartNew(delegate { contx.SetPreference("profile.default_content_setting_values.plugins", 1, out string err); }); cefBrowser = new ChromiumWebBrowser("https://www.google.co.jp/");これで再生できるようになるはずです。
- 投稿日:2020-05-29T08:47:44+09:00
螺旋状にオブジェクトを生成する
説明
Unityで螺旋状にオブジェクトを生成します。
前回 は円状にオブジェクトを生成しました。
そのコードから単に、Z軸を少しずつずらしているだけです。奥行きの距離を表すために、Length設定値を復活させています。
また、位置決めの前に Z軸の 足し算を行っているので、
初期値は1回分足し込み分を引いています。数が少ないと、わかりにくいのでオブジェクトを100個生成してみました。
コード
前回まで Creater って書いていた。。
Creatorでした・・・using UnityEngine; public class SpiralObjectCreator : MonoBehaviour { [SerializeField] private GameObject createObject; // 生成するオブジェクト [SerializeField] private int itemCount = 100; // 生成するオブジェクトの数 [SerializeField] private float radius = 5f; // 半径 [SerializeField] private float repeat = 5f; // 何周期するか [SerializeField] private float length = 50f; // Z軸の長さ void Start () { var oneCycle = 2.0f * Mathf.PI; // sin の周期は 2π var oneLength = length / itemCount; // Z軸の1単位 var z = transform.position.z - oneLength; // Z軸初期位置 (生成前に足しこみをしているので、一回分引いておく) for (var i = 0; i < itemCount; ++i) { var point = ((float)i / itemCount) * oneCycle; // 周期の位置 (1.0 = 100% の時 2π となる) var repeatPoint = point * repeat; // 繰り返し位置 var x = Mathf.Sin(repeatPoint) * radius; var y = Mathf.Cos(repeatPoint) * radius; z += oneLength; var position = new Vector3(x, y, z); Instantiate( createObject, position, Quaternion.identity, transform ); } } }GitHub
- 投稿日:2020-05-29T08:43:56+09:00
円状にオブジェクトを生成する
説明
Unityで円状にオブジェクトを生成します。
前回のサインカーブにオブジェクト生成するものからの発展です。
ちょっとリファクタリングしてますが、yの生成方法は前回と同じです。
さらに、xを同様にCosで求めるだけで、円形になります。それぞれの周期が半分ずつずれてるので、うまい具合に円になってくれるのですが、
xとyにそれぞれCosとSin与えておけば円になるって覚えとけばなんとかなります。あと、距離は円ということで 半径radiusに置き換えました。
円形なので、繰り返しについては正直意味がないんですが、
次の発展絵使えるのでとりあえず残しています。また、真ん中の球はコピー元のオブジェクトです。
邪魔な場合は、createObjectにPrefabを指定するか、
円を生成後、createObject自体を消滅させても良いでしょう。サンプルコード
using UnityEngine; public class CircleObjectCreater : MonoBehaviour { [SerializeField] private GameObject createObject; // 生成するオブジェクト [SerializeField] private int itemCount = 40; // 生成するオブジェクトの数 [SerializeField] private float radius = 5f; // 半径 [SerializeField] private float repeat = 2f; // 何周期するか void Start () { var oneCycle = 2.0f * Mathf.PI; // sin の周期は 2π for (var i = 0; i < itemCount; ++i) { var point = ((float)i / itemCount) * oneCycle; // 周期の位置 (1.0 = 100% の時 2π となる) var repeatPoint = point * repeat; // 繰り返し位置 var x = Mathf.Cos(repeatPoint) * radius; var y = Mathf.Sin(repeatPoint) * radius; var position = new Vector3(x, y); Instantiate( createObject, position, Quaternion.identity, transform ); } } }GitHub
- 投稿日:2020-05-29T08:40:11+09:00
サインカーブ状にオブジェクトを生成する
説明
Unityでサインカーブ状にオブジェクトを生成します。
赤線は説明用の手書きです。
sinカーブを2周期で、20個のオブジェクトを生成。
全体の距離が20で、上下の振幅を2倍大げさに見せています。各オブジェクトの配置間隔は
距離 / オブジェクト数
で求めています。createObjectに設定したGameObjectを繰り返して貼り付けます。
パラメータの詳細はコードのコメントを参照してください。
サンプルコード
using UnityEngine; public class ObjectCreaterCurve : MonoBehaviour { [SerializeField] private GameObject createObject; // 生成するオブジェクト [SerializeField] private int itemCount = 20; // 生成するオブジェクトの数 [SerializeField] private float length = 20f; // アイテムの広がる距離 [SerializeField] private float expantion = 2f; // 高さ変動の拡大値 [SerializeField] private float repeat = 2f; // 何周期するか void Start () { var unit = length / itemCount; // オブジェクトの配置間隔 var oneCycle = 2.0f * Mathf.PI; // sin の周期は 2π for (var i = 0; i < itemCount; ++i) { var ratio = (float)i / itemCount; // 周期の位置 (1.0 = 100% の時 2π となる) var x = i * unit; var y = Mathf.Sin(ratio * oneCycle * repeat) * expantion; var position = new Vector3(x, y); Instantiate( createObject, position, Quaternion.identity, transform ); } } }プロジェクトファイル
gitHubにプロジェクト毎アップロードしています。
https://github.com/becky3/unity-test-SinCurve
- 投稿日:2020-05-29T02:46:44+09:00
Blazor WebAssembly を触ってみる - その③コードを少し書いてみる
Blazor WebAssembly を触ってみる - その①環境を整える、サンプルを動かす
Blazor WebAssembly を触ってみる - その②デバッグしてみる
の続きです。
コンポーネント (Razor コンポーネント)
コードを書いて見る前に少し用語を勉強しておく。
今日の参考資料はこれ。ASP.NET Core Blazor の概要
https://docs.microsoft.com/ja-jp/aspnet/core/blazor/?view=aspnetcore-3.1#blazor-webassembly
- Blazor のアプリはコンポーネント (.razor ファイル) によって構成される。コンポーネントってのは「ページ、ダイアログ、データ エントリ フォームなどの UI の要素」らしい。
- C# コードに HTML マークアップを結合するための Razor 構文で記述する
触ってみる
Before
一部コメントアウトしましたが、サンプルでは文字を表示するだけのシンプルな HTML のコードでした。
Pages\Index.razor@page "/" <h1>Hello, world!</h1> Welcome to your new app. <!--<SurveyPrompt Title="How is Blazor working for you?" /> -->After
Dialog.razor という子コンポーネントのファイルを作成します。
ここでは HTML の div 要素とその中の変数 Title や ChildConponent、onclick 時に呼ばれそうな OnYes 関数の定義などを記述しています。Pages\Dialog.razor<div> <h1>@Title</h1> @ChildContent <button @onclick="OnYes">Yes!</button> </div> @code { [Parameter] public string Title { get; set; } // 親子間のコンポーネントの受け渡しに不可欠で名前変更はできない (これや @ChildContent がないと実行時にエラー) [Parameter] public RenderFragment ChildContent { get; set; } private void OnYes() { Console.WriteLine("Write to the console in C#! 'Yes' button was selected."); } }この div 要素やボタンを配置してくれそうな子コンポーネント Dialog.razor を親コンポーネント Index.razor で表示します。
Pages\Index.razor@page "/" <h1>Hello, world!</h1> Welcome to your new app. <!--<SurveyPrompt Title="How is Blazor working for you?" />--> <!-- コンポーネントの名前、変数 Title に渡す値など Dialog.razor に渡す値を記述 (関数の引数みたい) --> <Dialog Title="Blazor"> Do you want to <i>learn more</i> about Blazor? </Dialog>何やら表示されました。これが Dialog.razor で記述した箇所ですね。
Yes! ボタンをクリックすると、Dialog.razor で定義した OnYes() 内の Console.WriteLine("Write to the console in C#! 'Yes' button was selected."); が実行され、Console に出力されました。
HTML 表示は以下ですが、blazor.webassembly.js を指定しているくらいです。
blazor.webassembly.js は .NET ランタイム、アプリ、およびアプリの依存関係のダウンロード、アプリを実行するランタイムの初期化を行うようです。<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>blazorApp1</title> <base href="/" /> <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" /> </head> <body> <app>Loading...</app> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">?</a> </div> <script src="_framework/blazor.webassembly.js"></script> </body> </html>今日はここまで。