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