C# Godot Template

repository·main·Indexed 41 days ago

https://github.com/csharpgodottools/template

A high-performance C# template for Godot Engine featuring advanced developer tools, optimized component patterns, and built-in debugging utilities. Includes a centralized component system for performance gains, a [Visualize] attribute for in-game inspection, a Metrics Overlay for profiling, and a high-performance multiplayer packet system using ClientPacket and ServerPacket. Provides integration for GDUnit4 testing in VSCode and Visual Studio, as well as a fluent Tweens API and a service management system.

Tokens
8.1K
Snippets
22
Records
33
Agent score
52%

What's inside C# Godot Template

  1. What PacketGen generates

    main

    PacketGen is a source generator that automates the creation of boilerplate code for network packets at compile time. This allows packet definitions to remain concise and explicit while handling the heavy lifting of serialization.

    For every valid packet type, PacketGen generates:

    • Write(PacketWriter writer)
    • Read(PacketReader reader)
    • Equals(object obj)
    • GetHashCode()

    If the [PacketRegistry] attribute is applied, it also generates a mapping registry for opcodes.

  2. PacketGen Project Layout and Key Files

    main

    The project is divided into the generator logic and the testing suite:

    Project Structure

    • PacketGen/: Contains the Roslyn incremental source generator logic.
    • PacketGen.Tests/: Contains generator tests that compile generated code and validate runtime behavior.

    Key Files for Reference

    • PacketGen/PacketGenerator.cs: The entry point for the incremental generator.
    • PacketGen/Generators/PacketGenerationOrchestrator.cs: Handles the assembly of source code for each packet.
    • PacketGen/Generators/PacketRegistryGenerator.cs: Generates the source for the packet registry.
    • PacketGen/Generators/TypeHandlers/*: Contains the serialization logic for various type shapes.
    • PacketGen/Generators/TypeHandlers/SerializablePropertySelector.cs: The central filter used to determine which properties are eligible for serialization.
    • PacketGen/PacketGenConstants.cs: Contains shared string constants used across the generator.
  3. Optimize performance with Centralized Component Scripts

    main

    For C# users, performance can be significantly improved (up to 3.5x) by avoiding thousands of individual _Process calls on separate nodes. Instead, use the Component class to implement a centralized component pattern.

    When extending from Component:

    • Use Ready(), Process(double delta), and ExitTree() instead of the standard Godot underscore-prefixed methods.
    • Process is disabled by default; you must call SetProcess(true) to enable it.
    • This allows a centralized manager to handle the execution of these components more efficiently.
    public class EntityMovementComponent(Player player) : Component(player)
    {
        protected override void Ready()
        {
            // Process is disabled by default and we must enable it ourselves
            SetProcess(true);
        }
    
        protected override void Process(double delta)
        {
            // Handle process...
        }
    
        protected override void ExitTree()
        {
            // Handle exit tree...
        }
    }
  4. Generate a Packet Registry

    main

    You can generate deterministic opcode maps for client and server packets by defining a registry class decorated with [PacketRegistry]. This is useful for mapping packet types to specific numeric IDs (opcodes).

    By default, the opcode type is byte. You can specify a different type, such as ushort, in the attribute.

    The generator produces the following dictionaries within the registry:

    • Dictionary<Type, PacketInfo<ClientPacket>> ClientPacketInfo
    • Dictionary<ushort, Type> ClientPacketTypes
    • Dictionary<Type, PacketInfo<ServerPacket>> ServerPacketInfo
    • Dictionary<ushort, Type> ServerPacketTypes
    [PacketRegistry(typeof(ushort))]
    public partial class PacketRegistry
    {
    }
  5. The Type Handler Model in PacketGen

    main

    PacketGen uses an ITypeHandler model to manage how different data types are serialized. Each handler is responsible for determining if it can process a specific type and then emitting the corresponding C# code for reading and writing that type.

    Every ITypeHandler must implement:

    • CanHandle(ITypeSymbol): Returns true if the handler supports the provided type symbol.
    • EmitWrite(...): Generates the C# lines required to write the type to a PacketWriter.
    • EmitRead(...): Generates the C# lines required to read the type from a PacketReader.

    Existing Handlers:

    • PrimitiveTypeHandler
    • ArrayTypeHandler
    • ListTypeHandler
    • DictionaryTypeHandler
    • ComplexTypeHandler (handles nested objects and manages recursion depth via MaxTraversalDepth)

    Note: CollectionLoopEmitter is a shared utility used by array and list handlers to avoid duplicating loop emission logic.

  6. How PacketGen generation flow works

    main

    PacketGen is a Roslyn incremental source generator that automates the creation of serialization logic for network packets. The process follows these steps:

    1. Discovery: The PacketGenerator scans the codebase for types marked with ClientPacket or ServerPacket attributes, as well as targets marked with the [PacketRegistry] attribute.
    2. Orchestration: The PacketGenerationOrchestrator analyzes each discovered packet type and generates the following methods:
      • Write(PacketWriter writer)
      • Read(PacketReader reader)
      • Equals(...)
      • GetHashCode()
    3. Delegation: Specific serialization logic for different data types is delegated to specialized ITypeHandler implementations via a TypeHandlerRegistry.
  7. Configure and customize network packets

    main

    Packets use source generation to automatically implement Write and Read methods for supported types.

    Excluding Properties

    Use the [NetExclude] attribute to prevent specific properties from being serialized into the packet.

    Manual Serialization

    If you need conditional logic (e.g., only sending data if a boolean is true) or if you are using unsupported types, manually override the Write(PacketWriter writer) and Read(PacketReader reader) methods. When you manually override these, the source generator will not produce them for you.

    Supported Types

    TypeSupported
    Primitives (int, bool, ulong, etc.)✅
    Vectors & byte arrays (Vector2, Vector3, byte[])✅
    Generics (List<T>, Dictionary<K, V>)✅
    Arrays (int[], bool[])✅
    Classes & Structs✅
    public partial class SPacketPlayerJoinLeave : ServerPacket
    {
        public uint Id { get; set; }
        public string Username { get; set; }
        public Vector2 Position { get; set; }
        public bool Joined { get; set; }
    
        public override void Write(PacketWriter writer)
        {
            writer.Write((uint)Id);
            writer.Write((bool)Joined);
    
            if (Joined)
            {
                writer.Write((string)Username);
                writer.Write((Vector2)Position);
            }
        }
    
        public override void Read(PacketReader reader)
        {
            Id = reader.ReadUInt();
            Joined = reader.ReadBool();
    
            if (Joined)
            {
                Username = reader.ReadString();
                Position = reader.ReadVector2();
            }
        }
    }
  8. Use strongly typed settings via OptionsGen

    main

    OptionsGen provides a mechanism to access all project settings through strongly typed properties on a settings object. Instead of using string keys or manual parsing, you can access and modify settings directly via an optionsService.Settings instance. This ensures type safety for values like int for difficulty or float for sensitivity.

    // Accessing and setting an integer setting
    int difficulty = optionsService.Settings.Difficulty;
    optionsService.Settings.Difficulty = 2;
    
    // Accessing and setting a float setting
    float sensitivity = optionsService.Settings.MouseSensitivity;
    optionsService.Settings.MouseSensitivity = 0.9f;
  9. Define packet types for PacketGen

    main

    To use PacketGen, your packet types must follow these requirements:

    1. The type must be a partial class (or partial struct).
    2. The type must inherit from either ClientPacket or ServerPacket.
    3. If the type already implements Write or Read methods, PacketGen will skip generation for that type.

    Supported properties include:

    • Primitives and common netcode types (e.g., bool, int, char, string, byte[], Godot.Vector2, Godot.Vector3).
    • Recursive collections: List<T>, Dictionary<TKey, TValue>, and arrays.
    • Nested structs and classes.

    Use the [NetExclude] attribute on individual properties to skip them during generation.

    public struct PlayerStats
    {
        public int Level { get; set; }
        public Vector2 Spawn { get; set; }
    }
    
    public class PlayerProfile
    {
        public string Name { get; set; }
        public PlayerStats Stats { get; set; }
    }
    
    // Must be partial and inherit from ClientPacket or ServerPacket
    public partial class CPacketPlayerInfo : ClientPacket
    {
        public uint Id { get; set; }
        public PlayerProfile Profile { get; set; }
    }
  10. Implement multiplayer netcode with ClientPacket and ServerPacket

    main

    The framework uses a high-performance, low-overhead packet system for multiplayer. Packets are defined as partial classes extending either ClientPacket (sent from client to server) or ServerPacket (sent from server to client).

    To implement netcode:

    1. Define your packet classes.
    2. Use GodotClient to handle client-side logic and GodotServer for server-side logic.
    3. Use OnPacket<T> to register handlers for incoming packets.
    4. Use Send(packet) to transmit data.
    5. Use the Net<TClient, TServer> class to orchestrate the connection in your game world.
    // 1. Define Packets
    public partial class CPlayerJoined : ClientPacket
    {
        public string Username { get; set; }
    }
    
    public partial class SPlayerPositions : ServerPacket
    {
        public Dictionary<uint, Vector2> Positions { get; set; }
    }
    
    // 2. Implement Client
    public partial class GameClient : GodotClient
    {
        public GameClient()
        {
            OnPacket<SPlayerPositions>(packet => {
                // Handle received positions
            });
        }
    
        protected override void OnConnected()
        {
            Send(new CPlayerJoined("Valk"));
        }
    }
    
    // 3. Implement Server
    public partial class GameServer : GodotServer
    {
        public GameServer()
        {
            OnPacket<CPlayerJoined>(peer => {
                // Handle player joining
                Send(new SPlayerPositions(new Dictionary<uint, Vector2>()), peer.PeerId);
            });
        }
    }
    
    // 4. Orchestrate in World
    public partial class World : Node
    {
        private Net<GameClient, GameServer> _net = null!;
    
        public override void _Ready()
        {
            _net = new Net<GameClient, GameServer>();
            _net.StartServer(25565);
            _net.StartClient("127.0.0.1", 25565);
        }
    }
  11. Install GodotUtils via local NuGet package

    main

    To use GodotUtils in your project, you must first build the project to generate the GodotUtils.1.0.0.nupkg file. Once generated, ensure the package is available in a local NuGet source accessible to your consuming project.

    After adding the local package source, reference the package in your .csproj file using a PackageReference.

    <PackageReference Include="GodotUtils" Version="1.0.0" />