-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12 from AkiKurisu/dev_merge_dsl
Merge AkiBT.DSL
- Loading branch information
Showing
213 changed files
with
4,770 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} | ||
} |
Oops, something went wrong.