Skip to content

A template used for quickly setting up new Godot 4 C# games.

License

Notifications You must be signed in to change notification settings

NicJames2378/ValksGodotTemplate

 
 

Repository files navigation

Godot 4 C# Template

Never again spend 5 minutes setting up a new project, ValksGodotTools/Template has got your back. ❤️

Want to get right into it? Start off by reading the setup guide.

  1. Setup Guide
  2. Features
  3. Tips
  4. Contributing
  5. Roadmap
  6. Credits

main-menu
options
keybindings

Setup Guide

1️⃣ Download the repo

  1. Download and install the latest Godot 4 C# release
  2. Clone with git clone --recursive https://github.com/ValksGodotTools/Template

If the GodotUtils folder is still empty for whatever reason, run git submodule update --init --recursive

2️⃣ Run the game with F5

Note

Steps 2 to 4 change setup settings and delete unneeded assets. These steps are optional.

You should see something like this

setup-scene

Enter a name for your game, this could be something like muffin blaster 3, this will be auto formatted to MuffinBlaster3. All namespaces in all scripts will be replaced with this name. The .csproj, .sln and project.godot files will also be modified with this new name.

Select the genre for your game. Currently there are only 3 types, "3D FPS", "2D Platformer" and "2D Top Down". Lets say you select "3D FPS", this means the "2D Platformer" and "2D Top Down" assets will be deleted and the 3D FPS assets will be moved to more accessible locations.

In all cases you will no longer see the following directories in your project.

asset-folders

3️⃣ Click "Apply Changes"

The following popup will appear, click "Reload"

popup1

Click "Don't Save" and close any IDE's you may have open

popup2

4️⃣ Double click on the main scene in res://Scenes/<your_scene>.tscn

Click "Fix Dependencies" and then click "Fix Broken". Then and only after clicking "Fix Broken", click on "Open Anyway"

popup3 popup4

If you selected "3D FPS" as an example then the 3D FPS scene should run when you press F5.

Caution

Avoid deleting res://Template and res://GodotUtils, doing so will cause certain features to stop working. I have tried my best to move all assets you would need to modify for your game outside of res://Template into res://. If you want to modify the contents of res://GodotUtils, please consider creating a pull request on the repo first.

Important

A internet connection is required when running the game with F5 for the first time. This is because the .csproj needs to retrieve the NuGet packages from the NuGet website.

Features

Multiplayer

The 2D Top Down genre has a client authorative multiplayer setup for showing players positions updating on each others screens. This netcode is the result of redoing the same multiplayer project over and over again. I've lost track how many times I've done this now. I hope you will find the multiplayer as useful as I have.

Multiplayer.Preview.mp4

Important

A very common mistake is to write one data type and read another data type. For example lets say you have the integer playerCount and you do writer.Write(playerCount) and then playerCount = reader.ReadByte(). Since you did not cast playerCount to byte on writing, you will receive malformed data. Lets always cast our data values before writing them even if it may seem redundant at times.

Caution

Do not directly access properties or methods across threads unless they are explicity marked as thread safe. Not following thread safety will result in random crashes with no errors logged to the console. If you want to avoid logs getting jumbled use Game.Log(...) over GD.Print(...).

Here is what a client packet could look like. The client is using this packet to tell the server its position. The Handle(...) is executed on the server thread so only things on that thread should be accessed.

public class CPacketPosition : ClientPacket
{
    public Vector2 Position { get; set; }

    public override void Write(PacketWriter writer)
    {
        writer.Write((Vector2)Position);
    }

    public override void Read(PacketReader reader)
    {
        Position = reader.ReadVector2();
    }

    public override void Handle(ENetServer s, Peer client)
    {
        GameServer server = (GameServer)s;
        server.Players[client.ID].Position = Position;
    }
}

Here is what a server packet could look like. The server is telling each client about all the others client position updates. The Handle(...) is executed on the client thread so only things on that thread should be accessed.

public class SPacketPlayerPositions : ServerPacket
{
    public Dictionary<uint, Vector2> Positions { get; set; }

    public override void Write(PacketWriter writer)
    {
        writer.Write((byte)Positions.Count);

        foreach (KeyValuePair<uint, Vector2> pair in Positions)
        {
            writer.Write((uint)pair.Key);
            writer.Write((Vector2)pair.Value);
        }
    }

    public override void Read(PacketReader reader)
    {
        Positions = new();

        byte count = reader.ReadByte();

        for (int i = 0; i < count; i++)
        {
            uint id = reader.ReadUInt();
            Vector2 position = reader.ReadVector2();

            Positions.Add(id, position);
        }
    }

    public override void Handle(ENetClient client)
    {
        Level level = Global.Services.Get<Level>();

        foreach (KeyValuePair <uint, Vector2> pair in Positions)
        {
            if (level.OtherPlayers.ContainsKey(pair.Key))
                level.OtherPlayers[pair.Key].LastServerPosition = pair.Value;
        }

        // Send a client position packet to the server immediately right after
        // a server positions packet is received
        level.Player.NetSendPosition();
    }
}

Sending a packet from the client

// Player.cs
Net net = Global.Services.Get<Net>();

net.Client.Send(new CPacketPosition
{
    Position = Position
});

Sending a packet from the server

Send(new SPacketPlayerPositions
{
    Positions = GetOtherPlayers(pair.Key).ToDictionary(x => x.Key, x => x.Value.Position)
}, Peers[pair.Key]);

Mod Loader

Note

Mods can replace game assets and execute C# scripts, although there are some limitations. You can find the example mod repository here.

Godot Utils

The submodule Godot Utils contains useful classes and extensions including netcode scripts.

Highlighted Classes

  • ServiceProvider (see Services)
  • EventManager (see Event Manager)
  • Logger (thread safe logger)
  • State (see State Manager)
  • GTween (wrapper for Godot.Tween)
  • GTimer (wrapper for Godot.Timer)

Highlighted Extensions

  • .PrintFull() (e.g. GD.Print(player.PrintFull()))
  • .ForEach()
  • .QueueFreeChildren()

Localisation

Note

Currently English, French and Japanese are supported for most of the UI elements. You can add in your own languages here.

Services

Important

In order to understand how useful Global.Services is, let me tell you why using the static keyword should be avoided. Lets say you are coding a multiplayer game and you make every property in GameServer.cs static. Everything works fine at first and you can easily access the game servers properties from almost anywhere but once you restart the server or leave the scene where the game server shouldn't be alive anymore, the old values for each static property will still exist from the last time the server was online. You would have to keep track of each individual property you made static and reset them. This is why static should be avoided.

In the _Ready() of any node add Global.Services.Add(this) (if the script does not extend from node, you can use Global.Services.Add<Type>)

public partial class UIVignette : ColorRect
{
    public override void _Ready()
    {
        // Set persistent to true if this is an autoload script
        // (scripts that do not extend from Node are persistent by default)

        // Non persistent services will get removed just before the scene is changed
        // Example of persistent service: AudioManager; a node like this should exist
        // for the entire duration of the game

        // However this UIVignette exists within the scene so it should not be persistent
        Global.Services.Add(this, persistent: false); 
    }

    public void LightPulse() { ... }
}

Now you can get the instance of UIVignette from anywhere! No static or long GetNode<T> paths involved. It's magic.

UIVignette vignette = Global.Services.Get<UIVignette>();
vignette.LightPulse();

Console Commands

Adding the ConsoleCommand attribute to any function will register it as a new console command.

Note

The in-game console can be brought up with F12

[ConsoleCommand("help")]
void Help()
{
    IEnumerable<string> cmds =
        Global.Services.Get<UIConsole>().Commands.Select(x => x.Name);

    Game.Log(cmds.Print());
}

Console commands can have aliases, this command has an alias named "exit"

[ConsoleCommand("quit", "exit")]
void Quit()
{
    GetTree().Root.GetNode<Global>("/root/Global").Quit();
}

Method parameters are supported

[ConsoleCommand("debug")]
void Debug(int x, string y)
{
    Game.Log($"Debug {x}, {y}");
}

AudioManager

AudioManager audioManager = Global.Services.Get<AudioManager>();

// Play a soundtrack
audioManager.PlayMusic(Music.Menu);

// Play a sound
audioManager.PlaySFX(Sounds.GameOver);

// Set the music volume
audioManager.SetMusicVolume(75);

// Set the sound volume
audioManager.SetSFXVolume(100);

// Gradually fade out all sounds
audioManager.FadeOutSFX();

SceneManager

// Switch to a scene instantly
Global.Services.Get<SceneManager>().SwitchScene("main_menu");

// Switch to a scene with a fade transition
Global.Services.Get<SceneManager>().SwitchScene("level_2D_top_down", 
    SceneManager.TransType.Fade);

State Manager

This state manager uses functions as states as suppose to using classes for states. The State class is provided in the GodotUtils submodule. Below an example is given.

Create a new file named Player.cs and add the following script to it.

public partial class Player : Entity // This script extends from Entity but it may extend from CharacterBody3D for you
{
    State curState;

    public override void _Ready()
    {
        curState = Idle();
        curState.Enter();
    }

    public override void _PhysicsProcess(double delta)
    {
        curState.Update(delta);
    }

    public void SwitchState(State newState)
    {
        GD.Print($"Switched from {curState} to {newState}"); // Useful for debugging. May be more appealing to just say "Switched to {newState}" instead.

        curState.Exit();
        newState.Enter();
        curState = newState;
    }
}

Create another file named PlayerIdle.cs and add the following.

public partial class Player
{
    State Idle()
    {
        var state = new State(this, nameof(Idle));

        state.Enter = () =>
        {
            // What happens on entering the idle state?
        };

        state.Update = delta =>
        {
            // What happens on every frame in the idle state?
        };

        state.Exit = () =>
        {
            // What happens on exiting the idle state?
        }

        return state;
    }
}

Do a similar process when adding new states.

Experimental EventManager

If you like the idea of having a universal static event manager that handles everything then try out the code below in your own project.

Event Enums

public enum EventGeneric
{
    OnKeyboardInput
}

public enum EventPlayer
{
    OnPlayerSpawn
}

Event Dictionaries

public static class Events
{
    public static EventManager<EventGeneric> Generic { get; } = new();
    public static EventManager<EventPlayer> Player { get; } = new();
}

Example #1

Events.Generic.AddListener(EventGeneric.OnKeyboardInput, (args) => 
{
    GD.Print(args[0]);
    GD.Print(args[1]);
    GD.Print(args[2]);
}, "someId");

Events.Generic.RemoveListeners(EventGeneric.OnKeyboardInput, "someId");

// Listener is never called because it was removed
Events.Generic.Notify(EventGeneric.OnKeyboardInput, 1, 2, 3);

Example #2

Events.Player.AddListener<PlayerSpawnArgs>(EventPlayer.OnPlayerSpawn, (args) => 
{
    GD.Print(args.Name);
    GD.Print(args.Location);
    GD.Print(args.Player);
});

Events.Player.Notify(EventPlayer.OnPlayerSpawn, new PlayerSpawnArgs(name, location, player));

Tips

Tip

If you need to execute code before the game quits you can listen to OnQuit.

// This is an async function because you way want to await certain processes before the game exists
Global.Services.Get<Global>().OnQuit += async () =>
{
    // Execute your code here
    await Task.FromResult(1);
}

Contributing

Important

Please have a quick look at the Projects Coding Style and contact me over Discord before contributing. My Discord username is valky5.

Note

Here are some good first issues to tackle.

Roadmap

3D FPS

  • Add an animated weapon model from Blender and add logic for it in game
  • Add a test environment
  • Fully implement multiplayer

2D Platformer

  • Add example states
  • Fully implement multiplayer

2D Top Down

  • Add a sword swing system
  • Add example states

Mod Loader

  • Beautify the mod loader scene and add the appropriate logic that follows it
  • Figure out how to ignore scripts in mods. For example I want to add a script in each mod that helps modders export the C# dll mods but this script can't be included because it will conflict with the same script from other mods.
  • Figure out how to allow duplicate scripts from different mods

Msc

  • Add a dialogue system that translates dialogue and choices in a text file to game logic
  • Add a inventory system
  • Add the ability to scroll in the credits scene
  • Implement a dedicated server authorative multiplayer model

Credits

Note

For all credit to in-game assets used, see credits.txt.

About

A template used for quickly setting up new Godot 4 C# games.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 100.0%