GameDemo Documentation

repository·main·Indexed 20 days ago

https://github.com/chickensoft-games/gamedemo

A third-person 3D game built with Godot and C# that demonstrates Chickensoft's opinionated architecture. It showcases best practices for state management using LogicBlocks, dependency injection via AutoInject, and unit testing with GoDotTest and GodotNodeInterfaces. The project implements a modular system for data persistence using Serialization and a centralized application lifecycle managed by the App class.

Tokens
1.9K
Snippets
6
Records
10
Agent score
68%

What's inside GameDemo

  1. How GameDemo works: Architecture Overview

    main

    GameDemo follows an opinionated architecture designed for testability and modularity using several Chickensoft packages:

    • State Management: Uses LogicBlocks to separate state from presentation via hierarchical state machines. This ensures the view is always synchronized with the state and allows for easy testing of logic in isolation.
    • Dependency Injection: Uses AutoInject for tree-based dependency provisioning. It resolves dependencies by looking up the node tree, mirroring Godot's node structure. It also supports Enhanced Lifecycles (two-phase initialization) to separate value creation from consumption.
    • Testing: Uses GoDotTest for local and CI/CD testing. It leverages GodotNodeInterfaces to provide interfaces for Godot objects, allowing for easy mocking and the use of fake scene trees during unit tests.
    • Data & Persistence: Implements saving and loading via Serialization, Serialization.Godot, and SaveFileBuilder.
    • Node Extensions: Uses Introspection (C# source generation) to add mixin-like functionality to node scripts at build time.
  2. Manage state with LogicBlocks

    main

    For nodes requiring state, GameDemo uses LogicBlocks to implement domain-driven design. This approach separates the node's logic (the state machine) from its visual presentation (the view).

    Benefits include:

    • Consistency: All complex logic is encapsulated in a logic block.
    • Testability: Logic blocks can be tested in isolation from the Godot scene tree.
    • Synchronization: Logic blocks can react to changes in domain repository objects, keeping active logic synchronized across the game (similar to an event-bus model).
    • Visualization: LogicBlocks can automatically generate state diagrams from your code.
  3. Use AutoInject for dependency provisioning and lifecycles

    main

    AutoInject provides a tree-based dependency resolution system that mirrors Godot's node hierarchy. This solves initialization order issues by allowing a child node to request a dependency that a parent (or ancestor) provides.

    Key features include:

    • Tree-based resolution: Automatically searches ancestors for required dependencies.
    • Enhanced Lifecycles: Splits initialization into two phases: 1) Creating required values, and 2) Consuming those values for setup.
    • Testing support: Provides an IsTesting property on nodes to allow skipping the first phase of initialization in favor of mock/fake objects.
  4. Setup the GameDemo development environment

    main

    To run the GameDemo, you must first resolve the binary assets using Git LFS. After that, ensure your Godot C# development environment is configured (compatible with current .NET LTS and stable Godot versions).

    Crucial Step: You must open the project in the Godot editor at least once before attempting to launch it from your code editor (e.g., VSCode) to ensure all necessary files and configurations are initialized.

    git lfs pull
  5. Launch the GameDemo application

    main

    The Main class serves as the entry point for the game. By default, it transitions the Godot scene tree to the main application scene located at res://src/app/App.tscn using CallDeferred.

    If you need to modify the core application startup logic (such as changing the initial scene), you should edit Game.tscn or Game.cs instead of Main.cs.

    # Note: Main.cs is the entry point, but logic is typically managed in Game.tscn/Game.cs
  6. Configure AppLogic bindings for side effects

    main

    The App class uses AppLogic.Bind() in its OnReady() method to define side effects that occur when the application state machine outputs specific signals. This is where the visual representation (Godot nodes) is synchronized with the logical state.

    Common outputs handled by the AppBinding include:

    • ShowSplashScreen: Hides menus and shows the splash screen.
    • SetupGameScene: Loads and instantiates the game scene from GAME_SCENE_PATH (res://src/game/Game.tscn).
    • ShowMainMenu: Displays the main menu.
    • ShowGame: Hides menus and fades in the game.
    • StartLoadingSaveFile: Triggers the asynchronous loading of the save file.
  7. Run tests using the RUN_TESTS flag

    main

    The Main class supports running automated tests via the Chickensoft.GoDotTest framework if the RUN_TESTS preprocessor directive is defined.

    When RUN_TESTS is enabled, the application inspects command line arguments using TestEnvironment.From(OS.GetCmdlineArgs()). If the environment indicates that tests should run (Environment.ShouldRunTests), the application sets RuntimeContext.IsTesting = true and executes the test suite using GoTest.RunTests instead of starting the game application.

    # To trigger tests, the project must be compiled with the RUN_TESTS symbol 
    # and executed with appropriate command line arguments recognized by TestEnvironment.
  8. Interact with AppLogic via Inputs

    main

    The App class manages the high-level application state machine (AppLogic). Instead of manipulating state directly, you should send inputs to the logic block. The App class provides several helper methods that map UI or system events to AppLogicState.Input types:

    • OnNewGame(): Sends AppLogicState.Input.NewGame.
    • OnLoadGame(): Sends AppLogicState.Input.LoadGame.
    • OnDeleteGame(): Sends AppLogicState.Input.DeleteGame.
    • OnAnimationFinished(StringName animation): Sends FadeInFinished or FadeOutFinished based on the animation name.
    // Example of triggering a state change via the App class
    app.OnNewGame();
  9. Access application services via IApp

    main

    The IApp interface allows you to retrieve core services using the IProvide pattern. This is useful for accessing the application's data repository or the save file system from other parts of the game.

    Key services provided by IApp:

    • IAppRepo: The application's data repository.
    • ISaveFile: The interface for managing game save files (GZip JSON format).

    You can access these via the IProvide<T>.Value() implementation.

    // Accessing services if you have a reference to IApp
    public void DoSomething(IApp app)
    {
        var repo = app.IProvide<IAppRepo>().Value();
        var saveFile = app.IProvide<ISaveFile>().Value();
    }
  10. Use the App class as the main application entrypoint

    main

    The App class is the central coordinator for the game's lifecycle, state, and external dependencies. It implements IApp and provides access to the application repository (IAppRepo) and the save file system (ISaveFile) via the IProvide pattern.

    To use App in a project, you must call Initialize() to set up the repository, logic, and instantiator, and OnReady() to configure serialization and start the application logic state machine.

    // Example of the expected lifecycle calls within the Godot environment
    public partial class App : CanvasLayer, IApp
    {
        public void Initialize()
        {
            // Sets up AppRepo, AppLogic, and Menu signal listeners
            this.Initialize(); 
        }
    
        public void OnReady()
        {
            // Configures serialization and starts the AppLogic state machine
            this.OnReady();
        }
    }