Chickensoft.GoDotNet
1.2.0-beta8
GoDotNet is no longer maintained and has been superseded by other Chickensoft projects. We really appreciate you using it, but there are better ways to do everything this package offered
State machines — please use Chickensoft.LogicBlocks — it is well tested, supports bindings, and can even generate a diagram of your code for you.
Notifiers - use C#'s BehaviorSubject from the Reactive Extensions.
Scheduler - use Godot's built-in call_deferred mechanism.
Thank you for your understanding. Please reach out in our Discord server if you have any questions. https://discord.gg/MjA6HUzzAE
See the version list below for details.
dotnet add package Chickensoft.GoDotNet --version 1.2.0-beta8
NuGet\Install-Package Chickensoft.GoDotNet -Version 1.2.0-beta8
<PackageReference Include="Chickensoft.GoDotNet" Version="1.2.0-beta8" />
paket add Chickensoft.GoDotNet --version 1.2.0-beta8
#r "nuget: Chickensoft.GoDotNet, 1.2.0-beta8"
// Install Chickensoft.GoDotNet as a Cake Addin #addin nuget:?package=Chickensoft.GoDotNet&version=1.2.0-beta8&prerelease // Install Chickensoft.GoDotNet as a Cake Tool #tool nuget:?package=Chickensoft.GoDotNet&version=1.2.0-beta8&prerelease
GoDotNet
State machines, notifiers, and other utilities for C# Godot development.
🚨 Looking for node-based dependency injection with providers and dependents? That functionality has been moved to it's own package, GoDotDep!
While developing our own game, we couldn't find any simple C# solutions for simple state machines, notifiers, and mechanisms for avoiding unnecessary marshalling with Godot. So, we built our own systems — hopefully you can benefit from them, too!
Are you on discord? If you're building games with Godot and C#, we'd love to see you in the Chickensoft Discord server!
Installation
Find the latest version of GoDotNet on nuget.
In your *.csproj
, add the following snippet in your <ItemGroup>
, save, and run dotnet restore
. Make sure to replace *VERSION*
with the latest version.
<PackageReference Include="Chickensoft.GoDotNet" Version="*VERSION*" />
GoDotNet is itself written in C# 10 for netstandard2.1
(the highest language version currently supported by Godot). If you want to setup your project the same way, look no further than the GoDotNet.csproj
file for inspiration!
Logging
Internally, GoDotNet uses GoDotLog for logging. GoDotLog allows you to easily create loggers that output nicely formatted, prefixed messages (in addition to asserts and other exception-aware execution utilities).
Autoloads
An autoload can be fetched easily from any node. Once an autoload is found on the root child, GoDotNet caches it's type, allowing it to be looked up instantaneously without calling into Godot.
public class MyEntity : Node {
private MyAutoloadType _myAutoload => this.Autoload<MyAutoloadType>();
public override void _Ready() {
_myAutoload.DoSomething();
var otherAutoload = this.Autoload<OtherAutoload>();
}
}
Scheduling
A Scheduler
node is included which allows callbacks to be run on the next frame, similar to CallDeferred. Unlike CallDeferred
, the scheduler uses vanilla C# to avoid marshalling types to Godot. Since Godot cannot marshal objects that don't extend Godot.Object
/Godot.Reference
, this utility is provided to perform the same function for records, custom types, and C# collections which otherwise couldn't be marshaled between C# and Godot.
Create a new autoload which extends the scheduler:
using GoDotNet;
public class GameScheduler : Scheduler { }
Add it to your project.godot
file (preferably the first entry):
[autoload]
GameScheduler="*res://autoload_folder/GameScheduler.cs"
...and simply schedule a callback to run on the next frame:
this.Autoload<Scheduler>().NextFrame(
() => _log.Print("I won't execute until the next frame.")
)
State Machines
GoDotNet provides a simple state machine implementation that emits a C# event when the state changes (since Godot signals are more fragile). If you try to update the machine to a state that isn't a valid transition from the current state, it throws an exception. The machine requires an initial state to avoid nullability issues during construction.
State machines are not extensible — instead, GoDotNet almost always prefers the pattern of composition over inheritance. The state machine relies on state equality to determine if the state has changed to avoid issuing unnecessary events. Using record
types for the state allows this to happen automatically.
States used with a state machine must implement IMachineState<T>
, where T is just the type of the machine state. Your machine states can optionally implement CanTransitionTo(IMachineState state)
, which should return true if the given "next state" is a valid transition. Otherwise, the default implementation returns true
to allow transitions to any state.
To create states for use with a machine, create an interface which implements IMachineState<IYourInterface>
. Then, create record types for each state which implement your interface, optionally overriding CanTransitionTo
for any states which only allow transitions to specific states.
public interface IGameState : IMachineState<IGameState> { }
public record GameMainMenuState : IGameState {
public bool CanTransitionTo(IGameState state) => state is GameLoadingState;
}
public record GameLoadingState : IGameState {
public bool CanTransitionTo(IGameState state) => state is GamePlayingState;
}
// States can store values!
public record GamePlayingState(string PlayerName) {
public bool CanTransitionTo(IGameState state) => state is GameMainMenuState;
}
Simply omit implementing CanTransitionTo
for any states which should allow transitions to any other state.
public interface GameSuspended : IGameState { }
Machines are fairly simple to use: create one with an initial state (and optionally register a machine state change event handler). A state machine will announce the state has changed as soon as it is constructed.
public class GameManager : Node {
private readonly Machine<IGameState> _machine;
// Expose the machine's event.
public event Machine<IGameState>.Changed OnChanged {
add => _machine.OnChanged += value;
remove => _machine.OnChanged -= value;
}
public override void _Ready() {
_machine = new Machine<IGameState>(new GameMainMenuState(), onChanged);
}
/// <summary>Starts the game.</summary>
public void Start(string name) {
_machine.Update(new GameLoadingState());
// do your loading...
// ...
// start the game!
_machine.Update(new GamePlayingState(name);
// Read the current state at any time:
var state = _machine.State;
if (state is GamePlayingState) { /* ... */ }
}
/// <summary>Goes back to the menu.</summary>
public void GoToMenu() => _machine.Update(new GameMainMenuState());
public void OnChanged(IGameState state) {
if (state is GamePlayingState playingState) {
var playerName = playingState.Name();
// ...
}
}
}
Read-only State Machines
If you want another object to only be able to read the current state of a state machine and subscribe to changes, but not be able to update the state of the machine, you can expose the machine as an IReadOnlyMachine<TState>
instead of as a Machine<TState>
.
public class AnObjectThatOnlyListensToAMachine {
public IReadOnlyMachine<string> Machine { get; set; }
// This object can listen and respond to state changes in Machine, but
// cannot mutate the state of the machine via `Machine.Update` :)
}
Notifiers
A notifier is an object which emits a signal when its value changes. Notifiers are similar to state machines, but they don't care about transitions. Any update that changes the value (determined by comparing the new value with the previous value using Object.Equals
) will emit a signal. It's often convenient to use record types as the value of a Notifier. Like state machines, the value of a notifier can never be null
— make sure you initialize with a valid value!
Because notifiers check equality to determine changes, they are convenient to use with "value" types (like primitives, records, and structs). Notifiers, like state machines, also emit a signal to announce their value as soon as they are constructed.
private var _notifier
= new Notifier<string>("Player", OnPlayerNameChanged);
private void OnPlayerNameChanged(string name) {
_log.Print($"Player name changed to $name");
}
Like state machines, notifiers should typically be kept private. Instead of letting consumers modify the value directly, you can create game logic classes which provide the appropriate methods to mutate the notifier. Game logic classes can provide an event which redirects to the notifier event, or they can emit their own events when certain pieces of the notifier value changes.
public record EnemyData(string Name, int Health);
public class EnemyManager {
private readonly Notifier<EnemyData> _notifier;
public event Notifier<EnemyData>.Changed OnChanged {
add => _notifier.OnChanged += value;
remove => _notifier.OnChanged -= value;
}
public EnemyData Value => _notifier.Value;
public EnemyManager(string name, int health) => _notifier = new(
new EnemyData(name, health)
);
public void UpdateName(string name) =>
_notifier.Update(_notifier.Value with { Name = name });
public void UpdateHealth(int health) =>
_notifier.Update(_notifier.Value with { Health = health });
}
The class above shows an enemy state class that emits an OnChanged
event whenever any part of the enemy's state changes. You can easily modify it to emit more specific events when certain pieces of the enemy state changes.
public class EnemyManager {
private readonly Notifier<EnemyData> _notifier;
public EnemyData Value => _notifier.Value;
public event Action<string>? OnNameChanged;
public event Action<int>? OnHealthChanged;
public EnemyManager(string name, int health) => _notifier = new(
new EnemyData(name, health),
OnChanged
);
public void UpdateName(string name) =>
_notifier.Update(_notifier.Value with { Name = name });
public void UpdateHealth(int health) =>
_notifier.Update(_notifier.Value with { Health = health });
private void OnChanged(EnemyData enemy, EnemyData? previous) {
// Emit different events depending on what changed.
if (!System.Object.Equals(enemy.Name, previous?.Name)) {
OnNameChanged?.Invoke(enemy.Name);
}
else if (!System.Object.Equals(enemy.Health, previous?.Health)) {
OnHealthChanged?.Invoke(enemy.Health);
}
}
}
By providing classes which wrap state machines or notifiers to dependent nodes, you can create nodes which easily respond to changes in the values provided by distant ancestor nodes. To easily provide dependencies to distant child nodes, check out go_dot_dep.
Read-only Notifiers
As with state machines, a notifiers can be referenced as an IReadOnlyNotifier<TValue>
to prevent objects using them from causing unwanted changes. The object(s) that own the notifier can reference it as a Notifier<TValue>
and mutate it accordingly, while the listener objects can simply reference it as an IReadOnlyNotifier<TValue>
.
public class AnObjectThatOnlyListensToANotifier {
public IReadOnlyNotifier<string> Notifier { get; set; }
// This object can listen and respond to changes in the Notifier's value, but
// cannot mutate the value of the notifier via `Notifier.Update` :)
}
Signals and Events
Godot supports emitting signals from C#. Because Godot signals pass through the Godot engine, any arguments given to the signal must be marshalled through Godot, forcing them to be classes which extend Godot.Object
/Godot.Reference
(records aren't allowed). Likewise, all the fields in the class must also be the same kind of types so they can be marshalled, and so on.
It's not possible to have static typing with signal parameters, so you don't find out until runtime if you accidentally passed the wrong parameter. The closest you can do is the following, which wouldn't break at compile time if the receiving function signature happened to be wrong.
public class ObjectType {
[Signal]
public delegate void DoSomething(string value);
}
public class MyNode : Node {
// Inside your node
public override void _Ready() {
_ = object.Connect(
nameof(ObjectType.DoSomething),
this,
nameof(MyDoSomething)
);
}
private void DoSomething(int value) {
// WARNING: Value should be string, not int!!
}
}
Because of these limitations, GoDotNet will avoid Godot signals except when necessary to interact with Godot components. For communication between C# game logic, it will typically be preferable to use C# events instead.
// Declare an event signature — no [Signal] attribute necessary.
public delegate void Changed(Type1 value1, Type2 value2);
// Add an event field to your emitter
public event Changed? OnChanged;
// Trigger an event from your emitter:
OnChanged?.Invoke(argument1, argument2);
// Listen to an event in your receiver:
var emitter = new MyEmitterObject();
emitter.OnChanged += MyOnChangedHandler;
// Event handler in your receiver:
private void MyOnChangedHandler(Type1 value1, Type2 value2) {
// respond to event we received
}
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- Chickensoft.GoDotLog (>= 1.0.2-beta8)
- Godot.SourceGenerators (>= 4.0.0-beta.8)
- GodotSharp (>= 4.0.0-beta.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated | |
---|---|---|---|
1.5.16 | 252 | 8/21/2023 | |
1.5.15 | 362 | 7/7/2023 | |
1.5.14-godot4.1.0-rc.3 | 133 | 7/4/2023 | |
1.5.13-godot4.1.0-rc.3 | 118 | 7/4/2023 | |
1.5.12-godot4.1.0-rc.2 | 109 | 6/30/2023 | |
1.5.11-godot4.1.0-rc.2 | 108 | 6/29/2023 | |
1.5.10-godot4.1.0-rc.1 | 110 | 6/27/2023 | |
1.5.9-godot4.1.0-beta.3 | 114 | 6/22/2023 | |
1.5.8-godot4.1.0-beta.2 | 99 | 6/15/2023 | |
1.5.7-godot4.1.0-beta.1 | 108 | 6/8/2023 | |
1.5.6 | 170 | 5/19/2023 | |
1.5.5-godot4.0.3-rc.2 | 111 | 5/12/2023 | |
1.5.4-godot4.0.3-rc.1 | 104 | 4/28/2023 | |
1.5.3 | 261 | 4/4/2023 | |
1.5.2-godot4.0.2-rc.1 | 112 | 4/3/2023 | |
1.5.1-godot4.0.2-rc.1 | 116 | 4/2/2023 | |
1.5.0 | 241 | 4/2/2023 | |
1.4.0-beta8 | 144 | 12/27/2022 | |
1.3.0-beta8 | 118 | 12/18/2022 | |
1.2.0-beta8 | 118 | 12/18/2022 | |
1.1.1-beta8 | 129 | 12/17/2022 | |
1.1.1-beta6 | 118 | 11/28/2022 | |
1.1.0-beta6 | 123 | 11/25/2022 | |
1.0.0 | 461 | 8/13/2022 | |
0.0.2 | 471 | 7/1/2022 | |
0.0.1 | 432 | 7/1/2022 |
GoDotNet release.