(:3[kanのメモ帳]

個人ゲーム開発者kan.kikuchiのメモ的技術ブログ。月木更新でUnity関連がメイン。

(:3[kanのメモ帳]


本ブログの運営者kan.kikuchiが個人で開発しているゲームです!


ビットフラグを使いやすくするアレコレ【Unity】【C#】【エディタ拡張】【拡張メソッド】【属性】


このエントリーをはてなブックマークに追加



この記事でのバージョン
Unity 5.4.0f3


はじめに

タイトルの通り、今回はUnity&C#で開発を行う際に

ビットフラグを使いやすくするアレコレのご紹介です!


ビットフラグ

そもそもビットフラグとは、名前の通り2進数(ビット)でフラグを管理するやり方です。


詳細は省きますが、以下の記事が参考になると思います。






EnumとFlags属性

ビットフラグをint型で実装する事も可能ですが、

enumにFlags属性を付けて使うとより分かりやすく、使いやすい形で実装できます。

using UnityEngine;
using System;
using System.Collections;

public class FlagsTest : MonoBehaviour {

  //enumをFlags属性を付けて作成(using Systemが必要)
  [Flags]
  private enum FlagTypes{
    Flag1 = 1 << 0,//2進数だと001 
    Flag2 = 1 << 1,//2進数だと010   
    Flag3 = 1 << 2,//2進数だと100  
  }

  private void Start () {

    //Flag1とFlag3を有効にする(101にする)
    FlagTypes currentFlag = FlagTypes.Flag1 | FlagTypes.Flag3;


    //Flag1とFlag3が有効になっているか
    if (
      (currentFlag & FlagTypes.Flag1  ) == FlagTypes.Flag1 &&
      (currentFlag & FlagTypes.Flag3  ) == FlagTypes.Flag3
    ){
      Debug.Log("フラグOK : " + currentFlag.ToString());
    }

  }

}


ちなみにFlags属性を付けずともビットフラグとして機能はします。

あたかも、これを付けないとフラグ判定できませんよ、とも読める。
が、要するに、作り手(プログラマ)の礼儀作法を教育しているように思われる。

毛の抜けたSEの独り言: C 系のビット操作と C# の enum



ただし、ToStringをした時の動作が変わります。上記コードのログだと、

Flagsを付けた場合は、フラグOK : Flag1, Flag3

Flagsを付けない場合は、フラグOK : 5

と表示されます。


HasFlag

先ほどのコードにもありましたが、

どのフラグが立っている時の判定がやや分かりづらい感じがあります。

//Flag1とFlag3が有効になっているか
if (
  (currentFlag & FlagTypes.Flag1  ) == FlagTypes.Flag1 &&
  (currentFlag & FlagTypes.Flag3  ) == FlagTypes.Flag3
){
  Debug.Log("フラグOK : " + currentFlag.ToString());
}


.NET Framework 4.0 以降であればHasFlagという関数が使えるのですが、

現状のUnityでは残念ながら使えません。


なのでHasFlagを拡張メソッドとして作るのが良いと思います。。




using System;

public static class EnumExtensions {

  /// <summary>
  /// 現在のインスタンスで 1 つ以上のビット フィールドが設定されているかどうかを判断します
  /// </summary>
  public static bool HasFlag(this Enum self, Enum flag){
    if (self.GetType() != flag.GetType()){
      throw new ArgumentException( "flag の型が、現在のインスタンスの型と異なっています。" );
    }

    var selfValue = Convert.ToUInt64(self);
    var flagValue = Convert.ToUInt64(flag);

    return (selfValue & flagValue) == flagValue;
  }

}


上記のEnumExtensions.csを作成すると、HasFlagが使えるようになります。

//Flag1とFlag3が有効になっているか
if (currentFlag.HasFlag(FlagTypes.Flag1 | FlagTypes.Flag3)){
  Debug.Log("フラグOK : " + currentFlag.ToString());
}



PropertyDrawer

enumの値をInspector上で設定しようとすると、以下のように表示されます。


f:id:kan_kikuchi:20160909134405j:plain


これだと一つしか選択できないため、

enumをビットフラグとして使おうとしているの場合には不便です。


なのでビットフラグとして使うenumのInspector上の見た目を変えるために、

EnumFlagsAttributeDrawerを用意しておくと良いかと思います。




using System;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif 

[AttributeUsage( AttributeTargets.Enum | AttributeTargets.Field )]
public sealed class EnumFlagsAttribute : PropertyAttribute{}

/// <summary>
/// Flag設定したEnumのインスペクター表示を変えるクラス
/// </summary>
#if UNITY_EDITOR
[CustomPropertyDrawer( typeof( EnumFlagsAttribute ) )]
public sealed class EnumFlagsAttributeDrawer : PropertyDrawer{
    public override void OnGUI( 
        Rect position, 
        SerializedProperty prop, 
        GUIContent label 
    )
    {
        var buttonsIntValue = 0;
        var enumLength      = prop.enumNames.Length;
        var labelWidth      = EditorGUIUtility.labelWidth;
        var buttonPressed   = new bool[ enumLength ];
        var buttonWidth     = ( position.width - labelWidth ) / enumLength;
 
        var labelPos = new Rect( 
            position.x, 
            position.y, 
            labelWidth, 
            position.height 
        );
        EditorGUI.LabelField( labelPos, label );
        EditorGUI.BeginChangeCheck();
 
        for ( int i = 0; i < enumLength; i++ )
        {
            buttonPressed[ i ] = ( prop.intValue & ( 1 << i ) ) == 1 << i;
 
            var buttonPos = new Rect(
                position.x + labelWidth + buttonWidth * i, 
                position.y, 
                buttonWidth, 
                position.height
            );
 
            buttonPressed[ i ] = GUI.Toggle( 
                buttonPos, 
                buttonPressed[ i ], 
                prop.enumNames[ i ], 
                "Button" 
            );
 
            if ( buttonPressed[ i ] )
            {
                buttonsIntValue += 1 << i;
            }
        }
 
        if ( EditorGUI.EndChangeCheck() )
        {
            prop.intValue = buttonsIntValue;
        }
    }
}
#endif


なお、PropertyDrawerとはInspectorでの見た目を変更できるクラスのことです。



上記のEnumFlagsAttributeDrawer.csを作成すると、EnumFlagsという属性が使えるようになります。

//enumをFlags属性を付けて作成(using Systemが必要)
[Flags]
public enum FlagTypes{
  Flag1 = 1 << 0,//2進数だと001 
  Flag2 = 1 << 1,//2進数だと010   
  Flag3 = 1 << 2,//2進数だと100  
}

//EnumFlags属性を付ける
[EnumFlags]
public FlagTypes FlagType;

f:id:kan_kikuchi:20160909134851j:plain


上記の通り、EnumFlagsを付加されると複数のタイプを設定出来るようになります。


エディタ拡張のウィンドウなどで使う場合

EditorWindowを継承したウィンドウなどでビットフラグを使いたい場合、

EditorGUILayout.EnumFlagsFieldを使うと以下のように簡単に設定用のGUIが実装出来ます。

public FlagTypes _flagTypes;

private void OnGUI(){
  _flagTypes = (FlagTypes)EditorGUILayout.EnumFlagsField(_flagTypes);
}
f:id:kan_kikuchi:20200117073119j:plain


複数のフラグを選択する事はもちろん、Nothingで全解除、Everythingで全選択する事も可能です。


おわりに

ある程度フラグの数がある場合はビットフラグを使ったほうが、フラグ管理が楽になると思います。

Unityの場合はInspectorでの設定も楽なので、テストプレイでも重宝します。