hirukoTimeのUnity備忘録

Unityでゲームを作成してます。

【Unity】Bit演算覚書【C#】

はじめに

執筆日は20222/11/14になります。
Unityのバージョンは2021.3.1f1を使用しています。
よく忘れて調べ直すので、自分でまとめ

自己紹介

活動名はHiruko、もしくはHirukoTimeで活動してます。
個人でSteamでゲームを販売したり、定期的にゲームジャムなどに参加をしています。

基本的な処理

初期化
//0で初期化[32bit]
int bit0 =  0 //[00000000000000000000000000000000]
//1で初期化[32bit]
int bit1 = -1 //[11111111111111111111111111111111]
左シフト
//[8]1000
var bit = 1 << 3

//ゲームなどでフラグ管理で使う場合はconstやEnumで使う
const int Flag_A = 1 << 3;

[System.Flags]
public enum Status
{
    None = 0,
    All = -1,
    //0001
    Attack = 1 << 0,
    //0010
    Jump = 1 << 1,
    //0100
    Crouch = 1 << 2,
    //1000
    Dead = 1 << 3,
}

Bitを左にずらす

UnityのInspectorでは自動でEverythingが追加される。

右シフト
var bit = 256;

var rightShift = bit >> 1;
//[100000000]bit 
//--------------
//[010000000]rightShift 

Bitを右にずらす。

OR演算
//左シフトの際のenum Statusを例として使う
var state = (Status.Attack | Status.Jump); 
//0001    Attack
//0010    Jump
//-----
//0011    stateに代入される値

どちらかのBitが1なら1、両方0なら0
[ | ]OR演算子 (縦棒、vertical bar)
[shifr] + [ ¥ ]キー([ \ ]キー)

AND演算
//左シフトの際のenum Statusを例として使う
var state = (Status.Attack | Status.Jump); 
//[0011]

state = state & Status.Attack;
//0011    現在のstate値
//0001    Attack
//-----
//0001    計算後の値

どちらのBitも1なら1、それ以外なら0
[ & ]AND演算子
[shifr] + [ 6 ]キー

XOR演算(排他的論理和
var a = 0b0101;
var b = 0b1001;

var flag = a ^ b; 
//0101    a
//1001    b
//-----
//1100    計算結果 flag

ビット同士が同じなら0、違うなら1
[ ^ ]XOR演算子 (ハット、キャレット)
[ ^ ]キー ([ ¥ ]キーの左)

NOT演算(補数、反転)
//左シフトの際のenum Statusを例として使う
var state = (Status.Attack | Status.Jump); 
//[0011]

state = ~state;
//0000-0000-0000-0000-0000-0000-0000-0011    現在のstate値
//-----
//1111-1111-1111-1111-1111-1111-1111-1100    計算後の値

0と1を入れ替える。
全部のビットを入れ替える。
[ ~ ]NOT演算子チルダ
[shifr] + [ ^ ]キー

フラグを追加する。

//左シフトの際のenum Statusを例として使う

//初期化
var state = (Status.Attack | Status.Jump); 

//OR演算を用いる
// state = (status | Status.Crouch); と同義
state |= Status.Crouch;
//0011    stateの現在値
//0100    Crouch
//-----
//0111    計算後のstate

フラグを削除する。

//左シフトの際のenum Statusを例として使う

//初期化
var state = (Status.Attack | Status.Jump); 

//AND演算とNOT演算を用いる。
// state = (status & ~Status.Jump); と同義
state &= ~Status.Jump;
//0011    stateの現在値
//1101    ~Status.Jump
//-----
//0001    計算後のstate

指定のフラグが立っているか

例1
//左シフトの際のenum Statusを例として使う

//初期化
var state = (Status.Attack | Status.Jump); 

//AND演算を用いる。
if( (state & Status.Crouch) == Status.Crouch)
{
    //何かの処理
}
//0011    stateの現在値
//0100    Crouch
//-----
//0000    (state & Status.Crouch)
//(state & Status.Crouch)[0000] == Crouch[0100]    一致しないのでfalse
例2
var state = (Status.Attack | Status.Jump | Status.Crouch ); 
var CrouchAttack = (Status.Attack | Status.Crouch );

if( (state & CrouchAttack ) == CrouchAttack )
{
    //しゃがみ攻撃!!
}

//0111    state 
//0101    CrouchAttack 
//-----
//0101    (state & CrouchAttack )
//(state & CrouchAttack )[0101] == CrouchAttack[0101]    一致するのでtrue

BitCount

int BitCount(int bits)
{
    bits = (bits & 0x55555555) + (bits >> 1 & 0x55555555);
    bits = (bits & 0x33333333) + (bits >> 2 & 0x33333333);
    bits = (bits & 0x0f0f0f0f) + (bits >> 4 & 0x0f0f0f0f);
    bits = (bits & 0x00ff00ff) + (bits >> 8 & 0x00ff00ff);
    return (bits & 0x0000ffff) + (bits >>16 & 0x0000ffff);
}

有名なアルゴリズム

ログで出力する際の変換

    public static string LogBit(int flag , int digit = 4 , bool isForce = false , char padding = '0')
    {
        //ToString( 変換元 , 進数指定 ) 第二引数に[2]を入れると2進数で表現する。
        //※[8]8進数 [16]16進数と言う感じ
        //左詰めする処理:PadLeft( 桁数 , 空埋する文字 )
        //PadLeft(4,'0')だと、ToStringの変換を4文字で埋めて、空の場合は左に0を埋める。
        //[flag = 3]の場合[11] => [0011]
        //Consoleの出力で確認する場合桁が揃った方が見やすいので必要だと思う。

        //digitより桁数が多い場合は表示がずれるので、Maskを掛ける。
        //デバッグ上問題があるのでisForceでmaskを無視する。
        var mask = !isForce ? ((1 << digit) - 1) : -1;

        return Convert.ToString(flag & mask, 2).PadLeft(digit,padding);

        //digit = 4
        //[10000 = 32]    1 << digit
        //[01111 = 31]    (1 << digit) - 1は(32) - 1
  
        //flag & maskで下位4bit以外を全部0にする  
        //XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX    flag  
        //0000-0000-0000-0000-0000-0000-0000-1111    mask  
        //-------------------------------------------------------  
        //0000-0000-0000-0000-0000-0000-0000-XXXX    計算結果 
    }

【Unity】GraphViewで遊んでみた。【第三回】

はじめに

この記事は第三回になります。
執筆日は20222/11/14になります。
Unityのバージョンは2021.3.1f1を使用しています。
第一回:Windowの作成方法
第二回:Nodeの作成方法

自己紹介

活動名はHiruko、もしくはHirukoTimeで活動してます。
個人でSteamでゲームを販売したり、定期的にゲームジャムなどに参加をしています。

今回のお題

グラフの保存機能を作る。

GraphViewに保存に関するものは無いので頑張って実装する。
まずは保存用のボタンを作成する。

ファイル構成

  • Editor
    • Node
      • BaseNode.cs
      • FloatNode
    • TestWindow.cs
    • TestGraphView.cs
    • SearchMenu.cs
  • Resources
    • UXMLファイル
    • USSファイル
  • Runtime
    • GraphData.cs

ブログ用でアクセサーは適当にpublic。
見た目の基本方針としては、ShaderGraphっぽくする。

アセットのダブルクリックでGraphが開く。 開いた時に自動ロード 保存と新規保存。

GraphData.cs
    using System;
    using System.Collections.Generic;
    using UnityEngine;

    public class GraphData : ScriptableObject
    {
        public List<NodeData> Nodes = new List<NodeData>();
        public List<EdgeData> Edges = new List<EdgeData>();

        public void Clear()
        {
            nodeDates.Clear();
            edgeDates.Clear();
        }
    }

    [Serializable]
    public class NodeData
    {
        public string guid;
        public string typeName;
        public string parameterJson;
        public Rect position;
        
        public NodeData(string targetGuid,string typeFullName,string paramJson,Rect pos)
        {
            guid = targetGuid;
            typeName = typeFullName;
            parameterJson = paramJson;
            position = pos;
        }
    }

    [Serializable]
    public class EdgeData
    {
        public string from;
        public string to;
        
        public EdgeData(string fromGuid, string toGuid)
        {
            from = fromGuid;
            to = toGuid;
        }
    }
TestWindow.cs
    public class TestWindow : GraphViewEditorWindow
    {
        [SerializeField] private GraphData data;
        private TestGraphView _graphView;
        
        private const string DefaultFileName = "NewGraphData";
        private const string Extension = "asset";
        
        //Attributeはなくして、アセットファイルから開くようにする。
        private static void ShowWindow(GraphData data)
        {
            if (data == null) return;
            
            //呼び出した段階でOnEnableが走る
            var window = GetWindow<TestWindow>(data.name);
                
            window.Initialize(data);
                
            window.Show();
        }

        //アセットを開く(ダブルクリックする)処理。
        [OnOpenAsset(0)]
        public static bool OnOpenAsset(int instanceId, int line)
        {
            var asset = EditorUtility.InstanceIDToObject(instanceId) as GraphData;
        
            if (asset == null) return false;
        
            ShowWindow(asset);
            return true;
        }
        
        //タイミング的にOnEnableの後になる。
        private void Initialize(GraphData graphData)
        {
            data = graphData;
            OnLoad();
        }
        
        private void OnEnable()
        {
            _graphView = new TestGraphView();
            _graphView.InitializeMenuWindow(this);
            rootVisualElement.Add(_graphView);

            var toolbar = new Toolbar();
            toolbar.styleSheets.Add(Resources.Load<StyleSheet>("TestToolBarStyle"));
            var saveButton = new ToolbarButton(OnSave){text = "Save",name = "save-button"};
            var saveAsButton = new ToolbarButton(CreateFile){text = "SaveAs",name = "save-as-button"};
            
            toolbar.Add(saveButton);
            toolbar.Add(saveAsButton);
            
            rootVisualElement.Add(toolbar);
            
            //ここのタイミングはPlayなどでリロードが入った場合に再読み込みする。
            OnLoad();
        }

        //このタイミングでSaveしないと編集内容が破棄されて保存前のデータがリロードされる。
        //今回は何もしてないが、ダイアログなどだして、注意メッセージ後に保存でもいいかもしれない
        private void OnDisable()
        {
            OnSave();
        }

        private void CreateFile()
        {
            var path 
                = EditorUtility
                    .SaveFilePanelInProject("Save Graph Data", DefaultFileName , Extension,"");

            //Panelをキャンセルした時は空文字列になる。
            if (string.IsNullOrEmpty(path)) return;

            var fileName = System.IO.Path.GetFileNameWithoutExtension(path);
            var newAsset = CreateInstance<GraphData>();
            newAsset.name = fileName;
            
            data = newAsset;
            
            OnSave();
            
            var oldAsset = AssetDatabase.LoadAssetAtPath<GraphData>(path);
            if (oldAsset == null) {
                AssetDatabase.CreateAsset(newAsset, path);
            }
            else {
                EditorUtility.CopySerializedIfDifferent(newAsset, oldAsset);
                AssetDatabase.SaveAssets();
            }

        }
        
        private void OnSave()
        {
            if (data.IsUnityNull()) return;
            
            data.Clear();
            
            foreach (var edge in _graphView.edges)
            {
                var to = edge.input.node as BaseNode;
                var from = edge.output.node as BaseNode;

                if (from == null || to == null) continue;
                
                var edgeData = new EdgeData(from.Guid, to.Guid);
                
                data.Edges.Add(edgeData);
            }
            
            foreach (var node in _graphView.nodes)
            {
                if (!(node is BaseNode n)) continue;
                
                var nodeData = 
                    new NodeData(
                        n.Guid,
                        n.GetType().FullName,
                        n.ToJson(), 
                        n.GetPosition());
                
                data.Nodes.Add(nodeData);
            }
        }

        private void OnLoad()
        {
            if (data.IsUnityNull()) return;

            _graphView.nodes.ForEach(x => x.RemoveFromHierarchy());
            _graphView.edges.ForEach(x => x.RemoveFromHierarchy());
            
            foreach (var n in data.Nodes)
            {
                var t = Type.GetType(n.typeName);
                
                if (t == null) continue;
                
                var instance = Activator.CreateInstance(t) as BaseNode;
                
                if (instance == null) continue;
                
                _graphView.AddElement(instance);
                instance.Initialize(n.guid,n.parameterJson,n.position);
            }

            var nodes = _graphView.nodes;
            
            foreach (var e in data.Edges)
            {
                var from = nodes.Select(x => x as BaseNode).FirstOrDefault(x => x?.Guid == e.from);
                var to = nodes.Select(x => x as BaseNode).FirstOrDefault(x => x?.Guid == e.to);

                if (from == null || to == null) continue;

                var input = to.inputContainer.Children().FirstOrDefault(x => x is Port) as Port;
                var output = from.outputContainer.Children().FirstOrDefault(x => x is Port) as Port;

                if (input == null || output == null) continue;
                
                var edge = new Edge() {input = input, output = output};
                edge.input.Connect(edge);
                edge.output.Connect(edge);
                _graphView.Add(edge);
            }
        }
    }
TestToolBarStyle.uss
.unity-toolbar-button{
    -unity-text-align: middle-center;
    margin: 0px 5px;
}
BaseNode.cs
  
public abstract class BaseNode : Node
{
    public string Guid { get; protected set; }
    protected BaseNode() : base() {}
    protected BaseNode(string uxmlPath) : base(uxmlPath){}

    public virtual void Initialize(string guid,string json,Rect pos)
    {
        Guid = guid;
        //JsonUtility.FromJson(json);
        SetPosition(pos);
    }

    public abstract string ToJson();
}
FloatNode.cs
    public class FloatNode : BaseNode
    {
        public FloatField Value { get; private set; }
    
        public FloatNode() : base()
        {
            Guid = System.Guid.NewGuid().ToString("N");
        
            UseDefaultStyling();
            base.title = "Float";

            var input 
                = base.InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi,typeof(float));

            input.portName = "X(1)";

            inputContainer.Add(input);
        
            var output 
                = base.InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi,typeof(float));

            output.portName = "Out(1)";
        
            outputContainer.Add(output);

            Value = new FloatField(){name = "Value",label = "Value"};
            extensionContainer.Add(Value);
        
            base.ToggleCollapse();
            base.ToggleCollapse();
        }

        public override string ToJson()
        {
            return JsonUtility.ToJson(Value);
        }

        public override void Initialize(string guid, string json, Rect pos)
        {
            Guid = guid;
            var field = JsonUtility.FromJson<FloatField>(json);
            Value.value = field.value;
            SetPosition(pos);
        }
    }

テスト動画(GIF)


一応、保存はできているが、不安が残る実装。
他にいい方法がないか検討。
変更箇所が多いので記載漏れがないか不安。

今後の予定

  • ブラックボード
  • ミニマップ
  • 他何か思いついたら

【Unity】結構前に作ったUnityでEasingを使う時のクラス【Easing】

はじめに

執筆日は20222/11/14になります。
Unityのバージョンは2021.3.1f1を使用しています。

自己紹介

活動名はHiruko、もしくはHirukoTimeで活動してます。
個人でSteamでゲームを販売したり、定期的にゲームジャムなどに参加をしています。

参考サイト

easings.net

実際のScript

Easing.cs
    public static class Easing
    {
        //定数
        private const float C1 = 1.70158f;
        private const float C2 = C1 * 1.525f;
        private const float C3 = C1 + 1;
        private const float C4 = (2 * Mathf.PI) / 3f;
        private const float C5 = (2 * Mathf.PI) / 4.5f;
        private const float N1 = 7.5625f;
        private const float D1 = 2.75f;
        
        //実際に使う関数
        public static float Value(float current, float min, float max , EasingPattern ease = EasingPattern.Linear)
        {
            if (current <= min) return 0;
            if (max <= current) return 1;
            
            var diff = max - min;
            var time = current - min;

            var x = time / diff;
            return ease switch
            {
                EasingPattern.Linear => Linear(x),
                
                EasingPattern.EaseInSin => EaseInSin(x),
                EasingPattern.EaseOutSin => EaseOutSin(x),
                EasingPattern.EaseInOutSin => EaseInOutSin(x),
                
                EasingPattern.EaseInQuad => EaseInQuad(x),
                EasingPattern.EaseOutQuad => EaseOutQuad(x),
                EasingPattern.EaseInOutQuad => EaseInOutQuad(x),
                
                EasingPattern.EaseInCubic => EaseInCubic(x),
                EasingPattern.EaseOutCubic => EaseOutCubic(x),
                EasingPattern.EaseInOutCubic => EaseInOutCubic(x),
                
                EasingPattern.EaseInQuart => EaseInQuart(x),
                EasingPattern.EaseOutQuart => EaseOutQuart(x),
                EasingPattern.EaseInOutQuart => EaseInOutQuart(x),
                
                EasingPattern.EaseInQuint => EaseInQuint(x),
                EasingPattern.EaseOutQuint => EaseOutQuint(x),
                EasingPattern.EaseInOutQuint => EaseInOutQuint(x),
                
                EasingPattern.EaseInExpo => EaseInExpo(x),
                EasingPattern.EaseOutExpo => EaseOutExpo(x),
                EasingPattern.EaseInOutExpo => EaseInOutExpo(x),
                
                EasingPattern.EaseInCirc => EaseInCirc(x),
                EasingPattern.EaseOutCirc => EaseOutCirc(x),
                EasingPattern.EaseInOutCirc => EaseInOutCirc(x),
                
                EasingPattern.EaseInBack => EaseInBack(x),
                EasingPattern.EaseOutBack => EaseOutBack(x),
                EasingPattern.EaseInOutBack => EaseInOutBack(x),
                
                EasingPattern.EaseInBounce => EaseInBounce(x),
                EasingPattern.EaseOutBounce => EaseOutBounce(x),
                EasingPattern.EaseInOutBounce => EaseInOutBounce(x),
                
                EasingPattern.EaseInZeroOne => EaseInZeroOne(x),
                EasingPattern.EaseOutZeroOne => EaseOutZeroOne(x),
                EasingPattern.EaseInOutZeroOne => EaseInOutZeroOne(x),
                
                _ => throw new ArgumentOutOfRangeException(nameof(ease), ease, null)
            };
        }

        private static float Linear(float x) => x;
        
        //EaseSign系
        private static float EaseInSin(float x) 
            => 1 - Mathf.Cos((x * Mathf.PI) / 2);

        private static float EaseOutSin(float x)
            => Mathf.Sin((x * Mathf.PI) / 2);

        private static float EaseInOutSin(float x) 
            => -(Mathf.Cos(Mathf.PI * x) - 1) / 2;
        
        //EaseQuad系
        private static float EaseInQuad(float x)
            => x * x;

        private static float EaseOutQuad(float x) 
            => 1 - (1 - x) * (1 - x);
        
        private static float EaseInOutQuad(float x)
            => x < 0.5 ? 2 * x * x : 1 - Mathf.Pow(-2 * x + 2, 2) / 2;
        
        //EaseCubic系
        private static float EaseInCubic(float x) 
            => x * x * x;
        
        private static float EaseOutCubic(float x) 
            =>  1 - Mathf.Pow(1 - x, 3);
        
        private static float EaseInOutCubic(float x)
            => x < 0.5 ? 4 * x * x * x : 1 - Mathf.Pow(-2 * x + 2, 3) / 2;

        //EaseQuart系
        private static float EaseInQuart(float x) 
            => x * x * x * x;

        private static float EaseOutQuart(float x) 
            => 1 - Mathf.Pow(1 - x, 4);

        private static float EaseInOutQuart(float x) 
            => x < 0.5 ? 8 * x * x * x * x : 1 - Mathf.Pow(-2 * x + 2, 4) / 2;
        
        //EaseQuint系
        private static float EaseInQuint(float x) 
            => x * x * x * x * x;

        private static float EaseOutQuint(float x) 
            => 1 - Mathf.Pow(1 - x, 5);

        private static float EaseInOutQuint(float x) 
            => x < 0.5 ? 16 * x * x * x * x * x : 1 - Mathf.Pow(-2 * x + 2, 5) / 2;
        
        //EaseExpo系
        private static float EaseInExpo(float x) 
            => x == 0 ? 0 : Mathf.Pow(2, 10 * x - 10);
        
        private static float EaseOutExpo(float x) 
            => Mathf.Approximately(x,1) ? 1 : 1 - Mathf.Pow(2, -10 * x);
        
        private static float EaseInOutExpo(float x) 
            => x == 0 ? 0 : Mathf.Approximately(x,1) ? 1 
                : x < 0.5 ? Mathf.Pow(2, 20 * x - 10) / 2 : (2 - Mathf.Pow(2, -20 * x + 10)) / 2;

        //EaseCirc系
        private static float EaseInCirc(float x) 
            => 1 - Mathf.Sqrt(1 - Mathf.Pow(x, 2));
        
        private static float EaseOutCirc(float x) 
            => Mathf.Sqrt(1 - Mathf.Pow(x - 1, 2));

        private static float EaseInOutCirc(float x)
        {
            return x < 0.5 ?
                (1 - Mathf.Sqrt(1 - Mathf.Pow(2 * x, 2))) / 2 :
                (Mathf.Sqrt(1 - Mathf.Pow(-2 * x + 2, 2)) + 1) / 2;
        }

        //EaseBack系
        private static float EaseInBack(float x)
            => C3 * x * x * x - C1 * x * x;
        
        private static float EaseOutBack(float x) 
            => 1 + C3 * Mathf.Pow(x - 1, 3) + C1 * Mathf.Pow(x - 1, 2);

        private static float EaseInOutBack(float x)
        {
            return x < 0.5 ?
                (Mathf.Pow(2 * x, 2) * ((C2 + 1) * 2 * x - C2)) / 2 :
                (Mathf.Pow(2 * x - 2, 2) * ((C2 + 1) * (x * 2 - 2) + C2) + 2) / 2;
        }
        
        // //EaseElastic系
        private static float EaseInElastic(float x)
            => x == 0 ? 0 : Mathf.Approximately(x,1) ? 1
                     : -Mathf.Pow(2, 10 * x - 10) * Mathf.Sin((x * 10f - 10.75f) * C4);
        
        private static float EaseOutElastic(float x)
            => x == 0 ? 0 : Mathf.Approximately(x,1) ? 1
                     : Mathf.Pow(2, -10 * x) * Mathf.Sin((x * 10f - 0.75f) * C4) + 1;

        private static float EaseInOutElastic(float x)
        {
            return x == 0 ? 0 : Mathf.Approximately(x,1) ? 1 : x < 0.5 ? 
                -(Mathf.Pow(2f, 20f * x - 10f) * Mathf.Sin((20f * x - 11.125f) * C5)) / 2f : 
                (Mathf.Pow(2f, -20f * x + 10f) * Mathf.Sin((20f * x - 11.125f) * C5)) / 2f + 1f;
        }
        
        //EaseBounce系
        private static float EaseInBounce(float x) 
            => 1 - EaseOutBounce(1 - x);
        
        private static float EaseOutBounce(float x)
        {
            return x switch
            {
                < 1 / D1    => N1 * x * x,
                < 2 / D1    => N1 * (x -= 1.5f / D1) * x + 0.75f,
                < 2.5f / D1 => N1 * (x -= 2.25f / D1) * x + 0.9375f,
                _           => N1 * (x -= 2.625f / D1) * x + 0.984375f
            };
        }

        private static float EaseInOutBounce(float x)
            => x < 0.5f ? (1 - EaseOutBounce(1 - 2 * x)) / 2f : (1 + EaseOutBounce(2 * x - 1)) / 2f;

        private static float EaseInZeroOne(float x) => 1;

        private static float EaseOutZeroOne(float x) => x < 1 ? 0 : 1;

        private static float EaseInOutZeroOne(float x) => x < 0.5f ? 0 : 1;
        
        // //Template Ease系
        // public static float EaseIn(this float x) 
        //     => ;
        //
        // public static float EaseOut(this float x) 
        //     => ;
        //
        // public static float EaseInOut(this float x) 
        //     => ;
    }
    public static class EaseExtension
    {
        public static void Ease(ref this float value ,float current, float min, float max, float pow = 1
            , EasingPattern ease = EasingPattern.Linear)
        {
            value = Easing.Value(current, min, max, ease) * pow;
        }

        public static void Ease(ref this Vector3 value, float current, float min, float max , float pow = 1 
            , EasingPattern ease = EasingPattern.Linear)
        {
            value = Vector3.one * (Easing.Value(current, min, max, ease) * pow);
        }

        public static void EaseScale(this Transform value, float current, float min, float max, float pow = 1
            , EasingPattern ease = EasingPattern.Linear)
        {
            var scale = value.localScale;
            scale.Ease(current, min, max, pow, ease);

            value.localScale = scale;
        }
        
        public static void EasePosition(this Transform value, float current, float min, float max, float pow = 1
            , EasingPattern ease = EasingPattern.Linear)
        {
            var pos = value.position;
            pos.Ease(current, min, max, pow, ease);

            value.position = pos;
        }
    }
    [Serializable]
    public enum EasingPattern
    {
        Linear,
        
        EaseInSin,
        EaseOutSin,
        EaseInOutSin,
        
        EaseInQuad,
        EaseOutQuad,
        EaseInOutQuad,
        
        EaseInCubic,
        EaseOutCubic,
        EaseInOutCubic,
        
        EaseInQuart,
        EaseOutQuart,
        EaseInOutQuart,
        
        EaseInQuint,
        EaseOutQuint,
        EaseInOutQuint,
        
        EaseInExpo,
        EaseOutExpo,
        EaseInOutExpo,
        
        EaseInCirc,
        EaseOutCirc,
        EaseInOutCirc,
        
        EaseInBack,
        EaseOutBack,
        EaseInOutBack,
        
        EaseInBounce,
        EaseOutBounce,
        EaseInOutBounce,        
        
        EaseInElastic,
        EaseOutElastic,
        EaseInOutElastic,
        
        EaseInZeroOne,
        EaseOutZeroOne,
        EaseInOutZeroOne,
    }

【Unity】GraphViewで遊んでみた。【第二回】

はじめに

この記事は第二回になります。Windowの作成方法などは第一回をご覧ください。
執筆日は20222/11/09になります。
Unityのバージョンは2021.3.1f1を使用しています。

hirukotime.hatenablog.jp

自己紹介

活動名はHiruko、もしくはHirukoTimeで活動してます。
個人でSteamでゲームを販売したり、定期的にゲームジャムなどに参加をしています。

Nodeの作成

ベースとなる抽象クラスの作成

BaseNode.cs
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
  
public abstract class BaseNode : Node
{
    //DefaultのUXMLでいい場合はこちらを使う
    protected BaseNode() : base() {}
    //OriginalのUXMLにしたい場合はこちらを使う
    protected BaseNode(string path) : base(path){}
}

適当にShaderGraphにあるNodeを参考に作る

FloatNode.cs
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;

public class FloatNode : BaseNode
{
    public FloatField Value { get; private set; }
    
    public FloatNode() : base()
    {
        base.title = "Float";
        
        
        //Orientation:ポートからでる線(Edge)の向き。Vertical、Horizontal
        //Direction:ポートの入出力の向き。Input、Output
        //Capacity:ポートに繋げられるEdgeの数。Multiは制限なし、Singleは1つだけ
        //Type:ポートのType
        //base.InstantiatePortは
        //Port.Create<Edge>(orientation, direction, capacity, type)を使っても同じ
        //内部で同じことしてる。
        var input 
            = base.InstantiatePort(
                Orientation.Horizontal, 
                Direction.Input, 
                Port.Capacity.Multi,
                typeof(float));

        //Portの横に出るラベルの設定。
        //Port.titleではなくPort.portNameを使う。前者はエラーが出るので注意
        input.portName = "X(1)";

        //Direction.InputのPortはinputContainer、
        //Direction.OutputのPortはoutputContainerに入れる。
        inputContainer.Add(input);
        
        var output 
            = base.InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi,typeof(float));

        output.portName = "Out(1)";
        
        outputContainer.Add(output);

        //一応、Floatの入力フィールドを作って置く。
        //extensionContainerはパラメーターなどのVisualElementを配置するのに使うっぽい。
        Value = new FloatField(){name = "Value",label = "Value"};
        extensionContainer.Add(Value);

        //extensionContainerはデフォルトの仕様だと閉じた状態でノードが生成される。
        //開いた状態にしたい場合は↓を2回呼ぶ。1回だとPortのContainerも閉じた状態になる。
        //もっと綺麗な方法がありそうではあるが見つかりませんでした。
        //base.ToggleCollapse();
        //base.ToggleCollapse();
    }
}

ShaderGraphのノード 今作ったノード 比べると見た目だけでも色々違いますが今はこれで保留。

Nodeの状態とContainerの位置の情報

  1. Defaultの状態
  2. ToggleCollapse()が1回の状態(ノード上の【V】みたいなボタンを1回押した状態)
  3. ToggleCollapse()が2回の状態(ノード上の【V】みたいなボタンを2回押した状態)

Defaultの状態は一度開閉するとその状態にならない。

Port同士を繋げられるようにする。

現状の状態だとPort同士を繋げられないので、それを繋げられるようにする。

TestGraphView.cs
//省略
using System.Collections.Generic;//追加
using System.Linq;//追加

public class TestGraphView : GraphView
{
    public TestGraphView(){}//前回の最後に表示したNode追加部分は消してある。
    
    //ドラッグしてるPortと繋げられるPortを探す処理。どこからか呼ばれてる。
    public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
    {
        var compatibleParts = new List<Port>();
        compatibleParts.AddRange( ports.ToList().Where(port => 
                startPort.node != port.node && 
                port.direction != startPort.direction && 
                port.portType == startPort.portType));
    
        return compatibleParts;
    }
}

GetCompatiblePortsがどかからか呼ばれて、Portをドラッグすると繋げられるようになります。

ノードを作成するメニューを表示させる。

メニュー作成するScripatableObjectを作成する。

SearchMenu.cs
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

public class SearchMenu : ScriptableObject , ISearchWindowProvider
{
    private GraphView _graphView;
    private EditorWindow _window;

    private readonly List<SearchTreeEntry> _entries = new List<SearchTreeEntry>();
    
    public void Initialize(GraphView graphView, EditorWindow editorWindow, params Type[] notInserts)
    {
        _graphView = graphView;
        _window = editorWindow;
        
        _entries.Add(new SearchTreeGroupEntry(new GUIContent("Create Node")));
        _entries.Add(new SearchTreeGroupEntry(new GUIContent("Input")){level = 1});
        _entries.Add(new SearchTreeEntry(new GUIContent(nameof(FloatNode)))
             { level = 2, userData = typeof(FloatNode) });
    }    
    
    public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context) => _entries;

    public bool OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
    {
        var type = searchTreeEntry.userData as Type;
        if (type == null || !(Activator.CreateInstance(type) is BaseNode node)) return false;

        var worldMousePos 
            = _window.rootVisualElement.ChangeCoordinatesTo(
                    _window.rootVisualElement.parent,
                    context.screenMousePosition - _window.position.position);

        var localMousePos = _graphView.contentViewContainer.WorldToLocal(worldMousePos);
        node.SetPosition(new Rect(localMousePos, new Vector2(100, 100)));

        _graphView.AddElement(node);
        return true;
    }
}

ScriptableObjectである必要性に疑問を感じたが、呼び出しにScriptableObjectであることが必要っぽい。

TestGraphView.cs
//省略
using UnityEditor;//追加

public class TestGraphView : GraphView
{
    public TestGraphView(){}//省略
    
    public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter){}//省略

    //追加
    public void InitializeMenuWindow(EditorWindow window)
    {
        var menuWindow = ScriptableObject.CreateInstance<SearchMenu>();
        menuWindow.Initialize(this, window );
        
        nodeCreationRequest += context =>
        {
            SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), menuWindow);
        };
    }
    
}
TestWindow.cs
//省略

public class TestWindow : GraphViewEditorWindow
{
    [MenuItem("Window/Test")]
    private static void ShowWindow(){}//省略

    private void OnEnable()
    {
        var graphView = new TestGraphView();
        graphView.InitializeMenuWindow(this);//追加
        rootVisualElement.Add(graphView);
    }
}


entries.Add(new SearchTreeGroupEntry(new GUIContent("Create Node")))
右クリックメニューの一番上にCreate Nodeが追加される

entries.Add(new SearchTreeGroupEntry(new GUIContent("Input")){level = 1});
右クリック>[Create Node]でこのメニューが出る。

_entries.Add(new SearchTreeEntry(new GUIContent(nameof(FloatNode)))
{ level = 2, userData = typeof(FloatNode) });
右クリック>[Create Node]>[Input]でこのメニュー。
FloatNodeをクリックすると、FloatNodeを生成できる。 各項目のlevelは適切に設定しないとその項目が行方不明になるので注意。

Nodeの形状や配置を変える。

元になるNodeのConstructorで以下のUXML要素が呼ばれているので、独自のUXMLを適用する場合は最低限必要になる。
例:"UXML内Name"[変数名]
"node-border" [mainContainer] public get ; private set;
"selection-border" [変数名無し] "input" [inputContainer] public get ; private set;
"output" [outputContainer] public get ; private set;
"collapsible-area" [m_CollapsibleArea] private
"extension" [exntensionContainer] public get ; private set;
"title" [titleContainer] public get ; private set;
"title-label" [m_TitleLabel] private
"title-button-container" [titleButtonContainer]public get ; private set;
"collapse-button" [m_CollapseButton]private

MyNode.uxml(デフォルトの構成)
<?xml version="1.0" encoding="utf-8"?>
<engine:UXML xmlns:engine="UnityEngine.UIElements" 
             xmlns:ui="UnityEngine.UIElements" 
             xmlns:editor="UnityEditor.UIElements" >
    <ui:VisualElement name="node-border">
        <ui:VisualElement name="title" >
            <ui:Label name="title-label" />
            <ui:VisualElement name="title-button-container">
                <ui:VisualElement name="collapse-button">
                    <ui:VisualElement name="icon" />
                </ui:VisualElement>
            </ui:VisualElement>
        </ui:VisualElement>
        <ui:VisualElement name="contents">
            <ui:VisualElement name="divider" class="horizontal" />
            <ui:VisualElement name="top">                
                <ui:VisualElement name="input" />
                <ui:VisualElement name="divider" class="vertical" />
                <ui:VisualElement name="output" />
            </ui:VisualElement>            
            <ui:VisualElement name="collapsible-area">
                <ui:VisualElement name="divider" class="horizontal" />
                <ui:VisualElement name="extension"/>
            </ui:VisualElement>
        </ui:VisualElement>
    </ui:VisualElement>
    <ui:VisualElement name="selection-border" picking-mode="Ignore"/>
</engine:UXML>
MyUniqueNode.cs
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;

public class MyUniqueNode : BaseNode
{
    public MyUniqueNode() : base("Assets/GraphViewTest/Resources/MyNode.uxml")
    {
        //Defaultのスタイルを設定する
        //"selection-border"の色が変わるStyleとなどが含まれるため、
        //適用しないと選択時に色が変わらなくなる。
        UseDefaultStyling();
        
        //なんか独自の要素とか処理
    }
}

少し形状に手を加えた例。

ここまでの成果


ScriptのリロードやEditorのPlayをするGraphがなくなる(保存されてない)
右クリック>[Delete]やDeleteキーから削除はできるが、Paste、Duplicateはできない。

第三回の予定

Graphの保存

【Unity】GraphViewで遊んでみた。【第一回】

はじめに

執筆日は20222/11/09になります。
Unityのバージョンは2021.3.1f1を使用しています。

自己紹介

活動名はHiruko、もしくはHirukoTimeで活動してます。
個人でSteamでゲームを販売したり、定期的にゲームジャムなどに参加をしています。

GraphViewについて

ShaderGraphみたいな機能を自作できるAPIです。
Runtimeな使用するAPIではなくEditor拡張のAPIです。
あくまで、データなどを作成する視覚的なツールを作るためのものになります。
現在はExperimentalなので、破壊的な変更が来る可能性はありますが、内部的にShaderGraphやVFXGraphなどで使用されているみたいなのでなくなることはないと思います。

GraphViewの要素について

主な要素

  • GraphView
  • Node
  • Port
  • Edge

補助、拡張的な要素

  • Blackboard
  • MiniMap
  • GraphViewToolWindowなど

知っておいた方がいい要素

  • USS
  • UXML
  • UIElements
  • Editor拡張系のAPIやワード

実際にいじってみる

まずはWindowの作成

TestWindow.cs
using UnityEditor;
using UnityEditor.Experimental.GraphView;

public class TestWindow : GraphViewEditorWindow
{
    [MenuItem("Window/Test")]
    private static void ShowWindow()
    {
        var window = GetWindow<TestWindow>("TestWindow");
        window.Show();
    }

    private void OnEnable()
    {
        rootVisualElement.Add(new BaseGraphView());
    }
}

今回はEditorWindowを直接継承せず、試したいこともあったのでGraphViewEditorWindowを継承します。GraphViewEditorWindowがEditorWindowを継承しています。

TestGraphView.cs
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

public class TestGraphView : GraphView
{
    public TestGraphView()
    {
        //形状や内容の設定
        SetupZoom(ContentZoomer.DefaultMinScale,ContentZoomer.DefaultMaxScale);
        this.StretchToParentSize();
        var backGround = new GridBackground();
        backGround.styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
        Insert(0,backGround);
            
        //操作を受け付けるようにする為
        this.AddManipulator(new ContentDragger());
        this.AddManipulator(new SelectionDragger());
        this.AddManipulator(new RectangleSelector());
    }
}

SetupZoom()はマウスホイールでのズームの設定 StretchToParentSize()はそのままの意味で親のサイズに合わせてくれる。 GridBackGroundはParameterの設定が面倒なためUSSを使用した方が楽。またGridBackGroundは内部でStretchToParentSize()を呼び出しているので、使用する時にしなくても大丈夫。

BackGround.uss
GridBackground {
    --grid-background-color:rgb(90,90,90);
    --line-color:rgba(80,80,80,255);
    --thick-line-color:rgba(40,40,40,153);
    --spacing:10;
    /*--thick-lines: 0 このパラメータは存在するが変更しても変化がない。*/    
}

ちょっと躓いた点としては、UI Toolkit Debugger上での表示はrgba(float,float,float,float)のfloat[0-1]になっているが、USSで指定する場合はParameterをrgba(int,int,int,int)でint[0-255]で指定する。

ここまでの段階で上部のメニューから[Window>Test]でウィンドウを表示すると↑の感じになる。 この段階で、ドラッグででグリッドを動かしたり、ホイールでズームや右クリックのメニューがでるようになります。

次はNodeを表示してみる。

TestGraphView.cs
//省略

public class TestGraphView : GraphView
{
    public TestGraphView()
    {
        //省略
          
        AddElement(new Node());
    }
}

とりあえず表示するだけならこれで表示される。
注意点としては、GraphViewにはGraphView.AddとGraphView.AddElementがあるが、NodeとかのGraph要素はAddElement。GridBackgroundの様な要素はAddと使い分ける必要がある。

何も指定せず生成すると左上にNodeがでる。
この状態でドラッグで動かしたり、右クリックのメニューからDeleteしたり、コピーしたりできる。
試しに表示しただけなので、AddElement(new Node());の部分は削除する。

第二回の予定

NodeにPortやParameterを配置する。
動的なNodeの作成。

【Unity】A* Pathfinding Project Pro 試してみた

・GridGraphのグリッド間の障害物で接続のブロック


グリッド配置のSRPG系のゲームで障害物をグリッド上ではなく、グリッドとグリッドの間に障害物を配置して、その間の接続をブロックする方法。


基本的にアセットの仕様として、移動阻害をする場合はグリッド自体を無効にする処理になるので、障害物としてオブジェクトを配置をする場合は、このようにグリッド(Node)自体が無効になる。

※赤い四角は無効になってるNodeの表示。緑のラインはNode-Nodeの接続


こんな感じでNodeは無効にせずに、接続だけをなくしたい時の方法。

公式のドキュメントにはやり方が見つからなかったのですが、フォーラムの方を探していたら数年前のスレッドだが、参考になるスレッドがありました。
スレッド自体がかなり前なので、修正すべきスクリプトの位置が大分違ったので苦労しました。

GridGenerator.cs/Class::GridGraph

public virtual bool IsValidConnection (GridNodeBase node1, GridNodeBase node2) 
{
    if (!node1.Walkable || !node2.Walkable)
    {
        return false;
    }

   
    //追加部分↓
    if (Physics.Linecast((Vector3) node1.position, (Vector3) node2.position))
    {
        return false;
    }
    //追加部分↑
    
    if (maxClimb <= 0 || collision.use2D) return true;
 
 //変更がないので省略
}

Graph生成時?Scan時?にNode間にLinecastし、障害物がある場合はConnectionを生成しないようにしている。実際に使用する場合はLinecastの第三引数にLayermaskを設定した方がいい。
GridGraphクラスは専用のEditorScriptが存在するのでLayermask変数を用意してInspectorから設定したい場合は
Assets/AstarPathfindingProject/Editor/GraphEditors/GridGeneratorEditor.cs
を編集する必要があるので少し手間がかかる。