Godot Project Godot project architecture Application Scopes In GFF, an application scope is an entry point available during a specific period of time in the application lifetime. Application is structured within two scopes: App ( res://sources/App.cs): The App scope, initialized at the start of the application, released before quitting. App act like a singleton. Game ( res://sources/Game.cs): The Game scope, initialized when starting or loading a game, released when going back to main menu. Each scope contains Services that can be accessed from anywhere within the scope. Services are declared in the RegisterServices method of an application scope by filling the dependency builder with instances. Current rendering driver is accessible in static field ApplicationScope.RenderingDriver. It can be checked to enable some compatibility features. Application Version Current version of the application can be accessed from the static field ApplicationScope.Version. It is modified when using the command gff release-version (you can also modify it manually in the csproj file). You may need to execute some code when moving from a version to another (migrate some settings, checking for unlock of newly created achievements, etc ...). You can do this by registering a delegate using ApplicationScope.RegisterVersionMigrationCallback(ApplicationVersionChangedEventHandler migrationCallback). Registered delegate will be called only the first time after a version change. App Scope The App scope is the first entry point of the Godot application. It's constructor parse command line arguments so they are accessible during the whole execution of the application ( App.Args). They are parsed by the CommandLine library. It also have a helper to checks for the availability of a feature (method  HasFeature(string tagName)). This method replace Godot's helper OS.HasFeature. In Debug mode, this method allows developers to force features (e.g., "demo", "steam") to report as available by checking specific arguments, overriding Godot feature status. Services The service locator pattern is a design pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer. This pattern uses a central registry known as the "service locator", which on request returns the information necessary to perform a certain task. The service locator pattern provides global access to services without the testability issues of singletons. This pattern allows swapping implementations for testing or different platforms while maintaining a simple API for service access. The API allows to declare services like this: // Register a service by giving its type. A new instance will automatically be created. dependencyBuilder.AddSingleton() // Abstract a service behind an interface. You can get the instance using the interface type. // This allows to change the instance depending on some conditions without having to change the code on the user side. if (App.HasFeature("steam")) { dependencyBuilder.AddSingleton(); } else { dependencyBuilder.AddSingleton(); } // It's also possible to manually instantiate the service (useful when having specific instanciation rules). dependencyBuilder.AddSingleton(new Application.AppSettings(this.GetTree().Root)) // You can also provide instances from the scene tree to make them globally accessible. dependencyBuilder.AddSingleton(this.GetChildren().First()) Services are then accessible from anywhere within the scope of its service locator (the Application Scope): AppSettings appSettings = App.Services.Get(); IDrmService drmService = App.Services.Get(); A service must implement the interface ISingleton and must declare a method named Bind that will receive as parameters other services that it depends on. This method is called by the dependency builder using dependency injection to fill parameters. // Declare IDrmService parameter to create a dependency to this service. public void Bind(IDrmService drmService) { // Then, use drmService to initialize the service... } There is 3 methods in ISingleton interface, Initialize and Release are called when the Application Scope start / exit. Update is called every frame. To ensure a specific order of call, you can set a dependency between two services. App State The execution flow of the application is controlled by a hierarchical state machine (HSM). It is registered as a service in the App scope ( res://sources/application/AppState.cs). HSM allow you to easily add new states without having to break your existing state machine. You can control general behavior using parent states, and more specific one with child states. You can add any number of hierarchy level you need. Another HSM is used to control game execution flow ( res://sources/gameplay/GameState.cs). Hierarchical State Machine Definition The state pattern allows objects to change behavior based on their internal state, effectively implementing finite state machines (FSM). This pattern is essential for managing AI behavior, player character states, game flow states, and animation controllers. State machines prevent spaghetti code from proliferating conditional statements and ensure clear state transitions. Complex AI and game flow often require hierarchical state machines (HSM) where states can contain sub-states. Hierarchical state machines reduce code duplication by allowing states to share common behavior through parent states. When transitioning between child states, the parent state remains active and continues executing its logic. Implementation In GFF, a HSM is implemented by creating a class that derives from HierarchicalStateMachine. TState and  TTransition are enum types. It is recommended to declare theses enums inside the HSM class since they are highly specific to its implementation. The HSM structure must be declared before updating it. It is recommended to do it in the constructor, then to call AssertCorrectness that verify the integrity of the HSM. You can register a new state using the method RegisterState, then you register transitions to other states of the state machine with RegisterTransitionTo. You also can register a pass (a delegate) for any trigger type with RegisterTransitionTo. Here are the available types: Begin: the pass is called when entering the state. Update: the pass is called each update while being in the state (after Begin the first time it enters the state, and before End the last time). End: the pass is called before leaving the state. Each parent state must have one and only one default child. An implicit transition is created between the parent and its default child. When a transition has a parent state as destination, the HSM will automatically transition to the default child. If you don't want to go to the default child, it means you need to declare a transition to a child state directly. Example For example, if we want to create the following state machine: using Core; public partial class MyHSM : HierarchicalStateMachine { public AppState() : base(Id.Bootstrap) { this.RegisterState(Id.Initial) .RegisterTransitionTo(Transition.Next, Id.Main); { StateInfo main = this.RegisterState(Id.Main) .RegisterPass(PassType.Begin, this.Main_Begin) .RegisterPass(PassType.End, this.Main_End) .RegisterTransitionTo(Transition.Next, Id.Final); outGame.RegisterState(Id.Main_SubState, defaultState: true) .RegisterPass(PassType.End, this.Main_SubState_Update) .RegisterTransitionTo(Transition.Toggle, Id.Main_AnotherSubState); outGame.RegisterState(Id.Main_AnotherSubState) .RegisterPass(PassType.End, this.Main_AnotherSubState_Update) .RegisterTransitionTo(Transition.Toggle, Id.Main_SubState); } this.RegisterState(Id.Final); this.AssertCorrectness(); } private void Main_Begin() { } private void Main_End() { } private void Main_SubState_Update() { } private void Main_AnotherSubState_Update() { } public enum Id { Initial, Main, Main_SubState, Main_AnotherSubState, Final, } public enum Transition { Next, Toggle, } } You can also see res://sources/application/AppState.cs and res://sources/gameplay/GameState.cs as real life example. They are setup to handle the application and game control flow in GFF. Usage Once your HSM is declared, your must call the Update method to run it once. During an update, the state machine will call the Update passes for all active states (from children to parent) then it will proceed to transition changes if there is pending one. To ask for a transition change, you must call the method void QueueTransition(TTransition transition). If the method is called from outside of the state machine (not in a pass during the update) the transition change is applied directly. You can ask the state machine if you are in a specific state with the method bool IsInState(TState state). It returns true for the current active state and all its parents. You can also ask if a specific transition is available in the current state with bool HasTransitionAvailable(TTransition transition), it makes it really easy to disable actions that are not available in the game in the current state. You can serialize the HSM. It will restore its current state on load automatically. Sources https://generalistprogrammer.com/game-design-patterns#state-pattern Serializer Serialization tools can be found in namespace Core.Serialization. Data Serialization Low level tool to serialize information. Overview In GFF, serialization is done with an abstract Serializer that uses a symetric declaration pattern (symetric read and write). Here is as an example how to describe the serialization of an object: public class SerializableObject : ISerializable { private int intField; private EnumType enumField; private SerializedObject child; public void Serialize(Serializer serializer) { this.intField = serializer.SerializeValue("Integer", this.intField); this.enumField = serializer.SerializeAsValue("Enum", this.enumField); this.child = serializer.Serialize("Child", this.child); } } Symetric serialization: It is important to send your value as parameter and to retrieve it from the return value of serialization methods. This way the system can handle Read and Write operation for reference types and value types in a safe way. You can then serialize it in any C# stream using a serializer (for now only JsonSerializer exists): using (MemoryStream stream = new MemoryStream()) { var objectToSerialize = new SerializableObject(); using (var serializer = new JsonSerializer(stream, JsonSerializer.SerializeMode.Writer)) { objectToSerialize.Serialize(serializer); } var derializedObject = new SerializableObject(); using (var serializer = new JsonSerializer(stream, JsonSerializer.SerializeMode.Reader)) { derializedObject.Serialize(serializer); } } API This section describe how to serialize data in a Serialize method. Object Serialization You can serialize any object instance, array or list of instances that implement the ISerializable interface using the Serialize methods. T Serialize(string key, in T serializableObject) where T : ISerializable; T[] Serialize(string key, in T[] array) where T : ISerializable; List Serialize(string key, in List list) where T : ISerializable; Value Serialization You can serialize any value type, array or list of value types using the SerializeValue methods. T SerializeValue(string key, in T value); T[] SerializeValue(string key, in T[] array); List SerializeValue(string key, in List list); Serialize simple objects as value Some data structure are simple enough to be serialized as value but are not value type. This special cases are handled using the SerializeAsValue methods. For now these methods can serialize enum as integer or string, C# type as string or event Godot RandomNumberGenerator. You can also implement the IValueConvertible interface to create a specific conversion on types you own. T SerializeAsValue(string key, in T serializableObject) where T : IValueConvertible; Type SerializeAsValue(string key, in Type type); RandomNumberGenerator SerializeAsValue(string key, in RandomNumberGenerator rng); T SerializeAsValue(string key, in T enumValue, bool serializeAsInteger = false) where T : struct, Enum; Queue SerializeAsValue(string key, in Queue enumQueue, bool serializeAsInteger = false) where T : struct, Enum; Versioning Handling version in serialization is mandatory if you want to handle backward compatibility in your game saves. You can write a version number to any Serialize method by using the SerializeVersion method. If no version is found the method return 0. public void Serialize(Serializer serializer) { int version = serializer.SerializeVersion(1); this.Legacyfield = serialize.Serialize("Legacyfield", this.Legacyfield); if (version >= 1) { this.Newfield = serialize.Serialize("NewField", this.NewField); } } The version number is unique per type so you can have different version number in inheritance level of a class hierarchy. Save Manager High level manager that handle game saves. Game Save System The SaveManager contains a list of save slots. It allows you to create, delete or edit any save slot. A save slot is represented by a single archive file in the save folder (it is a zip file using the sav extension). Save Info When you retrieve a save slot you get a SaveInfo reference. This structure contains the metadata about the save slot content. These metadata are stored as a config file in the game slot archive. You can add any custom metadata to the save (allowing you to gather information about the save without deserializing everything to display it in menus). Metadata are organized in sections, the Main section contains all general metadata. Then there is one section per save entry. Save Entries You can store any instance that implement ISerializationEntry in a save slot as an entry. It creates one file per entry in the archive (plus an associated section in the metadata). Use the WriteEntry and ReadEntry to store and retrieve entry instances from a save slot. Post Load Pass Sometimes you need to serialize references to other serialized objects and you want to resolve these references during loading to just have a C# reference to use in your code. PostLoad pass just serve this purpose, it calls the PostLoad method on all instances that implements ISerializableWithPostLoad interfaces. This call is done after the deserialization process so all instances exists and have valid deserialized data. PostLoad methods are called in reverse order than the Serialize methods (from child to parent).