在做自己的 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