Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f76335c
Start working on Blackholio for Godot
lisandroct Apr 20, 2026
af02077
Wrap up part 1
lisandroct Apr 22, 2026
68dbd41
Wrap up demo
lisandroct Apr 23, 2026
7248039
Update tutorial up to part 3
lisandroct Apr 24, 2026
cbf2edd
Update Godot docs
lisandroct Apr 27, 2026
5e544ed
Fix and polish part 1 and part 2 of the Godot tutorial
lisandroct Apr 28, 2026
4a1c1c2
Fix and polish part 3 and part 4 of the Godot tutorial
lisandroct Apr 28, 2026
3816a84
Cleanup Unity tutorial part 2
lisandroct Apr 28, 2026
e80e557
Fix and polish part 3 and part 4 of the Unity tutorial
lisandroct Apr 28, 2026
73721b7
Add Godot SDK
lisandroct Apr 29, 2026
231b008
Add notes about auth tokens and splitting into multiple circles
lisandroct Apr 30, 2026
c2f70df
Update demo files
lisandroct Apr 30, 2026
398a669
Add Godot SDK csproj
lisandroct May 5, 2026
dea7cbf
Update demo project
lisandroct May 5, 2026
379608c
Fix tutorial
lisandroct May 5, 2026
db000b8
Use ZIndex in the Arena
lisandroct May 5, 2026
09ea49b
Use ZIndex in code
lisandroct May 5, 2026
8198a73
Update csproj to 2.2.0
lisandroct May 6, 2026
1c348e9
Add Scheduled Table attribute to MoveAllPlayersTimer
lisandroct May 6, 2026
28f063a
Fix images
lisandroct May 7, 2026
aada75e
Fix images urls
lisandroct May 7, 2026
faf3e7e
Fix ci problems
lisandroct May 8, 2026
275a310
Add Godot SDK to regen dlls tool
lisandroct May 8, 2026
a1f4504
Adding .meta files for new .cs scripts
rekhoff May 8, 2026
f72ca27
Fix .NET test
lisandroct May 8, 2026
d2d30ec
Fix smoketest
lisandroct May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ jobs:

- name: Run .NET tests
working-directory: sdks/csharp
run: dotnet test -warnaserror --no-restore
run: dotnet test -warnaserror --no-restore SpacetimeDB.ClientSDK.csproj

- name: Verify C# formatting
working-directory: sdks/csharp
Expand Down
43 changes: 36 additions & 7 deletions crates/smoketests/tests/smoketests/quickstart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,32 @@ fn create_nuget_config(sources: &[(String, PathBuf)], mappings: &[(String, Strin

/// Override nuget config to use a local NuGet package on a .NET project.
fn override_nuget_package(project_dir: &Path, package: &str, source_dir: &Path, build_subdir: &str) -> Result<()> {
override_nuget_package_from_project(project_dir, package, source_dir, None, build_subdir)
}

/// Override nuget config to use a local NuGet package built from a specific .NET project.
fn override_nuget_package_from_project(
project_dir: &Path,
package: &str,
source_dir: &Path,
source_project: Option<&str>,
build_subdir: &str,
) -> Result<()> {
println!("Override {package}: {project_dir:?} with {source_dir:?}");

// Make sure the local package is built
let workspace = workspace_root();
let repo_nuget_config = workspace.join("NuGet.Config");
let source_project_path = source_project.map(|project| source_dir.join(project));
if repo_nuget_config.exists() {
let output = Command::new("dotnet")
.args(["restore", "--configfile", repo_nuget_config.to_str().unwrap()])
let mut command = Command::new("dotnet");
command.arg("restore");
if let Some(source_project_path) = &source_project_path {
command.arg(source_project_path);
}
let output = command
.arg("--configfile")
.arg(&repo_nuget_config)
.current_dir(source_dir)
.output()
.context("Failed to run dotnet restore")?;
Expand All @@ -152,8 +170,13 @@ fn override_nuget_package(project_dir: &Path, package: &str, source_dir: &Path,
);
}

let output = Command::new("dotnet")
.args(["pack", "-c", "Release", "--no-restore"])
let mut command = Command::new("dotnet");
command.arg("pack");
if let Some(source_project_path) = &source_project_path {
command.arg(source_project_path);
}
let output = command
.args(["-c", "Release", "--no-restore"])
.current_dir(source_dir)
.output()
.context("Failed to run dotnet pack")?;
Expand All @@ -165,8 +188,13 @@ fn override_nuget_package(project_dir: &Path, package: &str, source_dir: &Path,
);
}
} else {
let output = Command::new("dotnet")
.args(["pack", "-c", "Release"])
let mut command = Command::new("dotnet");
command.arg("pack");
if let Some(source_project_path) = &source_project_path {
command.arg(source_project_path);
}
let output = command
.args(["-c", "Release"])
.current_dir(source_dir)
.output()
.context("Failed to run dotnet pack")?;
Expand Down Expand Up @@ -637,10 +665,11 @@ log = "0.4"
&workspace.join("crates/bindings-csharp/BSATN.Runtime"),
"bin/Release",
)?;
override_nuget_package(
override_nuget_package_from_project(
client_path,
"SpacetimeDB.ClientSDK",
&workspace.join("sdks/csharp"),
Some("SpacetimeDB.ClientSDK.csproj"),
"bin~/Release",
)?;

Expand Down
4 changes: 4 additions & 0 deletions demo/Blackholio/client-godot/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
root = true

[*]
charset = utf-8
2 changes: 2 additions & 0 deletions demo/Blackholio/client-godot/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
3 changes: 3 additions & 0 deletions demo/Blackholio/client-godot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/
49 changes: 49 additions & 0 deletions demo/Blackholio/client-godot/CameraController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Godot;

public partial class CameraController : Camera2D
{
[Export]
public float BaseVisibleRadius { get; set; } = 50.0f;

[Export]
public float FollowLerpSpeed { get; set; } = 8.0f;

[Export]
public float ZoomLerpSpeed { get; set; } = 2.0f;

private float WorldSize { get; }

public CameraController(float worldSize)
{
WorldSize = worldSize;
}

public override void _Process(double delta)
{
Vector2 targetPosition;
if (GameManager.IsConnected() && PlayerController.Local != null && PlayerController.Local.TryGetCenterOfMass(out var centerOfMass))
{
targetPosition = centerOfMass;
}
else
{
var hWorldSize = WorldSize * 0.5f;
targetPosition = new Vector2(hWorldSize, hWorldSize);
}

GlobalPosition = GlobalPosition.Lerp(targetPosition, (float)delta * FollowLerpSpeed);

if (PlayerController.Local == null)
{
return;
}

var targetCameraSize = CalculateCameraSize(PlayerController.Local);
var desiredZoom = Vector2.One * (BaseVisibleRadius / Mathf.Max(targetCameraSize, 1.0f));
Zoom = Zoom.Lerp(desiredZoom, (float)delta * ZoomLerpSpeed);
}

private static float CalculateCameraSize(PlayerController player) => 10.0f
+ Mathf.Min(10.0f, player.TotalMass() / 5.0f)
+ Mathf.Min(player.NumberOfOwnedCircles - 1, 1) * 30.0f;
}
1 change: 1 addition & 0 deletions demo/Blackholio/client-godot/CameraController.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c5uuweuf2vu0e
34 changes: 34 additions & 0 deletions demo/Blackholio/client-godot/Circle2D.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Godot;

public partial class Circle2D : Node2D
{
private float _radius = 10.0f;
[Export]
public float Radius
{
get => _radius;
set
{
if (Mathf.IsEqualApprox(_radius, value)) return;

_radius = value;
QueueRedraw();
}
}

private Color _color = Colors.Brown;
[Export]
public Color Color
{
get => _color;
set
{
if (_color == value) return;

_color = value;
QueueRedraw();
}
}

public override void _Draw() => DrawCircle(Vector2.Zero, Radius, Color);
}
1 change: 1 addition & 0 deletions demo/Blackholio/client-godot/Circle2D.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://7lmrsq2i7mi1
115 changes: 115 additions & 0 deletions demo/Blackholio/client-godot/CircleController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using Godot;
using SpacetimeDB.Types;

public partial class CircleController : EntityController
{
private static readonly Color[] ColorPalette =
[
//Yellow
new(175 / 255.0f, 159 / 255.0f, 49 / 255.0f),
new(175 / 255.0f, 116 / 255.0f, 49 / 255.0f),
//Purple
new(112 / 255.0f, 47 / 255.0f, 252 / 255.0f),
new(51 / 255.0f, 91 / 255.0f, 252 / 255.0f),
//Red
new(176 / 255.0f, 54 / 255.0f, 54 / 255.0f),
new(176 / 255.0f, 109 / 255.0f, 54 / 255.0f),
new(141 / 255.0f, 43 / 255.0f, 99 / 255.0f),
//Blue
new(2 / 255.0f, 188 / 255.0f, 250 / 255.0f),
new(7 / 255.0f, 50 / 255.0f, 251 / 255.0f),
new(2 / 255.0f, 28 / 255.0f, 146 / 255.0f)
];

private static CanvasLayer _labelLayer;
private static CanvasLayer LabelLayer
{
get
{
if (_labelLayer == null)
{

if (Engine.GetMainLoop() is not SceneTree sceneTree) return null;
var root = sceneTree.Root;
if (root == null) return null;
_labelLayer = new CanvasLayer { Name = "CircleLabelLayer" };
root.AddChild(_labelLayer);
}
return _labelLayer;
}
}
private static Control _labelRoot;
private static Control LabelRoot
{
get
{
if (_labelRoot == null)
{
_labelRoot = new Control
{
Name = "CircleLabelRoot",
MouseFilter = Control.MouseFilterEnum.Ignore
};
_labelRoot.SetAnchorsPreset(Control.LayoutPreset.FullRect);

LabelLayer.AddChild(_labelRoot);
}
return _labelRoot;
}
}

private Label _label;
private Label Label
{
get
{
if (_label == null)
{
_label = new Label
{
Name = $"{Name}_Label",
TopLevel = false,
MouseFilter = Control.MouseFilterEnum.Ignore
};
LabelRoot.AddChild(_label);
}
return _label;
}
}

private PlayerController OwnerPlayer { get; set; }

public CircleController(Circle circle, PlayerController ownerPlayer) : base(circle.EntityId, ColorPalette[circle.PlayerId % ColorPalette.Length])
{
OwnerPlayer = ownerPlayer;
Label.Text = ownerPlayer.Username;

ownerPlayer.OnCircleSpawned(this);
}

public override void _Process(double delta)
{
base._Process(delta);
UpdateScreenLabelPosition();
}

public override void OnDelete()
{
base.OnDelete();

if (IsInstanceValid(Label))
{
Label.QueueFree();
}

OwnerPlayer?.OnCircleDeleted(this);
}

private void UpdateScreenLabelPosition()
{
Label.Size = Label.GetCombinedMinimumSize();
var screenPosition = GetGlobalTransformWithCanvas().Origin;
var offset = new Vector2(0.0f, Radius + 8.0f);
Label.Position = screenPosition + offset - (Label.Size / 2.0f);
}
}
1 change: 1 addition & 0 deletions demo/Blackholio/client-godot/CircleController.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c328rnk2rjynl
48 changes: 48 additions & 0 deletions demo/Blackholio/client-godot/EntityController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Godot;
using SpacetimeDB.Types;

public abstract partial class EntityController : Circle2D
{
private const float LerpDurationSec = 0.1f;

public int EntityId { get; private set; }

private float LerpTime { get; set; }
private Vector2 LerpStartPosition { get; set; }
private Vector2 TargetPosition { get; set; }
private float TargetRadius { get; set; }

protected EntityController(int entityId, Color color)
{
EntityId = entityId;
Color = color;

var entity = GameManager.Conn.Db.Entity.EntityId.Find(entityId);
var position = (Vector2)entity.Position;
LerpStartPosition = position;
TargetPosition = position;
GlobalPosition = position;
Radius = 0;
TargetRadius = MassToRadius(entity.Mass);
}

public void OnEntityUpdated(Entity newRow)
{
LerpTime = 0.0f;
LerpStartPosition = GlobalPosition;
TargetPosition = (Vector2)newRow.Position;
TargetRadius = MassToRadius(newRow.Mass);
}

public virtual void OnDelete() => QueueFree();

public override void _Process(double delta)
{
var frameDelta = (float)delta;
LerpTime = Mathf.Min(LerpTime + frameDelta, LerpDurationSec);
GlobalPosition = LerpStartPosition.Lerp(TargetPosition, LerpTime / LerpDurationSec);
Radius = Mathf.Lerp(Radius, TargetRadius, frameDelta * 8.0f);
}

private static float MassToRadius(int mass) => Mathf.Sqrt(mass);
}
1 change: 1 addition & 0 deletions demo/Blackholio/client-godot/EntityController.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bhtg3omtsxp7d
10 changes: 10 additions & 0 deletions demo/Blackholio/client-godot/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Godot;

namespace SpacetimeDB.Types
{
public partial class DbVector2
{
public static implicit operator Vector2(DbVector2 vec) => new(vec.X, vec.Y);
public static implicit operator DbVector2(Vector2 vec) => new(vec.X, vec.Y);
}
}
1 change: 1 addition & 0 deletions demo/Blackholio/client-godot/Extensions.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cxx8tt4m0yxbr
17 changes: 17 additions & 0 deletions demo/Blackholio/client-godot/FoodController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Godot;
using SpacetimeDB.Types;

public partial class FoodController : EntityController
{
private static readonly Color[] ColorPalette =
[
new(119 / 255.0f, 252 / 255.0f, 173 / 255.0f),
new(76 / 255.0f, 250 / 255.0f, 146 / 255.0f),
new(35 / 255.0f, 246 / 255.0f, 120 / 255.0f),
new(119 / 255.0f, 251 / 255.0f, 201 / 255.0f),
new(76 / 255.0f, 249 / 255.0f, 184 / 255.0f),
new(35 / 255.0f, 245 / 255.0f, 165 / 255.0f),
];

public FoodController(Food food) : base(food.EntityId, ColorPalette[food.EntityId % ColorPalette.Length]) { }
}
1 change: 1 addition & 0 deletions demo/Blackholio/client-godot/FoodController.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cr4egm415cpw4
Loading
Loading