FC2ブログ
ヽ|∵|ゝ(Fantom) の 開発blog? ホーム »Unityリファレンス
このページの記事一覧

【Unity】【C#】UniRx で「1フレームごと待機して処理」してみる  


 今まで「VRM Live Viewer」では非同期処理に「Await Extensions」というライブラリを使っていたのだが、「UniRx」の非同期処理関連の資料を見てたら、同じようなことができるとわかったので、移行してみようと考えた。


 ただ注意しなくてはならないのは、「UniRx」の非同期処理は Scripting Runtime を「.NET 4.x」にすることと、C# 7.0が必須のため、現時点(Unity2018.2.x)では「Incremental Compiler」を導入する必要があるとのことだ。

UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合

 「Incremental Compiler」はまだ Preview 版のため、商用利用は控えたほうが良いともあるね。まぁ、「VRM Live Viewer」はフリーなのと、色々実験してみたが、安定性にも問題無いようなので(どちらかというと Stable[安定版] が出たら、メソッドの仕様(引数など)が変わって、書き換え必須となることの方が辛いね(笑))、導入を試みてみたら成功した。実際「UniRx」上で使える「UniTask」は「async/await/Task」の上位互換で、現在私が使っている機能もそのまま移行できたので、基本的な使い方ならば、バージョンアップ・正式版が出てもそれほど問題ないとも思う。また今後UI処理などを少し強化したいしね。そういったものは「UniRx」はとても強い(笑)。

 なので、現時点では導入する人は少ないかも知れないが、「.NET 4.x」の非同期処理「async/await/Task」が使えるようになると、コルーチンで書いていたものがスッキリする上に、別スレッドでバックグラウンド処理などもとても簡単になるので、いずれは誰しも使うようになるだろう(笑)。そしてはじめに使いたいのはやはり、今までコルーチンなどで書いていた「1フレーム待機して処理」かなと(笑)。ググったらなぜかあまりハッキリとした答えがなかったので、ちょっと実験してみた感じ。たぶん他の人も同じことを調べるだろうしね。

■「1フレームごとに待機して処理」っぽくなりそうなものを、色々やってみる
 ●コルーチンで1フレームごとに待機して処理(ログ出力のみ。動作の基準)
 ●Observable.NextFrame() で1フレームごとに待機して処理?
 ●Observable.TimerFrame() で1フレームごとに待機して処理(引数=1)?
 ●Observable.TimerFrame() で1フレームごとに待機して処理(引数=0)?
 ●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=1)?
 ●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=0)?
 ●UniTask.DelayFrame() で1フレームごとに待機して処理?
 ●UniTask.WaitUntil() で1フレームごとに待機して処理?
■とりあえず「1フレームごとに待機して処理」を簡単に(実験してみた結果)

(※) Unity 2018.2.1f1 / UniRx 6.2.2 / Incremental Complier 0.0.42(Preview) / Windows10(x64) で確認



■「1フレームごとに待機して処理」っぽくなりそうなものを、色々やってみる

 まずは今まで通り、コルーチンで「1フレームごとに待機して処理」をしてみる。処理自体はただのログ出力なので、実際の処理の重さは考慮に入れてない。ちなみに「Await Extensions」では「yield return new WaitForEndOfFrame()」の代わりに「await new WaitForEndOfFrame()」が使える(そしてメソッドに async が使えるので非同期処理が簡潔に書ける。StartCoroutine() もいらない)。


●コルーチンで1フレームごとに待機して処理(ログ出力のみ。動作の基準)
using System.Collections;
using UnityEngine;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

StartCoroutine(WaitForEndOfFrameCoroutineTest());
}

IEnumerator WaitForEndOfFrameCoroutineTest()
{
Debug.Log("frame : " + Time.frameCount); //1
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //2
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //3
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //4
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //5
}
}

(Start) frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5

 なんてことはないコルーチンで順次処理していくときのようなコード(本来は同じ処理ならループにするだろうが、これはあくまでサンプルなので、実際には別々の処理が入ると考えて欲しい)。これを基準として「UniRx」でオペレータなども使って色々試してみよう。なお、ここでは「yield return new WaitForEndOfFrame()」を使っているが、実行タイミングを気にしないなら「yield return null」でも良い。

イベント関数の実行順



●Observable.NextFrame() で1フレームごとに待機して処理?

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableNextFrameTest();
}

async void ObservableNextFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //3
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //5
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //7
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //9
}
}

(Start) frame : 1
frame : 1
frame : 3
frame : 5
frame : 7
frame : 9

 UniRx で一番始めに目についたのは「Observable.NextFrame()」だったが、どうやら await で1フレーム、NextFrame() で1フレーム待機のようだ。フレームが1つ飛びになっていた。期待していた処理とは違う。



●Observable.TimerFrame() で1フレームごとに待機して処理(引数=1)?

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableTimerFrameTest();
}

async void ObservableTimerFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //5
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //7
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //9
}
}

(Start) frame : 1
frame : 1
frame : 3
frame : 5
frame : 7
frame : 9

 Observable.NextFrame() と結果は同じになった。ではちょっと引数を0にしてみようと実験してみると…


●Observable.TimerFrame() で1フレームごとに待機して処理(引数=0)

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableTimerFrameTest();
}

async void ObservableTimerFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //2
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //4
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //5
}
}

(Start) frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5

 いけた(笑)。これは期待していた処理と合致する。



●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=1)?

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableDelayFrameTest();
}

async void ObservableDelayFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //5
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //7
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //9
}
}

(Start) frame : 1
frame : 1
frame : 3
frame : 5
frame : 7
frame : 9

 これも Observable.NextFrame() と結果は同じになった。では引数を0にしてみると…


●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=0)

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableDelayFrameTest();
}

async void ObservableDelayFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //2
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //4
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //5
}
}

(Start) frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5

 これもいけた。これは期待していた処理と合致する。実装自体は Observable.TimerFrame() とは違うみたいだけどね。



●UniTask.DelayFrame() で1フレームごとに待機して処理?

using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

UniTaskDelayFrameTest();
}

async void UniTaskDelayFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //2
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //3
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //4
}
}

(Start) frame : 1
frame : 1
frame : 1
frame : 2
frame : 3
frame : 4

 せっかくのなで、UniTask も試してみた。するとあれ?なぜか初めの1回は待機されてない。なんか理由があるのだろうけど、今回は放おっておこう(←誰か調べて(笑))。こちらは「using UniRx.Async;」が必要。



●UniTask.WaitUntil() で1フレームごとに待機して処理?

using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

UniTaskWaitUntilTest();
}

async void UniTaskWaitUntilTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //2
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //3
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //4
}
}

(Start) frame : 1
frame : 1
frame : 1
frame : 2
frame : 3
frame : 4

 UniTask.DelayFrame() と同じ結果になった。これもはじめの1回が待機されてない…?ま、まぁ、今回はコルーチンでの表記をそのまま代替できる書き方を探していただけなので(パフォーマンスなども考慮に入れてない)、これで勘弁してやろう(笑)。誰かそのうち調べてくれるだろうと期待してる(←投げっぱなし(笑))。



■とりあえず「1フレームごとに待機して処理」を簡単に(実験してみた結果)

 他にも色々実験している記事もあったが、単純に面倒なので、とりあえず「Observable.TimerFrame(0)」または「Observable.ReturnUnit().DelayFrame(0)」を代替として使うことにしてみた。もしからしたら正しい使い方ではないかも知れないので、static な関数(WaitForFrame)にしておいて、後で書き換えられるようにしておけば、修正も楽かも知れない。「Observable.TimerFrame(0)」は UniRx 特有の「マイクロコルーチン」というものを使っているので(負荷が軽いらしい)、今回はこちらで書いておこう。まぁ、戻値の型の問題はあるが、あくまでコルーチンでの「yield return null」みたいな使い方を想定しているので、今回は気にしないとする(笑)。Observable のように拡張メソッドにしても良いと思うけど(「Observable_Joins.cs」に書くとか)、その辺はご自由に。

●とりあえず1フレームごとに待機して処理
using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

WaitForFrameTest();
}

async void WaitForFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //2
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //3
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //4
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //5
}

public static IObservable<long> WaitForFrame()
{
return Observable.TimerFrame(0);
}
}

(Start) frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5

 「Observable.TimerFrame()」だと戻値の型が long となっているので、Unit にしたいなら、以下のようにしても良いかも知れない(まぁ、今回の使い方のように、捨て値なら無駄な処理が増えるだけだが…)。

●戻値を Unit に変えた例
public static IObservable<Unit> WaitForFrame()
{
return Observable.TimerFrame(0).AsUnitObservable();
}


 こちらの記事にも書いてあるけど「コルーチンをほぼ駆逐」できると表記がすっきりし、しかもコルーチンと違って、別スレッド動作や非アクティブなオブジェクトでも動くので(というより、MonoBehaviour に依存しないで動作できるので)、いずれは「async/await」的な書き方の方が主流になるかもね。「VRM Live Viewer」は商用アプリにするつもりは無いし、実験的にやってみることに意義があると思ってるので(ファイルドロップ→非同期なファイル読み込みは既に実装されてる)、前のめりで新しい機能を導入していきたい(笑)。

(参考)
Unity UniRxとasync/awaitでフレーム管理
UniRx.Async機能紹介
UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合/a>
Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映





(関連記事)
【Unity】VRM(VRoid)をライブステージで踊らせるアプリを作ってみた


スポンサーサイト

category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityリファレンス  Unityライブラリ  実証実験 
tb: 0   cm: --

【Unity】【C#】RectTransform の矩形の実座標を取得する  


 ちょっとパネルのドラッグ機能を付けたときに必要だったので、備忘録的にメモ。


 特にスマホなどは解像度によって見た目の大きさと実際のピクセルサイズが違うので、動的に取得する必要があった。調べたらすぐにわかったが、視覚的な解説が無かったので、簡単に書いてみた。

(※) Unity 2018.2.1f1 / Windows10(x64) で確認


●RectTransform の矩形の実際の座標を取得する
using UnityEngine;

public class Sample : MonoBehaviour {

public RectTransform target;

//RectTransform の矩形の実座標を Rect にして返す
public Rect GetRect(RectTransform rt)
{
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
return new Rect(corners[0], corners[2] - corners[0]);
}

//Rect 型にして、左下-右上座標を表示
void DisplayRect()
{
Rect rect = GetRect(target);
Debug.Log("(" + rect.xMin + ", " + rect.yMin + ") - ("
+ rect.xMax + ", " + rect.yMax + ")");
}

//4コーナー(左下, 左上, 右上, 右下)座標を表示
void DisplayCorners()
{
Vector3[] corners = new Vector3[4];
target.GetWorldCorners(corners);

for (var i = 0; i < 4; i++)
{
Debug.Log("corners[" + i + "] : " + corners[i]);
}
}

// Use this for initialization
private void Start () {
if (target == null)
target = GetComponent();

DisplayCorners();
DisplayRect();
}
}

corners[0] : (460.0, 240.0, 0.0)
corners[1] : (460.0, 480.0, 0.0)
corners[2] : (820.0, 480.0, 0.0)
corners[3] : (820.0, 240.0, 0.0)
(460, 240) - (820, 480)

 RectTransform.GetWorldCorners() は画面の左下を(0, 0)、右上が解像度の最大座標として、[0]:左下, [1]:左上, [2]:右上, [3]:右下 の座標を返す。


 この例では Canvas の設定が「Canvas Scaler」で「UI Scale Mode>Scale With Screen Size」, 「Reference Resolution>800x600 となっていて、パネルのサイズは 300x200 となっている。また、画面解像度は 1280x720 になっているときの値だ。


 ちなみに画面解像度を 2560x1440 にすると以下の値となる。
corners[0] : (920.0, 480.0, 0.0)
corners[1] : (920.0, 960.0, 0.0)
corners[2] : (1640.0, 960.0, 0.0)
corners[3] : (1640.0, 480.0, 0.0)
(920, 480) - (1640, 960)

 また、DisplayCorners() は公式のサンプルそのものだが、ちょっと手を加えて Rect 型で返す関数を GetRect() としても定義しておいた。矩形領域だけなら [0]:左下 - [2]:右上 の座標だけでも十分なことも多いからだ。


 冒頭に書いたパネル上での使用方法は、パネルをロックしたときに、パネル外ではカメラ回転、パネル内では無視するために座標を取っている(パネル上でマウスでカーソル合わせたりして、動かすたびにカメラが回転するのは鬱陶しいので(笑))。

 私はエクスプローラからのファイルドラッグ&ドロップにも対応しているが、UIの矩形座標を取れば、ドロップした先で処理を分岐するのも可能だろう。色々使える気がする(笑)。






(関連記事)
【Unity】【C#】uGUI ドロップダウンの要素をコードで設定と取得、外観のカスタマイズなど
【Unity】VRM(VRoid)をライブステージで踊らせるアプリを作ってみた


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityリファレンス  C# 
tb: 0   cm: --

【Unity】【C#】テキストファイルの読み書きをする(リソース/ストレージ)  


 Unity では内部リソースからテキストファイルを読み込む方法が非常に簡単に用意されているが、それとは別に外部のリソース(ストレージ等)でテキストファイルを読み書きする方法も書いておこう。

 またついでに Android でプラグインを利用して読み書きする方法も紹介しておく。ちなみに Unity から C# で直接 SDカードに書き込もうとすると、アクセス拒否で失敗するが、プラグインを利用する場合は、SDカードにも書き込むことが可能だ。エクスプローラみたいなファイル選択も使えるので、ユーザーが自由にファイルの読み書きを行えるようなシステムを作るには役に立つだろう。

 ここでは C# スクリプト例はインスタンスメソッドとして書いておくが、static なライブラリとして利用したいなら、例外処理を throw などすれば良いと思う。とりあえず簡単な実装を書いておこう。自由に改造でもして使って欲しい。


(※) Unity 5.6.3p1 - 2018.1.8f1 / Windows10(x64) で確認



■リソースからテキストアセットを読み込む(Unity ビルトイン機能)

 ここでは Unity の機能を使って、アプリに内包したテキストファイルを読み込んでみよう。

 これは Unity のエディタ上であらかじめリソースとして持っておき、ランタイム時にロードする方法だ。Unity では「Resources」というフォルダを作っておくと、起動後にリソースとしてテクスチャやデータなどを読み込むことができる。ここにテキストファイルを置いておけば、「TextAsset」として簡単に扱える。それを利用して読み込んでみよう。

●リソースからテキストアセットを読み込む(メソッド定義)
using UnityEngine;
using UnityEngine.UI;

public string textAssetName = "Texts/テスト"; //拡張子はいらない
public Text displayText; //表示するUI-Text

//リソースからテキストアセットを読み込む
void LoadTextAsset(string name)
{
if (string.IsNullOrEmpty(name) || displayText == null)
return;

TextAsset textAsset = (TextAsset)Resources.Load(name, typeof(TextAsset));
if (textAsset != null)
displayText.text = textAsset.text;
else
Debug.Log("Not found TextAsset : " + name);
}

●使用例(メインでのコードなど)
using UnityEngine;

//ロードボタンのコールバックハンドラ
public void OnLoadTextAsset()
{
LoadTextAsset(textAssetName);
}

 読み込み先テキストには UI-Text(displayText)を用いているが、もちろん string 型の引数のメソッドを作って直接読み込んでも良い。これはあくまでも例なので、その辺りは用途に応じて書き換えて欲しい。

●「Resources」フォルダを作り、その配下にテキストファイル(UTF-8)をインポートしておく。

 テストするには UI-Button などを適当に配置し、「Button」の「OnClick()」にスクリプトの「OnLoadTextAsset()」を登録すれば良い。





●「テスト用スクリプトのアタッチ例


●ファイルの内容は任意(UTF-8)

※ランタイム時にログを見るにはプラグインライブラリのプレファブ「DebugConsole」をシーンに置き、コード中の Debug.Log() を XDebug.Log() に置き換えて下さい。また、インスペクタで「displayText」に「DebugConsole」以下の「Text」を登録します。



■ローカルストレージなどでテキストファイルの読み書きをする(C# スクリプト)

 次にパス(ファイル名)を指定してテキストファイル(UTF-8)を読み書きすることをやってみよう。

 例ではフォルダ(ディレクトリ)に「Application.persistentDataPath」を使っているが、これはプラットフォームによって変わるデフォルトの永続的な保存先となる。また、会社名(company name)やアプリ名(product name)でもフォルダ分けされる。詳しくは公式マニュアルで確認して欲しい。

(参考)Application.persistentDataPath

●ローカルストレージなどでテキストファイルの読み書きをする(メソッド定義)
using System;
using System.IO;
using System.Text;

//テキストをファイルから読み込む(行読み)
//※Android で SD カードから読み込みをするには、「AndroidManifest.xml」にパーミッション("READ_EXTERNAL_STORAGE" または "WRITE_EXTERNAL_STORAGE")が必要。
string LoadText(string path)
{
StringBuilder sb = new StringBuilder(1024); //※capacity は任意

try
{
using (StreamReader reader = new StreamReader(path))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
sb.Append(line).Append("\n");
}
}
}
catch (Exception e)
{
Debug.Log(e.Message);
return null;
}

return sb.ToString();
}

//テキストをファイルに保存
//※Android で External Storage に書き込みをするには、「AndroidManifest.xml」にパーミッション("WRITE_EXTERNAL_STORAGE")が必要。
//※セキュリティ上、Unity から直接 SD カードには保存できない。
bool SaveText(string text, string path)
{
try
{
using (StreamWriter writer = new StreamWriter(path))
{
writer.Write(text);
writer.Flush();
writer.Close();
}
}
catch (Exception e)
{
Debug.Log(e.Message); //Access to the path "filename" is denied. → パーミッションが無い, 書き込みアクセス不可(SDカードなど)
return false;
}
return true;
}

●使用例(メインでのコードなど)
using UnityEngine;
using UnityEngine.UI;

public string filename = "test.txt";
public Text displayText; //表示するUI-Text

//ロードボタンのコールバックハンドラ
public void OnLoadClick()
{
if (string.IsNullOrEmpty(filename) || displayText == null)
return;

string path = Path.Combine(Application.persistentDataPath, filename); //プラットフォームによってパスは異なる

if (!File.Exists(path))
{
Debug.Log("Not found : " + path);
return;
}

string text = LoadText(path);
if (!string.IsNullOrEmpty(text))
displayText.text = text;
}

//セーブボタンのコールバックハンドラ
public void OnSaveClick()
{
if (string.IsNullOrEmpty(filename) || displayText == null)
return;

string path = Path.Combine(Application.persistentDataPath, filename); //プラットフォームによってパスは異なる

if (SaveText(displayText.text, path))
Debug.Log("Save to : " + path);
}

 テストするには UI-Button などを適当に配置し、「Button」の「OnClick()」にスクリプトの「OnLoadClick()」「OnSaveClick()」をそれぞれ登録すれば良い。
●UI-Button 配置とテスト用スクリプトのアタッチ例

●UI-Button のコールバック登録例

 読み書きするテキストには UI-Text(displayText)を用いているが、もちろん string 型の引数のメソッドを作って直接読み書きしても良い。これはあくまでも例なので、その辺りは用途に応じて書き換えて欲しい。

 なお、Android でSDカードに読み書きする場合は以下のパーミッションが必要になる。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />'

 パーミッションについての説明が必要なら以前の記事を参照して欲しい。


※ランタイム時にログを見るにはプラグインライブラリのプレファブ「DebugConsole」をシーンに置き、コード中の Debug.Log() を XDebug.Log() に置き換えて下さい。また、インスペクタで「displayText」に「DebugConsole」以下の「Text」を登録します。



■Android でファイル選択+テキストファイルの読み書きをする(プラグイン利用)

 ついでにプラグインに入っている「StorageLoadTextController」と「StorageSaveTextController」(~/FantomLib/Prefabs/System/ 以下。検索で探すと簡単)というテキストファイル読み書きの機能を使ってみよう。これら「~Controller」は Android 標準のファイル選択機能を使い、テキストファイルを読み込み・書き込みをしてくれるものだ。これら機能はプラグイン内部に実装されているので、自分でコードを書く部分は、機能呼び出しと結果受け取りだけで良い。

 ではまず、その機能呼び出しと結果受け取りのコードだけ書いてしまおう。名前は任意で良い。これらは各「~Controller」や UI-Button のコールバックハンドラとなる。

●コールバックハンドラ例(メインでのコードなど)
using UnityEngine;
using UnityEngine.UI;
using FantomLib;

public Text displayText; //表示するUI-Text

//読み込んだテキストを UI-Text に表示する
public void OnStorageLoad(string text)
{
if (string.IsNullOrEmpty(text) || displayText == null)
return;

displayText.text = text;
}

public StorageSaveTextController storageSaveTextController; //※インスペクタで登録

//UI-Text のテキストをファイルに保存する
public void OnStorageSave()
{
if (displayText == null || string.IsNullOrEmpty(displayText.text)) //※空は保存しない場合
return;

if (storageSaveTextController != null)
storageSaveTextController.Show(displayText.text);
}

//エラーステータス用コールバックハンドラ
public void OnError(string message)
{
Debug.Log(message);
}

 あとはヒエラルキーに「StorageLoadTextController」と「StorageSaveTextController」(~/FantomLib/Prefabs/System/ 以下。検索で探すと簡単)を置き、UI-Button など(※名前は任意)を Canvas に配置したら、インスペクタでコールバックを設定するだけだ。

●「StorageLoadTextController」と「StorageSaveTextController」を置く。テスト用スクリプトのアタッチ例。

●スクリプト(TextFileTest)へコールバック登録する
※または、StorageLoadTextController.OnResult に直接 UI.Text.text を登録しても良い。

●UI-Button から機能呼び出しをする


 あとはビルドするだけだが、プラグインを含む場合のビルドは「AndroidManifest.xml」が必要になる。その辺りは以前の記事にまとめてあるので参照して欲しい。もちろん「Build Settings...」にシーンを追加するのを忘れずに。

●ファイルの内容は任意(UTF-8)

※ランタイム時にログを見るにはプラグインライブラリのプレファブ「DebugConsole」をシーンに置き、コード中の Debug.Log() を XDebug.Log() に置き換えて下さい。また、インスペクタで「displayText」に「DebugConsole」以下の「Text」を登録します。


※この記事のUnityアセットはプラグインとして配布されています。


※とりあえず試してみたい方は、最新版をビルドした apk デモをダウンロードできます。動作確認にもどうぞ。

プラグインデモをダウンロード
(Google Drive を利用)


Android 4.2以上
※「提供元不明アプリのインストール」許可が必要です。


(関連記事)
【Unity】AssetStore版 FantomPlugin のセットアップ
【Unity】Androidのトーストやダイアログ、通知、音声認識、ハード音量操作など基本的な機能を使えるプラグインを作ってみた
【Unity】【C#】UnityEvent, Action, delegate, interface でのコールバック実装方法とインスペクタでの登録


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityリファレンス  Unityプラグイン  C# 
tb: 0   cm: --

【Unity】【C#】インスペクタでの UnityEvent のコールバック登録の有無を調べる  


 利用頻度はあまり高くはないかもだが、インスペクタでの UnityEvent のコールバック登録があるか否かを調べたいときがある。実際に利用するには調べたい条件によってコードを変えるしかないかも知れないが、簡単な例を2つばかり挙げておこう。また、UnityEvent にはジェネリックパラメタの数違いのパターンがあるので、それらをカバーするためのオーバーロードも簡単に作っておく。

 ここでは汎用的に使えるように、拡張メソッドとして定義したものを書いておこう。ライブラリとして保存しておけば使い回しもできると思う。拡張メソッドにしたくない場合は、メソッドの引数の this を取り除けば良い。


(※) Unity 5.6.3p1 - 2018.1.8f1 / Windows10(x64) で確認



■インスペクタの UnityEvent にコールバックが1つでも登録されてるか否かを返す

 このメソッドたちは1つでもコールバック名が有るか無いかという判別をするものである。UnityEvent が null のときは常に false になる。ジェネリックパラメタの数違いはオーバーロードとして定義している。

●インスペクタの UnityEvent にコールバックメソッドが存在するか否かを返す(関数定義)
using System;
using UnityEngine;
using UnityEngine.Events;

public static class Extensions //※クラス名は任意
{
//インスペクタの UnityEvent にコールバックが1つでも登録されてるか否かを返す。
//※AddListener() で登録したものは無視されるので注意。
public static bool HasRegistered(this UnityEventBase obj)
{
if (obj != null)
{
int count = obj.GetPersistentEventCount(); //※AddListener() には無効
for (int i = 0; i < count; i++)
{
if (!string.IsNullOrEmpty(obj.GetPersistentMethodName(i))) //※AddListener() には無効
return true;
}
}
return false;
}

//※以下、パラメタ違いオーバーロード
public static bool HasRegistered(this UnityEvent obj)
{
return HasRegistered((UnityEventBase)obj);
}

public static bool HasRegistered<T0>(this UnityEvent<T0> obj)
{
return HasRegistered((UnityEventBase)obj);
}

public static bool HasRegistered<T0, T1>(this UnityEvent<T0, T1> obj)
{
return HasRegistered((UnityEventBase)obj);
}

public static bool HasRegistered<T0, T1, T2>(this UnityEvent<T0, T1, T2> obj)
{
return HasRegistered((UnityEventBase)obj);
}

public static bool HasRegistered<T0, T1, T2, T3>(this UnityEvent<T0, T1, T2, T3> obj)
{
return HasRegistered((UnityEventBase)obj);
}
}

●使用例(メインでのコードなど)
using UnityEngine;

Debug.Log("OnNothing.HasRegistered = " + OnNothing.HasRegistered());
Debug.Log("OnSingle.HasRegistered = " + OnSingle.HasRegistered());
Debug.Log("OnDouble.HasRegistered = " + OnDouble.HasRegistered());
Debug.Log("OnTriple.HasRegistered = " + OnTriple.HasRegistered());
Debug.Log("OnQuadruple.HasRegistered = " + OnQuadruple.HasRegistered());


//コールバックハンドラなど
public void ReceiveNothing()
{
Debug.Log("ReceiveNothing called.");
}

public void ReceiveSingle(string s)
{
Debug.Log("ReceiveSingle called.");
}

public void ReceiveDouble(string s, int i)
{
Debug.Log("ReceiveDouble called.");
}

public void ReceiveTriple(string s, int i, float f)
{
Debug.Log("ReceiveTriple called.");
}

public void ReceiveQuadruple(string s, int i, float f, bool b)
{
Debug.Log("ReceiveQuadruple called.");
}

● False になる例

OnNothing.HasRegistered = False
OnSingle.HasRegistered = False
OnDouble.HasRegistered = False
OnTriple.HasRegistered = False
OnQuadruple.HasRegistered = False

● True になる例

OnNothing.HasRegistered = True
OnSingle.HasRegistered = True
OnDouble.HasRegistered = True
OnTriple.HasRegistered = True
OnQuadruple.HasRegistered = True

 注意して欲しいのは「GetPersistentEventCount()」や「GetPersistentMethodName()」というメソッドを使っているが、これらは「AddListener()」でランタイム時に追加したメソッドには無効ということだ。なので、基本的にはインスペクタでの登録のみと考えて欲しい(タイトルが「インスペクタでの~」となってるのはそのため)。

 また、メソッドの存在は「GetPersistentMethodName()」で空か否かで判断しているだけなので、何らかミスなどで "<Missing ~>"(※ここではわざと無効な Dummy() メソッドを登録している)となっているメソッドも存在していると判断されてしまうので注意しよう。しかし通常、インスペクタでの登録は正常であるという前提なら問題はない。



■インスペクタの UnityEvent にシグニチャが合致しているコールバックが1つでも登録されてるか否かを返す

 次のメソッドたちは前述のコールバック名の存在有無より厳密に、コールバックの引数のシグニチャと同じものだけを検出するものである。実際インスペクタでの登録は違うシグニチャも登録できるわけで(例:コールバック引数が(string s, int i)であっても、引数が(string s)[※インスペクタで文字列を入力] のメソッドを登録することができる)、便利ではあるが前述の方法では正しくコールバックできるメソッドが存在するかは判定できないことにもなる。以下の方法では完全に正しいシグニチャを持つメソッドしか検出しないので、"<Missing ~>"(※ここではわざと無効な Dummy() メソッドを登録している)となっているものも排除できる。

●インスペクタの UnityEvent に引数が正しいコールバックメソッドが存在するか否かを返す(関数定義)
using System;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;

public static class Extensions //※クラス名は任意
{
//インスペクタの UnityEvent にシグニチャが合致しているコールバックが1つでも登録されてるか否かを返す。
public static bool HasValidMethod(this UnityEventBase obj, Type[] argumentTypes)
{
if (obj != null)
{
int count = obj.GetPersistentEventCount(); //※AddListener() には無効
for (int i = 0; i < count; i++)
{
string method = obj.GetPersistentMethodName(i); //※AddListener() には無効
if (!string.IsNullOrEmpty(method))
{
object target = obj.GetPersistentTarget(i); //※AddListener() には無効
MethodInfo info = UnityEventBase.GetValidMethodInfo(target, method, argumentTypes);
if (info != null)
return true;
}
}
}
return false;
}

//※以下、パラメタ違いオーバーロード
public static bool HasValidMethod(this UnityEvent obj)
{
return HasValidMethod(obj, new Type[]{});
}

public static bool HasValidMethod<T0>(this UnityEvent<T0> obj)
{
return HasValidMethod(obj, new Type[]{typeof(T0)});
}

public static bool HasValidMethod<T0, T1>(this UnityEvent<T0, T1> obj)
{
return HasValidMethod(obj, new Type[]{typeof(T0), typeof(T1)});
}

public static bool HasValidMethod<T0, T1, T2>(this UnityEvent<T0, T1, T2> obj)
{
return HasValidMethod(obj, new Type[]{typeof(T0), typeof(T1), typeof(T2)});
}

public static bool HasValidMethod<T0, T1, T2, T3>(this UnityEvent<T0, T1, T2, T3> obj)
{
return HasValidMethod(obj, new Type[]{typeof(T0), typeof(T1), typeof(T2), typeof(T3)});
}
}

●使用例(メインでのコードなど)
using UnityEngine;

Debug.Log("OnNothing.HasValidMethod = " + OnNothing.HasValidMethod());
Debug.Log("OnSingle.HasValidMethod = " + OnSingle.HasValidMethod());
Debug.Log("OnDouble.HasValidMethod = " + OnDouble.HasValidMethod());
Debug.Log("OnTriple.HasValidMethod = " + OnTriple.HasValidMethod());
Debug.Log("OnQuadruple.HasValidMethod = " + OnQuadruple.HasValidMethod());

//・・・コールバックハンドラなどは前述と同じで良いので省略・・・

● False になる例

OnNothing.HasValidMethod = False
OnSingle.HasValidMethod = False
OnDouble.HasValidMethod = False
OnTriple.HasValidMethod = False
OnQuadruple.HasValidMethod = False

● True になる例

OnNothing.HasValidMethod = True
OnSingle.HasValidMethod = True
OnDouble.HasValidMethod = True
OnTriple.HasValidMethod = True
OnQuadruple.HasValidMethod = True

 使い分けは大まかに「何でも良いからメソッドが登録されてる」ときと「正確に値を返せるメソッドが登録されてる」となるだろう。どちらを使うかはケースバイケースだと思う。


(関連記事)
【Unity】【C#】UnityEvent, Action, delegate, interface でのコールバック実装方法とインスペクタでの登録
【Unity】【C#】長押し(ロングタップ)を取得してコールバックする
【Unity】【C#】スワイプ(フリック)を判定、方向を取得してコールバックする
【Unity】【C#】ピンチ操作を取得してコールバックする
【Unity】【C#】VideoPlayer で動画の終了判定をする


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityライブラリ  Unityリファレンス  C# 
tb: 0   cm: --

【Unity】AnimationCurve をコードで初期化する  


 ただのメモ。簡単にイージングなどの効果の得られる「AnimationCurve」をインスペクタで設定できるようにしたとき、あらかじめ初期値を入れておきたいことってあるよね。カーブは一度エディタで登録しておけば使い回しできるけど、デフォルトに無い曲線などのときはコードで設定おくと便利。ググれば複雑なカーブのライブラリもあるけど、手軽に作ってしまいたいときの簡易リファレンス的な。


(※) Unity 2017.2.0b9 / Windows10(x64) で確認



■直線的なカーブ(Static Function)

using UnityEngine;

public class AnimationCurveTest : MonoBehaviour {

public AnimationCurve curve = AnimationCurve.Linear(0, 0, 1, 1); //timeStart, valueStart, timeEnd, valueEnd

}

●カーブエディタでは

●移動に使ってみると

 AnimationCurve.Linear() は static なメソッドである。引数は開始と終了の時間と値のペアを列挙したものになる。



■イージング的なカーブ(ゆっくりと始まり、ゆっくりと終わる)(Static Function)

using UnityEngine;

public class AnimationCurveTest : MonoBehaviour {

public AnimationCurve curve = AnimationCurve.EaseInOut(0, 0, 1, 1); //timeStart, valueStart, timeEnd, valueEnd

}

●カーブエディタでは

●移動に使ってみると

 AnimationCurve.EaseInOut() も static なメソッドである。引数は開始と終了の時間と値のペアを列挙したものになる。ゆっくりと始まり、ゆっくりと終わる移動などが簡単に作れる。



■トップスピード→だんだんゆっくりになっていくイージング[※プリセットと同じ](カスタム:Keyframe を2つ使う)

using UnityEngine;

public class AnimationCurveTest : MonoBehaviour {

public AnimationCurve curve = new AnimationCurve(
new Keyframe(0f, 0f, 0f, 2f), //time, value, inTangent, outTangent
new Keyframe(1f, 1f, 0f, 0f) //time, value, inTangent, outTangent
);
}

●カーブエディタでは

●移動に使ってみると

 このカーブはエディタのプリセットにも入っているが、上記のようにコードで設定することもできる。独自にカーブのポイントを打っていくには「Keyframe」を「AnimationCurve」のコンストラクタに列挙していく。

 「inTangent, outTangent」とはエディタのグラフから見れば、それぞれのポイントの左側・右側の傾きで、タンジェント(Tangent)というのは数学的に y/x で表せる傾きのことだから、0.5f(=1/2) なら「上に1, 右に2」の傾き、2f(=2/1) なら「上に2, 右に1」の傾きと考えれば良いのではないだろうか。時間軸に対する値なので、時間や値の逆数(分子と分母を入れ替える)を設定すれば簡単に反転したカーブも作れる。



■ジャンプのようなイージング(カスタム:Keyframe を3つ使う)

using UnityEngine;

public class AnimationCurveTest : MonoBehaviour {

public AnimationCurve curve = new AnimationCurve(
new Keyframe(0f, 0f, 0f, 4f), //time, value, inTangent, outTangent
new Keyframe(0.5f, 1f, 0f, 0f), //time, value, inTangent, outTangent
new Keyframe(1f, 0f, -4f, 0f) //time, value, inTangent, outTangent
);
}

●カーブエディタでは

●移動に使ってみると(Y軸方向)

 「トップスピード→だんだんゆっくり→頂点まで来たら反転→だんだん速く」といったジャンプのような動きになる。独自にカーブのポイントを打っていくには「Keyframe」を「AnimationCurve」のコンストラクタに列挙していく。

 「inTangent, outTangent」とはエディタのグラフから見れば、それぞれのポイントの左側・右側の傾きで、タンジェント(Tangent)というのは数学的に y/x で表せる傾きのことだから、0.5f(=1/2) なら「上に1, 右に2」の傾き、2f(=2/1) なら「上に2, 右に1」の傾きと考えれば良いのではないだろうか。時間軸に対する値なので、時間や値の逆数(分子と分母を入れ替える)を設定すれば簡単に反転したカーブも作れる。



■線形補間に利用する

 一応使い方を書いておくと、「AnimationCurve.Evaluate()」を使って、時間軸に対する値を取り出せばカーブのような変化になる。特に線形補間系のメソッド「~.Lerp()」と組み合わせると使い勝手が良い。以下のサンプルはオブジェクトの位置をスイっと動かす、かなりの手抜きのコードだが、応用すればアルファでフェードイン・アウトしたり、スケールを変化してアクションを表現したり、スワイプでスイっと動かすなんてこともできる(もちろんエディタ上ではアニメーションクリップを使う手もある)。

using UnityEngine;

public class AnimationCurveTest : MonoBehaviour {

public AnimationCurve curve = new AnimationCurve(
new Keyframe(0f, 0f, 0f, 2f), //time, value, inTangent, outTangent
new Keyframe(1f, 1f, 0f, 0f) //time, value, inTangent, outTangent
);

public Vector3 destination;
public float duration = 1f;

Vector3 origin;
float passed = 0;

// Use this for initialization
void Start () {
origin = transform.position;
}

// Update is called once per frame
void Update () {
passed += Time.deltaTime;
float t = curve.Evaluate(Mathf.Clamp01(passed / duration));
transform.position = Vector3.Lerp(origin, destination, t);
}
}


 もっと複雑なカーブが欲しい・イージングタイプを知りたい場合は、以下の参考URLを使うのも良いだろう。

(参考)
AnimationCurveのプリセットにペナーイージングを加える
AnimationCurveをInspectorで設定し、スクリプトから使う
Easing Function早見表

※トゥイーンとして使うなら DOTween みたいなものでも良いかも。
DOTween (Asset Store)
DOTween (公式:英語)
DOTweenをふわっとまとめてみた
【Unity】Tween アニメーション(DOTween)の話

■DOTween 参考書籍



●床や柱のイルミネーションにイージングを入れて、いい感じにフェードイン・アウトさせている


(関連記事)
【Unity】【C#】スワイプ(フリック)を判定、方向を取得してコールバックする


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityリファレンス  C# 
tb: 0   cm: --


プロフィール

Social

検索フォーム

全記事一覧

カテゴリ

ユーザータグ

最新記事

リンク

PR

▲ Pagetop