Unity3d, EditorGUI, custom emoji selection

在做自己的 Dialogue 系統的時候的點子. 表情的狀態還是與模型本身的動畫分開設定才更有開發彈性.
然後就去做 Emotional 的 research, 發覺其實表情還真的滿多的.
看著 Emotion Wheel 就覺得想直接用滑鼠點一下就決定好的話就更方便了.

表情分區是參考自: http://www.6seconds.org/2017/04/27/plutchiks-model-of-emotions/

具體的表情圖:

EmotionalData.cs

using UnityEngine;

namespace Kit.UI.Dialogue
{
	/// <summary>Emotional enum selector</summary>
	/// <see cref="http://www.6seconds.org/2017/04/27/plutchiks-model-of-emotions/"/>
	[System.Serializable]
	public struct EmotionalData
	{
		[SerializeField] float x, y;

		private const float ANGLE_OFFSET = -112.5f;
		public static readonly Vector2 BIAS_DIRECTION_ANCHOR = new Vector2(Mathf.Cos(ANGLE_OFFSET * Mathf.Deg2Rad), Mathf.Sin(ANGLE_OFFSET * Mathf.Deg2Rad)).normalized;
		
		public eEmotional GetEmotion()
		{
			Vector2 vector = new Vector2(x, y);
			float distance = vector.magnitude;

			if (distance < float.Epsilon)
				return eEmotional.None;

			int level = (distance >= 0f && distance < 0.5f) ? 0 :
				(distance >= 0.5f && distance < 0.9f) ? 10 :
				20;

			Vector2 lhs = vector.normalized;
			Vector2 rhs = BIAS_DIRECTION_ANCHOR;
			var sin = rhs.x * lhs.y - lhs.x * rhs.y;
			var cos = lhs.x * rhs.x + lhs.y * rhs.y;
			float degree = Mathf.Atan2(sin, cos) * Mathf.Rad2Deg;
			// Debug.Log("Vector = "+ vector.ToString("F2") + ", Degree = "+ degree + ", distance = "+ distance + ", level ="+ level);
			if (degree < 0)
				degree += 360f;
			

			int sector = 1;
			const float session = 45f;
			while (degree > session)
			{
				degree -= session;
				sector++;
			}

			int tmp = sector + level;
			eEmotional rst = (eEmotional)tmp;
			return rst;
		}

		public static string GetEmojiText(eEmotional emotional)
		{
			switch (emotional)
			{
				case eEmotional.None: return "・ิ_・ิ";

				case eEmotional.ecstasy: return "^▽^"; // 狂喜
				case eEmotional.joy: return "´∀`"; // 喜悅
				case eEmotional.serenity: return "'‿'"; // 寧靜

				case eEmotional.admiration: return "♥‿♥"; // 欽佩
				case eEmotional.trust: return "◠‿◕"; // 信任
				case eEmotional.acceptance: return "•ω•"; // 接受

				case eEmotional.terror: return "☉д⊙"; // 恐佈
				case eEmotional.fear: return "゚д゚"; //"ಠ▃ಠ"; // 恐懼
				case eEmotional.apprehension: return "ºΔº"; // 顧慮

				case eEmotional.amazement: return "⊙̃.o"; // 驚愕
				case eEmotional.surprise: return "๏_๏"; // 驚訝
				case eEmotional.distraction: return "˚–˚"; // 分神

				case eEmotional.grief: return "╥﹏╥"; // 哀痛
				case eEmotional.sadness: return "☍﹏⁰"; // 悲
				case eEmotional.pensiveness: return "′~‵"; // 優思

				case eEmotional.loathing: return "ಠ益ಠ"; // 非常討壓
				case eEmotional.disgust: return "ಠ╭╮ಠ"; // 厭惡
				case eEmotional.boredom: return "ㅍ_ㅍ"; // 無聊

				case eEmotional.rage: return "◣_◢"; // 憤怒
				case eEmotional.anger: return "⋋_⋌"; // 怒
				case eEmotional.annoyance: return "≖︿≖"; // 煩惱

				case eEmotional.vigilance: return "✪ω✪"; // 警覺
				case eEmotional.anticipation: return "◕‿◕"; // 預期
				case eEmotional.interest: return "◉‿◉"; // 興趣

				default: return "Err";
			}
		}

		public static explicit operator EmotionalData(Vector2 vector)
		{
			return new EmotionalData() { x = vector.x, y = vector.y };
		}

		public static implicit operator Vector2(EmotionalData emotion)
		{
			return new Vector2(emotion.x, emotion.y);
		}

		public static implicit operator eEmotional(EmotionalData data)
		{
			return data.GetEmotion();
		}
	}

	public enum eEmotional
	{
		None = 0,

		serenity = 1,   // 0 ~ 45
		acceptance,     // 45 ~ 90
		apprehension,   // 90 ~ 135
		distraction,    // 135 ~ 180
		pensiveness,    // 180 ~ 225
		boredom,        // 225 ~ 270
		annoyance,      // 270 ~ 315
		interest,       // 315 ~ 360

		joy = 11,
		trust,
		fear,
		surprise,
		sadness,
		disgust,
		anger,
		anticipation,

		ecstasy = 21,
		admiration,
		terror,
		amazement,
		grief,
		loathing,
		rage,
		vigilance,
	}

}

EmotionalDataDrawer.cs

using UnityEngine;
using UnityEditor;
using Kit.Extend;

namespace Kit.UI.Dialogue
{
	[CustomPropertyDrawer(typeof(EmotionalData))]
	public class EmotionalDataDrawer : PropertyDrawer
	{
		const float halfSize = 55f;
		const float controlHalfSize = 5f;
		const float radius = halfSize - controlHalfSize;
		const float sqrRadius = radius * radius;
		static readonly Vector2 halfRange = Vector2.one * halfSize;
		static readonly Color thumbColor = new Color(95f / 255f, 131f / 255f, 221f / 255f, .3f);
		static readonly Color faceColor = new Color(211f / 255f, 188f / 255f, 152f / 255f, 1f);
		static readonly GUIStyle emojiStyle = new GUIStyle(GUI.skin.label)
		{
			alignment = TextAnchor.MiddleCenter,
			richText = true,
			stretchWidth = true,
			stretchHeight = true,
			fontSize = 30,
		};

		private enum eState
		{
			Idle = 0,
			Drag,
			DragEnd,
		}
		private eState m_State = eState.Idle;

		public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
		{
			EditorGUI.BeginProperty(position, label, property);
			Rect line = new Rect(position.x, position.y + 5f, halfSize * 2f, halfSize * 2f);
			SerializedProperty xProp = property.FindPropertyRelative("x");
			SerializedProperty yProp = property.FindPropertyRelative("y");
			Event evt = Event.current;

			// identiry mouse event.
			if (evt.type == EventType.MouseDown && line.Contains(evt.mousePosition, false))
			{
				m_State = eState.Drag;
			}
			else if (evt.type == EventType.MouseUp && m_State == eState.Drag)
			{
				m_State = eState.DragEnd;
			}

			// data source location
			Vector2 inputCircle;
			if (m_State != eState.Idle)
			{
				inputCircle = new Vector2(
					Mathf.Clamp(evt.mousePosition.x - (line.x + halfSize), -halfSize, halfSize),
					Mathf.Clamp(evt.mousePosition.y - (line.y + halfSize), -halfSize, halfSize)
					);
				if (inputCircle.sqrMagnitude > sqrRadius)
					inputCircle = inputCircle.normalized * (halfSize - controlHalfSize);
			}
			else
			{
				inputCircle = new Vector2(xProp.floatValue, yProp.floatValue).ConvertSquareToCircle().Scale(-1f, 1f, -radius, radius);
			}
			Vector2 square01 = inputCircle.Scale(-radius, radius, -1f, 1f).ConvertCircleToSquare();
			EmotionalData emoji = (EmotionalData)square01;
			eEmotional emojiID = emoji.GetEmotion();

			// UI
			// Emoji face
			GUI.BeginClip(line);
			Handles.color = faceColor;
			Handles.DrawSolidDisc(halfRange, Vector3.forward, halfSize);
			GUI.EndClip();

			GUI.Label(line, EmotionalData.GetEmojiText(emojiID), emojiStyle);
			
			// UI Handle
			GUI.BeginClip(line);
			Handles.BeginGUI();
			Handles.color = Color.black;
			Handles.DrawWireDisc(halfRange, Vector3.forward, halfSize);
			Handles.color = thumbColor;
			Handles.DrawSolidDisc(halfRange + inputCircle, Vector3.forward, controlHalfSize);
			Handles.EndGUI();
			GUI.EndClip();

			// Vector2 field
			line.y += line.height;
			line.height = 20f;
			EditorGUI.LabelField(line, emojiID.ToString(), EditorStyles.helpBox);

			line.y += line.height;
			EditorGUI.BeginChangeCheck();
			Vector2 tmp = EditorGUI.Vector2Field(line, GUIContent.none, square01);
			if (EditorGUI.EndChangeCheck())
			{
				tmp.x = Mathf.Clamp(tmp.x, -1f, 1f);
				tmp.y = Mathf.Clamp(tmp.y, -1f, 1f);
				tmp = tmp.ConvertSquareToCircle();
				inputCircle = tmp.Scale(-1f, 1f, -radius, radius);
				if (inputCircle.sqrMagnitude > sqrRadius)
					inputCircle = inputCircle.normalized * radius;
				square01 = inputCircle.Scale(-radius, radius, -1f, 1f).ConvertCircleToSquare();
				m_State = eState.DragEnd;
			}

			// State & apply change
			if (m_State == eState.DragEnd)
			{
				m_State = eState.Idle;
				xProp.floatValue = square01.x;
				yProp.floatValue = square01.y;
				property.serializedObject.ApplyModifiedProperties();
			}
			//else if (m_State == eState.Drag || !(evt.type == EventType.Repaint || evt.type == EventType.Layout))
			//{
			//	// lower update rate.
			//	EditorUtility.SetDirty(property.serializedObject.targetObject);
			//}
			EditorGUI.EndProperty();

			EditorUtility.SetDirty(property.serializedObject.targetObject);
		}

		public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
		{
			return 160f;
		}
	}
}

Study result:

  • 直接用 Event.current 操作介面, 棄用傳統 EditorGUI 等繪畫方式.
  • Vector2 的角度向量, 由正方到圓形的轉換
  • Handle 配上 GUI.BeginClip / GUI.BeginGroup 也可以在 inspector 輕鬆使用.
    • GUI.BeginClip 在 inspector 上劃出繪畫區, 裡頭的 Rect 會被重置歸零.
  • 開始亂用 implicit operator…. XD

發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *

*