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.
- Download and install the latest Godot 4 C# release
- 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
Note
Steps 2 to 4 change setup settings and delete unneeded assets. These steps are optional.
You should see something like this
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.
The following popup will appear, click "Reload"
Click "Don't Save" and close any IDE's you may have open
Click "Fix Dependencies" and then click "Fix Broken". Then and only after clicking "Fix Broken", click on "Open Anyway"
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.
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]);
Note
Mods can replace game assets and execute C# scripts, although there are some limitations. You can find the example mod repository here.
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()
Note
Currently English, French and Japanese are supported for most of the UI elements. You can add in your own languages here.
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();
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 = 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();
// 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);
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.
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.
public enum EventGeneric
{
OnKeyboardInput
}
public enum EventPlayer
{
OnPlayerSpawn
}
public static class Events
{
public static EventManager<EventGeneric> Generic { get; } = new();
public static EventManager<EventPlayer> Player { get; } = new();
}
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);
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));
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);
}
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.
- Add an animated weapon model from Blender and add logic for it in game
- Add a test environment
- Fully implement multiplayer
- Add example states
- Fully implement multiplayer
- Add a sword swing system
- Add example states
- 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
- 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
Note
For all credit to in-game assets used, see credits.txt.