Skip to content

Commit

Permalink
Add statemachine editor
Browse files Browse the repository at this point in the history
Use UniTask to update state.
Fix AudioFileAssist index.
Add an editor window to edit statemachine. (may change to graph editor if more features are added)
  • Loading branch information
AkiKurisu committed Apr 12, 2024
1 parent 293be50 commit 3945b7c
Show file tree
Hide file tree
Showing 40 changed files with 1,298 additions and 123 deletions.
3 changes: 2 additions & 1 deletion Editor/Kurisu.UniChat.Editor.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "Kurisu.UniChat.Editor",
"rootNamespace": "Kurisu.UniChat.Editor",
"references": [
"GUID:b2879b934d4e9c94bab967ace5085611"
"GUID:b2879b934d4e9c94bab967ace5085611",
"GUID:f51ebe6a0ceec4240a699833d6309b23"
],
"includePlatforms": [
"Editor"
Expand Down
8 changes: 8 additions & 0 deletions Editor/StateMachine.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

106 changes: 106 additions & 0 deletions Editor/StateMachine/BehaviorNodeDrawer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
namespace Kurisu.UniChat.StateMachine.Editor
{
[CustomPropertyDrawer(typeof(ChatStateMachineGraph.BehaviorNode))]
public class BehaviorNodeDrawer : PropertyDrawer
{
private const string NullType = "Null";
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight
+ GenericBehaviorWrapperDrawer.CalculatePropertyHeight(property.FindPropertyRelative("container"))
+ EditorGUIUtility.standardVerticalSpacing;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
var totalHeight = position.height;
position.height = EditorGUIUtility.singleLineHeight;
var reference = property.FindPropertyRelative("serializedType");
var container = property.FindPropertyRelative("container");
var type = SerializedType.FromString(reference.stringValue);
string id = type != null ? type.Name : NullType;
if (type != null && container.objectReferenceValue == null)
{
container.objectReferenceValue = SerializedBehaviorUtils.Wrap(Activator.CreateInstance(type));
property.serializedObject.ApplyModifiedProperties();
}
if (EditorGUI.DropdownButton(position, new GUIContent(id), FocusType.Keyboard))
{
var provider = ScriptableObject.CreateInstance<StateMachineBehaviorSearchWindow>();
provider.Initialize((selectType) =>
{
reference.stringValue = selectType != null ? SerializedType.ToString(selectType) : NullType;
if (selectType != null)
{
var wrapper = SerializedBehaviorUtils.Wrap(Activator.CreateInstance(selectType));
container.objectReferenceValue = wrapper;
}
else
{
container.objectReferenceValue = null;
}
property.serializedObject.ApplyModifiedProperties();
});
SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(Event.current.mousePosition)), provider);
}
position.y += position.height + EditorGUIUtility.standardVerticalSpacing;
position.height = totalHeight - position.height - EditorGUIUtility.standardVerticalSpacing;
EditorGUI.PropertyField(position, container, true);
EditorGUI.EndProperty();
}
}
public class StateMachineBehaviorSearchWindow : ScriptableObject, ISearchWindowProvider
{
private Texture2D _indentationIcon;
private Action<Type> typeSelectCallBack;
public void Initialize(Action<Type> typeSelectCallBack)
{
this.typeSelectCallBack = typeSelectCallBack;
_indentationIcon = new Texture2D(1, 1);
_indentationIcon.SetPixel(0, 0, new Color(0, 0, 0, 0));
_indentationIcon.Apply();
}
List<SearchTreeEntry> ISearchWindowProvider.CreateSearchTree(SearchWindowContext context)
{
var entries = new List<SearchTreeEntry>
{
new SearchTreeGroupEntry(new GUIContent("Select StateMachineBehavior"), 0),
new(new GUIContent("<Null>", _indentationIcon)) { level = 1, userData = null }
};
List<Type> nodeTypes = FindSubClasses(typeof(ChatStateMachineBehavior))
.Where(x => x != typeof(InvalidStateMachineBehavior))
.ToList();
var groups = nodeTypes.GroupBy(t => t.Assembly);
foreach (var group in groups)
{
entries.Add(new SearchTreeGroupEntry(new GUIContent($"Select {group.Key.GetName().Name}"), 1));
var subGroups = group.GroupBy(x => x.Namespace);
foreach (var subGroup in subGroups)
{
entries.Add(new SearchTreeGroupEntry(new GUIContent($"Select {subGroup.Key}"), 2));
foreach (var type in subGroup)
{
entries.Add(new SearchTreeEntry(new GUIContent(type.Name, _indentationIcon)) { level = 3, userData = type });
}
}
}
return entries;
}
bool ISearchWindowProvider.OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
{
var type = searchTreeEntry.userData as Type;
typeSelectCallBack?.Invoke(type);
return true;
}
private static IEnumerable<Type> FindSubClasses(Type father)
{
return AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Where(t => t.IsSubclassOf(father) && !t.IsAbstract);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions Editor/StateMachine/ChatStateMachineEditorWindow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace Kurisu.UniChat.StateMachine.Editor
{
public class ChatStateMachineEditorWindow : EditorWindow
{
private SerializedObject targetObject;
private ChatStateMachineGraphEditorCtrl graphCtrl;
public delegate Vector2 BeginVerticalScrollViewFunc(Vector2 scrollPosition, bool alwaysShowVertical, GUIStyle verticalScrollbar, GUIStyle background, params GUILayoutOption[] options);
private static BeginVerticalScrollViewFunc s_func;
private Vector2 m_ScrollPosition;
private string path;
public string fileName = "NewChatFSM";
private static BeginVerticalScrollViewFunc BeginVerticalScrollView
{
get
{
if (s_func == null)
{
var methods = typeof(EditorGUILayout).GetMethods(BindingFlags.Static | BindingFlags.NonPublic).Where(x => x.Name == "BeginVerticalScrollView").ToArray();
var method = methods.First(x => x.GetParameters()[1].ParameterType == typeof(bool));
s_func = (BeginVerticalScrollViewFunc)method.CreateDelegate(typeof(BeginVerticalScrollViewFunc));
}
return s_func;
}
}
[MenuItem("Tools/UniChat/Chat StateMachine Editor")]
private static void ShowEditorWindow()
{
GetWindow<ChatStateMachineEditorWindow>("Chat StateMachine Editor");
}
private void OnEnable()
{
graphCtrl = CreateInstance<ChatStateMachineGraphEditorCtrl>();
targetObject = new(graphCtrl);
}
private void OnGUI()
{
fileName = EditorGUILayout.TextField(new GUIContent("File Name"), fileName);
m_ScrollPosition = BeginVerticalScrollView(m_ScrollPosition, false, GUI.skin.verticalScrollbar, "OL Box");
DrawModel();
EditorGUILayout.EndScrollView();
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Load"))
{
path = EditorUtility.OpenFilePanel("Select fsm bytes file", PathUtil.UserDataPath, "bytes");
if (string.IsNullOrEmpty(path)) return;
fileName = Path.GetFileNameWithoutExtension(path);
graphCtrl.Load(path);
targetObject.Update();
}
if (GUILayout.Button("New"))
{
graphCtrl.Reset();
targetObject.Update();
}
if (GUILayout.Button("Save"))
{
path = EditorUtility.OpenFolderPanel("Select folder", PathUtil.UserDataPath, "");
if (string.IsNullOrEmpty(path)) return;
graphCtrl.Save(Path.Combine(path, $"{fileName}.bytes"));
}
EditorGUILayout.EndHorizontal();
}
private void DrawModel()
{
EditorGUI.BeginChangeCheck();
SerializedProperty prop = targetObject.GetIterator();
prop.NextVisible(true);
while (prop.NextVisible(false))
{
if (prop.name == "graph") continue;
EditorGUILayout.PropertyField(prop, true);
}
if (EditorGUI.EndChangeCheck())
{
targetObject.ApplyModifiedProperties();
}
}
}
}
11 changes: 11 additions & 0 deletions Editor/StateMachine/ChatStateMachineEditorWindow.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions Editor/StateMachine/ChatStateMachineGraphEditorCtrl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using UnityEngine;
namespace Kurisu.UniChat.StateMachine.Editor
{
public class ChatStateMachineGraphEditorCtrl : ScriptableObject
{
public ChatStateMachineGraph.Layer[] layers;
private ChatStateMachineGraph graph = new();
public void Save(string path)
{
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write);
using var bw = new BinaryWriter(stream);
graph.layers = layers;
graph.layers.SelectMany(x => x.states).SelectMany(x => x.behaviors)
.ForEach(x =>
{
x.jsonData = JsonConvert.SerializeObject(x.container.Value);
});
bw.Write(JsonConvert.SerializeObject(graph));
}
public void Load(string path)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
using var br = new BinaryReader(stream);
string json = br.ReadString();
graph = JsonConvert.DeserializeObject<ChatStateMachineGraph>(json);
graph.layers.SelectMany(x => x.states).SelectMany(x => x.behaviors)
.ForEach(x =>
{
x.container = SerializedBehaviorUtils.Wrap(x.Deserialize());
});
layers = graph.layers;
}

public void Reset()
{
layers = new ChatStateMachineGraph.Layer[1] { new(){
states = new ChatStateMachineGraph.StateNode[1]{new()
{
name="Start"
}}
}};
}
}
}
11 changes: 11 additions & 0 deletions Editor/StateMachine/ChatStateMachineGraphEditorCtrl.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions Editor/StateMachine/DynamicTypeBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace Kurisu.UniChat.StateMachine.Editor
{
public static class DynamicTypeBuilder
{
private const string kDynamicTypeBuilderAssemblyName = "Kurisu.UniChat.Emit";
private static ModuleBuilder m_ModuleBuilder;

private static ModuleBuilder CreateModuleBuilder()
{
if (m_ModuleBuilder != null)
return m_ModuleBuilder;

var appDomain = AppDomain.CurrentDomain;
var assemblyName = new AssemblyName(kDynamicTypeBuilderAssemblyName);
var assemblyBuilder = appDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
m_ModuleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name);
return m_ModuleBuilder;
}

public static Type MakeDerivedType(Type baseClass, Type parameterType)
{
ModuleBuilder moduleBuilder = CreateModuleBuilder();
string tAssemblyName = parameterType.Assembly.GetName().Name;
string typeName = $"{baseClass.Namespace}_{baseClass.Name}";

string currentAssemblyName = typeof(DynamicTypeBuilder).Assembly.GetName().Name.Replace('.', '_');

if (baseClass.IsGenericType)
{
//Get rid of the '`N' after the class name for the # of generic args
//TODO: If there are >= 10 args (highly unlikely) this will break (:
typeName = typeName[..^2];
typeName += $"_{string.Join("_", baseClass.GetGenericArguments().Select(t => t.FullName))}";
}

string typeNameWithoutAssembly = typeName.Replace('.', '_');

typeName = $"{tAssemblyName}_{typeName}";
typeName = typeName.Replace('.', '_');
Type[] moduleBuilderTypes = moduleBuilder.GetTypes();


var existingType = moduleBuilderTypes.SingleOrDefault(t => t.Name.EndsWith(typeNameWithoutAssembly) && !t.Name.StartsWith($"{currentAssemblyName}"));
if (existingType != null)
{
return existingType;
}


existingType = moduleBuilderTypes.SingleOrDefault(t => t.Name == typeName);
if (existingType != null)
{
return existingType;
}
var baseConstructor = baseClass.GetConstructor(BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance, null, new Type[0], null);
var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Public, baseClass);
var constructor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, null);
var ilGenerator = constructor.GetILGenerator();

if (baseConstructor != null)
{
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Call, baseConstructor);
}

ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Ret);
return typeBuilder.CreateType();
}
}
}
11 changes: 11 additions & 0 deletions Editor/StateMachine/DynamicTypeBuilder.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3945b7c

Please sign in to comment.