diff --git a/data/2.5D Physics.txt b/data/2.5D Physics.txt new file mode 100644 index 0000000000000000000000000000000000000000..8a1d58795fa4a9435a9ed23d17bfee0b834d5327 --- /dev/null +++ b/data/2.5D Physics.txt @@ -0,0 +1,31 @@ +Introduction +Using 2.5D Physics you are able to add Height, or thickness depending on your perspective, while still benefiting from most performance advantages available in 2D. N.B.: Use Vertical Transform has to be manually enabled in the SimulationConfig asset's Physics settings. + +Back To Top + + +2.5D Physics With Vertical Data +StaticCollider2D can have 'thickness' in the 3rd dimension using Quantum's 2.5D physics; simply set the Height: + +Adding Height to a Static Collider +Adding Height to a Static Collider. +For Entities, just add the Transform2DVertical component and set its Height and Position. On a Quantum XZ-oriented game, this adds height on the Y axis, for example. N.B.: Transform2DVertical requires the Transform2D component. + +var transform2dVertical = new Transform2DVertical(); +transform2dVertical.Height = FP._1; +transform2dVertical.Position = FP._1; + +f.Set(entity, transform2dVertical); +Adding Height to an Entity Prototype +Adding Height to an Entity Prototype. +If entities or statics have a 3rd dimension, the physics engine will take into consideration when solving collisions. This allows for 'aerial' entities to fly over 'ground-based' ones, etc. + +Back To Top + + +Physics Engine Implications + +Entity Separation +Important: When a collision is detected, the collision solver does not use the extra dimension information. This can result in entity bounce when separation is performed on the basic 2D plane of the physics engine. + +It is possible to simulate 3-dimensional gravity by manually applying speed and forces directly on Transform2DVertical.Position. The physics engine will use that information only for collision detection though. \ No newline at end of file diff --git a/data/AIAction.cs b/data/AIAction.cs new file mode 100644 index 0000000000000000000000000000000000000000..da7835232e35e1c865a683b569fe5d4482eb497b --- /dev/null +++ b/data/AIAction.cs @@ -0,0 +1,14 @@ +using System; +using Photon.Deterministic; + +namespace Quantum +{ + public abstract unsafe partial class AIAction + { + public string Label; + public const int NEXT_ACTION_DEFAULT = -1; + + public abstract void Update(Frame frame, EntityRef entity); + public virtual int NextAction(Frame frame, EntityRef entity) { return NEXT_ACTION_DEFAULT; } + } +} diff --git a/data/AIAction.qtn b/data/AIAction.qtn new file mode 100644 index 0000000000000000000000000000000000000000..74b1fa62f7d4a02cd94fcebbf1ccbc534b4ea21f --- /dev/null +++ b/data/AIAction.qtn @@ -0,0 +1 @@ +asset AIAction; diff --git a/data/AIBlackboard.cs b/data/AIBlackboard.cs new file mode 100644 index 0000000000000000000000000000000000000000..39e73ae0dd92807883d9406f7ffd847a308fb1dc --- /dev/null +++ b/data/AIBlackboard.cs @@ -0,0 +1,69 @@ +using Photon.Deterministic; +using System; +using System.Collections.Generic; + +namespace Quantum +{ + public unsafe partial class AIBlackboard + { + public AIBlackboardEntry[] Entries; + + [NonSerialized] public Dictionary Map; + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + Map = new Dictionary(); + + for (Int32 i = 0; i < Entries.Length; i++) + { + Map.Add(Entries[i].Key.Key, i); + } + } + + public Int32 GetEntryID(string key) + { + Assert.Check(string.IsNullOrEmpty(key) == false, "The Key cannot be empty or null."); + Assert.Check(Map.ContainsKey(key) == true, $"Key {0} not present in the Blackboard", key); + + return Map[key]; + } + + public bool TryGetEntryID(string key, out Int32 id) + { + return Map.TryGetValue(key, out id); + } + + public string GetEntryName(Int32 id) + { + return Entries[id].Key.Key; + } + + public bool HasEntry(string key) + { + for (int i = 0; i < Entries.Length; i++) + { + if (Entries[i].Key.Key == key) + { + return true; + } + } + + return false; + } + + public AIBlackboardEntry GetEntry(string key) + { + for (int i = 0; i < Entries.Length; i++) + { + if (Entries[i].Key.Key == key) + { + return Entries[i]; + } + } + + return default; + } + } +} diff --git a/data/AIBlackboardComponent.cs b/data/AIBlackboardComponent.cs new file mode 100644 index 0000000000000000000000000000000000000000..3bbf6b0baf47837ab28e2d47caaf893db062ed68 --- /dev/null +++ b/data/AIBlackboardComponent.cs @@ -0,0 +1,290 @@ +using Photon.Deterministic; +using System; +using Quantum.Collections; + +namespace Quantum +{ + public unsafe partial struct AIBlackboardComponent + { + #region Init/Free + public void InitializeBlackboardComponent(Frame frame, AIBlackboard blackboardAsset) + { + Board = blackboardAsset; + + var assetEntries = blackboardAsset.Entries; + + if (Entries.Ptr != default) + { + FreeBlackboardComponent(frame); + } + + QList entriesList = frame.AllocateList(blackboardAsset.Entries.Length); + + for (int i = 0; i < assetEntries.Length; i++) + { + BlackboardValue newValue = CreateValueFromEntry(assetEntries[i]); + entriesList.Add(new BlackboardEntry { Value = newValue }); + //entriesList.Add(newValue); + } + + Entries = entriesList; + } + + public void FreeBlackboardComponent(Frame frame) + { + if (Entries.Ptr != default) + { + frame.FreeList(Entries); + Entries = default; + } + } + + private BlackboardValue CreateValueFromEntry(AIBlackboardEntry entry) + { + BlackboardValue newValue = new BlackboardValue(); + + if (entry.Type == AIBlackboardValueType.Boolean) + { + *newValue.BooleanValue = default; + } + + if (entry.Type == AIBlackboardValueType.Byte) + { + *newValue.ByteValue = default; + } + + if (entry.Type == AIBlackboardValueType.Integer) + { + *newValue.IntegerValue = default; + } + + if (entry.Type == AIBlackboardValueType.FP) + { + *newValue.FPValue = default; + } + + if (entry.Type == AIBlackboardValueType.Vector2) + { + *newValue.FPVector2Value = default; + } + + if (entry.Type == AIBlackboardValueType.Vector3) + { + *newValue.FPVector3Value = default; + } + + if (entry.Type == AIBlackboardValueType.EntityRef) + { + *newValue.EntityRefValue = default; + } + + return newValue; + } + #endregion + + #region Getters + public QBoolean GetBoolean(Frame frame, string key) + { + var bbValue = GetBlackboardValue(frame, key); + return *bbValue.BooleanValue; + } + + public byte GetByte(Frame frame, string key) + { + var bbValue = GetBlackboardValue(frame, key); + return *bbValue.ByteValue; + } + + public Int32 GetInteger(Frame frame, string key) + { + var bbValue = GetBlackboardValue(frame, key); + return *bbValue.IntegerValue; + } + + public FP GetFP(Frame frame, string key) + { + var bbValue = GetBlackboardValue(frame, key); + return *bbValue.FPValue; + } + + public FPVector2 GetVector2(Frame frame, string key) + { + var bbValue = GetBlackboardValue(frame, key); + return *bbValue.FPVector2Value; + } + + public FPVector3 GetVector3(Frame frame, string key) + { + var bbValue = GetBlackboardValue(frame, key); + return *bbValue.FPVector3Value; + } + + public EntityRef GetEntityRef(Frame frame, string key) + { + var bbValue = GetBlackboardValue(frame, key); + return *bbValue.EntityRefValue; + } + #endregion + + #region Setters + public BlackboardEntry* Set(Frame frame, string key, QBoolean value) + { + QList valueList = frame.ResolveList(Entries); + var ID = GetID(frame, key); + *valueList.GetPointer(ID)->Value.BooleanValue = value; + + return valueList.GetPointer(ID); + } + + public BlackboardEntry* Set(Frame frame, string key, byte value) + { + QList valueList = frame.ResolveList(Entries); + var ID = GetID(frame, key); + *valueList.GetPointer(ID)->Value.ByteValue = value; + + return valueList.GetPointer(ID); + } + + public BlackboardEntry* Set(Frame frame, string key, Int32 value) + { + QList valueList = frame.ResolveList(Entries); + var ID = GetID(frame, key); + *valueList.GetPointer(ID)->Value.IntegerValue = value; + + return valueList.GetPointer(ID); + } + + public BlackboardEntry* Set(Frame frame, string key, FP value) + { + QList valueList = frame.ResolveList(Entries); + var ID = GetID(frame, key); + *valueList.GetPointer(ID)->Value.FPValue = value; + + return valueList.GetPointer(ID); + } + + public BlackboardEntry* Set(Frame frame, string key, FPVector2 value) + { + QList valueList = frame.ResolveList(Entries); + var ID = GetID(frame, key); + *valueList.GetPointer(ID)->Value.FPVector2Value = value; + + return valueList.GetPointer(ID); + } + + public BlackboardEntry* Set(Frame frame, string key, FPVector3 value) + { + QList valueList = frame.ResolveList(Entries); + var ID = GetID(frame, key); + *valueList.GetPointer(ID)->Value.FPVector3Value = value; + + return valueList.GetPointer(ID); + + } + + public BlackboardEntry* Set(Frame frame, string key, EntityRef value) + { + QList valueList = frame.ResolveList(Entries); + var ID = GetID(frame, key); + *valueList.GetPointer(ID)->Value.EntityRefValue = value; + + return valueList.GetPointer(ID); + } + #endregion + + #region Helpers + public BlackboardEntry* GetBlackboardEntry(Frame frame, string key) + { + var bbAsset = frame.FindAsset(Board.Id); + var ID = bbAsset.GetEntryID(key); + var values = frame.ResolveList(Entries); + return values.GetPointer(ID); + } + + public BlackboardValue GetBlackboardValue(Frame frame, string key) + { + Assert.Check(string.IsNullOrEmpty(key) == false, "The Key cannot be empty or null."); + + var bbAsset = frame.FindAsset(Board.Id); + var ID = bbAsset.GetEntryID(key); + var values = frame.ResolveList(Entries); + + return values[ID].Value; + } + + public Int32 GetID(Frame frame, string key) + { + Assert.Check(string.IsNullOrEmpty(key) == false, "The Key cannot be empty or null."); + + var bbAsset = frame.FindAsset(Board.Id); + var ID = bbAsset.GetEntryID(key); + + return ID; + } + + public bool HasEntry(Frame frame, string key) + { + var boardAsset = frame.FindAsset(Board.Id); + return boardAsset.HasEntry(key); + } + #endregion + + #region BT Specific + public void RegisterReactiveDecorator(Frame frame, string key, BTDecorator decorator) + { + var blackboardEntry = GetBlackboardEntry(frame, key); + + QList reactiveDecorators; + if (blackboardEntry->ReactiveDecorators.Ptr == default) + { + reactiveDecorators = frame.AllocateList(); + } + else + { + reactiveDecorators = frame.ResolveList(blackboardEntry->ReactiveDecorators); + } + reactiveDecorators.Add(decorator); + + blackboardEntry->ReactiveDecorators = reactiveDecorators; + } + + public void UnregisterReactiveDecorator(Frame frame, string key, BTDecorator decorator) + { + var blackboardEntry = GetBlackboardEntry(frame, key); + + if (blackboardEntry->ReactiveDecorators.Ptr != default) + { + QList reactiveDecorators = frame.ResolveList(blackboardEntry->ReactiveDecorators); + reactiveDecorators.Remove(decorator); + blackboardEntry->ReactiveDecorators = reactiveDecorators; + } + } + #endregion + + #region Debug + public void Dump(Frame frame) + { + string dumpText = ""; + var bbAsset = frame.FindAsset(Board.Id); + dumpText += "Blackboard Path and ID: " + bbAsset.Path + " | " + Board.Id.Value; + + var valuesList = frame.ResolveList(Entries); + for (int i = 0; i < valuesList.Count; i++) + { + string value = "NONE"; + if (valuesList[i].Value.Field == BlackboardValue.BOOLEANVALUE) value = valuesList[i].Value.BooleanValue->Value.ToString(); + if (valuesList[i].Value.Field == BlackboardValue.BYTEVALUE) value = valuesList[i].Value.ByteValue->ToString(); + if (valuesList[i].Value.Field == BlackboardValue.INTEGERVALUE) value = valuesList[i].Value.IntegerValue->ToString(); + if (valuesList[i].Value.Field == BlackboardValue.FPVALUE) value = valuesList[i].Value.FPValue->ToString(); + if (valuesList[i].Value.Field == BlackboardValue.FPVECTOR2VALUE) value = valuesList[i].Value.FPVector2Value->ToString(); + if (valuesList[i].Value.Field == BlackboardValue.FPVECTOR3VALUE) value = valuesList[i].Value.FPVector3Value->ToString(); + if (valuesList[i].Value.Field == BlackboardValue.ENTITYREFVALUE) value = valuesList[i].Value.EntityRefValue->ToString(); + + dumpText += "\nName: " + bbAsset.GetEntryName(i) + ", Value: " + value; + } + + Log.Info(dumpText); + } + #endregion + } +} diff --git a/data/AIBlackboardEntry.cs b/data/AIBlackboardEntry.cs new file mode 100644 index 0000000000000000000000000000000000000000..4429be56ab6f0bbd2441b194e242458bafc64545 --- /dev/null +++ b/data/AIBlackboardEntry.cs @@ -0,0 +1,13 @@ +using System; + +namespace Quantum +{ + // This struct is stored on the blackboard asset + // It is NOT the one used on the Blackboard component + [Serializable] + public struct AIBlackboardEntry + { + public AIBlackboardValueType Type; + public AIBlackboardValueKey Key; + } +} diff --git a/data/AIBlackboardInitializer.cs b/data/AIBlackboardInitializer.cs new file mode 100644 index 0000000000000000000000000000000000000000..1bb400020c34b9aefdb10b8372d4a03581b09843 --- /dev/null +++ b/data/AIBlackboardInitializer.cs @@ -0,0 +1,87 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public unsafe partial class AIBlackboardInitializer + { + [Serializable] + public struct AIBlackboardInitialValue + { + public Boolean AsBoolean; + public Byte AsByte; + public Int32 AsInteger; + public FP AsFP; + public FPVector2 AsFPVector2; + public FPVector3 AsFPVector3; + public EntityRef AsEntityRef; + } + + [Serializable] + public struct AIBlackboardInitialValueEntry + { + public string Key; + public AIBlackboardInitialValue Value; + } + + public bool ReportMissingEntries = true; + + public AssetRefAIBlackboard AIBlackboard; + public AIBlackboardInitialValueEntry[] InitialValues; + + + public unsafe static void InitializeBlackboard(Frame frame, AIBlackboardComponent* blackboard, AIBlackboardInitializer blackboardInitializer, AIBlackboardInitialValueEntry[] blackboardOverrides = null) + { + AIBlackboard board = frame.FindAsset(blackboardInitializer.AIBlackboard.Id); + + blackboard->InitializeBlackboardComponent(frame, board); + + ApplyEntries(frame, blackboard, blackboardInitializer, blackboardInitializer.InitialValues); + ApplyEntries(frame, blackboard, blackboardInitializer, blackboardOverrides); + } + + public unsafe static void ApplyEntries(Frame frame, AIBlackboardComponent* blackboard, AIBlackboardInitializer blackboardInitializer, AIBlackboardInitialValueEntry[] values) + { + if (values == null) return; + + for (int i = 0; i < values.Length; i++) + { + string key = values[i].Key; + if (blackboard->HasEntry(frame, key) == false) + { + if (blackboardInitializer.ReportMissingEntries) + { + Quantum.Log.Warn($"Blackboard {blackboard->Board} does not have an entry with a key called '{key}'"); + } + continue; + } + + BlackboardValue value = blackboard->GetBlackboardValue(frame, key); + switch (value.Field) + { + case BlackboardValue.BOOLEANVALUE: + blackboard->Set(frame, key, values[i].Value.AsBoolean); + break; + case BlackboardValue.BYTEVALUE: + blackboard->Set(frame, key, values[i].Value.AsByte); + break; + case BlackboardValue.ENTITYREFVALUE: + blackboard->Set(frame, key, values[i].Value.AsEntityRef); + break; + case BlackboardValue.FPVALUE: + blackboard->Set(frame, key, values[i].Value.AsFP); + break; + case BlackboardValue.INTEGERVALUE: + blackboard->Set(frame, key, values[i].Value.AsInteger); + break; + case BlackboardValue.FPVECTOR2VALUE: + blackboard->Set(frame, key, values[i].Value.AsFPVector2); + break; + case BlackboardValue.FPVECTOR3VALUE: + blackboard->Set(frame, key, values[i].Value.AsFPVector3); + break; + } + } + } + } +} diff --git a/data/AIBlackboardValueKey.cs b/data/AIBlackboardValueKey.cs new file mode 100644 index 0000000000000000000000000000000000000000..5422a6882a4e8db8b55c7da408f3642d9573fed5 --- /dev/null +++ b/data/AIBlackboardValueKey.cs @@ -0,0 +1,11 @@ +using System; + +namespace Quantum +{ + // Wrapping the blackboard value key inside a struct gives us a nice way to overload the Unity inspector. + [Serializable] + public struct AIBlackboardValueKey + { + public String Key; + } +} diff --git a/data/AIBlackboardValueType.cs b/data/AIBlackboardValueType.cs new file mode 100644 index 0000000000000000000000000000000000000000..47cd065eff109671e0c1ff4061bb4413696b2cfe --- /dev/null +++ b/data/AIBlackboardValueType.cs @@ -0,0 +1,14 @@ + +namespace Quantum +{ + public enum AIBlackboardValueType + { + Boolean, + Byte, + Integer, + FP, + Vector2, + Vector3, + EntityRef + } +} diff --git a/data/AIConfig.cs b/data/AIConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..ea98daa806facdc1e0d9e5ad94c119f770b43acf --- /dev/null +++ b/data/AIConfig.cs @@ -0,0 +1,124 @@ +using Photon.Deterministic; +using System; +using System.Collections.Generic; + +namespace Quantum +{ + public partial class AIConfig : AssetObject + { + public enum EValueType + { + None, + Int, + Bool, + Byte, + FP, + FPVector2, + FPVector3, + String, + EntityRef, + } + + [Serializable] + public class KeyValuePair + { + public string Key; + public EValueType Type; + public Value Value; + } + + [Serializable] + public struct Value + { + public Int32 Integer; + public Boolean Boolean; + public Byte Byte; + public FP FP; + public FPVector2 FPVector2; + public FPVector3 FPVector3; + public string String; + public EntityRef EntityRef; + } + + public int Count { get { return KeyValuePairs.Count; } } + + public AssetRefAIConfig DefaultConfig; + public List KeyValuePairs = new List(32); + + public KeyValuePair Get(string key) + { + for (int i = 0; i < KeyValuePairs.Count; i++) + { + if (KeyValuePairs[i].Key == key) + return KeyValuePairs[i]; + } + + return null; + } + + public void Set(string key, T value) + { + if (string.IsNullOrEmpty(key) == true) + return; + + KeyValuePair pair = Get(key); + + if (pair == null) + { + pair = new KeyValuePair(); + pair.Key = key; + KeyValuePairs.Add(pair); + } + + Set(pair, value); + } + + private void Set(KeyValuePair pair, T value) + { + if (value is int intValue) + { + pair.Type = EValueType.Int; + pair.Value.Integer = intValue; + } + else if (value is bool boolValue) + { + pair.Type = EValueType.Bool; + pair.Value.Boolean = boolValue; + } + else if (value is Byte byteValue) + { + pair.Type = EValueType.Byte; + pair.Value.Byte = byteValue; + } + else if (value is FP fpValue) + { + pair.Type = EValueType.FP; + pair.Value.FP = fpValue; + } + else if (value is FPVector2 fpVector2Value) + { + pair.Type = EValueType.FPVector2; + pair.Value.FPVector2 = fpVector2Value; + } + else if (value is FPVector3 fpVector3Value) + { + pair.Type = EValueType.FPVector3; + pair.Value.FPVector3 = fpVector3Value; + } + else if (value is string stringValue) + { + pair.Type = EValueType.String; + pair.Value.String = stringValue; + } + else if (value is EntityRef entityRefValue) + { + pair.Type = EValueType.EntityRef; + pair.Value.EntityRef = entityRefValue; + } + else + { + throw new NotSupportedException(string.Format("AIConfig - Type not supported. Type: {0} Key: {1}", typeof(T), pair.Key)); + } + } + } +} diff --git a/data/AIConfig.qtn b/data/AIConfig.qtn new file mode 100644 index 0000000000000000000000000000000000000000..c7d17780c5b63e50c47ffacc6f8945b1360858e9 --- /dev/null +++ b/data/AIConfig.qtn @@ -0,0 +1 @@ +asset AIConfig; \ No newline at end of file diff --git a/data/AIFunction.qtn b/data/AIFunction.qtn new file mode 100644 index 0000000000000000000000000000000000000000..c1d93bcf99fc7fdbd94a3a8d1c72c597fdb90eb0 --- /dev/null +++ b/data/AIFunction.qtn @@ -0,0 +1,7 @@ +asset AIFunctionByte; +asset AIFunctionBool; +asset AIFunctionInt; +asset AIFunctionFP; +asset AIFunctionFPVector2; +asset AIFunctionFPVector3; +asset AIFunctionEntityRef; \ No newline at end of file diff --git a/data/AIFunctionAND.cs b/data/AIFunctionAND.cs new file mode 100644 index 0000000000000000000000000000000000000000..315edac78fb432e1535893d94bdefd79c4a77b20 --- /dev/null +++ b/data/AIFunctionAND.cs @@ -0,0 +1,14 @@ +namespace Quantum +{ + [System.Serializable] + public unsafe partial class AIFunctionAND : AIFunctionBool + { + public AIParamBool ValueA; + public AIParamBool ValueB; + + public override bool Execute(Frame frame, EntityRef entity) + { + return ValueA.ResolveFunction(frame, entity) && ValueB.ResolveFunction(frame, entity); + } + } +} diff --git a/data/AIFunctionBool.cs b/data/AIFunctionBool.cs new file mode 100644 index 0000000000000000000000000000000000000000..813dadf5f1c62bb8556480a1ba82062764a8bb1d --- /dev/null +++ b/data/AIFunctionBool.cs @@ -0,0 +1,17 @@ +namespace Quantum +{ + public unsafe abstract partial class AIFunctionBool + { + public abstract bool Execute(Frame frame, EntityRef entity); + } + + [BotSDKHidden] + [System.Serializable] + public unsafe partial class DefaultAIFunctionBool : AIFunctionBool + { + public override bool Execute(Frame frame, EntityRef entity) + { + return false; + } + } +} diff --git a/data/AIFunctionByte.cs b/data/AIFunctionByte.cs new file mode 100644 index 0000000000000000000000000000000000000000..383dfc89ac56e0c4166d6ddfecbdd093638af0ef --- /dev/null +++ b/data/AIFunctionByte.cs @@ -0,0 +1,17 @@ +namespace Quantum +{ + public unsafe abstract partial class AIFunctionByte + { + public abstract byte Execute(Frame frame, EntityRef entity); + } + + [BotSDKHidden] + [System.Serializable] + public unsafe partial class DefaultAIFunctionByte : AIFunctionByte + { + public override byte Execute(Frame frame, EntityRef entity) + { + return 0; + } + } +} diff --git a/data/AIFunctionEntityRef.cs b/data/AIFunctionEntityRef.cs new file mode 100644 index 0000000000000000000000000000000000000000..37c5866fae82cd1200277fba2cd8a97f42fb41f3 --- /dev/null +++ b/data/AIFunctionEntityRef.cs @@ -0,0 +1,17 @@ +namespace Quantum +{ + public unsafe abstract partial class AIFunctionEntityRef + { + public abstract EntityRef Execute(Frame frame, EntityRef entity); + } + + [BotSDKHidden] + [System.Serializable] + public unsafe partial class DefaultAIFunctionEntityRef : AIFunctionEntityRef + { + public override EntityRef Execute(Frame frame, EntityRef entity) + { + return default(EntityRef); + } + } +} diff --git a/data/AIFunctionFP.cs b/data/AIFunctionFP.cs new file mode 100644 index 0000000000000000000000000000000000000000..ade4524ac96c1dd601d43d838eac895e5a3a0d30 --- /dev/null +++ b/data/AIFunctionFP.cs @@ -0,0 +1,19 @@ +using Photon.Deterministic; + +namespace Quantum +{ + public abstract unsafe partial class AIFunctionFP + { + public abstract FP Execute(Frame frame, EntityRef entity); + } + + [BotSDKHidden] + [System.Serializable] + public unsafe partial class DefaultAIFunctionFP : AIFunctionFP + { + public override FP Execute(Frame frame, EntityRef entity) + { + return FP._0; + } + } +} diff --git a/data/AIFunctionFPVector2.cs b/data/AIFunctionFPVector2.cs new file mode 100644 index 0000000000000000000000000000000000000000..b1fd17c38c5fc50b2f3772d72bf0957c52f9a865 --- /dev/null +++ b/data/AIFunctionFPVector2.cs @@ -0,0 +1,19 @@ +using Photon.Deterministic; + +namespace Quantum +{ + public unsafe abstract partial class AIFunctionFPVector2 + { + public abstract FPVector2 Execute(Frame frame, EntityRef entity); + } + + [BotSDKHidden] + [System.Serializable] + public unsafe partial class DefaultAIFunctionFPVector2 : AIFunctionFPVector2 + { + public override FPVector2 Execute(Frame frame, EntityRef entity) + { + return FPVector2.Zero; + } + } +} diff --git a/data/AIFunctionFPVector3.cs b/data/AIFunctionFPVector3.cs new file mode 100644 index 0000000000000000000000000000000000000000..c2da0b9837355308f771177a982103f54bd0603e --- /dev/null +++ b/data/AIFunctionFPVector3.cs @@ -0,0 +1,19 @@ +using Photon.Deterministic; + +namespace Quantum +{ + public unsafe abstract partial class AIFunctionFPVector3 + { + public abstract FPVector3 Execute(Frame frame, EntityRef entity); + } + + [BotSDKHidden] + [System.Serializable] + public unsafe partial class DefaultAIFunctionFPVector3 : AIFunctionFPVector3 + { + public override FPVector3 Execute(Frame frame, EntityRef entity) + { + return FPVector3.Zero; + } + } +} diff --git a/data/AIFunctionInt.cs b/data/AIFunctionInt.cs new file mode 100644 index 0000000000000000000000000000000000000000..7b9390013811548557a718bc2c773ed6f2cdef44 --- /dev/null +++ b/data/AIFunctionInt.cs @@ -0,0 +1,17 @@ +namespace Quantum +{ + public unsafe abstract partial class AIFunctionInt + { + public abstract int Execute(Frame frame, EntityRef entity); + } + + [BotSDKHidden] + [System.Serializable] + public unsafe partial class DefaultAIFunctionInt : AIFunctionInt + { + public override int Execute(Frame frame, EntityRef entity) + { + return 0; + } + } +} diff --git a/data/AIFunctionNOT.cs b/data/AIFunctionNOT.cs new file mode 100644 index 0000000000000000000000000000000000000000..ac982b17ab65a2e9740a315900d1f0d4b65fce31 --- /dev/null +++ b/data/AIFunctionNOT.cs @@ -0,0 +1,13 @@ +namespace Quantum +{ + [System.Serializable] + public unsafe partial class AIFunctionNOT : AIFunctionBool + { + public AIParamBool Value; + + public override bool Execute(Frame frame, EntityRef entity) + { + return !Value.ResolveFunction(frame, entity); + } + } +} diff --git a/data/AIFunctionOR.cs b/data/AIFunctionOR.cs new file mode 100644 index 0000000000000000000000000000000000000000..ffa739267f938e3c3625ca694d7d1604ac8ed5c8 --- /dev/null +++ b/data/AIFunctionOR.cs @@ -0,0 +1,14 @@ +namespace Quantum +{ + [System.Serializable] + public unsafe partial class AIFunctionOR : AIFunctionBool + { + public AIParamBool ValueA; + public AIParamBool ValueB; + + public override bool Execute(Frame frame, EntityRef entity) + { + return ValueA.ResolveFunction(frame, entity) || ValueB.ResolveFunction(frame, entity); + } + } +} diff --git a/data/AIParam.Types.cs b/data/AIParam.Types.cs new file mode 100644 index 0000000000000000000000000000000000000000..3fbe50bff1cfe02d156785388f90768364c96f91 --- /dev/null +++ b/data/AIParam.Types.cs @@ -0,0 +1,236 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + [System.Serializable] + public unsafe sealed class AIParamInt : AIParam + { + public static implicit operator AIParamInt(int value) { return new AIParamInt() { DefaultValue = value }; } + + public AssetRefAIFunctionInt FunctionRef; + + [NonSerialized] private AIFunctionInt _cachedFunction; + + protected override int GetBlackboardValue(BlackboardValue value) + { + return *value.IntegerValue; + } + + protected override int GetConfigValue(AIConfig.KeyValuePair configPair) + { + return configPair.Value.Integer; + } + + protected override int GetFunctionValue(Frame frame, EntityRef entity) + { + if (_cachedFunction == null) + { + _cachedFunction = frame.FindAsset(FunctionRef.Id); + } + + return _cachedFunction.Execute(frame, entity); + } + } + + [System.Serializable] + public unsafe sealed class AIParamBool : AIParam + { + public static implicit operator AIParamBool(bool value) { return new AIParamBool() { DefaultValue = value }; } + + public AssetRefAIFunctionBool FunctionRef; + + [NonSerialized] private AIFunctionBool _cachedFunction; + + protected override bool GetBlackboardValue(BlackboardValue value) + { + return *value.BooleanValue; + } + + protected override bool GetConfigValue(AIConfig.KeyValuePair configPair) + { + return configPair.Value.Boolean; + } + + protected override bool GetFunctionValue(Frame frame, EntityRef entity) + { + if (_cachedFunction == null) + { + _cachedFunction = frame.FindAsset(FunctionRef.Id); + } + + return _cachedFunction.Execute(frame, entity); + } + } + + [System.Serializable] + public unsafe sealed class AIParamByte : AIParam + { + public static implicit operator AIParamByte(byte value) { return new AIParamByte() { DefaultValue = value }; } + + public AssetRefAIFunctionByte FunctionRef; + + [NonSerialized] private AIFunctionByte _cachedFunction; + + protected override byte GetBlackboardValue(BlackboardValue value) + { + return *value.ByteValue; + } + + protected override byte GetConfigValue(AIConfig.KeyValuePair configPair) + { + return configPair.Value.Byte; + } + + protected override byte GetFunctionValue(Frame frame, EntityRef entity) + { + if (_cachedFunction == null) + { + _cachedFunction = frame.FindAsset(FunctionRef.Id); + } + + return _cachedFunction.Execute(frame, entity); + } + } + + [System.Serializable] + public unsafe sealed class AIParamFP : AIParam + { + public static implicit operator AIParamFP(FP value) { return new AIParamFP() { DefaultValue = value }; } + + public AssetRefAIFunctionFP FunctionRef; + + [NonSerialized] private AIFunctionFP _cachedFunction; + + protected override FP GetBlackboardValue(BlackboardValue value) + { + return *value.FPValue; + } + + protected override FP GetConfigValue(AIConfig.KeyValuePair configPair) + { + return configPair.Value.FP; + } + + protected override FP GetFunctionValue(Frame frame, EntityRef entity) + { + if (_cachedFunction == null) + { + _cachedFunction = frame.FindAsset(FunctionRef.Id); + } + + return _cachedFunction.Execute(frame, entity); + } + } + + [System.Serializable] + public unsafe sealed class AIParamFPVector2 : AIParam + { + public static implicit operator AIParamFPVector2(FPVector2 value) { return new AIParamFPVector2() { DefaultValue = value }; } + + public AssetRefAIFunctionFPVector2 FunctionRef; + + [NonSerialized] private AIFunctionFPVector2 _cachedFunction; + + protected override FPVector2 GetBlackboardValue(BlackboardValue value) + { + return *value.FPVector2Value; + } + + protected override FPVector2 GetConfigValue(AIConfig.KeyValuePair configPair) + { + return configPair.Value.FPVector2; + } + + protected override FPVector2 GetFunctionValue(Frame frame, EntityRef entity) + { + if (_cachedFunction == null) + { + _cachedFunction = frame.FindAsset(FunctionRef.Id); + } + + return _cachedFunction.Execute(frame, entity); + } + } + + [System.Serializable] + public unsafe sealed class AIParamFPVector3 : AIParam + { + public static implicit operator AIParamFPVector3(FPVector3 value) { return new AIParamFPVector3() { DefaultValue = value }; } + + public AssetRefAIFunctionFPVector3 FunctionRef; + + [NonSerialized] private AIFunctionFPVector3 _cachedFunction; + + protected override FPVector3 GetBlackboardValue(BlackboardValue value) + { + return *value.FPVector3Value; + } + + protected override FPVector3 GetConfigValue(AIConfig.KeyValuePair configPair) + { + return configPair.Value.FPVector3; + } + + protected override FPVector3 GetFunctionValue(Frame frame, EntityRef entity) + { + if (_cachedFunction == null) + { + _cachedFunction = frame.FindAsset(FunctionRef.Id); + } + + return _cachedFunction.Execute(frame, entity); + } + } + + [System.Serializable] + public unsafe sealed class AIParamString : AIParam + { + public static implicit operator AIParamString(string value) { return new AIParamString() { DefaultValue = value }; } + + protected override string GetBlackboardValue(BlackboardValue value) + { + throw new NotSupportedException("Blackboard variables as strings are not supported."); + } + + protected override string GetConfigValue(AIConfig.KeyValuePair configPair) + { + return configPair.Value.String; + } + + protected override string GetFunctionValue(Frame frame, EntityRef entity) + { + return default; + } + } + + [System.Serializable] + public unsafe sealed class AIParamEntityRef : AIParam + { + public static implicit operator AIParamEntityRef(EntityRef value) { return new AIParamEntityRef() { DefaultValue = value }; } + + public AssetRefAIFunctionEntityRef FunctionRef; + + [NonSerialized] private AIFunctionEntityRef _cachedFunction; + + protected override EntityRef GetBlackboardValue(BlackboardValue value) + { + return *value.EntityRefValue; + } + + protected override EntityRef GetConfigValue(AIConfig.KeyValuePair configPair) + { + return configPair.Value.EntityRef; + } + + protected override EntityRef GetFunctionValue(Frame frame, EntityRef entity) + { + if (_cachedFunction == null) + { + _cachedFunction = frame.FindAsset(FunctionRef.Id); + } + + return _cachedFunction.Execute(frame, entity); + } + } +} diff --git a/data/AIParam.cs b/data/AIParam.cs new file mode 100644 index 0000000000000000000000000000000000000000..de467ba8a11fa7d63a93b176f62990eeed91ef0e --- /dev/null +++ b/data/AIParam.cs @@ -0,0 +1,76 @@ +using System; + +namespace Quantum +{ + public enum AIParamSource + { + None, + Value, + Config, + Blackboard, + Function, + } + + [Serializable] + public abstract unsafe class AIParam + { + public AIParamSource Source = AIParamSource.Value; + public string Key; + public T DefaultValue; + + /// + /// Use this to solve the AIParam value when the source of the value is unkown + /// + public T Resolve(Frame frame, EntityRef entity, AIBlackboardComponent* blackboard, AIConfig aiConfig) + { + if (Source == AIParamSource.Value || (Source != AIParamSource.Function && string.IsNullOrEmpty(Key) == true)) + return DefaultValue; + + switch (Source) + { + case AIParamSource.Blackboard: + BlackboardValue blackboardValue = blackboard->GetBlackboardValue(frame, Key); + return GetBlackboardValue(blackboardValue); + + case AIParamSource.Config: + AIConfig.KeyValuePair configPair = aiConfig != null ? aiConfig.Get(Key) : null; + return configPair != null ? GetConfigValue(configPair) : DefaultValue; + + case AIParamSource.Function: + return GetFunctionValue(frame, entity); + } + + return default(T); + } + + /// + /// Use this if the it is known that the AIParam stores specifically a Blackboard value + /// + public unsafe T ResolveBlackboard(Frame frame, AIBlackboardComponent* blackboard) + { + BlackboardValue blackboardValue = blackboard->GetBlackboardValue(frame, Key); + return GetBlackboardValue(blackboardValue); + } + + /// + /// Use this if the it is known that the AIParam stores specifically a Config value + /// + public unsafe T ResolveConfig(Frame frame, AIConfig aiConfig) + { + AIConfig.KeyValuePair configPair = aiConfig != null ? aiConfig.Get(Key) : null; + return configPair != null ? GetConfigValue(configPair) : DefaultValue; + } + + /// + /// Use this if the it is known that the AIParam stores specifically a Func + /// + public unsafe T ResolveFunction(Frame frame, EntityRef entity) + { + return GetFunctionValue(frame, entity); + } + + protected abstract T GetBlackboardValue(BlackboardValue value); + protected abstract T GetConfigValue(AIConfig.KeyValuePair configPair); + protected abstract T GetFunctionValue(Frame frame, EntityRef entity); + } +} diff --git a/data/AIParamExtensions.cs b/data/AIParamExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..a1168cadc5414f8f2be905d69651aef7cc9ff0b3 --- /dev/null +++ b/data/AIParamExtensions.cs @@ -0,0 +1,45 @@ +namespace Quantum +{ + public static unsafe partial class AIParamExtensions + { + public static T ResolveFromHFSM(this AIParam aiParam, Frame frame, EntityRef entity) + { + var aiConfigRef = aiParam.Source == AIParamSource.Config + ? frame.Unsafe.GetPointer(entity)->Config + : default; + + return aiParam.Resolve(frame, entity, aiConfigRef); + } + + public static T ResolveFromGOAP(this AIParam aiParam, Frame frame, EntityRef entity) + { + var aiConfigRef = aiParam.Source == AIParamSource.Config + ? frame.Unsafe.GetPointer(entity)->Config + : default; + + return aiParam.Resolve(frame, entity, aiConfigRef); + } + + public static T ResolveFromBT(this AIParam aiParam, Frame frame, EntityRef entity) + { + var aiConfigRef = aiParam.Source == AIParamSource.Config + ? frame.Unsafe.GetPointer(entity)->Config + : default; + + return aiParam.Resolve(frame, entity, aiConfigRef); + } + + public static T Resolve(this AIParam aiParam, Frame frame, EntityRef entity, AssetRefAIConfig aiConfigRef) + { + var blackboard = aiParam.Source == AIParamSource.Blackboard + ? frame.Unsafe.GetPointer(entity) + : null; + + var aiConfig = aiParam.Source == AIParamSource.Config + ? frame.FindAsset(aiConfigRef.Id) + : null; + + return aiParam.Resolve(frame, entity, blackboard, aiConfig); + } + } +} \ No newline at end of file diff --git a/data/AssemblyInfo.cs b/data/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..365fcd9e06b12e2e096b94ff408caa4765b806d0 --- /dev/null +++ b/data/AssemblyInfo.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("quantum.code")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("quantum.code")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: Guid("fbf32099-b197-4ab9-8e5a-b44d9d3750bd")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] +[assembly: AssemblyInformationalVersion("2.0.0 RC2N 501 2.0/develop (99c627e06)")] + diff --git a/data/BTAbort.cs b/data/BTAbort.cs new file mode 100644 index 0000000000000000000000000000000000000000..02722ff1feca146f4133899c46ff67a038926e38 --- /dev/null +++ b/data/BTAbort.cs @@ -0,0 +1,25 @@ +using System; + +namespace Quantum +{ + public enum BTAbort + { + None, + Self, + LowerPriority, + Both + } + + public static class BTAbortExtensions + { + public static Boolean IsSelf(this BTAbort abort) + { + return abort == BTAbort.Self || abort == BTAbort.Both; + } + + public static Boolean IsLowerPriority(this BTAbort abort) + { + return abort == BTAbort.LowerPriority || abort == BTAbort.Both; + } + } +} diff --git a/data/BTAgent.User.Data.cs b/data/BTAgent.User.Data.cs new file mode 100644 index 0000000000000000000000000000000000000000..3f626afcda832100f5144a54ca09944505718f0e --- /dev/null +++ b/data/BTAgent.User.Data.cs @@ -0,0 +1,96 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public unsafe partial struct BTAgent + { + #region Int and FP Data + // Getter / Setters of node FP and Int32 data + public void AddFPData(Frame frame, FP fpValue) + { + var nodesDataList = frame.ResolveList(BTDataValues); + BTDataValue newDataValue = new BTDataValue(); + *newDataValue.FPValue = fpValue; + nodesDataList.Add(newDataValue); + } + + public void AddIntData(Frame frame, Int32 intValue) + { + var nodesDataList = frame.ResolveList(BTDataValues); + BTDataValue newDataValue = new BTDataValue(); + *newDataValue.IntValue = intValue; + nodesDataList.Add(newDataValue); + } + + public void SetFPData(Frame frame, FP value, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + *nodesDataList.GetPointer(index)->FPValue = value; + } + + public void SetIntData(Frame frame, Int32 value, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + *nodesDataList.GetPointer(index)->IntValue = value; + } + + public FP GetFPData(Frame frame, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + return *nodesDataList.GetPointer(index)->FPValue; + } + + public Int32 GetIntData(Frame frame, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + return *nodesDataList.GetPointer(index)->IntValue; + } + #endregion + + // -- THREADSAFE + + #region THREADSAFE Int and FP Data + // Getter / Setters of node FP and Int32 data + public void AddFPData(FrameThreadSafe frame, FP fpValue) + { + var nodesDataList = frame.ResolveList(BTDataValues); + BTDataValue newDataValue = new BTDataValue(); + *newDataValue.FPValue = fpValue; + nodesDataList.Add(newDataValue); + } + + public void AddIntData(FrameThreadSafe frame, Int32 intValue) + { + var nodesDataList = frame.ResolveList(BTDataValues); + BTDataValue newDataValue = new BTDataValue(); + *newDataValue.IntValue = intValue; + nodesDataList.Add(newDataValue); + } + + public void SetFPData(FrameThreadSafe frame, FP value, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + *nodesDataList.GetPointer(index)->FPValue = value; + } + + public void SetIntData(FrameThreadSafe frame, Int32 value, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + *nodesDataList.GetPointer(index)->IntValue = value; + } + + public FP GetFPData(FrameThreadSafe frame, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + return *nodesDataList.GetPointer(index)->FPValue; + } + + public Int32 GetIntData(FrameThreadSafe frame, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + return *nodesDataList.GetPointer(index)->IntValue; + } + #endregion + } +} \ No newline at end of file diff --git a/data/BTAgent.User.cs b/data/BTAgent.User.cs new file mode 100644 index 0000000000000000000000000000000000000000..939e57af829869e2977fe042170a04ec81bee228 --- /dev/null +++ b/data/BTAgent.User.cs @@ -0,0 +1,235 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public unsafe partial struct BTAgent + { + // Used to setup info on the Unity debugger + public string GetTreeAssetName(Frame frame) => frame.FindAsset(Tree.Id).Path; + + public bool IsAborting => AbortNodeId != 0; + + public AIConfig GetConfig(Frame frame) + { + return frame.FindAsset(Config.Id); + } + + public void Initialize(Frame frame, EntityRef entityRef, BTAgent* agent, AssetRefBTNode tree, bool force = false) + { + if (this.Tree != default && force == false) + return; + + // -- Cache the tree + BTRoot treeAsset = frame.FindAsset(tree.Id); + this.Tree = treeAsset; + + // -- Allocate data + // Success/Fail/Running + NodesStatus = frame.AllocateList(treeAsset.NodesAmount); + + // Next tick in which each service shall be updated + ServicesEndTimes = frame.AllocateList(4); + + // Node data, such as FP for timers, Integers for IDs + BTDataValues = frame.AllocateList(4); + + // The Services contained in the current sub-tree, + // which should be updated considering its intervals + ActiveServices = frame.AllocateList(4); + + // The Dynamic Composites contained in the current sub-tree, + // which should be re-checked every tick + DynamicComposites = frame.AllocateList(4); + + // -- Cache the Blackboard (if any) + AIBlackboardComponent* blackboard = null; + if (frame.Has(entityRef)) + { + blackboard = frame.Unsafe.GetPointer(entityRef); + } + + // -- Initialize the tree + treeAsset.InitializeTree(frame, agent, blackboard); + + // -- Trigger the debugging event (mostly for the Unity side) + BTManager.OnSetupDebugger?.Invoke(entityRef, treeAsset.Path); + } + + public void Free(Frame frame) + { + Tree = default; + frame.FreeList(NodesStatus); + frame.FreeList(ServicesEndTimes); + frame.FreeList(BTDataValues); + frame.FreeList(ActiveServices); + frame.FreeList(DynamicComposites); + } + + #region Int and FP Data + // Getter / Setters of node FP and Int32 data + public void AddFPData(Frame frame, FP fpValue) + { + var nodesDataList = frame.ResolveList(BTDataValues); + BTDataValue newDataValue = new BTDataValue(); + *newDataValue.FPValue = fpValue; + nodesDataList.Add(newDataValue); + } + + public void AddIntData(Frame frame, Int32 intValue) + { + var nodesDataList = frame.ResolveList(BTDataValues); + BTDataValue newDataValue = new BTDataValue(); + *newDataValue.IntValue = intValue; + nodesDataList.Add(newDataValue); + } + + public void SetFPData(Frame frame, FP value, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + *nodesDataList.GetPointer(index)->FPValue = value; + } + + public void SetIntData(Frame frame, Int32 value, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + *nodesDataList.GetPointer(index)->IntValue = value; + } + + public FP GetFPData(Frame frame, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + return *nodesDataList.GetPointer(index)->FPValue; + } + + public Int32 GetIntData(Frame frame, Int32 index) + { + var nodesDataList = frame.ResolveList(BTDataValues); + return *nodesDataList.GetPointer(index)->IntValue; + } + #endregion + + public void Update(ref BTParams btParams) + { + if (btParams.Agent->Current == null) + { + btParams.Agent->Current = btParams.Agent->Tree; + } + + RunDynamicComposites(btParams); + + BTNode node = btParams.Frame.FindAsset(btParams.Agent->Current.Id); + UpdateSubtree(btParams, node); + + BTManager.ClearBTParams(btParams); + } + + // We run the dynamic composites contained on the current sub-tree (if any) + // If any of them result in "False", we abort the current sub-tree + // and take the execution back to the topmost decorator so the agent can choose another path + private void RunDynamicComposites(BTParams btParams) + { + var frame = btParams.Frame; + var dynamicComposites = frame.ResolveList(DynamicComposites); + + for (int i = 0; i < dynamicComposites.Count; i++) + { + var compositeRef = dynamicComposites.GetPointer(i); + var composite = frame.FindAsset(compositeRef->Id); + var dynamicResult = composite.OnDynamicRun(btParams); + + if (dynamicResult == false) + { + btParams.Agent->Current = composite.TopmostDecorator; + dynamicComposites.Remove(*compositeRef); + composite.OnReset(btParams); + return; + } + } + } + + private void UpdateSubtree(BTParams btParams, BTNode node, bool continuingAbort = false) + { + // Start updating the tree from the Current agent's node + var result = node.RunUpdate(btParams, continuingAbort); + + // If the current node completes, go up in the tree until we hit a composite + // Run that one. On success or fail continue going up. + while (result != BTStatus.Running && node.Parent != null) + { + // As we are traversing the tree up, we allow nodes to remove any + // data that is only needed locally + node.OnExit(btParams); + + node = node.Parent; + if (node.NodeType == BTNodeType.Composite) + { + ((BTComposite)node).ChildCompletedRunning(btParams, result); + result = node.RunUpdate(btParams, continuingAbort); + } + + if (node.NodeType == BTNodeType.Decorator) + { + ((BTDecorator)node).EvaluateAbortNode(btParams); + } + } + + BTService.TickServices(btParams); + + if (result != BTStatus.Running) + { + BTNode tree = btParams.Frame.FindAsset(btParams.Agent->Tree.Id); + tree.OnReset(btParams); + btParams.Agent->Current = btParams.Agent->Tree; + BTManager.OnTreeCompleted?.Invoke(btParams.Entity); + //Log.Info("Behaviour Tree completed with result '{0}'. It will re-start from '{1}'", result, btParams.Agent->Current.Id); + } + } + + public unsafe void AbortLowerPriority(BTParams btParams, BTNode node) + { + // Go up and find the next interesting node (composite or root) + var topNode = node; + while ( + topNode.NodeType != BTNodeType.Composite && + topNode.NodeType != BTNodeType.Root) + { + topNode = topNode.Parent; + } + + if (topNode.NodeType == BTNodeType.Root) + { + return; + } + + var nodeAsComposite = (topNode as BTComposite); + nodeAsComposite.AbortNodes(btParams, nodeAsComposite.GetCurrentChild(btParams.Frame, btParams.Agent) + 1); + } + + // Used to react to blackboard changes which are observed by Decorators + // This is triggered by the Blackboard Entry itself, which has a list of Decorators that observes it + public unsafe void OnDecoratorReaction(BTParams btParams, BTNode node, BTAbort abort, out bool abortSelf, out bool abortLowerPriotity) + { + abortSelf = false; + abortLowerPriotity = false; + + var status = node.GetStatus(btParams.Frame, btParams.Agent); + + if (abort.IsSelf() && (status == BTStatus.Running || status == BTStatus.Inactive)) + { + // Check condition again + if (node.DryRun(btParams) == false) + { + abortSelf = true; + node.OnAbort(btParams); + } + } + + if (abort.IsLowerPriority()) + { + AbortLowerPriority(btParams, node); + abortLowerPriotity = true; + } + } + } +} \ No newline at end of file diff --git a/data/BTBlackboardCompare.cs b/data/BTBlackboardCompare.cs new file mode 100644 index 0000000000000000000000000000000000000000..02c32c4ccdce6af73317022d609270e153a1df49 --- /dev/null +++ b/data/BTBlackboardCompare.cs @@ -0,0 +1,51 @@ +using System; + +namespace Quantum +{ + /// + /// Reactive Decorator sample. Listens to changes on two Blackboard entries. + /// + [Serializable] + public unsafe class BTBlackboardCompare : BTDecorator + { + // We let the user define, on the Visual Editor, which Blackboard entries + // shall be observed by this Decorator + public AIBlackboardValueKey BlackboardKeyA; + public AIBlackboardValueKey BlackboardKeyB; + + public override void OnEnter(BTParams btParams) + { + base.OnEnter(btParams); + + // Whenever we enter this Decorator... + // We register it as a Reactive Decorator so, whenever the entries are changed, + // the DryRun is executed again, possibly aborting the current execution + btParams.Blackboard->RegisterReactiveDecorator(btParams.Frame, BlackboardKeyA.Key, this); + btParams.Blackboard->RegisterReactiveDecorator(btParams.Frame, BlackboardKeyB.Key, this); + } + + public override void OnExit(BTParams btParams) + { + base.OnExit(btParams); + // Whenever the execution goes higher, it means that this Decorator isn't in the current subtree anymore + // So we unregister this Decorator from the Reactive list. This means that if the Blackboard entries + // get changed, this Decorator will not react anymore + btParams.Blackboard->UnregisterReactiveDecorator(btParams.Frame, BlackboardKeyA.Key, this); + btParams.Blackboard->UnregisterReactiveDecorator(btParams.Frame, BlackboardKeyB.Key, this); + } + + // We just check if A is greater than B. If that's the case + // PS: this gets called in THREE possible situations: + // 1 - When the execution is goign DOWN on the tree and this Decorator is found + // 2 - If changes to the observed blackboard entries happen + // 3 - If this is inside a Dynamic Composite node + public override Boolean DryRun(BTParams btParams) + { + var blackboard = btParams.Blackboard; + var A = blackboard->GetInteger(btParams.Frame, BlackboardKeyA.Key); + var B = blackboard->GetInteger(btParams.Frame, BlackboardKeyB.Key); + + return A > B; + } + } +} diff --git a/data/BTComposite.cs b/data/BTComposite.cs new file mode 100644 index 0000000000000000000000000000000000000000..f9ad47e0130a2c0bb64e3ab436307200506ef484 --- /dev/null +++ b/data/BTComposite.cs @@ -0,0 +1,178 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public unsafe abstract partial class BTComposite : BTNode + { + public AssetRefBTNode[] Children; + public AssetRefBTService[] Services; + [BotSDKHidden] public AssetRefBTNode TopmostDecorator; + + public BTDataIndex CurrentChildIndex; + + protected BTNode[] _childInstances; + protected BTService[] _serviceInstances; + protected BTNode _topmostDecoratorInstance; + + public bool IsDynamic; + + public BTNode[] ChildInstances + { + get + { + return _childInstances; + } + } + + public BTService[] ServiceInstances + { + get + { + return _serviceInstances; + } + } + + public override BTNodeType NodeType + { + get + { + return BTNodeType.Composite; + } + } + + internal Int32 GetCurrentChild(Frame frame, BTAgent* agent) + { + Byte currentChild = (Byte)agent->GetIntData(frame, CurrentChildIndex.Index); + return currentChild; + } + + internal void SetCurrentChild(Frame frame, Int32 currentIndex, BTAgent* agent) + { + agent->SetIntData(frame, currentIndex, CurrentChildIndex.Index); + } + + /// + /// When a Composite node is Updated, it only increase the current child updated + /// when the child results in either FAIL/SUCCESS. So we need this callback + /// to be used when the child was RUNNING and then had some result, to properly increase the current + /// child ID + /// + /// + /// + internal virtual void ChildCompletedRunning(BTParams btParams, BTStatus childResult) + { + } + + public override void Init(Frame frame, AIBlackboardComponent* blackboard, BTAgent* agent) + { + base.Init(frame, blackboard, agent); + + agent->AddIntData(frame, 0); + + for (Int32 i = 0; i < Services.Length; i++) + { + BTService service = frame.FindAsset(Services[i].Id); + service.Init(frame, agent, blackboard); + } + } + + public override void OnEnter(BTParams btParams) + { + BTManager.OnNodeEnter?.Invoke(btParams.Entity, Guid.Value); + SetCurrentChild(btParams.Frame, 0, btParams.Agent); + } + + public override void OnEnterRunning(BTParams btParams) + { + var activeServicesList = btParams.Frame.ResolveList(btParams.Agent->ActiveServices); + + for (Int32 i = 0; i < _serviceInstances.Length; i++) + { + _serviceInstances[i].OnEnter(btParams); + + activeServicesList.Add(Services[i]); + } + + if (IsDynamic == true) + { + var dynamicComposites = btParams.Frame.ResolveList(btParams.Agent->DynamicComposites); + dynamicComposites.Add(this); + } + } + + public override void OnReset(BTParams btParams) + { + base.OnReset(btParams); + + OnExit(btParams); + + for (Int32 i = 0; i < _childInstances.Length; i++) + _childInstances[i].OnReset(btParams); + } + + public override void OnExit(BTParams btParams) + { + base.OnExit(btParams); + + BTManager.OnNodeExit?.Invoke(btParams.Entity, Guid.Value); + + var activeServicesList = btParams.Frame.ResolveList(btParams.Agent->ActiveServices); + for (Int32 i = 0; i < _serviceInstances.Length; i++) + { + activeServicesList.Remove(Services[i]); + } + + if (IsDynamic == true) + { + var dynamicComposites = btParams.Frame.ResolveList(btParams.Agent->DynamicComposites); + dynamicComposites.Remove(this); + } + } + + public override bool OnDynamicRun(BTParams btParams) + { + if (_topmostDecoratorInstance != null) + { + return _topmostDecoratorInstance.OnDynamicRun(btParams); + } + + return true; + } + + + public void AbortNodes(BTParams btParams, Int32 firstIndex = 0) + { + for (int i = firstIndex; i < _childInstances.Length; i++) + { + _childInstances[i].SetStatus(btParams.Frame, BTStatus.Abort, btParams.Agent); + } + } + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + // Cache the child assets links + _childInstances = new BTNode[Children.Length]; + for (Int32 i = 0; i < Children.Length; i++) + { + _childInstances[i] = (BTNode)resourceManager.GetAsset(Children[i].Id); + _childInstances[i].Parent = this; + _childInstances[i].ParentIndex = i; + } + + // Cache the service assets links + _serviceInstances = new BTService[Services.Length]; + for (Int32 i = 0; i < Services.Length; i++) + { + _serviceInstances[i] = (BTService)resourceManager.GetAsset(Services[i].Id); + } + + if (TopmostDecorator != null) + { + _topmostDecoratorInstance = (BTDecorator)resourceManager.GetAsset(TopmostDecorator.Id); + } + } + } +} \ No newline at end of file diff --git a/data/BTCooldown.cs b/data/BTCooldown.cs new file mode 100644 index 0000000000000000000000000000000000000000..c12290bacf768d52f88cfe06fd0115d27b6550da --- /dev/null +++ b/data/BTCooldown.cs @@ -0,0 +1,54 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + [Serializable] + public unsafe partial class BTCooldown : BTDecorator + { + // How many time should we wait + public FP CooldownTime; + + // An indexer so we know when the time started counting + public BTDataIndex StartTimeIndex; + + public override void Init(Frame frame, AIBlackboardComponent* blackboard, BTAgent* agent) + { + base.Init(frame, blackboard, agent); + + // We allocate space on the BTAgent so we can store the Start Time + agent->AddFPData(frame, 0); + } + + protected override BTStatus OnUpdate(BTParams btParams) + { + var result = base.OnUpdate(btParams); + + // We let the time check, which happens on the DryRun, happen + // If it results in success, then we store on the BTAgent the time value of the moment that it happened + if (result == BTStatus.Success) + { + var currentTime = btParams.Frame.DeltaTime * btParams.Frame.Number; + + var frame = btParams.Frame; + var entity = btParams.Entity; + btParams.Agent->SetFPData(frame, currentTime, StartTimeIndex.Index); + } + + return result; + } + + // We get the Start Time stored on the BTAgent, then we check if the time + cooldown is already over + // If it is not over, then we return False, blocking the execution of the children nodes + public override Boolean DryRun(BTParams btParams) + { + var frame = btParams.Frame; + var entity = btParams.Entity; + FP startTime = btParams.Agent->GetFPData(frame, StartTimeIndex.Index); + + var currentTime = btParams.Frame.DeltaTime * btParams.Frame.Number; + + return currentTime >= startTime + CooldownTime; + } + } +} diff --git a/data/BTDataIndex.User.cs b/data/BTDataIndex.User.cs new file mode 100644 index 0000000000000000000000000000000000000000..405e9c915181148f71ac25ed23f71ad41928cb04 --- /dev/null +++ b/data/BTDataIndex.User.cs @@ -0,0 +1,11 @@ +using System; + +namespace Quantum +{ + [BotSDKHidden] + [Serializable] + // Used so we can track it easier on the Visual Editor + partial struct BTDataIndex + { + } +} diff --git a/data/BTDecorator.cs b/data/BTDecorator.cs new file mode 100644 index 0000000000000000000000000000000000000000..b0538c300bb33d581b9dbdcc7e23e61d3557e438 --- /dev/null +++ b/data/BTDecorator.cs @@ -0,0 +1,97 @@ +using Photon.Deterministic; + +namespace Quantum +{ + public abstract unsafe partial class BTDecorator : BTNode + { + [BotSDKHidden] public AssetRefBTNode Child; + protected BTNode _childInstance; + public BTAbort AbortType; + + public BTNode ChildInstance + { + get + { + return _childInstance; + } + } + + public override BTNodeType NodeType + { + get + { + return BTNodeType.Decorator; + } + } + + public override void OnReset(BTParams btParams) + { + base.OnReset(btParams); + + OnExit(btParams); + + if (_childInstance != null) + _childInstance.OnReset(btParams); + + BTManager.OnDecoratorReset?.Invoke(btParams.Entity, Guid.Value); + } + + public override void OnExit(BTParams btParams) + { + base.OnExit(btParams); + } + + protected override BTStatus OnUpdate(BTParams btParams) + { + if (DryRun(btParams) == true) + { + BTManager.OnDecoratorChecked?.Invoke(btParams.Entity, Guid.Value, true); + + if (_childInstance != null) + { + var childResult = _childInstance.RunUpdate(btParams); + if (childResult == BTStatus.Abort) + { + EvaluateAbortNode(btParams); + SetStatus(btParams.Frame, BTStatus.Abort, btParams.Agent); + return BTStatus.Abort; + } + + return childResult; + } + + return BTStatus.Success; + } + + BTManager.OnDecoratorChecked?.Invoke(btParams.Entity, Guid.Value, false); + + return BTStatus.Failure; + } + + public override bool OnDynamicRun(BTParams btParams) + { + var result = DryRun(btParams); + if (result == false) + { + return false; + } + else if (ChildInstance.NodeType != BTNodeType.Decorator) + { + return true; + } + else + { + return ChildInstance.OnDynamicRun(btParams); + } + } + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + // Cache the child + _childInstance = (BTNode)resourceManager.GetAsset(Child.Id); + _childInstance.Parent = this; + } + } +} \ No newline at end of file diff --git a/data/BTForceResult.cs b/data/BTForceResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..96c3a7915787ced5882eaf4655ca4c56d9ac8df9 --- /dev/null +++ b/data/BTForceResult.cs @@ -0,0 +1,23 @@ +using System; + +namespace Quantum +{ + [Serializable] + public unsafe partial class BTForceResult : BTDecorator + { + public BTStatus Result; + + protected override BTStatus OnUpdate(BTParams btParams) + { + if (_childInstance != null) + _childInstance.RunUpdate(btParams); + + return Result; + } + + public override Boolean DryRun(BTParams btParams) + { + return true; + } + } +} diff --git a/data/BTLeaf.cs b/data/BTLeaf.cs new file mode 100644 index 0000000000000000000000000000000000000000..932bf607a008b9a66ca64685560cc90072620a95 --- /dev/null +++ b/data/BTLeaf.cs @@ -0,0 +1,82 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public unsafe abstract partial class BTLeaf : BTNode + { + public AssetRefBTService[] Services; + protected BTService[] _serviceInstances; + public BTService[] ServiceInstances + { + get + { + return _serviceInstances; + } + } + + public override BTNodeType NodeType + { + get + { + return BTNodeType.Leaf; + } + } + + public override unsafe void Init(Frame frame, AIBlackboardComponent* blackboard, BTAgent* agent) + { + base.Init(frame, blackboard, agent); + + for (int i = 0; i < Services.Length; i++) + { + BTService service = frame.FindAsset(Services[i].Id); + service.Init(frame, agent, blackboard); + } + } + + public override void OnEnterRunning(BTParams btParams) + { + var activeServicesList = btParams.Frame.ResolveList(btParams.Agent->ActiveServices); + for (int i = 0; i < _serviceInstances.Length; i++) + { + _serviceInstances[i].OnEnter(btParams); + activeServicesList.Add(Services[i]); + } + } + + public override void OnEnter(BTParams btParams) + { + base.OnEnter(btParams); + BTManager.OnNodeEnter?.Invoke(btParams.Entity, Guid.Value); + } + + public override void OnExit(BTParams btParams) + { + var activeServicesList = btParams.Frame.ResolveList(btParams.Agent->ActiveServices); + for (Int32 i = 0; i < _serviceInstances.Length; i++) + { + activeServicesList.Remove(Services[i]); + } + + BTManager.OnNodeExit?.Invoke(btParams.Entity, Guid.Value); + } + + public override void OnReset(BTParams btParams) + { + base.OnReset(btParams); + OnExit(btParams); + } + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + // Cache the service assets links + _serviceInstances = new BTService[Services.Length]; + for (int i = 0; i < Services.Length; i++) + { + _serviceInstances[i] = (BTService)resourceManager.GetAsset(Services[i].Id); + } + } + } +} \ No newline at end of file diff --git a/data/BTLoop.cs b/data/BTLoop.cs new file mode 100644 index 0000000000000000000000000000000000000000..7f244ed555cecd76d970d1b4e78fa7408a8aea16 --- /dev/null +++ b/data/BTLoop.cs @@ -0,0 +1,87 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + [Serializable] + public unsafe partial class BTLoop : BTDecorator + { + public Int32 LoopIterations; + public Boolean LoopForever; + public FP LoopTimeout = -FP._1; + + public BTDataIndex StartTimeIndex; + public BTDataIndex IterationCountIndex; + + public override void Init(Frame frame, AIBlackboardComponent* blackboard, BTAgent* agent) + { + base.Init(frame, blackboard, agent); + + agent->AddFPData(frame, 0); + agent->AddIntData(frame, 0); + } + + public override void OnEnter(BTParams btParams) + { + base.OnEnter(btParams); + + var frame = btParams.Frame; + var currentTime = frame.DeltaTime * frame.Number; + + btParams.Agent->SetFPData(frame, currentTime, StartTimeIndex.Index); + btParams.Agent->SetIntData(frame, 0, IterationCountIndex.Index); + } + + protected override BTStatus OnUpdate(BTParams btParams) + { + var frame = btParams.Frame; + + int iteration = btParams.Agent->GetIntData(frame, IterationCountIndex.Index) + 1; + btParams.Agent->SetIntData(frame, iteration, IterationCountIndex.Index); + + if (DryRun(btParams) == false) + { + return BTStatus.Success; + } + + var childResult = BTStatus.Failure; + if (_childInstance != null) + { + _childInstance.SetStatus(btParams.Frame, BTStatus.Inactive, btParams.Agent); + childResult = _childInstance.RunUpdate(btParams); + } + + return childResult; + } + + public override Boolean DryRun(BTParams btParams) + { + if (LoopForever && LoopTimeout < FP._0) + { + return true; + } + else if (LoopForever) + { + var frame = btParams.Frame; + FP startTime = btParams.Agent->GetFPData(frame, StartTimeIndex.Index); + + var currentTime = frame.DeltaTime * frame.Number; + if (currentTime < startTime + LoopTimeout) + { + return true; + } + } + else + { + var frame = btParams.Frame; + int iteration = btParams.Agent->GetIntData(frame, IterationCountIndex.Index); + if (iteration <= LoopIterations) + { + return true; + } + } + + return false; + } + } +} diff --git a/data/BTNode.cs b/data/BTNode.cs new file mode 100644 index 0000000000000000000000000000000000000000..32aaa1c793e15e484402187c2038566673cc46f5 --- /dev/null +++ b/data/BTNode.cs @@ -0,0 +1,178 @@ +using System; + +namespace Quantum +{ + + public unsafe abstract partial class BTNode + { + + [BotSDKHidden] public String Label; + [BotSDKHidden] public Int32 Id; + + [NonSerialized] internal BTNode Parent; + [NonSerialized] internal Int32 ParentIndex; + + public abstract BTNodeType NodeType { get; } + + /// + /// Called once, for every Node, when the BT is being initialized + /// + public virtual void Init(Frame frame, AIBlackboardComponent* blackboard, BTAgent* agent) + { + var statusList = frame.ResolveList(agent->NodesStatus); + statusList.Add(0); + } + + // -- STATUS -- + public BTStatus GetStatus(Frame frame, BTAgent* agent) + { + var nodesAndStatus = frame.ResolveList(agent->NodesStatus); + return (BTStatus)nodesAndStatus[Id]; + } + + public void SetStatus(Frame frame, BTStatus status, BTAgent* agent) + { + var nodesAndStatus = frame.ResolveList(agent->NodesStatus); + nodesAndStatus[Id] = (Byte)status; + } + + /// + /// Called whenever the BT execution includes this node as part of the current context + /// + /// + public virtual void OnEnter(BTParams btParams) { } + + public virtual void OnEnterRunning(BTParams btParams) { } + + /// + /// Called when traversing the tree upwards and the node is already finished with its job. + /// Used by Composites and Leafs to remove their Services from the list of active services + /// as it is not anymore part of the current subtree. + /// Dynamic Composites also remove themselves + /// + /// + public virtual void OnExit(BTParams btParams) { } + + public virtual void OnAbort(BTParams btParams) + { + } + + /// + /// Called when getting out of a sub-branch and this node is being discarded + /// + /// + public unsafe virtual void OnReset(BTParams btParams) + { + SetStatus(btParams.Frame, BTStatus.Inactive, btParams.Agent); + } + + public void EvaluateAbortNode(BTParams btParams) + { + if (btParams.Agent->AbortNodeId == Id) + { + btParams.Agent->AbortNodeId = 0; + } + } + + public BTStatus RunUpdate(BTParams btParams, bool continuingAbort = false) + { + var oldStatus = GetStatus(btParams.Frame, btParams.Agent); + + if (oldStatus == BTStatus.Success || oldStatus == BTStatus.Failure) + { + return oldStatus; + } + + if (oldStatus == BTStatus.Abort) + { + if (btParams.Agent->IsAborting == true) + { + EvaluateAbortNode(btParams); + } + return oldStatus; + } + + // If this node was inactive, this means that we're entering on it for the first time, so we call OnEnter + // An exception from this rule is when we chose this node to continue an abort process. In that case, + // we already executed OnEnter before, so we don't repeat it + if (oldStatus == BTStatus.Inactive && continuingAbort == false) + { + OnEnter(btParams); + } + + var newStatus = BTStatus.Failure; + try + { + newStatus = OnUpdate(btParams); + + if (btParams.Agent->IsAborting) + { + newStatus = BTStatus.Abort; + } + + // Used for debugging purposes + if (newStatus == BTStatus.Success) + { + BTManager.OnNodeSuccess?.Invoke(btParams.Entity, Guid.Value); + BTManager.OnNodeExit?.Invoke(btParams.Entity, Guid.Value); + } + + if (newStatus == BTStatus.Failure) + { + BTManager.OnNodeFailure?.Invoke(btParams.Entity, Guid.Value); + BTManager.OnNodeExit?.Invoke(btParams.Entity, Guid.Value); + } + } + catch (Exception e) + { + Log.Error("Exception in Behaviour Tree node '{0}' ({1}) - setting node status to Failure", Label, Guid); + Log.Exception(e); + } + + SetStatus(btParams.Frame, newStatus, btParams.Agent); + + if ((newStatus == BTStatus.Running || newStatus == BTStatus.Success) && + (oldStatus == BTStatus.Failure || oldStatus == BTStatus.Inactive)) + { + OnEnterRunning(btParams); + } + + if (newStatus == BTStatus.Running && NodeType == BTNodeType.Leaf) + { + // If we are a leaf, we can store the current node + // We know that there has only one leaf node running at any time, no parallel branches possible + // The Run() method also return a tuple + btParams.Agent->Current = this; + } + + return newStatus; + } + + /// + /// Used by Decorators to evaluate if a condition succeeds or not. + /// Upon success, allow the flow to continue. + /// Upon failure, blocks the execution so another path is taken + /// + /// + /// + public virtual Boolean DryRun(BTParams btParams) + { + return false; + } + + public virtual Boolean OnDynamicRun(BTParams btParams) + { + return true; + } + + /// + /// Called every tick while this Node is part of the current sub-tree. + /// Returning "Success/Failure" will make the tree continue its execution. + /// Returning "Running" will store this Node as the Current Node and re-execute it on the next frame + /// unless something else interrputs + /// + /// + /// + protected abstract BTStatus OnUpdate(BTParams btParams); + } +} \ No newline at end of file diff --git a/data/BTNodeType.cs b/data/BTNodeType.cs new file mode 100644 index 0000000000000000000000000000000000000000..4beed57a5881615e1ae244cfea232f4bd6370f32 --- /dev/null +++ b/data/BTNodeType.cs @@ -0,0 +1,11 @@ +namespace Quantum +{ + public enum BTNodeType + { + Root, + Leaf, + Decorator, + Composite, + Service + } +} diff --git a/data/BTParams.cs b/data/BTParams.cs new file mode 100644 index 0000000000000000000000000000000000000000..dbb4ecd3319106eadf8f3f4b46bb4ecfa510a770 --- /dev/null +++ b/data/BTParams.cs @@ -0,0 +1,44 @@ +using System.Runtime.InteropServices; + +namespace Quantum +{ + [StructLayout(LayoutKind.Auto)] + public unsafe partial struct BTParams + { + private Frame _frame; + private BTAgent* _agent; + private EntityRef _entity; + private AIBlackboardComponent* _blackboard; + + private BTParamsUser _userParams; + + public Frame Frame { get => _frame; } + public BTAgent* Agent { get => _agent; } + public EntityRef Entity { get => _entity; } + public AIBlackboardComponent* Blackboard { get => _blackboard; } + + public BTParamsUser UserParams { get => _userParams; set => _userParams = value; } + + public void SetDefaultParams(Frame frame, BTAgent* agent, EntityRef entity, AIBlackboardComponent* blackboard = null) + { + _frame = frame; + _agent = agent; + _entity = entity; + _blackboard = blackboard; + } + + public void Reset(Frame frame) + { + _frame = default; + _agent = default; + _entity = default; + _blackboard = default; + + _userParams = default; + } + } + + public partial struct BTParamsUser + { + } +} \ No newline at end of file diff --git a/data/BTRoot.cs b/data/BTRoot.cs new file mode 100644 index 0000000000000000000000000000000000000000..309e949742a951b7db614c9a23112b7d94104b7e --- /dev/null +++ b/data/BTRoot.cs @@ -0,0 +1,58 @@ +using Photon.Deterministic; +using System; +using System.Collections.Generic; + +namespace Quantum +{ + [Serializable] + public unsafe partial class BTRoot : BTDecorator + { + [BotSDKHidden] public Int32 NodesAmount; + + public override BTNodeType NodeType + { + get + { + return BTNodeType.Root; + } + } + + protected unsafe override BTStatus OnUpdate(BTParams btParams) + { + + btParams.Agent->Current = this; + + if (_childInstance != null) + { + return _childInstance.RunUpdate(btParams); + } + + return BTStatus.Success; + } + + public void InitializeTree(Frame frame, BTAgent* agent, AIBlackboardComponent* blackboard) + { + InitNodesRecursively(frame, this, agent, blackboard); + } + + private static void InitNodesRecursively(Frame frame, BTNode node, BTAgent* agent, AIBlackboardComponent* blackboard) + { + node.Init(frame, blackboard, agent); + + if (node is BTDecorator decoratorNode) + { + BTNode childNode = frame.FindAsset(decoratorNode.Child.Id); + InitNodesRecursively(frame, childNode, agent, blackboard); + } + + if (node is BTComposite compositeNode) + { + foreach (var child in compositeNode.Children) + { + BTNode childNode = frame.FindAsset(child.Id); + InitNodesRecursively(frame, childNode, agent, blackboard); + } + } + } + } +} \ No newline at end of file diff --git a/data/BTSelector.cs b/data/BTSelector.cs new file mode 100644 index 0000000000000000000000000000000000000000..5fedaea6e4533b9223527e304dccce4862eb0109 --- /dev/null +++ b/data/BTSelector.cs @@ -0,0 +1,64 @@ +using System; + +namespace Quantum +{ + + /// + /// The selector task is similar to an or operation. It will return success as soon as one of its child tasks return success. + /// If a child task returns failure then it will sequentially run the next task. If no child task returns success then it will return failure. + /// + [Serializable] + public unsafe partial class BTSelector : BTComposite + { + protected override BTStatus OnUpdate(BTParams btParams) + { + BTStatus status = BTStatus.Success; + + while (GetCurrentChild(btParams.Frame, btParams.Agent) < _childInstances.Length) + { + var currentChildId = GetCurrentChild(btParams.Frame, btParams.Agent); + var child = _childInstances[currentChildId]; + status = child.RunUpdate(btParams); + + if (status == BTStatus.Abort && btParams.Agent->IsAborting == true) + { + return BTStatus.Abort; + } + + if (status == BTStatus.Failure || status == BTStatus.Abort) + { + SetCurrentChild(btParams.Frame, currentChildId + 1, btParams.Agent); + } + else + break; + } + + return status; + } + + internal override void ChildCompletedRunning(BTParams btParams, BTStatus childResult) + { + if (childResult == BTStatus.Abort) + { + return; + } + + if (childResult == BTStatus.Failure) + { + var currentChild = GetCurrentChild(btParams.Frame, btParams.Agent); + SetCurrentChild(btParams.Frame, currentChild + 1, btParams.Agent); + } + else + { + SetCurrentChild(btParams.Frame, _childInstances.Length, btParams.Agent); + + // If the child succeeded, then we already know that this sequence succeeded, so we can force it + SetStatus(btParams.Frame, BTStatus.Success, btParams.Agent); + + // Trigger the debugging callbacks + BTManager.OnNodeSuccess?.Invoke(btParams.Entity, Guid.Value); + BTManager.OnNodeExit?.Invoke(btParams.Entity, Guid.Value); + } + } + } +} \ No newline at end of file diff --git a/data/BTSequence.cs b/data/BTSequence.cs new file mode 100644 index 0000000000000000000000000000000000000000..deefa3674565505d4bccd50ac5a7d69675781684 --- /dev/null +++ b/data/BTSequence.cs @@ -0,0 +1,72 @@ +using System; + +namespace Quantum +{ + /// + /// The sequence task is similar to an and operation. It will return failure as soon as one of its child tasks return failure. + /// If a child task returns success then it will sequentially run the next task. If all child tasks return success then it will return success. + /// + [Serializable] + public unsafe partial class BTSequence : BTComposite + { + protected override BTStatus OnUpdate(BTParams btParams) + { + BTStatus status = BTStatus.Success; + + while (GetCurrentChild(btParams.Frame, btParams.Agent) < _childInstances.Length) + { + var currentChildId = GetCurrentChild(btParams.Frame, btParams.Agent); + var child = _childInstances[currentChildId]; + status = child.RunUpdate(btParams); + + if (status == BTStatus.Abort) + { + if (btParams.Agent->IsAborting == true) + { + return BTStatus.Abort; + } + else + { + return BTStatus.Failure; + } + } + + if (status == BTStatus.Success) + { + SetCurrentChild(btParams.Frame, currentChildId + 1, btParams.Agent); + } + else + { + break; + } + } + + return status; + } + + internal override void ChildCompletedRunning(BTParams btParams, BTStatus childResult) + { + if (childResult == BTStatus.Abort) + { + return; + } + + if (childResult == BTStatus.Failure) + { + SetCurrentChild(btParams.Frame, _childInstances.Length, btParams.Agent); + + // If the child failed, then we already know that this sequence failed, so we can force it + SetStatus(btParams.Frame, BTStatus.Failure, btParams.Agent); + + // Trigger the debugging callbacks + BTManager.OnNodeFailure?.Invoke(btParams.Entity, Guid.Value); + BTManager.OnNodeExit?.Invoke(btParams.Entity, Guid.Value); + } + else + { + var currentChild = GetCurrentChild(btParams.Frame, btParams.Agent); + SetCurrentChild(btParams.Frame, currentChild + 1, btParams.Agent); + } + } + } +} \ No newline at end of file diff --git a/data/BTService.cs b/data/BTService.cs new file mode 100644 index 0000000000000000000000000000000000000000..2aa85a95189c5c88f27dcda5c100bd2c9122fdbe --- /dev/null +++ b/data/BTService.cs @@ -0,0 +1,70 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public unsafe abstract partial class BTService + { + public FP IntervalInSec; + + [BotSDKHidden] public Int32 Id; + + public virtual void Init(Frame frame, BTAgent* agent, AIBlackboardComponent* blackboard) + { + var endTimesList = frame.ResolveList(agent->ServicesEndTimes); + endTimesList.Add(0); + } + + public void SetEndTime(Frame frame, BTAgent* agent) + { + var endTimesList = frame.ResolveList(agent->ServicesEndTimes); + endTimesList[Id] = frame.BotSDKGameTime + IntervalInSec; + } + + public FP GetEndTime(Frame frame, BTAgent* agent) + { + var endTime = frame.ResolveList(agent->ServicesEndTimes); + return endTime[Id]; + } + + public virtual void RunUpdate(BTParams btParams) + { + var endTime = GetEndTime(btParams.Frame, btParams.Agent); + if (btParams.Frame.BotSDKGameTime >= endTime) + { + OnUpdate(btParams); + SetEndTime(btParams.Frame, btParams.Agent); + } + } + + public virtual void OnEnter(BTParams btParams) + { + SetEndTime(btParams.Frame, btParams.Agent); + } + + /// + /// Called whenever the Service is part of the current subtree + /// and its waiting time is already over + /// + protected abstract void OnUpdate(BTParams btParams); + + public static void TickServices(BTParams btParams) + { + var activeServicesList = btParams.Frame.ResolveList(btParams.Agent->ActiveServices); + + for (int i = 0; i < activeServicesList.Count; i++) + { + var service = btParams.Frame.FindAsset(activeServicesList[i].Id); + try + { + service.RunUpdate(btParams); + } + catch (Exception e) + { + Log.Error("Exception in Behaviour Tree service '{0}' ({1}) - setting node status to Failure", service.GetType().ToString(), service.Guid); + Log.Exception(e); + } + } + } + } +} diff --git a/data/BTStatus.cs b/data/BTStatus.cs new file mode 100644 index 0000000000000000000000000000000000000000..a92ccf6216e53a4add2d5ab2e170175e354204c4 --- /dev/null +++ b/data/BTStatus.cs @@ -0,0 +1,11 @@ +namespace Quantum +{ + public enum BTStatus + { + Inactive, + Success, + Failure, + Running, + Abort + } +} diff --git a/data/BallPoolSpec.cs b/data/BallPoolSpec.cs new file mode 100644 index 0000000000000000000000000000000000000000..ad2aa7fcd2b21942da62740ccbe4c967c8244793 --- /dev/null +++ b/data/BallPoolSpec.cs @@ -0,0 +1,14 @@ +using System; +using Photon.Deterministic; + +namespace Quantum +{ + unsafe partial class BallPoolSpec + { + public FP SpinMultiplier; + public FP EndOfMovementVelocityThreshold; + public Int32 EndOfMovementWaitingInTicks; + + public Int32 Layer { get; set; } + } +} diff --git a/data/BehaviourTree.Manager.cs b/data/BehaviourTree.Manager.cs new file mode 100644 index 0000000000000000000000000000000000000000..155d20154887eead4b2d7f60f291eeef2797d028 --- /dev/null +++ b/data/BehaviourTree.Manager.cs @@ -0,0 +1,76 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public static unsafe partial class BTManager + { + public static Action OnSetupDebugger; + + public static Action OnNodeEnter; + public static Action OnNodeExit; + public static Action OnNodeSuccess; + public static Action OnNodeFailure; + public static Action OnDecoratorChecked; + public static Action OnDecoratorReset; + public static Action OnTreeCompleted; + + /// + /// Call this once, to initialize the BTAgent. + /// This method internally looks for a Blackboard Component on the entity + /// and passes it down the pipeline. + /// + /// + /// + /// + public static void Init(Frame frame, EntityRef entity, BTRoot root) + { + if (frame.Unsafe.TryGetPointer(entity, out BTAgent* agent)) + { + agent->Initialize(frame, entity, agent, root, true); + } + else + { + Log.Error("[Bot SDK] Tried to initialize an entity which has no BTAgent component"); + } + } + + /// + /// Made for internal use only. + /// + public static void ClearBTParams(BTParams btParams) + { + btParams.Reset(btParams.Frame); + } + + /// + /// Call this method every frame to update your BT Agent. + /// You can optionally pass a Blackboard Component to it, if your Agent use it + /// + public static void Update(Frame frame, EntityRef entity, AIBlackboardComponent* blackboard = null) + { + var agent = frame.Unsafe.GetPointer(entity); + BTParams btParams = new BTParams(); + btParams.SetDefaultParams(frame, agent, entity, blackboard); + + agent->Update(ref btParams); + } + + /// + /// CAUTION: Use this overload with care.
+ /// It allows the definition of custom parameters which are passed through the entire BT pipeline, for easy access.
+ /// The user parameters struct needs to be created from scratch every time BEFORE calling the BT Update method.
+ /// Make sure to also implement BTParamsUser.ClearUser(frame). + ///
+ /// Used to define custom user data. It needs to be created from scratch every time before calling this method. + public static void Update(Frame frame, EntityRef entity, ref BTParamsUser userParams, AIBlackboardComponent* blackboard = null) + { + var agent = frame.Unsafe.GetPointer(entity); + BTParams btParams = new BTParams(); + btParams.SetDefaultParams(frame, agent, entity, blackboard); + btParams.UserParams = userParams; + + agent->Update(ref btParams); + } + } +} diff --git a/data/Blackboard.qtn b/data/Blackboard.qtn new file mode 100644 index 0000000000000000000000000000000000000000..808cf6696b6a9604e429e83252af79432f526d5d --- /dev/null +++ b/data/Blackboard.qtn @@ -0,0 +1,23 @@ +asset AIBlackboard; +asset AIBlackboardInitializer; + +union BlackboardValue { + QBoolean BooleanValue; + byte ByteValue; + Int32 IntegerValue; + FP FPValue; + FPVector2 FPVector2Value; + FPVector3 FPVector3Value; + entity_ref EntityRefValue; +} + +component AIBlackboardComponent { + asset_ref Board; + list Entries; +} + + +struct BlackboardEntry{ + BlackboardValue Value; + list ReactiveDecorators; +} \ No newline at end of file diff --git a/data/BlackboardEntry.cs b/data/BlackboardEntry.cs new file mode 100644 index 0000000000000000000000000000000000000000..da9c654cc0d35b6efc3e7489b6eb56af58356b20 --- /dev/null +++ b/data/BlackboardEntry.cs @@ -0,0 +1,36 @@ +using Quantum.Collections; + +namespace Quantum +{ + public unsafe partial struct BlackboardEntry + { + /// + /// Iterate through all Decorators that watches this Blackboard entry + /// Re-check the Decorators so it can check if an abort is needed + /// + /// + public void TriggerDecorators(BTParams btParams) + { + var frame = btParams.Frame; + + // If the reactive decorators list was already allocated... + if (ReactiveDecorators.Ptr != default) + { + // Solve it and trigger the decorators checks + var reactiveDecorators = frame.ResolveList(ReactiveDecorators); + for (int i = 0; i < reactiveDecorators.Count; i++) + { + var reactiveDecoratorRef = reactiveDecorators[i]; + var decoratorInstance = frame.FindAsset(reactiveDecoratorRef.Id); + btParams.Agent->OnDecoratorReaction(btParams, decoratorInstance, decoratorInstance.AbortType, out bool abortSelf, out bool abortLowerPriority); + + // If at least one Decorator resulted in abort, we stop and return already + if (abortSelf == true) + { + btParams.Agent->AbortNodeId = decoratorInstance.Id; + } + } + } + } + } +} diff --git a/data/BlackboardValue.cs b/data/BlackboardValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..e4295de84119eb60215bd73db727e2074c6efee3 --- /dev/null +++ b/data/BlackboardValue.cs @@ -0,0 +1,42 @@ +using System; + +namespace Quantum +{ + public unsafe partial struct BlackboardValue + { + public String ValueToString() + { + switch (Field) + { + case BlackboardValue.BOOLEANVALUE: return string.Format("{0}", _BooleanValue); + case BlackboardValue.BYTEVALUE: return string.Format("{0}", _ByteValue); + case BlackboardValue.INTEGERVALUE: return string.Format("{0}", _IntegerValue); + case BlackboardValue.FPVALUE: return string.Format("{0}", _FPValue); + case BlackboardValue.FPVECTOR2VALUE: return string.Format("{0}", _FPVector2Value); + case BlackboardValue.FPVECTOR3VALUE: return string.Format("{0}", _FPVector3Value); + case BlackboardValue.ENTITYREFVALUE: return string.Format("{0}", _EntityRefValue); + } + + return base.ToString(); + } + } + + public unsafe partial struct BlackboardValue + { + public String TypeToString() + { + switch (Field) + { + case BlackboardValue.BOOLEANVALUE: return "Boolean"; + case BlackboardValue.BYTEVALUE: return "Byte"; + case BlackboardValue.INTEGERVALUE: return "Integer"; + case BlackboardValue.FPVALUE: return "FP"; + case BlackboardValue.FPVECTOR2VALUE: return "Vector2"; + case BlackboardValue.FPVECTOR3VALUE: return "Vector3"; + case BlackboardValue.ENTITYREFVALUE: return "EntityRef"; + } + + return base.ToString(); + } + } +} diff --git a/data/BotSDK.Frame.User.cs b/data/BotSDK.Frame.User.cs new file mode 100644 index 0000000000000000000000000000000000000000..6c70abd4b86b294528783f071ce70c9d8fc97b28 --- /dev/null +++ b/data/BotSDK.Frame.User.cs @@ -0,0 +1,32 @@ +using Photon.Deterministic; + +namespace Quantum +{ + public unsafe partial class Frame + { + // If you are interested, check BotSDKTimerSystem in order to see how the time counter logic is implemented + internal FP BotSDKGameTime + { + get + { + // Try to get a game time value from a user implementation + // If the value is not greater thane zero, either because there is no implementation + // or because it calculates the time wrongly, it will use the default Bot SDK + // time polling calculus + FP gameTime = -FP._1; + CalculateBotSDKGameTime(ref gameTime); + if (gameTime >= FP._0) + { + return gameTime; + } + + // We use division of integers in order to avoid accuracy issues with multiplications with DeltaTime + return (FP)Global->BotSDKData.ElapsedTicks / SessionConfig.UpdateFPS; + } + } + + // Method meant to be used for user implementation of the time counter, if needed + // Store the calculation result on the gameTime variable. The result has to be greater than zero + partial void CalculateBotSDKGameTime(ref FP gameTime); + } +} diff --git a/data/BotSDK.qtn b/data/BotSDK.qtn new file mode 100644 index 0000000000000000000000000000000000000000..b5ccd117fbdfacf6557ea13b336b4428c78a8920 --- /dev/null +++ b/data/BotSDK.qtn @@ -0,0 +1,11 @@ +struct BotSDKData +{ + FP OriginalDeltaTime; + Int32 ElapsedTicks; + FP ElapsedPartialTicks; +} + +global +{ + BotSDKData BotSDKData; +} \ No newline at end of file diff --git a/data/BotSDKCompilerCallbacks.cs b/data/BotSDKCompilerCallbacks.cs new file mode 100644 index 0000000000000000000000000000000000000000..e67161974b4ee4f703df4af3d29c5c5563702cc9 --- /dev/null +++ b/data/BotSDKCompilerCallbacks.cs @@ -0,0 +1,10 @@ +namespace Quantum +{ + public static class BotSDKCompilerCallbacks + { + public static System.Action HFSMCompiled; + public static System.Action BTCompiled; + public static System.Action UTCompiled; + public static System.Action GOAPCompiled; + } +} diff --git a/data/BotSDKDebuggerSystem.cs b/data/BotSDKDebuggerSystem.cs new file mode 100644 index 0000000000000000000000000000000000000000..488100b0f16017e68d1bc7ae48f9dd203d2dcce5 --- /dev/null +++ b/data/BotSDKDebuggerSystem.cs @@ -0,0 +1,36 @@ +using System; + +namespace Quantum +{ + /// + /// Using this system is optional. It is only used to aim the Debugger on the Unity side. + /// It is also safe to copy logic from this system into your own systems, if it better suits your architecture. + /// + public class BotSDKDebuggerSystem : SystemMainThread + { + // Used for DEBUGGING purposes only + public static Action OnVerifiedFrame; + public static Action SetEntityDebugLabel; + + /// + /// Use this to add an entity to the Debugger Window on Unity. + /// You can provide a custom label of your preference if you want to identify your bots in a custom way. + /// Use the separator '/' on the custom label if you want to create an Hierarchy on the Debugger Window. + /// + public static void AddToDebugger(EntityRef entity, string customLabel = default) + { + if (SetEntityDebugLabel != null) + { + SetEntityDebugLabel(entity, customLabel); + } + } + + public override void Update(Frame frame) + { + if (frame.IsVerified) + { + OnVerifiedFrame?.Invoke(frame); + } + } + } +} diff --git a/data/BotSDKHiddenAttribute.cs b/data/BotSDKHiddenAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..2e9f6545a3be0e86843598d440013b7c647be604 --- /dev/null +++ b/data/BotSDKHiddenAttribute.cs @@ -0,0 +1,7 @@ +namespace Quantum +{ + [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Class | System.AttributeTargets.Struct)] + public class BotSDKHiddenAttribute : System.Attribute + { + } +} diff --git a/data/BotSDKSystem.cs b/data/BotSDKSystem.cs new file mode 100644 index 0000000000000000000000000000000000000000..c172dc7b952f6208104c7fe36b3bf1448ee6055a --- /dev/null +++ b/data/BotSDKSystem.cs @@ -0,0 +1,78 @@ +using System; + +namespace Quantum +{ + /// + /// Using this system is optional. It is only used to aim the Debugger on the Unity side. + /// It is also safe to copy logic from this system into your own systems, if it better suits your architecture. + /// + public unsafe class BotSDKSystem : SystemSignalsOnly, ISignalOnComponentAdded, + ISignalOnComponentAdded, ISignalOnComponentRemoved, + ISignalOnComponentAdded, ISignalOnComponentRemoved, + ISignalOnComponentAdded, ISignalOnComponentRemoved, + ISignalOnComponentRemoved + { + // -- HFSM + public void OnAdded(Frame frame, EntityRef entity, HFSMAgent* component) + { + HFSMData* hfsmData = &component->Data; + if (hfsmData->Root == default) + return; + + HFSMRoot rootAsset = frame.FindAsset(hfsmData->Root.Id); + HFSMManager.Init(frame, entity, rootAsset); + } + + // -- BT + public void OnAdded(Frame frame, EntityRef entity, BTAgent* component) + { + // Mainly used to automatically initialize entity prototypes + // If the prototype's Tree reference is not default and the BTAgent + // is not initialized yet, then it is initialized here; + if (component->Tree != default) + { + component->Initialize(frame, entity, component, component->Tree, false); + } + } + + public void OnRemoved(Frame frame, EntityRef entity, BTAgent* component) + { + component->Free(frame); + } + + // -- UT + + public void OnAdded(Frame frame, EntityRef entity, UTAgent* component) + { + UTManager.Init(frame, &component->UtilityReasoner, component->UtilityReasoner.UTRoot, entity); + } + + public void OnRemoved(Frame frame, EntityRef entity, UTAgent* component) + { + component->UtilityReasoner.Free(frame); + } + + // -- GOAP + + void ISignalOnComponentAdded.OnAdded(Frame frame, EntityRef entity, GOAPAgent* component) + { + if (component->Root == default) + return; + + var rootAsset = frame.FindAsset(component->Root.Id); + GOAPManager.Initialize(frame, entity, rootAsset); + } + + void ISignalOnComponentRemoved.OnRemoved(Frame frame, EntityRef entity, GOAPAgent* component) + { + GOAPManager.Deinitialize(frame, entity); + } + + // -- Blackboard + + public void OnRemoved(Frame frame, EntityRef entity, AIBlackboardComponent* component) + { + component->FreeBlackboardComponent(frame); + } + } +} diff --git a/data/BotSDKTimerSystem.cs b/data/BotSDKTimerSystem.cs new file mode 100644 index 0000000000000000000000000000000000000000..ca2acd96afa533f9308e5813d7d87e2c79a43a1d --- /dev/null +++ b/data/BotSDKTimerSystem.cs @@ -0,0 +1,40 @@ +using Photon.Deterministic; + +namespace Quantum +{ + public unsafe class BotSDKTimerSystem : SystemMainThread + { + public override void OnInit(Frame frame) + { + // We store the game's initial delta time so we can use it to further know + // if the delta time was changed (i.e to create slowdown or speedups) + frame.Global->BotSDKData.OriginalDeltaTime = FP._1 / frame.SessionConfig.UpdateFPS; + } + + public override void Update(Frame frame) + { + BotSDKData* botSDKData = &frame.Global->BotSDKData; + + // If the delta time was not changed, this tick counts as 1 + if (botSDKData->OriginalDeltaTime == frame.DeltaTime) + { + botSDKData->ElapsedTicks += 1; + } + else + { + // If the delta time was changed, we accumulate it as a "partial tick" value + // Once the partial tick value reaches at least 1 tick, we get the integer part of it + // to add to actual elapsed ticks + // Basically, if the simulation is running at half of the initial delta time, it will take double the time for one + // elapsed tick to be counted + botSDKData->ElapsedPartialTicks += frame.DeltaTime * frame.SessionConfig.UpdateFPS; + if (botSDKData->ElapsedPartialTicks >= 1) + { + var integerPart = FPMath.FloorToInt(botSDKData->ElapsedPartialTicks); + botSDKData->ElapsedTicks += integerPart; + botSDKData->ElapsedPartialTicks -= integerPart; + } + } + } + } +} diff --git a/data/Buff.cs b/data/Buff.cs new file mode 100644 index 0000000000000000000000000000000000000000..bc324687cfabd3233e2033799ad130bd01848433 --- /dev/null +++ b/data/Buff.cs @@ -0,0 +1,67 @@ +namespace Quantum +{ + using Photon.Deterministic; + + public unsafe partial struct Buff + { + // PUBLIC MEMBERS + + public bool IsFinished { get { return Flags.IsBitSet(0); } set { Flags = Flags.SetBit(0, value); } } + + // PUBLIC METHODS + + public void Initialize(Frame frame, EntityRef entity, Buff* buff, byte level) + { + var behaviors = frame.ResolveList(Behaviors); + + for (int idx = 0, count = behaviors.Count; idx < count; idx++) + { + behaviors.GetPointer(idx)->Initialize(frame, entity, buff, level); + } + } + + public void Deinitialize(Frame frame, EntityRef entity, Buff* buff) + { + var behaviors = frame.ResolveList(Behaviors); + + for (int idx = 0, count = behaviors.Count; idx < count; idx++) + { + behaviors.GetPointer(idx)->Deinitialize(frame, entity, buff); + } + } + + public void Refresh(Frame frame, EntityRef entity, Buff* buff) + { + var behaviors = frame.ResolveList(Behaviors); + + for (int idx = 0, count = behaviors.Count; idx < count; idx++) + { + behaviors.GetPointer(idx)->Refresh(buff); + } + } + + public void Update(Frame frame, Buff* buff) + { + var behaviors = frame.ResolveList(Behaviors); + + for (int idx = 0, count = behaviors.Count; idx < count; idx++) + { + behaviors.GetPointer(idx)->Update(frame, buff); + } + } + + public (FP, FP) GetDuration(Frame frame) + { + var behaviors = frame.ResolveList(Behaviors); + + for (int idx = 0, count = behaviors.Count; idx < count; idx++) + { + var (duration, maxDuration) = behaviors[idx].GetDuration(); + if (maxDuration > FP._0) + return (duration, maxDuration); + } + + return (default, default); + } + } +} diff --git a/data/Buffs.cs b/data/Buffs.cs new file mode 100644 index 0000000000000000000000000000000000000000..173cd9c2be116252121b392a4406cf4896d0751c --- /dev/null +++ b/data/Buffs.cs @@ -0,0 +1,153 @@ +namespace Quantum +{ + using Quantum.Collections; + + public unsafe partial struct Buffs + { + // PUBLIC METHODS + + public void Initialize(Frame frame) + { + BuffList = frame.AllocateList(); + } + + public void Deinitalize(Frame frame) + { + var buffs = frame.ResolveList(BuffList); + for (int idx = buffs.Count; idx --> 0;) + { + var buffEntity = buffs[idx]; + var buff = frame.Unsafe.GetPointer(buffEntity); + + buff->Deinitialize(frame, buffEntity, buff); + } + + frame.FreeList(BuffList); + BuffList = default; + } + + public void Update(Frame frame, EntityRef entity) + { + var buffs = frame.ResolveList(BuffList); + for (int idx = buffs.Count; idx --> 0;) + { + var buffEntity = buffs[idx]; + var buff = frame.Unsafe.GetPointer(buffEntity); + + buff->Update(frame, buff); + + if (buff->IsFinished == true) + { + buff->Deinitialize(frame, buffEntity, buff); + buffs.RemoveAtUnordered(idx); + + frame.Destroy(buffEntity); + } + } + } + + public bool AddBuff(Frame frame, EntityRef owner, EntityRef target, AssetRefEntityPrototype prototype, byte level) + { + var buffs = frame.ResolveList(BuffList); + + if (TryRefresh(frame, owner, prototype.Id.Value, buffs) == true) + return true; + + var buffEntity = frame.Create(prototype); + var buff = frame.Unsafe.GetPointer(buffEntity); + + buff->ID = prototype.Id.Value; + buff->Owner = owner; + buff->Target = target; + + buff->Initialize(frame, buffEntity, buff, level); + + buffs.Add(buffEntity); + + return true; + } + + public bool RemoveBuff(Frame frame, EntityRef owner, AssetRefEntityPrototype prototype) + { + var buffID = prototype.Id.Value; + var buffs = frame.ResolveList(BuffList); + + for (int idx = buffs.Count; idx --> 0;) + { + var buffEntity = buffs[idx]; + var buff = frame.Unsafe.GetPointer(buffEntity); + + if (buff->ID != buffID) + continue; + if (buff->Owner != owner) + continue; + + buff->Deinitialize(frame, buffEntity, buff); + + buffs.RemoveAtUnordered(idx); + frame.Destroy(buffEntity); + + return true; + } + + return false; + } + + public bool RemoveBuff(Frame frame, EntityRef buffEntity) + { + var buffs = frame.ResolveList(BuffList); + + for (int idx = buffs.Count; idx --> 0;) + { + if (buffs[idx] == buffEntity) + { + var buff = frame.Unsafe.GetPointer(buffEntity); + buff->Deinitialize(frame, buffEntity, buff); + + buffs.RemoveAtUnordered(idx); + frame.Destroy(buffEntity); + return true; + } + } + + return false; + } + + public void RemoveAll(Frame frame) + { + var buffs = frame.ResolveList(BuffList); + + for (int idx = buffs.Count; idx --> 0;) + { + var buffEntity = buffs[idx]; + var buff = frame.Unsafe.GetPointer(buffEntity); + buff->Deinitialize(frame, buffEntity, buff); + + frame.Destroy(buffEntity); + } + + buffs.Clear(); + } + + // PRIVATE MEMBERS + + private bool TryRefresh(Frame frame, EntityRef owner, long buffID, QList buffs) + { + for (int idx = buffs.Count; idx --> 0;) + { + var buffEntity = buffs[idx]; + var buff = frame.Unsafe.GetPointer(buffEntity); + + if (buff->ID != buffID) + continue; + if (buff->Owner != owner) + continue; + + buff->Refresh(frame, buffEntity, buff); + return true; + } + + return false; + } + } +} diff --git a/data/Buffs.qtn b/data/Buffs.qtn new file mode 100644 index 0000000000000000000000000000000000000000..94a54f4929637bc3c08d38dd3d4f0bdbf04630a2 --- /dev/null +++ b/data/Buffs.qtn @@ -0,0 +1,47 @@ +component Buffs +{ + [ExcludeFromPrototype] list BuffList; +} + +component Buff +{ + [ExcludeFromPrototype] long ID; + [ExcludeFromPrototype] byte Flags; + [ExcludeFromPrototype] EntityRef Owner; + [ExcludeFromPrototype] EntityRef Target; + + list Behaviors; +} + +union BuffBehavior +{ + BuffBehavior_Duration Duration; + BuffBehavior_HealthOverTime HealthOverTime; + BuffBehavior_Stats Stats; +} + +struct BuffBehavior_Duration +{ + [ExcludeFromPrototype] FP Duration; + FP MaxDuration; + FP DurationPerLevelIncrease; +} + +struct BuffBehavior_HealthOverTime +{ + [ExcludeFromPrototype] FP TimeToTick; + [ExcludeFromPrototype] byte RemainingTicks; + FP ValuePerTick; + FP ValuePerLevelIncrease; + EHealthAction Action; + FP TickTime; + byte TickCount; +} + +struct BuffBehavior_Stats +{ + EStatType StatType; + FP AbsoluteValue; + FP PercentValue; + FP ValuePerLevelIncrease; +} \ No newline at end of file diff --git a/data/animation.txt b/data/animation.txt new file mode 100644 index 0000000000000000000000000000000000000000..f81ac133093ad147aa90614f576af5d113fb76df --- /dev/null +++ b/data/animation.txt @@ -0,0 +1,167 @@ +Animation +Overview +Polling Based Animation +Trigger Events +Tips +Deterministic Animation +Tips + +Overview +In Quantum there are two distinct ways to handle animation: + +poll the game state from Unity; and, +deterministic animation using the Custom Animator. +Back To Top + + +Polling Based Animation +Most games use animation to communicate the state of an object to the player. For instance, when the playable character is walking or jumping, the animations are actually In Place animations and the perceived movement is driven by code. + +In other words, the scripts managing the characters' respective (Unity) animator are stateless and simply derive values to pass on as animation parameters based on data polled from the game simulation (in Quantum). + +N.B.: If the gameplay systems rely on Root Motion or have to be aware of the animation state, then skip to the next section. + +The polling based animation concept has been implemented in the API Sample. The following snippet is extracted from the PlayerAnimations script and drives the movement animation. + +When the entity is instantiated, its Initialize() method is called which caches the entity's EntityRef and the current QuantumGame; the latter one is purely for convenience. +Every Unity Update(), the MovementAnimation() function is called. +The MovementAnimation() function polls data from the CharacterController3D using the previously cached EntityRef. +The relevant animator parameters are derived from the polled data. +The computed data is passed on to the Unity Animator. +// This snippet is extracted from the Quantum API Sample. + +public unsafe class PlayerAnimation : MonoBehaviour +{ + [SerializeField] private Animator _animator = null; + + private EntityRef _entityRef = default; + private QuantumGame _game = null; + + // Animator Parameters + private const string FLOAT_MOVEMENT_SPEED = "floatMovementSpeed"; + private const string FLOAT_MOVEMENT_VERTICAL = "floatVerticalMovement"; + private const string BOOL_IS_MOVING = "boolIsMoving"; + + // This method is called from the PlayerSetup.cs Initialize() method which is registered + // to the EntityView's OnEntityInstantiated event located on the parent GameObject + public void Initialize(PlayerRef playerRef, EntityRef entityRef){ + _playerRef = playerRef; + _entityRef = entityRef; + _game = QuantumRunner.Default.Game; + } + + // Update is called once per frame + void Update(){ + MovementAnimation(); + } + + private void MovementAnimation() { + var kcc = _game.Frames.Verified.Unsafe.GetPointer(_entityRef); + bool isMoving = kcc->Velocity.Magnitude.AsFloat > 0.2f; + + _animator.SetBool(BOOL_IS_MOVING, isMoving); + + if (isMoving) { + _animator.SetFloat(FLOAT_MOVEMENT_SPEED, kcc->Velocity.Magnitude.AsFloat); + _animator.SetFloat(FLOAT_MOVEMENT_VERTICAL, kcc->Velocity.Z.AsFloat); + } + else { + _animator.SetFloat(FLOAT_MOVEMENT_SPEED, 0.0f); + _animator.SetFloat(FLOAT_MOVEMENT_VERTICAL, 0.0f); + } + } +Back To Top + + +Trigger Events +Some animations are based on a particular events taking place in the game; e.g. a player pressing jump or getting hit by an enemy. In these cases, it is usually preferable to raise an event from the simulation and have the view listen to it. This ensures decoupling and work well in conjunction with the polling based animation approach. + +For a comprehensive explanation on events and callbacks, refer to the Quantum ECS > Game Callbacks page in the Manual. + +The API Sample uses events to communicate when punctual actions happen that should result in a visual reaction; e.g. the playable character jumping. + +The MovementSystem in Quantum reads the player input and computes the movement values before passing them on to the CharacterController3D. The system also listens to the Jump key press. If the key WasPressed, it raises a PlayerJump event and calls the CharacterController3D's Jump() method. + +using Photon.Deterministic; +namespace Quantum +{ + public unsafe struct PlayerMovementFilter + { + public EntityRef EntityRef; + public PlayerID* PlayerID; + public Transform3D* Transform; + public CharacterController3D* Kcc; + } + + unsafe class MovementSystem : SystemMainThreadFilter + { + public override void Update(Frame f, ref PlayerMovementFilter filter) + { + var input = f.GetPlayerInput(filter.PlayerID->PlayerRef); + + // Other Logic + + if (input->Jump.WasPressed) + { + f.Events.PlayerJump(filter.PlayerID->PlayerRef); + filter.Kcc->Jump(f); + } + + // Other Logic + } + } +} +On the Unity side, the PlayerAnimation script listens to the PlayerJump event and reacts to it. These steps are necessary to achieve this: + +Define a method that can receive the event - void Jump(EventPlayerJump e). +Subscribe to the event in question. +When the event is received, check it is meant for the GameObject the script is located on by comparing the PlayerRef contained in the event against the one cached earlier. +Trigger / Set the parameter/s in the Unity Animator. +// This snippet is extracted from the Quantum API Sample. + +public unsafe class PlayerAnimation : MonoBehaviour +{ + [SerializeField] private Animator _animator = null; + + private PlayerRef _playerRef = default; + private QuantumGame _game = null; + + // Animator Parameters + private const string TRIGGER_JUMP = "triggerJump"; + + public void Initialize(PlayerRef playerRef, EntityRef entityRef) + { + _playerRef = playerRef; + + // Other Logic + + QuantumEvent.Subscribe(this, Jump); + } + + private void Jump(EventPlayerJump e) + { + if (e.PlayerRef != _playerRef) return; + _animator.SetTrigger(TRIGGER_JUMP); + } +Back To Top + + +Tips +Place the model and its animator component on a child object. +Events are not part of the game state and thus are not available to late/re-joining players. It is therefore advisable to first initialize the animation state by polling the latest game state if the game has already started. +Use synchronised events for animations that need to be triggered with 100% accuracy, e.g. a victory celebration. +Use regular non-synchronised events for animations that need to be snappy, e.g. getting hit. +Use the EventCanceled callback to graciously exit from animations triggered by a cancelled non-synchronised events. This can happen when the event was raised as part of a prediction but was rolled back during a verified frame. +Back To Top + + +Deterministic Animation +The main advantage of using a deterministic animation system is tick precise animations which are 100% synchronised across all clients and will snap to the correct state in case of a rollback. While this may sounds ideal, it comes with a performance impact since animations and their state are now part of the simulated game state. In reality only few games require and benefit from a deterministic animation system; among those are Fighting games and some Sports games,. + +The Custom Animator is a tool enabling deterministic animation. It works by baking information from Unity’s Mecanim Controller and importing every configuration such as the states, the transitions between the states, the motion clips and so on. + +Development of the Custom Animator has been halted due to the dependencies it created with Unity's Mecanim. However, the code has been open sourced and is available for download on the Addons > Custom Animator page. This page also provides an overview and a quick-guide on how to import and use the Custom Animator. + +Keep in mind its features are limited and it will likely have to be adapted to your needs. + + diff --git a/data/assets simulation.txt b/data/assets simulation.txt new file mode 100644 index 0000000000000000000000000000000000000000..91cb2f00b79a497e6117449281dde24db43b0c67 --- /dev/null +++ b/data/assets simulation.txt @@ -0,0 +1,257 @@ +Data Asset Classes +Quantum assets are normal C# classes that will act as immutable data containers during runtime. A few rules define how these assets must be designed, implemented and used in Quantum. + +Here is a minimal definition of an asset class (for a character spec) with some simple deterministic properties: + +namespace Quantum { + partial class CharacterSpec { + public FP Speed; + public FP MaxHealth; + } +} +Notice that the asset class definition must be partial and it has to be contained specifically in the Quantum namespace. + +Creating and loading instances of asset classes into the database (editing from Unity) will be covered later in this chapter. + +Back To Top + + +Using And Linking Assets +To tell Quantum this is an asset class (adding internal meta-data by making it inherit the basic AssetObject class and preparing the database to contain instances of it): + +// this goes into a DSL file +asset CharacterSpec; +Asset instances are immutable objects that must be carried as references. Because normal C# object references are not allowed to be included into our memory aligned ECS structs, the asset_ref special type must be used inside the DSL to declare properties inside the game state (from entities, components or any other transient data structure): + +component CharacterData { + // reference to an immutable instance of CharacterSpec (from the Quantum asset database) + asset_ref Spec; + // other component data +} +To assign an asset reference when creating a Character entity, one option is to obtain the instance directly from the frame asset database and set it to the property: + +// assuming cd is a pointer to the CharacterData component +// using the SLOW string path option (fast data driven asset refs will be explained next) +cd->Spec = frame.FindAsset("path-to-spec"); +The basic use of assets is to read data in runtime and apply it to any computation inside systems. The following example uses the Speed value from the assigned CharacterSpec to compute the corresponding character velocity (physics engine): + +// consider cd a CharacterData*, and body a PhysicsBody2D* (from a component filter, for example) +var spec = frame.FindAsset(cd->Spec.Id); +body->Velocity = FPVector2.Right * spec.Speed; +Back To Top + + +A Note On Determinism +Notice that the above code only reads the Speed property to compute the desired velocity for the character during runtime, but its value (speed) is never changed. + +It is completely safe and valid to switch a game state asset reference in runtime from inside an Update (as asset_ref is a rollback-able type which hence can be part of the game state). + +However, changing the values of properties of a data asset is NOT DETERMINISTIC (as the internal data on assets is not considered part of the game state, so it is never rolled back). + +The following snippet shows examples of what is safe (switching refs) and not safe (changing internal data) during runtime: + +// cd is a CharacterData* + +// this is VALID and SAFE, as the CharacterSpec asset ref is part of the game state +cd->Spec = frame.FindAsset("anotherCharacterSpec-path"); + +// this is NOR valid NEITHER deterministic, as the internal data from an asset is NOT part of the transient game state: +var spec = frame.FindAsset("anotherCharacterSpec-path"); +// (DO NOT do this) changing a value directly in the asset object instance +spec.Speed = 10; +Back To Top + + +AssetObjectConfig Attribute +You can fine-tune the asset linking script generation with the AssetObjectConfig attribute. + +[AssetObjectConfig(GenerateLinkingScripts = false)] +partial class CharacterSpec { + // ... +} +GenerateLinkingScripts (default=true) - prevent the generation of any scripts that make the asset editable in Unity. +GenerateAssetCreateMenu (default=true) - prevent the generation of the Unity CreateAssetMenu attribute for this asset. +GenerateAssetResetMethod (default=true) - prevent the generation of the Unity Scriptable Object Reset() method (where a Guid is automatically generated when the asset is created). +CustomCreateAssetMenuName (default=null) - overwrite the CreateAssetMenu name. If set to null an menu path is generated automatically using the inheritance graph. +CustomCreateAssetMenuOrder (default=-1) - overwrite the CreateAssetMenu order. If set to -1 an alphabetical order is used. +Back To Top + + +Overwrite Asset Script Location And Disable AOT File Generation +Overwrite the default asset script destination folder and disable AOT file generation. + +See the tool section: quantum codegen unity + +Back To Top + + +Asset Inheritance +It is possible to use inheritance in data assets, which gives much more flexibility to the developer (specially when used together with polymorphic methods). + +The basic step for inheritance is to create an abstract base asset class (we'll continue with our CharacterSpec example): + +namespace Quantum { + partial abstract class CharacterSpec { + public FP Speed; + public FP MaxHealth; + } +} +Concrete sub-classes of CharacterSpec may add custom data properties of their own, and must be marked as Serializable types: + +namespace Quantum { + [Serializable] + public partial class MageSpec : CharacterSpec { + public FP HealthRegenerationFactor; + } + + [Serializable] + public partial class WarriorSpec : CharacterSpec { + public FP Armour; + } +} +Back To Top + + +In The DSL +Once you have declared an asset in the DSL, you can use the asset_ref type in the DSL to hold references to the base class and any of its subclasses. + +component CharacterData { + // Accepts asset references to CharacterSpec base class and its sub-classes(MageSpec and WarriorSpec). + asset_ref ClassDefinition; + FP CooldownTimer; +} +Should you want to keep a reference specific to a sub-class, the derived asset needs to be declared in the DSL first using asset import: + +asset CharacterSpec; +asset import MageSpec; +If the derived asset has already been declared in the DSL, simply use asset_ref as for the base class. For instance, to use the MageSpec directly in the DSL instead of CharacterSpec, we would have to write the following: + +component MageData { + // Only accepts asset references to MageSpec class. + asset_ref ClassDefinition; + FP CooldownTimer; +} +Back To Top + + +Data-Driven Polymorphism +Having gameplay logic to direct evaluate (in if or switch statements) the concrete CharacterSpec class would be very bad design, so asset inheritance makes more sense when coupled with polymorphic methods. + +Notice that adding logic to data assets means implementing logic in the quantum.state project and this logic still have to consider the following restrictions: + +Operate on transient game state data: that means logic methods in data assets must receive transient data as parameters (either entity pointers or the Frame object itself); +Only read, never modify data on the assets themselves: assets must still be treated as immutable read-only instances; +The following example adds a virtual method to the base class, and a custom implementation to one of the subclasses (notice we use the Health field defined for the Character entity more to the top of this document): + +namespace Quantum { + partial unsafe abstract class CharacterSpec { + public FP Speed; + public FP MaxHealth; + public virtual void Update(Frame f, EntityRef e, CharacterData* cd) { + if (cd->Health < 0) + f.Destroy(e); + } + } + + [Serializable] + public partial unsafe class MageSpec : CharacterSpec { + public FP HealthRegenerationFactor; + // reads data from own instance and uses it to update transient health of Character pointer passed as param + public override void Update(Frame f, EntityRef e, CharacterData* cd) { + cd->Health += HealthRegenerationFactor * f.DeltaTime; + base.Update(f, e, cd); + } + } +} +To use this flexible method implementation independently of the concrete asset assigned to each CharacterData, this could be executed from any System: + +// Assuming cd is the pointer to a specific entity's CharacterData component, and entity is the corresponding EntityRef: + +var spec = frame.FindAsset(cd->Spec.Id); +// Updating Health using data-driven polymorphism (behavior depends on the data asset type and instance assigned to character +spec.Update(frame, entity, cd); +Back To Top + + +Using DSL Generated Structs In Assets +Structs defined in the DSL can also be used on assets. The DSL struct must be annotated with the [Serializable] attribute, otherwise the data is not inspectable in Unity. + +[Serializable] +struct Foo { + int Bar; +} + +asset FooUser; +Using the DSL struct in a Quantum asset. + +namespace Quantum { + public partial class FooUser { + public Foo F; + } +} +If a struct is not [Serializable]-friendly (e.g. because it is an union or contains a Quantum collection), prototype can be used instead: + +using Quantum.Prototypes; +namespace Quantum { + public partial class FooUser { + public Foo_Prototype F; + } +} +The prototype can be materialized into the simulation struct when needed: + +Foo f = new Foo(); +fooUser.F.Materialize(frame, ref f, default); +Back To Top + + +Dynamic Assets +Assets can be created at runtime, by the simulation. This feature is called DynamicAssetDB. + +var assetGuid = frame.AddAsset(new MageSpec() { + Speed = 1, + MaxHealth = 100, + HealthRegenerationFactor = 1 +}); +Such asset can be loaded and disposed of just like any other asset: + +MageSpec asset = frame.FindAsset(assetGuid); +frame.DisposeAsset(assetGuid); +Dynamic assets are not synced between peers. Instead, the code that creates new assets needs to be deterministic and ensure that each peer will an asset using the same values. + +The only exception to the rule above is when there is a late-join - the new client will receive a snapshot of the DynamicAssetDB along with the latest frame data. Unlike serialization of the frame, serialization and deserialization of dynamic assets is delegated outside of the simulation, to IAssetSerializer interface. When run in Unity, QuantumUnityJsonSerializer is used by default: it is able to serialize/deserialize any Unity-serializable type. + +Back To Top + + +Initializing DynamicAssetDB +Simulation can be initialized with preexisting dynamic assets. Similar to adding assets during the simulation, these need to be deterministic across clients. + +First, an instance of DynamicAssetDB needs to be created and filled with assets: + +var initialAssets = new DynamicAssetDB(); +initialAssets.AddAsset(new MageSpec() { + HealthRegenerationFactor = 10 +}); +initialAssets.AddAsset(new WarriorSpec() { + Armour = 100 +}); +... +Second, QuantumGame.StartParameters.InitialDynamicAssets needs to be used to pass the instance to a new simulation. In Unity, since it is the QuantumRunner behaviour that manages a QuantumGame, QuantumRunner.StartParamters.InitialDynamicAssets is used instead. + +Back To Top + + +Built In Assets +Quantum also comes shipped with several built-in assets, such as: + +SimulationConfig - defines many specifications for a Quantum simulation, from scene management setup, heap configuration, thread count and Physics/Navigation settings; +DeterministicConfig - specifies details on the game session, such as it's simulation rate, the checksum interval and lots of configuration regarding Input related to both the client and the server; +QuantumEditorSettings - has the definition for many editor-only details, like the folder the DB should be based in, the color of the Gizmos and auto build options for automatically baking maps, nav meshes, etc; +BinaryDataAsset - an asset that allows the user to reference arbitrary binary information (in the form of a byte[]). For example, by default the Physics and the Navigation engines uses binary data assets to store information like the static triangles data. This asset also has built in utilities to compress and decompress the data using gzip. +CharacterController3DConfig - config asset for the built in 3D KCC. +CharacterController2DConfig - config asset for the built in 2D KCC. +PhysicsMaterial - defines a Physics Material for Quantum's 3D physics engine. +PolygonCollider - defines a Polygon Collider for Quantum's 2D physics engine. +NavMeshAsset - defines a NavMesh used by Quantum's navigation system. +NavMeshAgentConfig - defines a NavMesh Agent Config for Quantum's navigation system. +MapAsset - stores many static per-scene information such as Physics settings, colliders, NavMesh settings, links, regions and also the Scene Entity Prototypes on that Map. Every Map is correlated with a single Unity scene. \ No newline at end of file diff --git a/data/assets unity.txt b/data/assets unity.txt new file mode 100644 index 0000000000000000000000000000000000000000..3f1cd1c172f354939065f1a828dd4b95e7ee9516 --- /dev/null +++ b/data/assets unity.txt @@ -0,0 +1,243 @@ +Assets in Unity +Overview +Finding Quantum Assets In Unity Scripts +Resources, Addressables And Asset Bundles +Drag-And-Dropping Assets In Unity +Map Asset Baking Pipeline +Preloading Addressable Assets +V1.16 Or Older +V1.17 Or Newer +Baking AssetBase Load Information +Updating Quantum Assets In Build +Updating Existing Assets +Adding New Assets +Example Implementation +Adding New Assets With DynamicAssetDB + +Overview +Quantum generates a ScriptableObject-based wrapper partial class for each each asset type available in Unity. The base class for such wrappers is AssetBase. The main class managing AssetBase instances is called UnityDB. + +Editing a data asset +Editing properties of a data asset from Unity. +The Quantum SDK will generate the unique GUIDs for each asset. +AssetGuids of all the available assets have to be known to the simulation when it starts. + +The process for this to happen is: + +In the Editor: +All AssetBase assets collected from the locations defined in QuantumEditorSettings.AssetSearchPaths (by default Assets/Resources/DB). +Each AssetBase has a generated entry containing the AssetGuid and the information needed to load the AssetBase at runtime. +Entries are saved into the AssetResourceContainer asset at defined in the QuantumEditorSettings.AssetResourcePath (by default Assets/Resources/AssetResources.asset). +At runtime: +The first time any of UnityDB members is used AssetResourceContainer is loaded using Resources.Load (based on QuantumEditorSettings.AssetResourcePath). +The list of entries is used to initialize the simulation's IResourceManager along with the information needed to load each asset dynamically. +To browse the list of Asset Objects currently part of the database, use the AssetDB Inspector window accessible via Quantum/Show AssetDB Inspector and Window/Quantum/AssetDB Inspector menu items. + + +Back To Top + + +Finding Quantum Assets In Unity Scripts +Every concrete asset class created by the user gets a corresponding class generated on the Unity side to enable their instantiation as actual Unity Scriptable Objects. + +Just to exemplify: a Quantum asset named CharacterData gets, in Unity, a class named CharacterDataAsset, where the word Asset is always the suffix added. The Unity class always contains a Property named AssetObject which can be cast to the Quantum class in order to access the simulation specific fields. + +Use the UnityDB class in order to find assets in the Unity side. Here is a complete snippet of a Quantum asset declaration, and how to access it's fields on Unity: + +In the Quantum side: + +// in any .qtn file: +asset CharacterData; + +// in a .cs file: +public unsafe partial class CharacterData +{ + public FP MaximumHealth; +} +In the Unity side: + +var characterDataAsset = UnityDB.FindAsset(myAssetRef.Id); +var characterData = characterDataAsset.Settings; +FP maximumHealth = characterData.MaximumHealth; +Back To Top + + +Resources, Addressables And Asset Bundles +Quantum never forms hard-references to AssetBase assets. This enables the use of any dynamic content delivery. The following methods of loading assets are supported out of the box: + +Resources +Addressables (needs to be explicitly enabled) +Asset Bundles (as a proof-of-concept, due to Asset Bundles demanding highly custom, per-project approach) +Enabling Addressables +Enabling Addressables in QuantumEditorSettings. Alternatively, define `QUANTUM_ADDRESSABLES` and `QUANTUM_ADDRESSABLES_WAIT_FOR_COMPLETION` (for Addressables 1.17 or newer). +There are not any extra steps needed for AssetBase to be loadable dynamically using any of the methods above. The details on how to load each asset are stored in AssetResourceContainer. This information is accessed when a simulation calls Frame.FindAsset or when UnityDB.FindAsset is called and leads to an appropriate method of loading being used. + +If an asset is in a Resource folder, it will be loaded using the Resources API. +If an asset has an address (explicit or implicit), it will be loaded using the Addressables API. +If an asset belongs to an Asset Bundle (explicitly or implicitly), there will be an attempt to load it using the AssetBundle API. +To make the list of the assets (AssetResourceContainer) dynamic itself some extra code is needed; pleasr refer to the Updating Quantum Assets At Runtime section for more information. + +User scripts can avoid hard references by using AssetRef types (e.g. AssetRefSimulationConfig) instead of AssetBase references (e.g. SimulationConfig) to reference Quantum assets. + +public class TestScript : MonoBehaviour { + // hard reference + public SimulationConfigAsset HardRef; + // soft reference + public AssetRefSimulationConfig SoftRef; + + void Start() { + // depending on the target asset's settings, this call may result in + // any of the supported loading methods being used + SimulationConfigAsset config = UnityDB.FindAsset(SoftRef.Id); + } +} +Back To Top + + +Drag-And-Dropping Assets In Unity +Adding asset instances and searching them through the Frame class from inside simulation Systems can only go so far. At convenient solution arises from the ability to have asset instances point to database references and being able to drag-and-drop these references inside Unity Editor. + +One common use is to extend the pre-build RuntimePlayer class to include an AssetRef to a particular CharacterSpec asset chosen by a player. The generated and type-safe asset_ref type is used for linking references between assets or other configuration objects. + +// this is added to the RuntimePlayer.User.cs file +namespace Quantum { + partial class RuntimePlayer { + public AssetRefCharacterSpec CharacterSpec; + + partial void SerializeUserData(BitStream stream) { + stream.Serialize(ref CharacterSpec); + } + } +} +This snippet will allow for an asset_ref field to generated which will only accept a link to an asset of type CharacterSpec. This field will show up in the Unity inspector and can be populated by drag-and-dropping an asset into the slot. + +Drag & Drop Asset +Asset ref properties are shown as type-safe slots for Quantum scriptable objects. +Back To Top + + +Map Asset Baking Pipeline +Another entry point for generating custom data in Quantum is the map baking pipeline. The MapAsset is the AssetBase-based wrapper for Map asset. + +The Map asset is required by a Quantum simulation and contains basic information such as Navmeshes and static colliders; additional custom data can be saved as part of the asset placed in its custom asset slot - this can be an instance of any custom data asset. The custom asset can be used to store any static data meant to be used during initialization or at runtime. A typical example would be an array of spawn point data such as position, spawned type, etc. + +In order for a Unity scene to be associated with a MapAsset, the MapData MonoBehaviour component needs to be present on a GameObject in the scene. Once MapData.Asset points to a valid MapAsset, the baking process can take place. By default, Quantum bakes navmeshes, static colldiers and scene prototypes automatically as a scene is saved or when entering play mode; this behaviour can be changed in QuantumEditorSettings. + +To assign a custom piece of code to be called every time the a bake happens, create a class inheriting from the abstract MapDataBakerCallback class. + +public abstract class MapDataBakerCallback { + public abstract void OnBake(MapData data); + public abstract void OnBeforeBake(MapData data); + public virtual void OnBakeNavMesh(MapData data) { } + public virtual void OnBeforeBakeNavMesh(MapData data) { } +} +Then override the mandatory OnBake(MapData data) and OnBakeBefore(MapData data) methods. + +public class MyCustomDataBaker: MapDataBakerCallback { + public void OnBake(MapData data) { + // any custom code to live-load data from scene to be baked into a custom asset + // generated custom asset can then be assigned to data.Asset.Settings.UserAsset + } + public void OnBeforeBake(MapData data) { + + } +} +Back To Top + + +Preloading Addressable Assets +Quantum needs assets to be loadable synchronously. + + +V1.16 Or Older +In the Addressables version prior to 1.17, there were no means to load Addressable assets synchronously other than preloading before the simulation started or using Unity's SyncAddressables sample. + +Back To Top + + +V1.17 Or Newer +WaitForCompletion was addded in Addressables 1.17 which added the ability to load assets synchronously. To enable it for Quantum, define QUANTUM_ADDRESSABLES_USE_WAIT_FOR_COMPLETION or use the toggles in the QuantumEditorSettings asset's Build Features section. + +Although synchronous loading is possible, there are situations in which preloading assets might still be preferable; the QuantumRunnerLocalDebug.cs script demonstrates how to achieve this. + +Back To Top + + +Baking AssetBase Load Information +AssetResourceContainer is a ScriptableObject containing information on how to load each AssetBase and maps them to AssetGuids. + +N.B.: `AssetBase` is not a Quantum asset itself. +Every time the menu option Quantum > Generate Asset Resources is used or an asset in one of QuantumEditorSettings.AssetSearchPaths is imported, the AssetResourceContainer is recreated in full at the location specified by the QuantumEditorSettings.AssetResourcePath. + +During the creation of the AssetResourceContainer, each AssetBase located in any QuantumEditorSettings.AssetSearchPaths is assigned to a group. By default, three groups exist: + +ResourcesGroup; +AssetBundlesGroup; and, +AddressablesGroup. +The process of deciding which group an asset is assigned to is shown in the diagram below. + +AssetResourceContainer generation +The flow of assigning an asset a group. +An asset is considered Addressable if: + +it has an address assigned; +any of its parent folders is Addressable; or, +it is nested in another Addressable asset. +The same logic applies to deciding whether an asset is a part of an Asset Bundle. + +To disable baking AssetBase when assets are imported, untick QuantumEditorSettings.UseAssetBasePostprocessor. + +Back To Top + + +Updating Quantum Assets In Build +It is possible for an external CMS to provide data assets; this is particularly useful for providing balancing updates to an already released game without making create a new build to which players would have to update. + +This approach allows balancing sheets containing information about data-driven aspects such as character specs, maps, NPC specs, etc... to be updated independently from the game build itself. In this case, game clients would always try to connect to the CMS service, check for whether there is an update and (if necessary) upgrade their game data to the most recent version before starting or joining online matches. + +Back To Top + + +Updating Existing Assets +The use of Addressables or Asset Bundles is recommended as these are supported out of the box. Any AssetBase that is an Addressable or part of an Asset Bundle will get loaded at runtime using the appropriate methods. + +To avoid unpredictable lag spikes resulting from downloading assets during the game simulation, consider downloading and preloading your assets as discussed here: Preloading Addressable Assets. + +Back To Top + + +Adding New Assets +The AssetResourceContainer generated in the editor will contain the list of all the assets present at its creation. If a project's dynamic content includes adding new Quantum assets during without creating a new build, a way to update the list needs to be implemented. + +The recommended approach to achieve this is with an extension of the partial UnityDB.LoadAssetResourceContainerUser method. When the first simulation starts or any UnityDB method is called, Quantum will make an attempt to load the AssetResourceContainer. By default it is assumed the AssetResourcesContainer is a Resource located at the QuantumEditorSettings.AssetResourcePath. To override this behaviour, UnityDB.LoadAssetResourceContainerUser needs to be implemented. + +Back To Top + + +Example Implementation +First, the AssetResourceContainer needs to be moved out of the Resources folder. This is done by setting the QuantumEditorSettings.AssetResourcePath: + +AssetResourceContainer Path Override +Second, the new AssetResourceContainer needs to be made into an Addressable: + +Addressable AssetResourceContainer +Finally, the snippet implementing the partial method: + +partial class UnityDB { + static partial void LoadAssetResourceContainerUser(ref AssetResourceContainer container) { + var path = QuantumEditorSettings.Instance.AssetResourcesPath; + +#if UNITY_EDITOR + if (!UnityEditor.EditorApplication.isPlaying) { + container = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + Debug.Assert(container != null); + return; + } +#endif + + var op = Addressables.LoadAssetAsync(path); + container = op.WaitForCompletion(); + Debug.Assert(container != null); + } +} +This is a simplified implementation and, depending on project's needs, some management of the `AsyncOperationHandle` returned by `Addressables.LoadAssetAsync` may need to be added. \ No newline at end of file diff --git a/data/bt.qtn b/data/bt.qtn new file mode 100644 index 0000000000000000000000000000000000000000..cbc31bc65340e950ee65a887bf072b0c97b6a1dd --- /dev/null +++ b/data/bt.qtn @@ -0,0 +1,27 @@ +asset BTNode; +asset BTService; + +component BTAgent { + asset_ref Tree; + asset_ref Current; + list NodesStatus; + list ServicesEndTimes; + list BTDataValues; + list ActiveServices; + list DynamicComposites; + AssetRefAIConfig Config; + Int32 AbortNodeId; +} + +struct BTDataIndex{ + Int32 Index; +} + +union BTDataValue{ + FP FPValue; + Int32 IntValue; +} + +asset import BTComposite; +asset import BTDecorator; +asset import BTRoot; \ No newline at end of file