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にあげてます。

github.com

こないだUnityの勉強会に行って、UnityのAssetでTweenアニメをしてくれるDoTweenというものがあるのを知りました。

dotween.demigiant.com


そこで出てきたサンプルコードで以下の様なものがありました。

obj.GetComponent<CanvasGroup>().DoFade( 0, 1 ).OnComplete( completeFunction )

おぉ!これはかっこいい!スマート!こんな風に書けるようなのを作ってみたい!って思ったので試してみました。やりたいこととしては、

  1. あたかもUnityEngine.UI.CanvasGroupがアニメーションの関数を持っているようにしたい
  2. アニメーションの関数を数珠つなぎして色々指定したい
  3. 上記関数を呼ぶ以外には、他に何の前処理も必要ないのが良い

という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に関しては、以下の記事が詳しいです。

tsubakit1.hateblo.jp


残念ながら事前に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;
        }
    }   
}

作ったアプリです!良かったらダウンロードお願いします!

Make 10 by using 4 numbers

Make 10 by using 4 numbers

  • Ken Watanabe
  • ゲーム
  • 無料
play.google.com

Excelから翻訳データを読み込んで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列目が英語訳です。

f:id:wkpn:20160728113451p:plain

使い方としては、Editor上で、こんな感じで$$から始まるキーを打ち込んでおきます。

f:id:wkpn:20160728113930p:plain

そんで、ゲームが起動した直後に、

    tagProcessor = new TagProcessor();//タグプロセッサー作成
    TranslateManager.Instance.SetTagProcessor( tagProcessor );//タグプロセッサー設定
    TranslateManager.Instance.Read( "Text/texts" );//Resourcesから読み込む
    TranslateManager.Instance.ApplyAllUIText();//ヒエラルキーにある全Textに適用する

としておくと、

このように、対応した翻訳データに置き換わります。

f:id:wkpn:20160728121837p:plain

また、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 );
}


上記を利用して作ったアプリです!良かったらダウンロードお願いします!

Make 10 by using 4 numbers

Make 10 by using 4 numbers

  • Ken Watanabe
  • ゲーム
  • 無料
play.google.com

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から計算する方法があるみたいです。


qiita.com


baba-s.hatenablog.com


自分の利用においては文字列じゃなくて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 );
}