## Contents 1. [Input](#Input) 2. [State Machines](#State-Machines) 3. [Event System](#Event-System) 4. [M](#Michael’s-Pseudo-Code) 5. [Serialization System](#Serialization-System) ### Input When Unity generates a C# script for an Input asset with the new InputSystem, it does so with a header like this. ```csharp public partial class @Controls: IInputActionCollection2, IDisposable { /**/ } ``` Unlike existing solutions, an instance of the `Controls` class needs to be created. Instead of passing the `Controls` object around or cluttering any components, we elevate it to static and to advantage of lazy initialization. ```csharp // declaration public static Controls global = new Controls(); // from anywhere... global.PlayerControls.Attack += _ => { }; if (global.UI.Select.WasPressedThisFrame()) { /**/ } ``` We could actually extend the partial `Controls` class to a new file using the same class declaration, but instead we'll create a static wrapper class. ```csharp public static class GameInput { public static Controls global { get; protected set; } static GameInput() { controls = new Controls(); controls.Enable(); // initialize the input... } } ``` A more flexible but involved solution involves using generics, so we can leverage this functionality with any generated input script. ```csharp // the interfaces let us access data from the input asset public static class GlobalInput<T> where T : IEnumerable<InputAction>, IInputActionCollection, IDisposable, new() { public static T instance { get; private set; } public static InputAction[] actions { get; private set; } /* the static constructor is called the first time it is referenced * which gives us on-demand Singleton behavior completely separated from * any kind of component dependency */ static GlobalInput() { instance = new T(); // initialize the input... instance.Enable(); actions = instance.ToArray(); // easily find input information } } ``` This class's responsibility ends here—offering easy global access to generated input scripts. The next step is to create the glue for our game logic, and to create useful extensions to make calling input easier. This can be split between several classes and managed however is convenient. ```csharp public class GameInput { // point to a consistent input reference... public static Controls instance => GlobalInput<Controls>.instance static GameInput() { PlayerState.onPossessCharacter += _ => stateMachine.SetState<GameplayState>(); Matchwork.Player.onPlayerDeath += stateMachine.SetState<PauseState>; PauseSystem.onPause += stateMachine.SetState<PauseState>; PauseSystem.onResume += stateMachine.SetState<GameplayState>; } public static Controls controls => GlobalInput<Controls>.instance; public static InputStateMachine<Controls> stateMachine => InputStateMachine<Controls>.instance; public static Controls.PlayerActions Player => controls.Player; public static Controls.GenericActions Generic => controls.Generic; public static Controls.UIActions UI => controls.UI; public static Vector2 Move => controls.Player.Move.ReadValue<Vector2>(); public static Vector2 Look => controls.Player.Look.ReadValue<Vector2>(); } ``` I have also included an easy way to select Inputs from the inspector. ```csharp // declare this field, where Controls is a generated input script public InputReference<Controls> input; // ... input.action.started += ... ``` ![](https://hackmd.io/_uploads/SySNUDXlT.png) Unlike Unity's `InputActionReference` it does not create a separated instance of the Action. Using the field in the code above, `input.action` and `GameInput<Controls>.[action]` will point to the same object. ----------------------------- ### State Machines State machines are useful in tons of scenarios in game development, so it's important to have the right structures in place. State machines in Interwoven start with a relatively simple base, then build features on top. ```csharp public interface IState { void Enter(); void Exit(); } public class StateMachine<T> where T : IState { public IState state { get; protected set; } public void Switch(T state) { this.state.Exit(); this.state = state; this.state.Enter(); } } ``` #### Common issues with state machines State machines are a wonderful, but limited tool. One issue is <b>Memory inefficiency</b>. Since a state has to be instantiated to be entered, this can become a problem if you are switching states often; <i>ie, for a character or combo state machine.</i> For this, I created a caching class that uses the state's Type as a key. ```csharp /// <summary>Dictionary keyed by Type that handles state initialization</summary> public class StateCache<T> where T : IState { protected Dictionary<Type, T> pool = new Dictionary<Type, T>(); public event Action<T> onStateAdded; /// <summary>Set the key-value pair</summary> public void Add(T state) { var type = state.GetType(); var update = !pool.ContainsKey(type); pool[type] = state; if (update) onStateAdded?.Invoke(state); } /// <summary>Only use if you know the state is instantiated</summary> public T Get<V>() where V : T { return pool[typeof(V)]; } /// <summary>Only use if you know the state is instantiated</summary> public T Get(Type type) { return pool[type]; } /// <summary> /// State type will need a parameterless constructor to be instantiated<br/> /// if not already in pool /// </summary> public T GetOrAdd<V>() where V : T, new() { bool exists = pool.TryGetValue(typeof(V), out var state); if (!exists) { state = new V(); onStateAdded?.Invoke(state); } return state; } } ``` Implementing this into the state machine enable switching states with just a type argument, and avoids allocating more memory every state switch. ```csharp public class StateMachine<T> where T : IState { /* code... */ public StateCache<T> cache = new StateCache<T>(); public void Switch<T>() where T : new { Switch(cache.GetOrAdd<T>()); } } ``` This solves the memory problem, but the `where T : new` restriction prevents us from passing parameters. We could also force our states to be structs, but this can lead to unexpected behavior with more complex states. Instead of handling dependencies from the ground-up, I've of course created a workaround beneath this sea of interfaces to pass type-safe structs as constructor arguments. ```csharp /// <summary> Base interface for identifying context structs </summary> public interface IConfiguration { } /// <summary> Create a struct for state-specific configuration </summary> /// <typeparam name="T"> State to be configured </typeparam> public interface IContext<T> : IConfiguration where T : IContext { void Configure(T target); } /// <summary> If the state requires configuration, tag it with this interface </summary> public interface IContext { } ``` It's a bit tedious, but here's what it looks like in practice ```csharp public class AttackState : ICharacterState, ILockable, IContext, ICombatState { public struct Config : IContext<AttackState> { public CustomAnimation animation; public Targeting target; public Config(CustomAnimation animation, Targeting target = null) { this.animation = animation; this.target = target; } public void Configure(AttackState state) { state.animation = animation; state.target = target; } } // code... } ``` Adding function to state machine... ```csharp public void SetState<V>(IContext<V> parameters) where V : T, IContext, new() { var state = cache.GetOrAdd<V>(); parameters.Configure((V)state); SetState(state); } ``` How to switch to a contextual state... ```csharp character.stateMachine.Switch(new AttackState.Config(attack, target)); ``` Alternatively, set the state up manually. ```csharp var state = stateMachine.GetOrAdd<AttackState>(); state.attack = attack; state.data = ... stateMachine.Switch(state); ``` Another common issue with state machines needs a more creative solution. It's the idea of 'resetting' the state, or to traverse states without needing to know what the next state is. My solution involves defining a priority for each state, then switching state only when the state being pushed has an equal or higher priority. States will remain in the stack until they are explicitly popped, meaning higher-priority states will need to pop themselves off the stack when they are finished to return to the next-highest pushed state. The rest of the section is my implementation. There is definitely room to expand and refactor. ```csharp public interface IPriority { int priority { get; } } /// <summary> /// For use with PriorityStateMachine /// </summary> public interface IPriorityState : IState, IPriority { } /// <summary> /// /// Is there a better name for this? /// /// More functional state stack.<br/> /// Created to handle transient and persistent states /// instead of confusing 'BaseState' logic /// </summary> public class PriorityStateMachine<T> where T : IPriorityState { protected PriorityDictionary<T> priorityDict = new PriorityDictionary<T>(); public StateCache<T> cache { get; protected set; } = new StateCache<T>(); public T currentState => priorityDict.CurrentItem; public event Action<T> onStateAdded { add => cache.onStateAdded += value; remove => cache.onStateAdded -= value; } public event Action<T> onStateChanged; /// <summary> /// Add states to the state machine's cache without activating them /// </summary> public void Add(params T[] items) { foreach (var item in items) cache.Add(item); } /// <summary> /// Ensure state of type exists in state cache, and return it /// </summary> public V GetOrAdd<V>() where V : T, new() { return (V)cache.GetOrAdd<V>(); } /// <summary> /// Ensure state of type exists in state cache, and return it /// </summary> public T GetOrAdd(Type type) { return cache.GetOrAdd(type); } public V Get<V>() where V : T { return (V)cache.Get<V>(); } public T Get(Type type) { return cache.Get(type); } /// <summary> /// Dynamically create state instance if not already in pool /// </summary> public V PushState<V>(IContext<V> parameters) where V : T, IContext, new() { var state = cache.GetOrAdd<V>(); if (parameters != null) parameters.Configure((V)state); PushState(state); return (V)state; } /// <summary> /// Dynamically create state instance if not already in pool /// </summary> public void PushState<V>() where V : T, new() { var state = cache.GetOrAdd<V>(); PushState(state); } /// <summary> /// Dynamically create state instance if not already in pool /// </summary> public void PushState(Type type) { var state = cache.GetOrAdd(type); PushState(state); } /// <summary> /// Assumes state instance already exists in pool<br/> /// then configures state /// </summary> public void PushCachedState<V>(IContext<V> parameters) where V : T, IContext { var state = cache.Get<V>(); if (parameters != null) parameters.Configure((V)state); if (state != null) PushState(state); } /// <summary> /// Assumes state already exists in pool /// </summary> public void PushCachedState<V>() where V : T { var state = cache.Get<V>(); if (state != null) PushState(state); } /// <summary> /// Assumes state already exists in pool /// </summary> public void PushCachedState(Type type) { var state = cache.Get(type); if (state != null) PushState(state); } /// <summary> /// Will only update state if priority >= currentState.priority /// </summary> public void PushState(T newState) { var hasUpdate = priorityDict.WillUpdateCurrent(newState); var prevState = currentState; priorityDict.AddOrUpdate(newState); cache.Add(newState); if (hasUpdate) { prevState?.Exit(); currentState.Enter(); onStateChanged?.Invoke(currentState); } } /// <summary> /// Exit state without knowing where to go next /// </summary> public void PopState(T state) { var hasUpdate = currentState.Equals(state); if (hasUpdate) { currentState.Exit(); priorityDict.Remove(state); // automatically updates currentState currentState?.Enter(); onStateChanged?.Invoke(currentState); } else { priorityDict.Remove(state); } } public void PopState<V>() where V : T { PopState(Get<V>()); } public void PopState(Type type) { PopState(Get(type)); } } /// <summary> /// /// How important is this class? Is there a better solution? I hate it. /// /// Generic data structure.<br/> /// Handles sorting during the push method in O(n) time <br/> /// A binary heap would be more appropriate if the number of items is very large. /// </summary> public class PriorityDictionary<T> where T : IPriority { private Dictionary<int, T> priorityDict = new Dictionary<int, T>(); public T CurrentItem { get; private set; } public bool WillUpdateCurrent(T newItem) { return CurrentItem == null //|| (newItem.priority >= CurrentItem.priority && !newItem.Equals(CurrentItem)); || (newItem.priority >= CurrentItem.priority); } public void AddOrUpdate(T newItem) { // If an item with the same priority exists, it gets replaced priorityDict[newItem.priority] = newItem; // Update current item if its priority is less than or equal to the new item if (CurrentItem == null || CurrentItem.priority <= newItem.priority) { CurrentItem = newItem; } } public void Remove(T item) { // Remove the item from the dictionary if it exists priorityDict.Remove(item.priority); // If the item being removed was the current item if (CurrentItem.Equals(item)) { // If there are still other items, update the current item if (priorityDict.Count > 0) { // Gets the item with the highest priority CurrentItem = priorityDict.Values.OrderByDescending(x => x.priority).First(); } else { // No other items left, so no current item CurrentItem = default; } } } } ``` ----------------- ### Event System The custom event system is similar to UnityEvents, but with a few differences - Support static code - Support more arguments - Support return values The class structure is as follows: ```csharp class GameCallback { string signature; // convert MethodInfo to custom string in the editor so it can be serialized MethodInfo method; // located on startup using signature Delegate del; // cache method call with simplified arguments (significantly speeds up subsequent Invokes) } class GameActionBase : GameCallback { } class GameFuncBase : GameCallback { } class GameAction : GameActionBase { void Invoke(); } /* serialized arguments only */ class GameAction<T> : GameActionBase{ void Invoke(T); } /* 1 dynamic argument, or T instance method */ class GameAction<T1,T2> : GameActionBase { void Invoke(T1,T2) } /* 2 dynamic arguments, or 1-parameter instance method */ class GameFunc<R> : GameFuncBase { R Invoke(); } /* serialized arguments only */ class GameFunc<R, T> : GameFuncBase { R Invoke(T); } /* 1 dynamic argument, or T instance function */ class GameFunc<R,T1,T2> : GameFuncBase { R Invoke(T1,T2); } /* 2 dynamic arguments, or 1-parameter instance function */ ``` Usage: ```csharp // require the method to be a Transform instance method or a static method accepting a Transform public GameAction<Transform> action; // ... action.Invoke(transform); // pass transform dynamically ``` Creates a method-selection dropdown menu in the editor. ![](https://hackmd.io/_uploads/SyeGQwmga.png) If we select this method ```csharp static async UniTask TowardPosition(this Transform,Vector3,CancellationToken,AnimationCurve) ``` It will be drawn like this ![](https://hackmd.io/_uploads/SygMXvmea.png) **Caveats**: the serialization is not perfect. Only basic types like Object reference, float, string, int, double, bool, enum, Vectors are supported. I can't figure out how to get serializable classes to be drawn. Neither `CancellationToken` and `AnimationCurve` are supported (and we don't handle the return) so the correct way to call this method from the editor would be to use a `GameFunc<CancellationToken,AnimationCurve>` and pass the non-serialized arguments through the invoke method. ```csharp public GameFunc<UniTask, CancellationToken, AnimationCurve> action; // ... await action.Invoke(cancellationToken,animationCurve) ``` result in the editor ![](https://hackmd.io/_uploads/Skxf7vmlT.png) Can't call code you need from a UnityEvent? ```csharp public GameAction<float> action; ``` ![](https://hackmd.io/_uploads/HJeMmPXeT.png) (the function I want to use is nested within an `Outlinable` field, and cannot be reached with a UnityEvent. Create a static method and point to that instead.) It works by creating delegates from `MethodInfos` which are invoked like this. ```csharp methodInfo.Invoke((object)target, (object[])args) // call an instance method methodInfo.Invoke(null, object[] args) // call a static method ``` But when we use the MethodInfo to cache the delegate, we convert its inputs to behave more simply, and to equally support static and instance methods. ```csharp delegate object ActionDelegate(object[] args); // target is args[0], args.Length = methodParams.Length + 1 ActionDelegate CreateDelegate(MethodInfo){ ... } // convert methodInfo and run once ``` For the generic types, the types used in the field declaration are type constraints for the method search. - `GameAction` is the most broad, and can be assigned to any static or instance method with any return type and any parameter count (assuming the parameter types can be serialized). - `GameFunc<R>` types require the method to return something that can be assigned to R. - `GameAction<T>` and `GameFunc<R,T>` require the method to accept a parameter or target of type T. The parameter in question will *not* be serialized, but instead will be dynamically passed from `Invoke(T)` to the internal argument array in the appropriate position before the full parameterized delegate is invoked. - `T` Parameter requirements find methods that **at least** require context from one `T` object. - The editor side attempts to serialize all additional parameters not included in the type constraints, including a reference to the calling object for instance methods. In this example, both methods have an identical signature, and are assignable from `GameAction`, `GameAction<string>`, `GameAction<Actor>`, `GameAction<Actor,String>` and `GameAction<string,Actor>`. ```csharp public class Actor : MonoBehaviour { public void Log(string) public static void Log(Actor,string) } ``` - `GameAction` will draw 2 arguments in the editor: a text field `string` and an object reference field for `Actor` - `GameAction<string>` will draw 1 object reference for `Actor` in the editor, and the `string` is passed using `Invoke(string)` - `GameAction<string,Actor>` will not draw any arguments in the editor. The `Invoke(string,Actor)` is still supported despite the arguments being in the wrong order. Compared to this there's a lot more editor scripting and I'm not even gonna try to explain that lol ----------------- ### Michael's Pseudo-Code Prefab: Transporter Scripts: Transporter_Station, TransporterManager, TransporterCarriage Layout: Transporter Prefab (Empty) Station1 Transporter_Station model Transporter_Satation script Hitbox Station2 Transporter_Station model Transporter_Satation script Hitbox Carriage Hitbox TransporterManager (Script) References: Station 1 and 2 Script, Delegates: Transporter_Arriving(string StationCode) Carriage Location TransporterStation Listeners: Hitbox OnEnter -> if PlatformMissing RequestPlatform() Transporter Carriage Listeners: Hitbox OnEnter -> MoveToOtherPlatform() Better way of doing this with a finite state graph to organize, go back and redo this Class BehaviorTreeObject (BTO) { //Could also easily be an interface } Class Branch : BTO { Var Condition; List <Var> ConditionReq; List <BTO> Paths Void UpdateCondition() { //Probably subscribed to an event monitoring it’s condition } } Class Leaf : BTO { Reference: Character State Machine //Does an action, i.e. walking to a location, performing an attack //doing this should update something like an internal cooldown monitored by a branch //causing the leaf to shift once the action has been performed } Class CharacterSpecifics { //Attached to each character. Is referenced by some branches in the behavior tree //in order to enable, disable, or adjust for character specific behaviors } Class BehaviorTree { CharacterSpecifics CharacterStateMachine //Probably gets passed to the BTOs as needed during runtime for ease of character switching [SerializeField] List <BTO> //Just make everything able to be setup in the Editor } ----------------------------- ### Serialization System The basic setup. ```csharp [SerializeReference, Polymorphic] public IContext context; ``` `[SerializeReference]` tells Unity to serialize to your inspector the reference of an object instead of its direct value, and `[Polymorphic]` flips again to draw the reference's value instead of the reference itself, and adds context switching Together interfaces become incredibly powerful, allowing dynamic object creation and context switching to fit the interface. ```csharp public interface IShiftStrategy { void Shift(Character c); } [Serializable] public struct Dodge : IShiftStrategy { // supports custom property attributes [Range(0, 1)] public float speed; public void Shift(Character c) { c.Dodge(...); } } [Serializable] // struct are supported by reference // (structs cannot be used with SerializeReference) public struct Sprint : IShiftStrategy { public float speed; public float acceleration; void Shift(Character c) { c.moveSpeed = ... } } [Serializable] public class MultiShift : IShiftStrategy { // supports nesting // supports lists and arrays [SerializeReference, Polymorphic] public List<IShiftStrategy> strategies; } // implementation public class Character : MonoBehaviour { [SerializeReference, Polymorphic] public IShiftStrategy shift; } ``` Supports any reference type including `object`, but to context-switch on this type limits the search to the entire universe and isn't practical. In this case, add type filters to the Polymorphic attribute for types you want to include. ```csharp [Serializable] public class Ultramorphism { [SerializeReference, Polymorphic( typeof(IWorldEvent), typeof(ICharacterContext), typeof(ICharacterEvent))] public object context; public void DoSomething() { switch (context) { case IWorldEvent w: // w... case ICharacterEvent e: // e... case ICharacterContext c: // c... } } } ``` ![Pasted image 20231129222623](https://hackmd.io/_uploads/SJnd0cHB6.png) ![Pasted image 20231129222517](https://hackmd.io/_uploads/Hksu05HSa.png)