usingとIDisposableを使って簡易的な処理計測クラスを作る
先日、書いたusingとDisposerを使って簡易的な処理計測クラスを作ってみました。
using UnityEngine;//for Debug.Log and Debug.Assert using System; public class ProcessTimer { System.Diagnostics.Stopwatch stopWatch = new System.Diagnostics.Stopwatch(); public void Start() { Debug.Assert( !stopWatch.IsRunning ); stopWatch.Start(); } public void Stop() { Debug.Assert( stopWatch.IsRunning ); stopWatch.Stop(); } public void Reset() { stopWatch.Reset(); } public TimeSpan GetResult() { return stopWatch.Elapsed; } } public class ScopedProcessTimer : IDisposable{ ProcessTimer timer = new ProcessTimer(); public ScopedProcessTimer() { timer.Start(); } public void Dispose() { timer.Stop(); Debug.Log( "Process Time : " + timer.GetResult().TotalMilliseconds.ToString() + "msec" ); } }
最初はDateTime.Nowを使い、その差分で経過時間を図っていたのですが、後からStopWatchという便利なクラスがあることを知って、もはやProcessTimerクラスの存在意義はあまりないです...
こんな感じで使います↓
using ( ScopedProcessTimer spt = new ScopedProcessTimer() ) { for(var i = 0; i < 1000; ++i) { Debug.Log("hoge"); } }
Unityでビルド後に自動でRomをアップロードする拡張
Unityで作ったAndroid用のRomを他の人にテストプレイしてもらうときに、FTPツールを使っていちいちアップロードしていたのだけど、それがいちいちめんどくさかった。そこで自動で行うようにしてみました。
c#でFTPを利用するのは以下を参考にしました。
Simple C# FTP Class - CodeProject
(元記事のupload関数は一部間違っている部分があるので修正が必要です。)
後はUnityのEditor拡張とかの仕方を調べて作ってみました。
下に載せたソース2つをEditorフォルダに入れて、PreferenceでURL(例:ftp://wkpn@wkpn.sakura.ne.jp/www/Output/)とユーザー名とパスワードを設定すると、Androidロムが出来上がった時に指定されたURLにUploadします。
using UnityEngine; using UnityEditor; using UnityEditor.Callbacks; public class RomUploader { private const string cKeyIsFtpUpload = "KeyFtpUpload"; static bool IsFtpUpload { get { string value = EditorUserSettings.GetConfigValue (cKeyIsFtpUpload); return!string.IsNullOrEmpty (value) && value.Equals ("True"); } set { EditorUserSettings.SetConfigValue (cKeyIsFtpUpload, value.ToString ()); } } private const string cKeyFTPURL = "KeyFTPURL"; static string FTPURL { get { return EditorUserSettings.GetConfigValue (cKeyFTPURL); } set { EditorUserSettings.SetConfigValue(cKeyFTPURL, value); } } private const string cKeyFTPUserName = "KeyFTPUserName"; static string UserName { get { return EditorUserSettings.GetConfigValue (cKeyFTPUserName); } set { EditorUserSettings.SetConfigValue(cKeyFTPUserName, value); } } private const string cKeyFTPPassword = "KeyFTPPassword"; static string FTPPass { get { return EditorUserSettings.GetConfigValue (cKeyFTPPassword); } set { EditorUserSettings.SetConfigValue(cKeyFTPPassword, value); } } [PreferenceItem("Rom Upload")] static void OnGUI() { IsFtpUpload = EditorGUILayout.BeginToggleGroup("Android Rom FTP Upload", IsFtpUpload); FTPURL = EditorGUILayout.TextField("FTP URL", FTPURL); UserName = EditorGUILayout.TextField("User Name", UserName); FTPPass = EditorGUILayout.PasswordField("Password", FTPPass); EditorGUILayout.EndToggleGroup(); } [PostProcessBuildAttribute(1)] static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject) { Debug.Log( pathToBuiltProject ); if( IsFtpUpload ) { #if UNITY_ANDROID FTPUploader ftp = new FTPUploader(FTPURL, UserName, FTPPass); string filename = System.IO.Path.GetFileName( pathToBuiltProject ); ftp.Upload(filename, pathToBuiltProject); ftp = null;// Release Resources #endif } } }
using UnityEngine; using System; using System.IO; using System.Net; class FTPUploader { private string host = null; private string user = null; private string pass = null; private FtpWebRequest ftpRequest = null; private Stream ftpStream = null; private const int bufferSize = 2048; public FTPUploader(string hostIP, string userName, string password) { host = hostIP; user = userName; pass = password; } public void Upload(string remote_file, string local_file) { try { /* Create an FTP Request */ ftpRequest = (FtpWebRequest)FtpWebRequest.Create(host + "/" + remote_file); /* Log in to the FTP Server with the User Name and Password Provided */ ftpRequest.Credentials = new NetworkCredential(user, pass); /* When in doubt, use these options */ ftpRequest.UseBinary = true; ftpRequest.UsePassive = true; ftpRequest.KeepAlive = true; /* Specify the Type of FTP Request */ ftpRequest.Method = WebRequestMethods.Ftp.UploadFile; /* Establish Return Communication with the FTP Server */ ftpStream = ftpRequest.GetRequestStream(); /* Open a File Stream to Read the File for Upload */ FileStream localFileStream = new FileStream(local_file, FileMode.Open); /* Buffer for the Downloaded Data */ byte[] byteBuffer = new byte[bufferSize]; int bytesSent = localFileStream.Read(byteBuffer, 0, bufferSize); /* Upload the File by Sending the Buffered Data Until the Transfer is Complete */ try { while (bytesSent != 0) { ftpStream.Write(byteBuffer, 0, bytesSent); bytesSent = localFileStream.Read(byteBuffer, 0, bufferSize); } } catch (Exception ex) { Debug.Log(ex.ToString()); } /* Resource Cleanup */ localFileStream.Close(); ftpStream.Close(); ftpRequest = null; } catch (Exception ex) { Debug.Log(ex.ToString()); } return; } }
今回学んだことリスト
- EditorUserSettingsでプロジェクト固有のデータを保存できる
- PostProcessBuildAttribute属性をstatic関数に付与すると、その関数はビルド後に処理が走る
- PostProcessBuildAttributeの引数は実行順
- PreferenceItem属性を付与することで、Preferenceに設定を追加できる
- EditorGUILayout.〜でGUIを作れる
- EditorGUILayout.BeginToggleGroupとEditorGUILayout.EndToggleGroup()で囲むことで、その間に記載したGUIをチェックボックスでOn/Offできる。
RAIIをC#でやるには?
RAIIとは?
Resource Acquisition Is Initializationの略で、直訳すると"リソースの取得は初期時に"となります。
これはC++やD言語で一般的なテクニックでリソースの取得と破棄を、変数のコンストラクタとデストラクタに関連付けて行うものです。
例えば、下記のように書いておくとスコープを抜けた時に自動でファイルをクローズしてくれるのでクローズし忘れを防いでくれます。
class MyFileStream { public: MyFileStream( str filenname ) { Open( filename ); } ~MyFileStream() { Close(); } ... //OpenとかCloseの実装 };
C#ではどうする?
しかし、このテクニックはC#では使えません。その代わりになるのがIDisposerインターフェースとusing句です。
class MyFileStream : IDisposer{ public MyFileStream( str filenname ) { Open( filename ); } public void Dispose() { Close(); } ...//OpenとかCloseの実装 };
と書いておき、使用の際には、
using( MyFileStream mfs = new MyFileStream( "hoge.txt" ) ) { //処理 }
と書いておくことでRAIIと同様のことができます。ちょっと使いづらい。
C#のLinqについてまとめた。
Linqについて
C#にはC++にはないLinqという素晴らしい仕組みがあるらしい。脳に刻みこむためにメモした。
以下のスライドを参照してまとめただけ。
http://www.slideshare.net/shotababa359/c-linq-to-objects
利用方法
using System.Linq を書くだけ。
!!Linqにある拡張メソッド一覧
名前 | 説明 | 例 |
---|---|---|
Count | 数を返す | var v = characters.Count( c=> c.Nenshu >= 10000000 ); |
Where | 要素抽出 | var v = characters.Where( c=> c.Nenshu >= 10000000 ); |
Select | 要素射影(年収だけのリスト) | var v = characters.Select( c=> c.Nenshu ); |
All | 全ての要素満たしてる? | var v = characters.All( c=> c.Nenshu >= 10000000 ); |
Any | いずれかが要素満たしてる? | var v = characters.Any( c=> c.Nenshu == 10000000 ); |
First | 条件を満たす最初の要素(ないと例外発生) | var v = characters.First( c=> c.Nenshu >= 10000000 ); |
FirstOrDefault | 条件を満たす最初の要素。ない場合は規定値※1 | var v = characters.FirstOrDefault( c=> c.Nenshu >= 10000000 ); |
Last | 条件を満たす最後の要素(ないと例外発生) | var v = characters.Last( c=> c.Nenshu == 10000000 ); |
LastOrDefault | 条件を満たす最後の要素。ない場合は規定値※1 | var v = characters.LastOrDefault( c=> c.Nenshu == 10000000 ); |
Max | 最大値を取得する | var v = characters.Max( c=> c.Nenshu ); |
Min | 最小値を取得する | var v = characters.Min( c=> c.Nenshu ); |
Sum | 合計値を取得する | var v = characters.Sum( c=> c.Nenshu ); |
Average | 平均値を取得する | var v = characters.Average( c=> c.Nenshu ); |
OfType |
指定された型に一致する要素を抽出 | var v = characters.OfType |
Take | 指定された数の要素を取得 | var v = characters.Take(10); |
Skip | 指定された数の要素を読み飛ばす | var v = characters.Skip(10); |
OrderBy | 昇順にソート | var v = characters.OrderBy( c => c.Nenshu ); |
OrderByDescending | 降順にソート | var v = characters.OrderByDescending( c => c.Nenshu ); |
Reverse | 逆順にする | var v = characters.Reverse(); |
Distinct | 重複を取り除く | var v = characters.Distinct(); |
- ※1 規定値はクラス型ならnullになります。
応用
最大値を持つ要素を取得
//一番金持ちのキャラを取得
var max = characters.Max( c=> c.Nenshu );
var richman = characters.Where( c=> c.Nenshu == max );
(※初投稿時WhereのところMaxになってました。ごめんなさい。)
指定した値で初期化された配列を取得
//trueで初期化されたbool値を100個取得 bool[] flags = Enumerable.Repeat(true,100).ToArray(); //"無駄"で初期化された文字列を1000個取得 string[] strs = Enumerable.Repeat("無駄", 1000));
N個スキップした先からM個取得
//1pageあたり10個ずつ表示するシステムで、P pageに表示する10個を取得 var v = characters.Skip( P * 10 ).Take( 10 );
DoTweenのcanvasGroup.DoFade( 0, 1 ).OnComplete( completeFunction )みたいなことがしたい
サンプルプロジェクトをgithubにあげてます。
こないだUnityの勉強会に行って、UnityのAssetでTweenアニメをしてくれるDoTweenというものがあるのを知りました。
そこで出てきたサンプルコードで以下の様なものがありました。
obj.GetComponent<CanvasGroup>().DoFade( 0, 1 ).OnComplete( completeFunction )
おぉ!これはかっこいい!スマート!こんな風に書けるようなのを作ってみたい!って思ったので試してみました。やりたいこととしては、
- あたかもUnityEngine.UI.CanvasGroupがアニメーションの関数を持っているようにしたい
- アニメーションの関数を数珠つなぎして色々指定したい
- 上記関数を呼ぶ以外には、他に何の前処理も必要ないのが良い
という3つ。
C++erな私は知らなかったのですが、1に関してはC#の拡張メソッドという機能を使えばできます。
public static class CanvasGroupExtention { //拡張メソッド public static void AnimFade(this CanvasGroup canvas_group, float to, float duration) { ... } }
って感じ書いておくと
canvasGroup.AnimFade( 0, 1.0 );
などとかけるようになります。
さて、2の「アニメーションの関数を数珠つなぎして色々指定したい」に関しては、関数がcanvasGroupを返すようにすりゃ良いかな?っと思ってました。こんな感じ...
public static class CanvasGroupExtention { //拡張メソッド public static CanvasGroup AnimFade(this CanvasGroup canvas_group, float to, float duration) { ... return canvas_group; } public static CanvasGroup OnComplete(this CanvasGroup canvas_group, CompleteFunction func ) { ... return canvas_group; } }
って思ってたんですが、OnCompleteのような関数は、その場で実行するんじゃなくてアニメーションが終わった後に、引数の関数を呼ばなきゃ駄目なので、どっかに渡された関数を覚えとかないと駄目なんですね。
というわけで、AnimUnitという別クラスを作って、それに覚えさせることに。そして、返り値は、その作ったAnimUnitを返すようにしました。
public class AnimUnit { ... public AnimUnit OnComplete(CompleteCallback callback) { OnCompleteCallback = callback; return this; } ... } public static class CanvasGroupExtention { //拡張メソッド public static AnimUnit AnimFade(this AnimUnit canvas_group, float to, float duration) { AnimUnit unit = new AnimUnit(); ... return unit; } }
んで、後はUnityのコルーチンを使ってFadeのアニメをさせます。そのへんは、もうAnimUnitクラスに持たせちゃいます。
ただ、一つ問題が。StartCoroutineってMonoBehaviorのインスタンスメソッドなので、なんらかのMonoBehaviorインスタンスがないと呼べない。
AnimUnitクラスをMonoBehaviorにするという手もあるけど、そうするとMonoBehaviorが増えて処理が重くなりそう。
仕方がないので、それ専用のMonoBehaviorを作ることに、
public class AnimationManager : SingletonMonoBehaviour<AnimationManager> { }
以上で、完成。
SingletonMonoBehaviourに関しては、以下の記事が詳しいです。
残念ながら事前にAnimationManagerを作っておくという前処理が必要になってしまいましたが、それ以外は当初の目的が達成しました。
以下のように、1frameかけてフェードアウトして終わったらActive falseにしてね。っていうのが簡単にかけます。
obj.GetComponent<CanvasGroup>().AnimFade( 0.0f, 1.0f ).OnComplete( () => canvas.gameObject.SetActive(false) );
CanvasGroup以外のクラスや、他のTweenアニメに関しても同様の方法で拡張できますね。
最後にソースコードを貼り付けておきます。
using UnityEngine; using UnityEngine.UI; using System.Collections; namespace Extentions { public class AnimUnit { public CanvasGroup CanvasGroup{ get; set; } public delegate void CompleteCallback(); public CompleteCallback OnCompleteCallback{ get; set; } public AnimUnit OnComplete(CompleteCallback callback) { OnCompleteCallback = callback; return this; } public IEnumerator FadeCoroutine(float to, float duration) { float speed = ( to - CanvasGroup.alpha ) / duration; while( ( duration = duration - Time.deltaTime ) > 0 ) { CanvasGroup.alpha += speed * Time.deltaTime; yield return null; } CanvasGroup.alpha = to; if(OnCompleteCallback != null) { OnCompleteCallback(); } } } public static class CanvasGroupExtention { public static AnimUnit AnimFade(this CanvasGroup canvas_group, float to, float duration) { AnimUnit unit = new AnimUnit(); unit.CanvasGroup = canvas_group; AnimationManager.Instance.StartCoroutine( unit.FadeCoroutine( to, duration ) ); return unit; } } }
作ったアプリです!良かったらダウンロードお願いします!
play.google.comExcelから翻訳データを読み込んでTextに突っ込むTranslateManagerを作った
タイトルでだいたい説明した感じですが、ポイントとしては、
- TagProcessorクラスを介することでタグ解釈ができる(後述)
- UnityEngine.UI.Textのtextに$$で始まるキーを入れておくと、対応する訳を入れてくれる
- Resources.FindObjectsOfTypeAll
()でヒエラルキーをなめて、Textに翻訳データをぶち込む
といった感じです。
さっそくですがサンプルプロジェクトを以下に置きました。(Unity version 5.3.5f1 )
github.com
以下、説明していきます。
TranslateManagerのソースはこのようになっています。(今は日本語と英語だけに対応しています。)
using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; using System.Xml; public class TranslateText { public string jp; public string eng; public TranslateText( string jp_, string eng_ ) { jp = jp_; eng = eng_; } } public class TranslateManager : Singleton< TranslateManager > { private Dictionary<string, TranslateText> texts = new Dictionary<string, TranslateText>(); ITagProcessor tagProcessor = null; //------------------------------------------------------------------------------ //Resourceフォルダ以下にあるExcelデータを読み込む public void Read( string path ) { TextAsset text_asset = Resources.Load<TextAsset>(path); Debug.Assert( text_asset != null ); XmlDocument xmldoc = new XmlDocument (); xmldoc.LoadXml ( text_asset.text ); var nsmgr = new XmlNamespaceManager(xmldoc.NameTable); nsmgr.AddNamespace("ss", "urn:schemas-microsoft-com:office:spreadsheet"); XmlNodeList rows = xmldoc.GetElementsByTagName("Row"); foreach (XmlNode row in rows){ XmlNodeList datas = row.SelectNodes("ss:Cell/ss:Data", nsmgr); string key = datas[0].InnerText; string jp = datas[1].InnerText; string eng = datas[2].InnerText; //改行コードを変換する //手元のExcelを使ってxml形式で保存すると改行が\r(CR)になっているので、 //それを変換する jp = jp.Replace( "\r", "\n" ); eng = eng.Replace( "\r", "\n" ); texts[ key ] = new TranslateText( jp, eng ); } } //------------------------------------------------------------------------------ //ヒエラルキーにあるテキスト仕込んだキーから変換 public void ApplyAllUIText() { Text[] text_components = Resources.FindObjectsOfTypeAll<Text>(); for (var i = 0; i < text_components.Length; ++i) { InterpretText( ref text_components[i] ); } } //------------------------------------------------------------------------------ //タグプロセッサーを設定 public void SetTagProcessor( ITagProcessor processor ) { tagProcessor = processor; } //------------------------------------------------------------------------------ //キーからテキスト取得 public string GetText( string key ) { string str = getTextRaw(key); if( tagProcessor != null ) { str = tagProcessor.Process( str ); } return str; } //------------------------------------------------------------------------------ //Textクラスを与えて、キーが入ってたら翻訳データを挿入 public void InterpretText( ref Text t ) { if( t.text.StartsWith( "$$" ) ) { string key = t.text.Substring(2).TrimEnd();//たまに間違って改行が入っている場合があって気づきにくいのでTrimEndしておく t.text = GetText( key ); } } //------------------------------------------------------------------------------ //タグ解釈していない、そのままのテキスト取得 private string getTextRaw( string key ) { // Debugしやすいように Application.systemLanguage を直接見ずにConfigというクラス経由で参照 if( Config.Language == SystemLanguage.Japanese ) { return texts[key].jp; } else { return texts[key].eng; } } }
Excelの翻訳データはこんな感じになっています。1列目がキー、2列目が日本語訳、3列目が英語訳です。
使い方としては、Editor上で、こんな感じで$$から始まるキーを打ち込んでおきます。
そんで、ゲームが起動した直後に、
tagProcessor = new TagProcessor();//タグプロセッサー作成 TranslateManager.Instance.SetTagProcessor( tagProcessor );//タグプロセッサー設定 TranslateManager.Instance.Read( "Text/texts" );//Resourcesから読み込む TranslateManager.Instance.ApplyAllUIText();//ヒエラルキーにある全Textに適用する
としておくと、
このように、対応した翻訳データに置き換わります。
また、Scriptで直接キーを指定して翻訳データをとってきたい場合には、
string text = TranslateManager.Instance.GetText( "1001" );
っとすることで取得できます。
TagProcessorとは?
先ほどのExcelデータの中で、#{MovieAward}っとなっている部分がタグです。
ゲームを作っていると文字列の中に変数を埋め込みたい時があります。
例えば、動画を見た時にコインをゲットできるけど、そのコイン枚数はレベルによって変わる。っという場合を考えてみます。
単純に考えると、レベルの数だけ文章を用意すれば良いということになります。すると、
- 10枚コインゲット!, You got 10 coins!
- 20枚コインゲット!, You got 20 coins!
- 40枚コインゲット!, You got 40 coins!
- ...
っと大量の翻訳データが必要になってしまいます。
それを、解決してくれるのがTagProcessorです。Excelに
Key | #{MovieAward}枚コインゲット! | You got #{MovieAward} coins!
と書いておき、TagProcessorの中で以下のように記述しておきます。
public class TagProcessor : ITagProcessor { protected int movieAward; public void SetMoviewAward( int award ) { movieAward = award; } //タグを処理する public string Process( string text ) { const string cTagWatchingMovieAward = "#{MovieAward}"; if( text.Contains( cTagWatchingMovieAward ) ) { //タグを置き換える text = text.Replace( cTagWatchingMovieAward, movieAward.ToString() ); } return text; } }
そして、TagProcessorに与える予定の枚数をSetMoviewAwardで与えておいて、
TranslateManager.Instance.GetText( Key )
っとすると、状況に応じた枚数の入った文章をゲットすることができます。
ちなみに、ITagProcessorの中身はこれだけです。
using UnityEngine; using System.Collections; public interface ITagProcessor { string Process( string text ); }
上記を利用して作ったアプリです!良かったらダウンロードお願いします!
Unityで16進数のColor表記からColorに変換する
はてなに知り合いができたから、Qiitaからはてなブログに戻ってみました。
ところで、16進数表記からUnityのColorクラスにしたいことってあると思います。けど、結構面倒です。例えば"#ffeeddff"という色をUnityのColorにしようと思ったら、
Color color = new Color( 1.0f, 0.933f, 0.867f,1.0f );
などとしないといけません。16進数から少数に変換するのが面倒です。
調べてみたところ、ColorUtility.TryParseHtmlStringを使う方法と、自前でuintから計算する方法があるみたいです。
自分の利用においては文字列じゃなくてuintで十分なことが多いので、今回は後者のアプローチで行くことにしました。その方が速度的にも多少メリットがありますし。
後ろの記事に書かれていたコードをベースに、アルファ成分が指定されていた場合も動作するようにしました。
public static Color GetRgbColor( uint color ) { float r,g,b,a; var inv = 1.0f / 255.0f; if( color > 0xffffff ) { r = ( ( color >> 24 ) & 0xff ) * inv; g = ( ( color >> 16 ) & 0xff ) * inv; b = ( ( color >> 8 ) & 0xff ) * inv; a = ( ( color ) & 0xff ) * inv; } else { r = ( ( color >> 16 ) & 0xff ) * inv; g = ( ( color >> 8 ) & 0xff ) * inv; b = ( ( color ) & 0xff ) * inv; a = 1.0f; } return new Color( r, g, b, a ); }