FC2ブログ
ヽ|∵|ゝ(Fantom) の 開発blog? ホーム »Unity
カテゴリー「Unity」の記事一覧

【Unity】【C#】StartsWith, EndsWith が遅いのなら、Equals も遅いのか?  


 Unity の公式マニュアルにも掲載されているのだが(ただし、情報が少し古い[Unity5])、文字列比較の StartsWith, EndsWith は遅いというのは以前から言われていた。実際に、そのページにある「CustomStartsWith」と「CustomEndsWith」と、標準ライブラリの StartsWith, EndsWith の速度比較をしてみると、かなり遅い。

文字列とテキスト - 非効率的なビルトイン String API

 ただ、その理由を読んでいたら「比較にはローカライズ用のデフォルトカルチャが効いているから」という事らしい(解説には Equals では "encyclopedia", "encyclopædia" のような表記違いは等価とみなすとある)。

 「あれ?それなら Equals も CustomEquals を作った方が良いのでは?」と思ったので、試しに作ってみた。といっても CustomStartsWith とほぼ同じなんだけどね。そしてその速度比較の結果を残しておこう。

(※) Unity 2019.4.21f1 [.NET 4.x] / Windows10(x64) で確認



●公式の CustomStartsWith を真似した単純な Equals
using System;

public static class Extentions //※名前は任意
{
/// <summary>
/// 公式の CustomStartsWith を真似した単純な Equals.
/// https://docs.unity3d.com/ja/current/Manual/BestPracticeUnderstandingPerformanceInUnity5.html
/// </summary>
public static bool CustomEquals(this string a, string b)
{
int aLen = a.Length;
if (aLen != b.Length)
return false;

int ap = 0;
while (ap < aLen && a [ap] == b [ap])
{
ap++;
}

return (ap == aLen);
}
}

 また比較には、公式の解説にある「ローカライズ用の調整を必要としない文字列比較に StringComparison.Ordinal を推奨」ともあったので、ついでに加えてみた(また Equals ではオペレータの "==" も加えてある)。以下に比較結果を載せておこう。

●StartsWith, EndsWith, Equlas の標準と Ordinal オプションと Custom~ を 100000 回×10回実行して、平均値を計測
●StartsWith
[StandardStartsWith] ave : 0.1930909 [sec]
[StandardStartsWithOrdinal] ave : 0.08576998 [sec]
[CustomStartsWith] ave : 0.08573665 [sec]

●EndsWith
[StandardEndsWith] ave : 1.135096 [sec]
[StandardEndsWithOrdinal] ave : 0.08583274 [sec]
[CustomEndsWith] ave : 0.0840721 [sec]

●Equals
[StandardEquals] ave : 0.09371759 [sec]
[StandardEqualsOrdinal] ave : 0.09588816 [sec]
[OperatorEquals] ave : 0.09638711 [sec]
[CustomEquals] ave : 0.09519003 [sec]

 コンパイルは「.NET 4.x」でやってるからか、意外なことに、特に Equals では誤差範囲くらいしか変わらなかった(ただし、半角英数や日本語だけの場合。他の言語では違いは出るのかも?)。

 Custom~ のメソッドには null チェックも付いてないので、それを追加したとしても大して変化は無い。結果を見てみると、どうやら .NET 4.x で日本語や英字ぐらいの範囲なら、StringComparison.Ordinal オプションを付ければ、実行速度は速いらしい。

 もちろん文字列の内容や長さにもよるだろうが、私が試した限りでは、10~100文字程度で日本語・英語混合くらいなら、ほとんど誤差範囲でしか変わらない。なので「a.StartsWith(b, StringComparison.Ordinal)」「a.EndsWith(b, StringComparison.Ordinal)」「a.Equals(b, StringComparison.Ordinal) (※a.Equlas(b) でもなぜか変わらない)」を機械的に使っても、実質問題無いようだ。

 まぁ、標準ライブラリ自体も改修されているからね。Unity の公式マニュアルは Unity5 時代で .NET 3.5 だったので、多少は変わったのかも知れない(実際に .NET 4.x になって GC 発生やコルーチンなども改修されてるとか何とか…)。

GCの発生要因を減らす

 特に古い情報の場合、一度現在のバージョン・環境で試してみるのも良いかもね。ちなみに Android でやってみても、同じような結果だった。現バージョンでは普段使いなら、Ordinal オプションを付けておけば、Custom~ は必要ないようだ。





(関連記事)
【Unity】【C#】文字列の内容をシャッフルする
【Unity】【C#】文字列の暗号化・復号化を簡単に行う
【C#】クラスのフィールド名を文字列の配列で取得する


スポンサーサイト



category: Unity

thread: プログラミング

janre: コンピュータ

tag: C#ライブラリ  文字列操作  検証 
tb: 0   cm: --

【Unity】【C#】文字列の内容をシャッフルする  


 どちらかというと、デバッグ・テスト用になるかも知れないが、文字列の内容をランダムにシャッフルする拡張メソッドを作ってみよう。

 実際私はよく実行速度の計測などをするのだが、同じデータ・同じ文字列などでテストするとメモリのキャッシュが効いてしまう可能性もあるので、なるべくランダムなデータを与えるようにしてる。

 そういった用途のため簡単に作ったものなので、パフォーマンスはそこそこかも知れないが、以前に紹介した配列のシャッフルの拡張メソッドを使えばすぐにできるものなので、応用みたいな感じで紹介だけしておこう。

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



●文字列の内容をシャッフルする 拡張メソッド
using System;

public static class Extentions //※名前は任意
{
/// <summary>
/// 文字列をシャッフルしたものを返す
/// 2021/04/04 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-395.html
/// </summary>
/// <param name="text">元の文字列</param>
/// <returns></returns>
public static string Shuffle(this string text)
{
var array = text.ToCharArray();
array.Shuffle(); //※以前の記事を参照
return new string(array);
}
}

●メインコード例
using System;
using UnityEngine;

var text = "abcあいう漢表示";
int length = 10;
for (int i = 0; i < length; i++)
{
var res = text.Shuffle();
Debug.Log($"[{i}] {res}");
}

[0] cabい示表漢うあ
[1] いあうb示c表a漢
[2] ab表あ示いcう漢
[3] うcあ漢ba示表い
[4] 示うbca表漢あい
[5] c示aいあ漢う表b
[6] 表ういabc漢あ示
[7] あい表うb漢示ac
[8] b示漢ac表ういあ
[9] baいう示c漢表あ

 配列をシャッフルするコードは以前の記事をコピペして欲しい。

配列・リストのシャッフル

 配列・リストのシャッフルのランダム関数(Random)は Unity 用になっているので、標準関数で使いたいなら System の方の Random に書き換えて欲しい。
 もし、UnityEngine と System で Random の曖昧エラーが出てるようなら、以下のように using で指定してやると簡単だ。

using Random = UnityEngine.Random;

 ちなみにこの「using」の使い方の左辺の名前は何でも良いので、単に長いクラス名を短く表記したいときにも役立つ(using Rnd = UnityEngine.Random; みたいにも書ける)。

 拡張メソッドの内容はなんてことはない、一度 string 型を char[] で取り出して、配列をシャッフルして、文字列に戻す、ということをしているだけだ。

 テスト用ならこれでも十分だろう。私は Unityエディタ上でよく使っているが、十分なパフォーマンスは出るようだ。

 まぁ、文字列が完全に決まっているなら、StringBuilder でシャッフル作るのも良いかも知れない。ランタイムでの文字列処理は GC を発生しやすいので、new をなるべく少なくするに越したことは無いからね。配列も使いまわしにすれば、更に効率的にもなる。ただ、その辺は別問題なので、自分で色々実装してみて欲しい(笑)。





(関連記事)
【Unity】【C#】配列・リストのシャッフル
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
【C#】配列やListなどの中身(要素)を見る拡張メソッド Dump


category: Unity

thread: プログラミング

janre: コンピュータ

tag: Unityライブラリ  C#ライブラリ  文字列操作 
tb: 0   cm: --

【Unity】「Missing (Script)」を修復する  


 最初に断っておきます。たぶん、正しいやり方というわけではないので、どうにもならないときの最終手段くらいに思って下さい(笑)。

 ちゃんとしたやり方なら、以下の記事を参照した方が良いかも?

Unityのプロジェクトはなぜ壊れるのか。原因と対策

 まぁ、Git 管理していて、すぐに元に戻せる状態なら、手早く直したいときの方法くらいで(笑)。

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



 残念ながら、Unity はバージョンのアップグレード等を行うと、アセットが壊れることがたまにある。運悪く、今回プロジェクトをアップデートしてたら、アタッチされてるスクリプトが「Missing」になってしまった。



 ただいつもの最小テスト用のプロジェクトだったので、実は何が壊れているかはだいたいわかっていた。しかし、せっかくなので、何か試しにツールでも入れて自動修復できないかとググってみたら(←何でも自動化できないかと考える人(笑))、あまり良い情報が出てこない(なぜか Animation の Missing 修復ばかり出てくる)。

 念の為、差分ツールでスクリプト自体をバージョン比較してみたけど、何1つ変更が加わってない。本当に「ただ参照が外れている」だけのようだ。

 そういうときには、壊れていると思われるスクリプトをプロジェクトビューで右クリックして、「Show in Explorer」でフォルダを開いてみよう。スクリプトと同じ名前で「.meta」ファイルがあるハズだ。

 そして、そのファイルを適当なテキストエディタで開き、guid を覗いてみる。もし、それが以前のバージョンと異なっているなら、参照が外れた原因になっている可能性が高い

●Missing になってるスクリプトの meta の guid


●以前の正常な状態のスクリプトの meta の guid

※異なっているなら、これが原因の場合も多い


 meta の内容が異なってることが確認できたら、Git で meta だけ戻しても良いし、古いプロジェクトから 「.meta」ファイルだけ取り出して、上書きコピーしても良い。要するにシーンが参照している guid と meta の guid が合っているなら、Missing が復活する。

(参考)Unityのプロジェクトはなぜ壊れるのか。原因と対策


※meta だけ以前のバージョンに戻したら、復活した


 かなり強硬策だが、これで直ることも多い。私はライブラリの一部が壊れたとき、正常にアタッチされていた旧 unitypackage で怪しいと思われるスクリプトの meta だけを上書きして直したこともある(全部やると原因がわかりずらいのと、インスペクタの値が変わってしまう可能性があるので、最小限で候補を絞って試すと特定しやすい)。



 例えば、アップデートする前に guid のスナップショットをとって、比較するツールなんかあると、自動化できるかもね。まぁ、いつも壊れるわけではないので(そして、大量のアセットのスキャンも時間もかかるだろうし)、Git で戻る方が簡単かも知れないが…。

 しかし、意外と需要ありそうなのに、自動修復ツールって無いんだなぁ…(Git で戻ってもいいけど、アップグレードって時間かかるので、全部は戻したくないんだよね…(笑))。






(関連記事)
【Unity】Unity2019以降で新規作成したアセットは、Unity2018以前にインポートすると壊れる?
【Unity】SymGetSymFromAddr64, GetLastError: '無効なアドレスにアクセスしようとしています。' と出たら…
【Unity】【C#】オーディオ再生でのエラー:An invalid seek position was passed to this function の対処法


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityトラブルシューティング 
tb: 0   cm: --

【Unity】【C#】インターフェイスの有無を調べて、自動 Dispose する Destroy を作る  


 今回は動的に生成したオブジェクト等を確実に破棄するための自動 Dispose を走らせるような Destroy を作ってみよう。実際、Unity では GameObject を Destroy しても、プロファイラで見てみると、それに使っているテクスチャやマテリアル等はメモリに残っているようだ。その場合、Resources.UnloadUnusedAssets を使えば、メモリから消えてはくれるが、このメソッドは GC 回収等をするので、一瞬画面が停止(プチフリーズ)することが多い。なので、あまり頻繁には使えないのが難点だ(画面遷移や Loading中 等、あまり目立たないタイミングで使うと良い)。

 大雑把な実装としては「インターフェイスの有無を調べる」ことと、それを「拡張メソッド Destroy で走らせる」こととなる。それではやってみよう。


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



■インターフェイスの有無を調べる(IDisposable の取得)

 まずは、インターフェイスの有無だが、これは調べたいクラスやインターフェイスなどをフルパスで与えれば、以下のようなメソッドで簡単に取得できる。

●インターフェイスの有無を調べる
using System;

var inf = obj.GetType().GetInterface("System.IDisposable"); //ここでは IDisposable インターフェイスを指定

 ここでは "System.IDisposable" を指定しているが、同じように他のクラスやインターフェイスでも文字列で指定すれば取得できる。取得できなかった場合は null が返ってくるので、例えば「このオブジェクトには IDisposable が実装されているか?」を調べるメソッドは以下のように書ける。

●IDisposable を実装しているかを調べる拡張メソッド
using System;

public static class Extensions //名前は任意
{
/// <summary>
/// System.IDisposable を実装しているか?
/// 2021/03/03 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-393.html
/// </summary>
/// <param name="obj">対象オブジェクト</param>
/// <returns>true = 実装している</returns>
public static bool HasDispose(this object obj)
{
if (obj != null)
{
return obj.GetType().GetInterface("System.IDisposable") != null;
}
return false;
}
}

 .NET 4.x 以降なら、以下のように簡潔に書いても良いかもね。
public static bool HasDispose(this object obj)
=> (obj != null) ? (obj.GetType().GetInterface("System.IDisposable") != null) : false;

 まぁ、書き方は好みで(笑)。1文にまとめると短くて済むが、後から条件を加えたいときなどは、かえってごちゃごちゃと読みにくくなる。この辺りはケースバイケースだね(このメソッド場合、あまり書き換えることも無いので、良いと思うが)。



■自動 Dispose する拡張 Destroy を作る

 せっかくなので、先に作った HasDispose を使って、拡張メソッド Destroy で自動的に IDisposable を走らせるようにしてみよう。なんのことは無い、よくある null チェック付きの拡張メソッドに HasDispose を入れただけのものである。でもこれだけで、Dispose() を走らせるのを忘れるのを防げるので、結構有用だったりする。

●実装してるなら Dispose してから、Destroy する (コンポーネント単体用)
using System;
using UnityEngine;

public static class Extensions //名前は任意
{
/// <summary>
/// null でないとき、実装してるなら Dispose してから、Destroy する。
/// ※どちらかというと、コンポーネント単体用。
/// 2021/03/03 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-393.html
/// </summary>
/// <param name="obj">対象オブジェクト</param>
public static void SafeDestroy(this UnityEngine.Object obj)
{
if (obj != null)
{
if (obj.HasDispose())
{
((IDisposable)obj).Dispose();
}

UnityEngine.Object.Destroy(obj);
}
}
}

 使い方は簡単で、例えば Instantiate したオブジェクトなどがあったら、そのオブジェクト(obj とする)を obj.SafeDestroy() とするだけだ。

 ただ、気をつけなくてはいけないのは、SafeDestroy を使うときの引数の「型」には注意。

 というのは、継承を多用しているときに、サブクラスに Dispose が実装してあっても、ベースクラスの方を引数に渡してしまうと、ベースクラスには Dispose が実装されてないので、実行されない。これはこのメソッドに限らずよくやってしまうミスだ。特に Unity では GetComponent で特定の型を取得して、処理させたりすることも多いが、その型に Dispose が無ければ、当然実行されない(Destroy はされる)。

 その場合は少し面倒だが、ベースクラスを override できるように予め Dispose を実装しておき、サブクラスからも呼んでおくことで解決できる。例えば、以下のように書いておけば良い。

●サブクラスの Dispose
public new void Dispose()  //ベースクラスが virtual なら、サブクラスは override でも良い
{
base.Dispose(); //ベースクラスの Dispose を実行
}

 ここではオーバーライドに new を使っているが、ベースクラスが virtual で実装してあるなら、override が良いだろう。

 また、GameObject を型とするなら、GetComponents が使えるので、単純にループを回した方が簡単だろう。

●GameObject の全てのコンポーネントを Dispose してから Destroy する (GameObject用)
using System;
using UnityEngine;

public static class Extensions //名前は任意
{
/// <summary>
/// GameObject の全てのコンポーネントを Dispose してから Destroy する
/// 2021/03/03 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-393.html
/// </summary>
/// <param name="obj">対象オブジェクト</param>
public static void SafeDestroy(this GameObject obj)
{
if (obj != null)
{
var infs = obj.GetComponents<IDisposable>();
if (infs != null)
{
foreach (var item in infs)
item.Dispose();
}

GameObject.Destroy(obj);
}
}
}

 まぁ、MonoBehaviour を使ってるオブジェクトの場合は OnDestroy が呼ばれるので、そこから Dispose を呼ぶだけでも良いと思う。

 私の場合、例えば VRM Live Viewer でサムネイルを大量に動的生成しているが、初期の頃は Destroy しても、テクスチャがメモリには残っているようで(プロファイラで見るとわかる)、スマホではよく落ちていた。なので、今はそれを確実に消すために、サムネイルのコンポーネントにテクスチャを破棄する Dispose を入れるようにしている。そうしないと、特にスマホアプリでは自動に任せてると、メモリ不足に陥るんだよね(Resources.UnloadUnusedAssets も使ってるが、冒頭に書いたようにプチフリーズが頻繁に起こるので、タイミングを見計らって使うしかない)。

 まぁ「なるべく明示的に破棄処理を実行する」ようにするだけのものなので、PC 等メモリに余裕がある機器なら、ある程度システムに任せても良いだろう。しかし、Unity って意外と素材関連(テクスチャやマテリアル、シェーダ等)はメモリに残り続けるので、長い時間アプリを使ってると、いつまでも解放されずにメモリリークしやすい。ランタイム中、少しでもメモリ確保したいときには、こういったメソッドを利用して、使い終わったオブジェクトを確実に破棄していくのもアリだろう。





(関連記事)
【Unity】【C#】動的にオーディオファイルの読み込みと再生をする
【Unity】【C#】Windows で mp3 をランタイムで再生する
【Unity】【C#】TGAをランタイムでロードする
【Unity】【C#】BMP をランタイムで読み込む
【Unity】【C#】Android で VRM(VRoid)を動的に読み込む


category: Unity

thread: ゲーム開発

janre: コンピュータ

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

【Unity】【C#】範囲を指定できる Mathf.Repeat  


 今回は数学的な関数のちょっとした便利拡張メソッド。

 Unity においては、特に角度の計算に Mathf.Repeat() はよく使う。例えば入力された値(角度)を -180~180 度に収めたいなどの時だ。
 しかし、標準の Mathf.Repeat() では引数が元の値(t)と長さ(length)[=最大(を含まない)] なので、0~max(を含まない) の計算となる。なので、0~360(359) 度などには良いが、0 以外の開始値だと少し不便だったりする。

 そこで、Mathf.Repeat() に少し手を加えて、-180~180(を含まない) または -180~180(を含む) のような、範囲内を返せる2つの拡張メソッドを作っておこう。

 ちなみに Mathf クラスは C# 標準の Math クラスの軽量版で、C# 標準が double の計算に対して、Unity では float で計算するものと考えて良い。要するに精度より速度を優先したものである。なので、高い計算精度が欲しいときは Math クラスを使った方が良いだろう(当然処理速度は重くなるので注意)。


(※) Unity 2019.4 / Windows10(x64) で確認 (※バージョン依存はないと思う)



●下限~上限(を含まない)範囲を返す Mathf.Repeat
using UnityEngine;

public static class MathfExtentions //※クラス名は任意
{
/// <summary>
/// 下限~上限(を含まない)範囲を返す Mathf.Repeat
/// 2020/12/20 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-390.html
/// </summary>
/// <param name="value">元の値</param>
/// <param name="min">下限 (inclusive)</param>
/// <param name="max">上限 (exclusive)</param>
/// <returns>min(inclusive)~max(exclusive) の Mathf.Repeat</returns>
public static float Repeat(this float value, float min, float max)
{
return Mathf.Repeat(value - min, max - min) + min;
}

//int 版
public static int Repeat(this int value, int min, int max)
{
return (int)Mathf.Repeat(value - min, max - min) + min;
}
}

●メインコード例
using UnityEngine;
using System.Text;

var sb = new StringBuilder(1024);
for (int i = -45; i <= 45; i++)
{
var x = i.Repeat(-45, 45);
if (sb.Length > 0)
sb.Append(", ");
sb.Append(x);
}

Debug.Log(sb.ToString());

-45, -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, -45

 注意点としては、値の上限(max)を「含まない」という点だ。

 この例の場合、max = 45 のため「…, 43, 44, -45, -44, …」のように値がループする。



●下限~上限(を含む)範囲を返す Mathf.Repeat
using UnityEngine;

public static class MathfExtentions //※クラス名は任意
{
/// <summary>
/// 下限~上限(を含む)範囲を返す Mathf.Repeat
/// 2020/12/20 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-390.html
/// </summary>
/// <param name="value">元の値</param>
/// <param name="min">下限 (inclusive)</param>
/// <param name="max">上限 (inclusive)</param>
/// <returns>min(inclusive)~max(inclusive) の Mathf.Repeat</returns>
public static float RepeatInclusive(this float value, float min, float max)
{
return (value == max) ? max : Repeat(value, min, max); //※「下限~上限(を含まない)」関数が必要
}

//int 版
public static int RepeatInclusive(this int value, int min, int max)
{
return (value == max) ? max : Repeat(value, min, max);
}
}

●メインコード例
using UnityEngine;
using System.Text;

var sb = new StringBuilder(1024);
for (int i = -45; i <= 45; i++)
{
var x = i.RepeatInclusive(-45, 45);
if (sb.Length > 0)
sb.Append(", ");
sb.Append(x);
}

Debug.Log(sb.ToString());

-45, -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45

 注意点としては、値の上限(max)を「含む」という点だ。関数内部で前述の「下限~上限(を含まない)」版も利用している。

 この例の場合、max = 45 のため「…, 43, 44, 45, -44, …」のように値がループする。

 また、値の意味としては「-45 と 45 は同値扱い」となる。これは我々の感覚として「-180度 と 180 度が同じ」のように使えるわけで、特に UI において、ユーザーから値を指定してもらう際には便利になる(-180~179 度でも良いが、-180~180 度の方が、人の感覚としてはわかり易い)。

 UI の実例としてはスライダー等に使うとわかり易いね。大したものではないが、意外と利用頻度は高いと思う。







(関連記事)
【Unity】【C#】倍数での Floor, Ceil, Round(一定間隔での切り捨て、切り上げ、四捨五入) [float 版]
【C#】最大公約数を求める/分数の約分をする(ユークリッドの互除法)
【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
【Java】3つ以上の最大公約数を求める(ユークリッドの互除法②)
【Java】拡張ユークリッドの互除法


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityライブラリ  Unityリファレンス  算術関数 
tb: 0   cm: --


プロフィール

Social

検索フォーム

全記事一覧

カテゴリ

ユーザータグ

最新記事

リンク

PR

▲ Pagetop