ちょこっとベターなSingletonMonoBehaviour

tsubakit1.hateblo.jp

こちらの記事を参考にしたSingletonMonoBehaviourを利用していたのですが、SingletonMonoBehaviourの絡むバグが発生し、それを発生しにくくできないかと思って、ちょこっと変えてみました。

上記のSingletonMonoBehaviourでは、Instanceにアクセスすると最初の一度だけ、 FindObjectOfType( typeof( T ) ); をしています。

この時に、そのオブジェクトのactiveがfalseだと見つからなくてエラーになるという問題がありました。

まあ、普通はactivateをfalseになんてしていないと思うんですが、例えば、

class HogeManager : SingletonMonoBehaviour<HogeManager>

というのがあったときに、

class HogeMain{
    public HogeManager hogeManager;//Inspectorで与えとく

    void Start()
    {
        hogeManager.SetActive( false );//最初は使わないから false にしとこう。

        ... //いろいろ処理

        HogeManager.Instance.Function();//HogeManagerのFunctionを呼ぼう! <- ここで死亡
    }
}

という感じで死んでしまったことがありました。

それでも、普通はAwakeが呼ばれたさいにinstanceに値が入るのでだいじょうぶなんですけど、往々にして継承先でAwakeを書きたくなるんで、継承元のAwakeを隠しちゃっていました。

隠しちゃってるよっていう、warning出ていたんですけどね。

継承先でAwakeを書きたくなることは結構あるので、継承元のAwakeをvirtualにしました。

そして、継承先で、

protected override void Awake()
{
    base.Awake();
    ...//継承先独自の処理
}

としています。

ただ、これでもAwake内でSetActive(false)されて、Instanceにアクセスされたり、そもそもEditor上でactiveをfalseにされていたら、同様にエラーになっちゃいますけど。

他にも、

abstractにしてたり、 where T : SingletonMonoBehaviourにしてたりと、ちょこっと安全にしました。

では、ソース。

using UnityEngine;

public abstract class SingletonMonoBehaviour<T> : MonoBehaviour where T : SingletonMonoBehaviour<T>
{
	private static T instance;
	public static T Instance {
		get {
            if ( instance == null ) {
                instance = ( T )FindObjectOfType( typeof( T ) );
                if ( instance == null ) {
                    Debug.LogError ( typeof( T ) + "is nothing" );
                }
            }
			return instance;
		}
	}

	protected virtual void Awake()
	{
		CheckInstance();
	}
	
	protected bool CheckInstance()
	{
		if( instance == null)
		{
			instance = (T)this;
			return true;
		}else if( Instance == this )
		{
			return true;
		}
		
		Destroy(this);
		return false;
	}

	void OnDestroy () {
		instance = null;
	}
}

以前作ったSoundManagerの改良

効果音を鳴らした後にスクリプトからFadeOutしたり、FadeInとかしたくなったので、以前作ったSoundManagerを改良しました。

qiita.com
(この時はqiita使ってた...)

  • Handleを返すようにして、Handle経由でFadeIn, FadeOutができるように
  • それにともない。同じフレームで同じSeが重複して鳴るのを回避する方式を変更

ソース

using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using System.IO;

[System.Serializable]
public class SoundVolume{
    public float bgm   = 1.0f;
    public float se    = 1.0f;

    public bool  mute  = false;

    public void Reset(){
        bgm  = 1.0f;
        se   = 1.0f;
        mute = false;
    }
}

public class SoundManager : SingletonMonoBehaviour< SoundManager > {

    public class Handle{
        public float volume    = 1.0f;
        public float fadeSpeed = 1.0f;
        public long  frame     = 0;

        public void FadeIn(){
            SoundManager.Instance.StartCoroutine( fadeIn() );
        }

        public void FadeOut(){
            SoundManager.Instance.StartCoroutine( fadeOut() );
        }

        public void ResetParams(){
            volume = 1.0f;
            fadeSpeed = 1.0f;
            frame = 0;
        }

        private IEnumerator fadeIn(){
            while( volume < 1.0f )
            {
                volume += fadeSpeed * Time.deltaTime;
                yield return null;
            }
            volume = 1.0f;
        }

        private IEnumerator fadeOut(){
            while( volume > 0.0f )
            {
                volume -= fadeSpeed * Time.deltaTime;
                yield return null;
            }
            volume = 0.0f;
        }
    }

    [SerializeField]
    private SoundVolume volume = new SoundVolume();
    public SoundVolume Volume { 
        get { return volume; }
        set { volume = value; } 
    }

	private AudioClip[] seClips;
	private AudioClip[] bgmClips;

	private Dictionary<string,int> seIndexes = new Dictionary<string,int>();
	private Dictionary<string,int> bgmIndexes = new Dictionary<string,int>();

    private const int cNumChannel = 16;

    private AudioSource bgmSource;
    private Handle      bgmHandle = new Handle();

    private AudioSource[] seSources = new AudioSource[cNumChannel];
    private Dictionary<Handle,AudioSource> seHandles = new Dictionary<Handle,AudioSource>();

    private long frameCounter;

	//------------------------------------------------------------------------------
	void Awake () {
		if( this != Instance )
		{
			Destroy(this);
			return;
		}

        bgmSource = gameObject.AddComponent<AudioSource>();
        bgmSource.loop = true;

        for(int i = 0 ; i < seSources.Length ; i++ ){
            seSources[i] = gameObject.AddComponent<AudioSource>();
            seHandles[new Handle()] = seSources[i];
        }

        seClips  = Resources.LoadAll<AudioClip>("Audio/SE");
        bgmClips = Resources.LoadAll<AudioClip>("Audio/BGM");

        for( int i = 0; i < seClips.Length; ++i )
        {
            seIndexes[seClips[i].name] = i;
        }

        for( int i = 0; i < bgmClips.Length; ++i )
        {
            bgmIndexes[bgmClips[i].name] = i;
        }
	}

	//------------------------------------------------------------------------------
	void Update()
	{
        bgmSource.mute = volume.mute;
        foreach(var source in seSources ){
            source.mute = volume.mute;
        }

        bgmSource.volume = volume.bgm * bgmHandle.volume;
        foreach(var pair in seHandles ){
            pair.Value.volume = volume.se * pair.Key.volume;
        }

        frameCounter++;
	}

	//------------------------------------------------------------------------------
	public int GetSeIndex( string name )
	{
        return seIndexes[name];
	}

	//------------------------------------------------------------------------------
	public int GetBgmIndex( string name )
	{
        return bgmIndexes[name];
	}

    //------------------------------------------------------------------------------
    public Handle PlayBgm( string name ){
        int index = bgmIndexes[name];
        return PlayBgm( index );
    }

    //------------------------------------------------------------------------------
    public Handle PlayBgm( int index ){
        if( 0 > index || bgmClips.Length <= index ){
            return null;
        }

        if( bgmSource.clip == bgmClips[index] ){
            return bgmHandle;
        }

        bgmSource.Stop();
        bgmSource.clip = bgmClips[index];
        bgmSource.Play();

        bgmHandle.ResetParams();
        bgmHandle.frame = frameCounter;

        return bgmHandle;
    }

    //------------------------------------------------------------------------------
    public void StopBgm(){
        bgmSource.Stop();
        bgmSource.clip = null;
    }

    //------------------------------------------------------------------------------
    public bool IsBgmPlaying{ get{ return bgmSource.isPlaying; } }

	//------------------------------------------------------------------------------
	public Handle PlaySe( string name )
    {
        return PlaySe( GetSeIndex( name ) );
    }
   
	//------------------------------------------------------------------------------
	public Handle PlaySe( int index )
    {
        if( 0 > index || seClips.Length <= index ){
            return null;
        }

        //@memo 二回ループは一回ループにまとめられるが、
        //可読性重視で二回ループにしておく
        //for avoiding duplicated sounds
        //同一フレームでの重複再生回避
        foreach( var k in seHandles ){
            AudioSource source = k.Value;
            Handle handle = k.Key;
            if( source.clip == seClips[index] && 
                 handle.frame == frameCounter )
            {
                return handle;
            }
        }

        foreach( var k in seHandles ){
            AudioSource source = k.Value;
            Handle handle = k.Key;
            if( false == source.isPlaying ){
                handle.ResetParams();
                source.clip = seClips[index];
                source.Play();
                handle.frame = frameCounter;
                return handle;
            }
        } 

        return null;
	}

    //------------------------------------------------------------------------------
    public void StopSe(){
        foreach(AudioSource source in seSources){
            source.Stop();
            source.clip = null;
        }  
    }
}

Unityの単位系メモ

今日は本当はUnity Remoteの話を書こうと思ったのですが、ちょっと後日に回します。

Unityのグリッドサイズとか、Cubeのサイズとか、ちょいちょい忘れるのでメモ。

項目 単位
主グリッド 1m*1m
副グリッド 10cm*10cm
Cubeのサイズ 10cm*10cm
Sphereのサイズ 直径10cm
Plane 1m*1m
Unity内の距離1.0f 10cm
UnityのRigidbodyのMass 1.0f 1.0kg

これらを考慮してオブジェクトのサイズを設計しないと物理挙動がおかしくなります。

UnityでiOS向けのビルドを短時間で終わらせる簡単な方法

この記事の環境:Unity version 5.4.0f3


Unityで開発していると、どうしても実機でテストしなくちゃならないことがあります。

例えば、加速度センサを使ったゲームとかはPC上ではテストできません。

そこでiOS用にビルドしなくちゃいけないんですが、これがめちゃくちゃ遅い。

Unityクラウドビルドを利用したり、自分でCI導入したりすべしという話もあるのですが、知ってる人には当たり前なビルド時間を短縮する方法があります。

それは、Player SettingsのConfiguration部分の設定を変えることです。

デフォルトでは、以下のようになっていると思います。
f:id:wkpn:20160814144518p:plain

ちょっと実機でテストする分には、この辺りの設定を変えてしまっても、ほとんど問題ありません。試しに、Scripting Backend、Target Device、Architectureの設定を変えて、空のシーンをビルドした時にビルド時間を比べてみました。

Scripting Backend Target Device Architecture Unity上でのビルド時間 XCodeでのビルド時間 合計のビルド時間
IL2CPP iPhone + iPad Universal 0:42 1:40 2:22
IL2CPP iPhone only Universal 0:36 1:42 2:18
IL2CPP iPhone only ARMv7 0:37 1:10 1:47
Mono2x iPhone only (ARMv7) 0:15 0:21 0:36

一番おそいのと、一番はやいのとだと約4倍の差が!

UniversalからARMv7にするだけでコンパイルするファイル数が160ファイルから80ファイルに、さらにMono2xにすると49ファイルに減りました。そりゃぁ早くなるわけだ。

最終確認は「IL2CPP,iPhone + iPad,Universal」の設定にする必要がありますが、ちょっと実機確認したいぐらいのときは「Mono2x,iPhone only」にしとくのが良さそうです。

UniversalにしてビルドしないとiTunes Connectへのアップロードができないので注意してください。




ってここまで書いて、上記の話をとある場所で言ったらUnity Remoteは使わんのかーって言われて、えっ何それ?(・o・)ってなったんで、明日は後日Unity Remoteの使ってみた感想を書こうと思います。


Divide Number Puzzle

Divide Number Puzzle

  • Ken Watanabe
  • Games
  • Free
play.google.com

BlenderからUnityへ出力する際のメモ

Blenderで作ったモデルをFBXに出力する際のスケールや回転が、出力設定によって色々変わって混乱したのでメモ。

BlenderでApply Transformにチェックを入れないで出力した場合

  • Blenderで出したモデルは頂点座標は 1.0/100.0 されて、スケールに100倍掛けられて出力される。
  • 座標系の違いは、オブジェクトのRotationとして入ってくる

画で説明すると、

f:id:wkpn:20160814113713p:plain
Blenderで、こういうモデルを

f:id:wkpn:20160814113744p:plain
こういう出力で出すと、

f:id:wkpn:20160814113757p:plain
Unityで読み込むとこんな感じになる。Scaleに100、Rotationに90度が入っている。もちろん見た目は一緒。

Apply Transformにチェックを入れて出力した場合

  • 頂点座標は、そのまま。スケールもそのまま出てくる。
  • 座標系の違いは、頂点座標が変換されて出力される。

画で説明すると、

f:id:wkpn:20160814114033p:plain
Blenderで、さっきと同じモデルを

f:id:wkpn:20160814114123p:plain
こういう出力で出すと、

f:id:wkpn:20160814114157p:plain
Unityで読み込むとこんな感じになる。Scaleも1だし、Rotationも特に入っていない(頂点座標として吸収されている)。

  • ※Apply Transformした場合でも、オブジェクトに設定しておいたSRTはそのまま出てくる(頂点座標として吸収されない)。
  • ちなみにBlender上でオブジェクトに設定したSRT変換は、Object->Applyで頂点に適用できる
    • 以下のApply Objectの項目を参照

User:Fade/Doc:2.6/Manual/3D interaction/Transform Control/Reset Object Transformations - BlenderWiki

  • 結論。できるだけBlender上でのSRT変換は頂点座標に適用させて出力するのがUnity上での取り扱いが楽だと思う。

その他

  • Blenderの1GridはUnityの1Grid、つまり1メートルである
  • textureを元のテクスチャ位置と同じ相対パスに、あらかじめ置いておかないとUnity上のマテリアルにはちゃんと適用されない。

Divide Number Puzzle

Divide Number Puzzle

  • Ken Watanabe
  • Games
  • Free
play.google.com

transformは遅い説の検証

qiita.com

こちらの記事によるとtransformは内部でGetComponentをしているので遅いらしいです。

気になったので自分でも試してみました。

テスト環境

まずは以下の様なコードで。

using UnityEngine;
using System.Collections;

public class TransformProfile : MonoBehaviour {
    protected Transform cachedTransform;
    System.Diagnostics.Stopwatch stopWatch0 = new System.Diagnostics.Stopwatch();
    System.Diagnostics.Stopwatch stopWatch1 = new System.Diagnostics.Stopwatch();
    const int cLoopMax = 1000;

	void Start () {
        cachedTransform = transform;

        stopWatch0.Reset();
        stopWatch0.Start();
        for( int i = 0; i < cLoopMax; ++i )
        {
            transform.localPosition += Vector3.one;
        }
        stopWatch0.Stop();
        Debug.Log( "transoform: " + stopWatch0.Elapsed.TotalMilliseconds.ToString() + "msec" );

        stopWatch1.Reset();
        stopWatch1.Start();
        for( int i = 0; i < cLoopMax; ++i )
        {
            cachedTransform.localPosition += Vector3.one;
        }
        stopWatch1.Stop();
        Debug.Log( "cached transoform: " + stopWatch1.Elapsed.TotalMilliseconds.ToString() + "msec" );
	}
}

PC(Editor)

cachedTransform 0.144 msec
transform 0.384 msec

iPhone

cachedTransform 0.174 msec
transform 0.295 msec


ざっくり2倍〜3倍ほどキャッシングしてあげたほうが早そうです。ただ、上記のコードだとメモリキャッシュが効いてまうので、あまり差が出にくいです。

そこで以下の様なコードでも試します。1フレーム経つ間にキャッシュが汚れることが期待し、コルーチン使って毎フレーム少しづつtransformに触る感じにしております。

using UnityEngine;
using System.Collections;

public class TransformProfile : MonoBehaviour {
    protected Transform cachedTransform;
    System.Diagnostics.Stopwatch stopWatch0 = new System.Diagnostics.Stopwatch();
    System.Diagnostics.Stopwatch stopWatch1 = new System.Diagnostics.Stopwatch();
    const int cLoopMax = 1000;

	void Start () {
        cachedTransform = transform;

        StartCoroutine(processTransform());
        StartCoroutine(processCachedTransform());
    }

	IEnumerator processTransform() {
        stopWatch0.Reset();
        for( int i = 0; i < cLoopMax; ++i )
        {
            stopWatch0.Start();
            transform.localPosition += Vector3.one;
            stopWatch0.Stop();
            yield return null;
        }
        Debug.Log( "transoform: " + stopWatch0.Elapsed.TotalMilliseconds.ToString() + "msec" );
    }

	IEnumerator processCachedTransform() {
        stopWatch1.Reset();
        for( int i = 0; i < cLoopMax; ++i )
        {
            stopWatch1.Start();
            cachedTransform.localPosition += Vector3.one;
            stopWatch1.Stop();
            yield return null;
        }
        Debug.Log( "cached transoform: " + stopWatch1.Elapsed.TotalMilliseconds.ToString() + "msec" );
    }
}

PC(Editor)

cachedTransform 0.586 msec
transform 4.415 msec

iPhone

cachedTransform 2.561 msec
transform 7.053 msec

先程より差がでました。PCだと8倍ぐらい差がでます?

なんとなくiPhoneでも、もうちょいちゃんとキャッシュを汚せれば優位な差が出る気がします。

transformを使う際は一度キャッシュしてから利用したほうが良さそうですね。

Divide Number Puzzle

Divide Number Puzzle

  • Ken Watanabe
  • Games
  • Free
play.google.com

Editorで編集中に気づくと何故かNullReferenceExceptionのエラーが出ている件

Editorで編集中に気づくと、たまに、
NullReferenceException: Object reference not set to an instance of an object
というエラーが出ていて、編集中に?何故?Editor拡張周りに何かバグが?って思っていたのですが、違いました。

理由は、Reset関数のせいでした。

以下、引用。Reset関数とは、

デフォルト値にリセットします

Reset はインスペクターのコンテキストメニューにある Reset ボタンやコンポーネントを初めて追加するときに呼び出されます。 この関数はエディターモードのみで呼び出されます。Reset はインスペクターでデフォルト値を設定するときにもっともよく使用されます。

ということです。そんな特別な関数になるとはつゆ知らず。普通にResetという名前の関数を作っていました。

例えば、以下の様な関数

public void Reset()
{
    cachedTransform.localPosition = startPosition;
}

cachedTransformは以下のようにStart関数で与えるようにしていたので問題が。

void Start()
{
    cachedTransform = transform;
}

当然、Editorで編集中はStart関数が呼ばれていないのでcachedTransformはnullなわけです。とほほ。

というわけで、Reset以外に良い名前はないかと考えてみたんですが、なかなかよい名前が思い浮かばない... 妥協してResetParamsという関数名に変更して対処しました。

それにしても、今回のResetもそうだけど、Unityの、StartとかUpdateとかAwakeとか、仮想関数でも無いのに勝手に呼ばれるのが怖い。

処理高速化のためとはいえ、なんとかならんものか。せめて命名規則とかでそれっぽくしておいて欲しい。全部頭にOnをつけて、

OnStart
OnUpdate
OnAwake
OnReset

ってするとか。


参考:
waken.hatenablog.com