Framework Initialization
LobsterFramework does some work before the gameplay loop to enable various editor and gameplay systems to function properly. This process has been generalized to allow developers to create initialization routine for custom systems. These services will be covered on this page.
Contents
Attributes Initialization
LobsterFramework
relies heavily on custom attributes to implement various features. One example is the editor of AbilityData
. The editor needs to be aware of the set of available abilities that can be added so it can present these options to the developers. We can manually list out every ability type we've defined. However, this approach is cumbersome as we need to remember to add/remove entries to the list of abilities everytime a new ability is defined/removed, and it does not work when we're working with the compiled dlls of the LobsterFramework
and LobsterFrameworkEditor
. In this case, knowledge of the types of the loaded assemblies is required. We can access this information via the C# reflection API, which provides a way to inspect the meta data generated by the C# compiler and determine what abilities need to be present in the menu by checking for AddAbilityMenu
attribute.
The set of features that relies on this mechanism is not limited to editor utilities, Some runtime systems such as the late binding of the interaction method via InteractionHandler
attribute, and the runtime check of the presence of required Component
references on AbilityManager
is also done through the same mechanism.
As more of these features are put in, more entries need to be added to the list of attributes to be fetched for types in loaded assemblies for inspection. This raised the incentive to generalize this process for newer attributes. LobsterFramework
provides a convenient way for defining these custom attributes that need to be fetched for some types of loaded assemblies before the game starts.
To begin with, the new attributes need to inherit from InitializationAttribute
and be sealed. Then, mark the new attribute with RegisterInitialization
. Here you have the option to set the priority of the attribute which will determine the order it'll be fetched in relation to other attributes. The higher the priority the earlier. Additionally, you can specify the initialization type by setting the AttributeType
property to one of the options defined in InitializationAttributeType
indicating you want this attribute to be fetch in editor/runtime only or both. Next, IntializationAttribute.Init()
needs to be implemented. This method is called for every type this attribute has been fetched on. Lastly, since reflection calls are expensive, we don't want to waste time trying to fetch attributes on types that we don't care about. To handle this, every InitializationAttribute
needs to define a static method that takes in a Type
object and return bool
to indicate whether the type is a valid target for this attribute, the name of this method is specified by CompatabilityCheckerMethodName
const property. The reason the magic method approach is adopted is because Unity currently does not support C#11 which features static methods in interfaces.
// CustomEditorAttribute.cs
// A custom attribute that should only be applied to type A and add it to a
// internally visible list to be used by editor scripts
using LobsterFramework.Init;
[RegisterInitialization(AttributeType = InitializationAttributeType.Dual)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class CustomEditorAttribute : InitializationAttribute {
public static bool IsCompatible(Type type){ return type == typeof(A); }
internal static List<Type> menuItems = new();
public override Init(Type type){
menuItems.Add(type);
}
}
A final thing to note is that if you're relying on this mechanism to fetch your custom attributes in other assemblies and those assmeblies do not directly reference LobsterFramework
, you need to use AttributeProvider
to mark the assembly where the custom attribute is defined so that LobsterFramework
attribute initialization routines will go through the assemblies referencing this assembly to fetch the custom attribtues defined in this assembly.
Player Loop Event
Unity has present us with various runtime callbacks such as Start()
, FixedUpdate()
, Update()
, LateUpdate()
to allow execution of custom code during parts of the gameplay loop. Sometimes we want to ensure some kind of order between our scripts and Unity allows us to edit execution priority of these scripts in the project settings. Personally I do not like this approach as it is easy to mess things up with project settings and priorities can collide with third party scripts. Also some gameplay systems are hard to maintain since you cannot control when methods will be called by client scripts. The solution I've came up with is to add additional events into the game loop. If some script is executed in a custom game loop event EarlyUpdate()
that happens before Update()
, it is guranteed to be executed before all of the scripts that gets called in Update()
, LateUpdate()
and so on.
This is possible since Unity provides a struct PlayerLoopSystem
from UnityEngine.LowLevel
to represent one or a group of events in the game loop and APIs to replace existing game loop settings. Each of the event in the player loop is identified by a type. LobsterFramework
provides a convenient way of defining custom events and will inject those at the start of the game (not while in editor). Using the ability system execution event that gets executed after Update()
but before LateUpdate()
as an example:
// AbilityInstanceManagement.cs
[PlayerLoopEventGroup(typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate), Priority = 0, InjectBefore = true)]
public sealed class AbilityInstanceManagement : IPlayerLoopEventGroup
{
/../
private class ExecuteAbilityInstance { }
[PlayerLoopEvent(typeof(ExecuteAbilityInstance))]
private static void Execute()
{
// Code that executes all queued ability instances
}
/../
}
Every player loop event must be contained in a group which is a sealed type marked by IPlayerLoopEventGroup
. The event group needs [PlayerEventGroup]
attribute to be applied to indicate which existing player loop event this group will be inserted next to. In this example we're inserting next to PreLateUpdate.ScriptRunBehaviorLateUpdate
which represents the late update event in game. Check out Unity documentation for list of events present in the player loop system. The event group will be identified by the type you specified here in the player loop system.
[PlayerEventGroup]
takes in the type representing the player loop event you wish to insert this event next to. By default it injects the event after it. Set InjectBefore
property to true will inject the event before the target event like in this example. Priority
property indicates the order of this event will be inserted compared to other custom events, higher priority means earlier injection.
Inside the event group, you can specify any number of events using [PlayerLoopEvent]
on methods that takes no parameter and has void
return type. The attribute takes in a type as the identifier in the player loop system. The order of event within the event group is from top to bottom, methods on the top will be inserted first compared to methods at the bottom.
Lastly, it is important to realize that the custom events are always executed every frame without being bounded to MonoBehaviors
and it is the developer's responsibility to ensure they behave correctly in all scenarios.
What Goes Next
- Custom Analyzers
- Check the event group type is sealed and public when applied with
[PlayerEventGroup]
- Check the event method signature is correct when applied with
[PlayerLoopEvent]
- Check if the event method is defined in a event group type with correct signature when applied with
[PlayerLoopEvent]
- Check the event group type is sealed and public when applied with
- Each event group can also be an event itself, need to add the ability to specify a method in the event type to be executed as the event.
- Custom Analyzer to check if there's less than 2 such methods defined in an event group type