Skip to content

Commit

Permalink
Merge pull request #12 from AkiKurisu/dev_merge_dsl
Browse files Browse the repository at this point in the history
Merge AkiBT.DSL
  • Loading branch information
AkiKurisu authored Jul 30, 2024
2 parents 326d6c0 + 94b7672 commit 710760b
Show file tree
Hide file tree
Showing 213 changed files with 4,770 additions and 32 deletions.
111 changes: 111 additions & 0 deletions Docs/dsl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# AkiBT DSL

AkBT.DSL is a domain-specific language designed for AkiBT, use standard AST (Abstract Syntax Tree) to build behavior tree directly instead of generating json as intermediate.

Inspired by Game AI Pro and .Net's C# version for LLVM's official tutorial of Kaleidoscope.

## What is DSL

A Domain-Specific Language (DSL) is a computer language that's targeted to a particular kind of problem, rather than a general purpose language that's aimed at any kind of software problem.

[See Reference Article](https://martinfowler.com/dsl.html)

## How to use

```
///
It's a comment
The following is a behavior tree written using AkiBTDSL
///
Vector3 destination (0,0,0)
Vector3 myPos (0,0,0)
Float distance 1
Vector3 subtract (0,0,0)
Parallel(children:[
Sequence(children:[
Vector3Random(xRange:(-10,10),yRange:(0,0),zRange:(-10,10),operation:1,
storeResult=>destination ),
DebugLog(logText:"This is a log: agent get a new destination."),
TimeWait(waitTime:10)
]),
Sequence(children:[
Sequence(children:[
TransformGetPosition(storeResult=>myPos),
Vector3Operator(operation:1,firstVector3=>myPos,
secondVector3=>destination,storeResult=>subtract),
Vector3GetSqrMagnitude(vector3=>subtract,result=>distance)
]),
Selector(abortOnConditionChanged: false, children:[
FloatComparison(evaluateOnRunning:false,float1=>distance,
float2:4,operation:5,child:
Sequence(abortOnConditionChanged:false,children:[
NavmeshStopAgent(isStopped:false),
NavmeshSetDestination(destination=>destination)
])
),
NavmeshStopAgent(isStopped:true)
])
])
])
```

The above behavior tree is the patrol AI behavior tree in AkiBT Example, it will get a new position every 10 seconds and move to it, if the distance from the target point is less than 2, it will stop

The main body of DSL can be divided into two parts, public variables and nodes. The declaration of public variables needs to specify the type, name and value.

If the type is wrapped with `$`, global variables will be bound at runtime, for example:

```
$Vector3$ TargetPosition (0,0,0)
```

If the variable type is Object (SharedObject), you can declare the type name before declaring the value. For example:
```
Object navAgent "UnityEngine.AIModule,UnityEngine.AI.NavMeshAgent" Null
```

The type name's format is `{Assembly Name},{NameSpace}.{ClassName}` or use `Type.AssemblyQualifiedName`.

Then the variable is set to global and has the ``NavMeshAgent`` type restriction.

For nodes, we will skip the Root node (because all behavior trees enter from the Root), and start writing directly from the Root's child nodes.


For a node, you need to declare its type.


For ordinary variables that do not use the default value of the node, you need to declare its name (or use AkiLabelAttribute to alter field's name) and add ':' to assign


For the shared variable in the node, if you don’t need to refer to the shared variable of the public variable, you can assign it directly, for example

```
TimeWait(waitTime:10)
```

For shared variables that need to be referenced, use the '=>' symbol plus the name of the public variable that needs to be referenced, for example
```
NavmeshSetDestination(destination=>myDestination)
```

## Compatible with AkiLabel

DSL supports use `AkiLabelAttribute`'s value as name to build behavior tree. For example:

```
Vector3 玩家位置 (0,0,0)
序列 (子节点:[
获取玩家位置 (位置=>玩家位置),
移动至玩家(目标=>玩家位置)
])
```

Which is quite cool!

## Reference

https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html

https://github.com/dotnet/LLVMSharp/blob/main/samples/KaleidoscopeTutorial
7 changes: 7 additions & 0 deletions Docs/dsl.md.meta

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

2 changes: 1 addition & 1 deletion Editor/Core/Editor/GraphEditorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private static bool OnOpenAsset(int instanceId, int _)
return false;
}
#pragma warning restore IDE0051
[MenuItem("Tools/AkiBT/AkiBT Editor")]
[MenuItem("Tools/AkiBT/AkiBT Editor", priority = 0)]
private static void ShowEditorWindow()
{
string path = EditorUtility.SaveFilePanel("Select ScriptableObject save path", Application.dataPath, "BehaviorTreeSO", "asset");
Expand Down
2 changes: 1 addition & 1 deletion Editor/Core/Editor/ServiceEditorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Kurisu.AkiBT.Editor
{
public class ServiceEditorWindow : EditorWindow
{
[MenuItem("Tools/AkiBT/AkiBT Service")]
[MenuItem("Tools/AkiBT/AkiBT Service", priority = 1)]
private static void ShowEditorWindow()
{
GetWindow<ServiceEditorWindow>("AkiBT Service");
Expand Down
File renamed without changes.
File renamed without changes.
38 changes: 21 additions & 17 deletions Editor/Core/Model/BehaviorTreeSetting.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.UIElements;
namespace Kurisu.AkiBT.Editor
Expand All @@ -23,8 +24,8 @@ internal class EditorSetting
}
public class BehaviorTreeSetting : ScriptableObject
{
public const string Version = "v1.4.9";
private const string k_BehaviorTreeSettingsPath = "Assets/AkiBTSetting.asset";
public const string Version = "v1.5.0";
private const string k_BehaviorTreeSettingsPath = "ProjectSettings/AkiBTSetting.asset";
private const string k_UserServiceSettingPath = "Assets/AkiBTUserServiceData.asset";
private const string GraphFallBackPath = "AkiBT/Graph";
private const string InspectorFallBackPath = "AkiBT/Inspector";
Expand Down Expand Up @@ -112,19 +113,17 @@ public StyleSheet GetNodeStyle(string editorName)
}
public static BehaviorTreeSetting GetOrCreateSettings()
{
var guids = AssetDatabase.FindAssets($"t:{nameof(BehaviorTreeSetting)}");
BehaviorTreeSetting setting = null;
if (guids.Length == 0)
{
setting = CreateInstance<BehaviorTreeSetting>();
Debug.Log($"AkiBT Setting saving path : {k_BehaviorTreeSettingsPath}");
AssetDatabase.CreateAsset(setting, k_BehaviorTreeSettingsPath);
AssetDatabase.SaveAssets();
}
else setting = AssetDatabase.LoadAssetAtPath<BehaviorTreeSetting>(AssetDatabase.GUIDToAssetPath(guids[0]));
var arr = InternalEditorUtility.LoadSerializedFileAndForget(k_BehaviorTreeSettingsPath);
setting = arr.Length > 0 ? arr[0] as BehaviorTreeSetting : setting != null ? setting : CreateInstance<BehaviorTreeSetting>();
return setting;
}

public void Save(bool saveAsText = true)
{
InternalEditorUtility.SaveToSerializedFileAndForget(new[] { this }, k_BehaviorTreeSettingsPath, saveAsText);
}

internal static SerializedObject GetSerializedSettings()
{
return new SerializedObject(GetOrCreateSettings());
Expand All @@ -133,26 +132,31 @@ internal static SerializedObject GetSerializedSettings()

internal class BehaviorTreeSettingsProvider : SettingsProvider
{
private SerializedObject m_Settings;
private class Styles
{
public static GUIContent GraphEditorSettingStyle = new("Graph Editor Setting");
public static GUIContent LayoutDistanceStyle = new("Layout Distance", "Auto node layout sibling distance");
public static GUIContent SerializeEditorDataStyle = new("Serialize Editor Data", "Serialize node editor data when use json serialization, turn off to decrease file size");
}
public BehaviorTreeSettingsProvider(string path, SettingsScope scope = SettingsScope.User) : base(path, scope) { }
private BehaviorTreeSetting setting;
private SerializedObject serializedObject;
public override void OnActivate(string searchContext, VisualElement rootElement)
{
m_Settings = BehaviorTreeSetting.GetSerializedSettings();
setting = BehaviorTreeSetting.GetOrCreateSettings();
serializedObject = new(setting);
}
public override void OnGUI(string searchContext)
{
GUILayout.BeginVertical("Editor Settings", GUI.skin.box);
GUILayout.Space(EditorGUIUtility.singleLineHeight);
EditorGUILayout.PropertyField(m_Settings.FindProperty("settings"), Styles.GraphEditorSettingStyle);
EditorGUILayout.PropertyField(m_Settings.FindProperty("autoLayoutSiblingDistance"), Styles.LayoutDistanceStyle);
EditorGUILayout.PropertyField(m_Settings.FindProperty("jsonSerializeEditorData"), Styles.SerializeEditorDataStyle);
m_Settings.ApplyModifiedPropertiesWithoutUndo();
EditorGUILayout.PropertyField(serializedObject.FindProperty("settings"), Styles.GraphEditorSettingStyle);
EditorGUILayout.PropertyField(serializedObject.FindProperty("autoLayoutSiblingDistance"), Styles.LayoutDistanceStyle);
EditorGUILayout.PropertyField(serializedObject.FindProperty("jsonSerializeEditorData"), Styles.SerializeEditorDataStyle);
if (serializedObject.ApplyModifiedPropertiesWithoutUndo())
{
setting.Save();
}
GUILayout.EndVertical();
}
[SettingsProvider]
Expand Down
2 changes: 1 addition & 1 deletion Samples~/Basic Example.meta → Editor/DSL.meta

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

161 changes: 161 additions & 0 deletions Editor/DSL/CodeSplitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Reflection;
using System.Linq;
using Newtonsoft.Json;
namespace Kurisu.AkiBT.DSL.Editor
{
/// <summary>
/// Tool to split fields and function, header file can be provided for UGC side.
/// User do not need to know implementation detail.
/// </summary>
public class CodeSplitter : EditorWindow
{
private class GeneratorSetting
{
public bool convertFieldsToPublic;
public string scriptFolderPath = string.Empty;
}
private GeneratorSetting setting;
private static string KeyName => Application.productName + "_AkiBT_DSL_SplitterSetting";
[MenuItem("Tools/AkiBT/DSL/Code Splitter", priority = 20)]
private static void GetWindow()
{
GetWindowWithRect<CodeSplitter>(new Rect(0, 0, 400, 200));
}
private void OnEnable()
{
var data = EditorPrefs.GetString(KeyName);
setting = JsonConvert.DeserializeObject<GeneratorSetting>(data);
setting ??= new GeneratorSetting();
}
private void OnDisable()
{
EditorPrefs.SetString(KeyName, JsonConvert.SerializeObject(setting));
}
private void OnGUI()
{
setting.convertFieldsToPublic = EditorGUILayout.Toggle("Convert Fields To Public", setting.convertFieldsToPublic);
if (GUILayout.Button("Select Script Folder"))
{
setting.scriptFolderPath = EditorUtility.OpenFolderPanel("Select Script Folder", "", "");
}
GUILayout.Label($"Path: {setting.scriptFolderPath}", new GUIStyle(GUI.skin.label) { wordWrap = true });
if (GUILayout.Button("Split Code"))
{
SplitCode();
}
if (GUILayout.Button("Restore Code"))
{
RestoreCode();
}
}
private void SplitCode()
{
if (string.IsNullOrEmpty(setting.scriptFolderPath))
{
Debug.LogError("Script folder path is not selected!");
return;
}
string[] scriptFiles = Directory.GetFiles(setting.scriptFolderPath, "*.cs", SearchOption.AllDirectories);
var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes());
foreach (string scriptFile in scriptFiles)
{
string scriptContent = File.ReadAllText(scriptFile).TrimEnd();
//ScriptName should be class name
string className = Path.GetFileNameWithoutExtension(scriptFile);
string namespaceName = GetNamespace(scriptContent);
//Find last field index
Type scriptType = types.FirstOrDefault(x => x.Name == className && x.Namespace == namespaceName);
if (scriptType == null)
{
Debug.Log($"Can not find type from {namespaceName}.{className}");
continue;
}
FieldInfo[] fields = scriptType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
//Suppose class is public
int classIndex = scriptContent.IndexOf("public class ");
if (classIndex == -1)
{
Debug.Log($"{namespaceName}.{className} is not public, generation was skipped");
continue;
}
int namespaceEndIndex = scriptContent.IndexOf('{');
int endIndex = GetLastFieldIndex(fields, scriptContent);
if (endIndex == -1)
{
//No fields contained => use class '{' index
endIndex = scriptContent.IndexOf('{', namespaceEndIndex + 1) + 1;
}
string fieldContent = scriptContent[..endIndex];
if (setting.convertFieldsToPublic)
{
fieldContent = fieldContent.Replace("private ", "public ").Replace("internal ", "public ").Replace("protected ", "public ");
}
string methodContent = scriptContent[(endIndex + 1)..];
//Replace to partial class
string headerScriptContent = fieldContent.Replace("public class", "public partial class") + "\n}\n}";
int indent = scriptContent.LastIndexOf('}', scriptContent.Length - 2) - scriptContent.LastIndexOf('\n', scriptContent.Length - 3) - 1;
string bodyScriptContent = scriptContent[..(namespaceEndIndex + 1)] + "\n" + new string(' ', indent) + "public partial class " + className + "\n" + new string(' ', indent) + "{\n" + methodContent;
string headerScriptPath = Path.GetDirectoryName(scriptFile) + "/" + className + "_Header.cs";
string bodyScriptPath = Path.GetDirectoryName(scriptFile) + "/" + className + ".cs";
string backUpPath = Path.GetDirectoryName(scriptFile) + "/" + className + "_Backup.txt";
//Add backup for original script
File.WriteAllText(backUpPath, scriptContent);
File.WriteAllText(headerScriptPath, headerScriptContent);
File.WriteAllText(bodyScriptPath, bodyScriptContent);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("Code generation complete!");
}
private void RestoreCode()
{
if (string.IsNullOrEmpty(setting.scriptFolderPath))
{
Debug.LogError("Script folder path is not selected!");
return;
}
string[] headerFiles = Directory.GetFiles(setting.scriptFolderPath, "*_Header.cs", SearchOption.AllDirectories);
foreach (var file in headerFiles) File.Delete(file);
string[] scriptFiles = Directory.GetFiles(setting.scriptFolderPath, "*_Backup.txt", SearchOption.AllDirectories);
foreach (var file in scriptFiles)
{
string fileName = Path.GetFileNameWithoutExtension(file)[..^7];
string filePath = Path.Combine(Path.GetDirectoryName(file), $"{fileName}.cs");
var scriptContent = File.ReadAllText(file);
File.WriteAllText(filePath, scriptContent);
File.Delete(file);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("Code restoration complete!");
}
private static string GetNamespace(string scriptContent)
{
int namespaceIndex = scriptContent.IndexOf("namespace ");
int namespaceEndIndex = scriptContent.IndexOf('{');
return scriptContent[(namespaceIndex + 10)..namespaceEndIndex].TrimEnd();
}
private static int GetLastFieldIndex(FieldInfo[] fieldInfos, string scriptContent)
{
int endIndex = -1;
foreach (FieldInfo fieldInfo in fieldInfos)
{
int index = scriptContent.IndexOf(" " + fieldInfo.Name + ";");
if (index != -1)
{
index += fieldInfo.Name.Length + 2;
if (index > endIndex)
{
endIndex = index;
}
}
}
return endIndex;
}

}
}
Loading

0 comments on commit 710760b

Please sign in to comment.