Connecting to SpacetimeDB
Need help with the tutorial? Join our Discord server!
This progressive tutorial is continued from part 1.
Project Structure
Now that we have our client project setup we can configure the module directory. Regardless of what language you choose, your module will always go into a spacetimedb directory within your client directory like this:
blackholio/ # Unreal project root
├── Binaries/
├── blackholio.sln
├── blackholio.uproject
├── Config/
├── Content/
├── Plugins/
│ └── SpacetimeDbSdk/ # This is where the SpacetimeDB Unreal SDK lives
├── ... rest of Unreal files
└── spacetimedb/ # This is where the server module lives
Create a Server Module
Ensure you have SpacetimeDB version >=1.4.0 installed to enable Unreal Engine code generation support. You can use spacetime --version to check your version and you can use spacetime version upgrade to install the latest version.
If you have not already installed the spacetime CLI, check out our Getting Started guide for instructions on how to install.
In the same directory that contains your blackholio project, run the following command to initialize the SpacetimeDB server module project with your desired language:
The blackholio directory specified here is the same blackholio directory you created during part 1.
- Rust
- C#
Run the following command to initialize the SpacetimeDB server module project with Rust as the language:
spacetime init --lang rust --server-only blackholioThis command creates a new folder named blackholio inside of your Unreal project blackholio directory and sets up the SpacetimeDB server project with Rust as the programming language.
Run the following command to initialize the SpacetimeDB server module project with C# as the language:
spacetime init --lang csharp --server-only blackholioThis command creates a new folder named blackholio inside of your Unreal project blackholio directory and sets up the SpacetimeDB server project with C# as the programming language.
SpacetimeDB Tables
- Rust
- C#
In this section we'll be making some edits to the file blackholio/spacetimedb/src/lib.rs. We recommend you open up this file in an IDE like VSCode or RustRover.
Important: Open the blackholio/spacetimedb/src/lib.rs file and delete its contents. We will be writing it from scratch here.
In this section we'll be making some edits to the file blackholio/spacetimedb/Lib.cs. We recommend you open up this file in an IDE like VSCode or Rider.
Important: Open the blackholio/spacetimedb/Lib.cs file and delete its contents. We will be writing it from scratch here.
First we need to add some imports at the top of the file. Some will remain unused for now.
- Rust
- C#
Copy and paste into lib.rs:
use std::time::Duration;
use spacetimedb::{rand::Rng, Identity, SpacetimeType, ReducerContext, ScheduleAt, Table, Timestamp};Copy and paste into Lib.cs:
using SpacetimeDB;
public static partial class Module
{
}We are going to start by defining a SpacetimeDB table. A table in SpacetimeDB is a relational database table which stores rows, similar to something you might find in SQL. SpacetimeDB tables differ from normal relational database tables in that they are stored fully in memory, are blazing fast to access, and are defined in your module code, rather than in SQL.
- Rust
- C#
Each row in a SpacetimeDB table is associated with a struct type in Rust.
Let's start by defining the Config table. This is a simple table which will store some metadata about our game's state. Add the following code to lib.rs.
// We're using this table as a singleton, so in this table
// there only be one element where the `id` is 0.
#[spacetimedb::table(name = config, public)]
pub struct Config {
#[primary_key]
pub id: i32,
pub world_size: i64,
}Let's break down this code. This defines a normal Rust struct with two fields: id and world_size. We have decorated the struct with the spacetimedb::table macro. This procedural Rust macro signals to SpacetimeDB that it should create a new SpacetimeDB table with the row type defined by the Config type's fields.
The spacetimedb::table macro takes two parameters, a name which is the name of the table and what you will use to query the table in SQL, and a public visibility modifier which ensures that the rows of this table are visible to everyone.
The #[primary_key] attribute, specifies that the id field should be used as the primary key of the table.
Each row in a SpacetimeDB table is associated with a struct type in C#.
Let's start by defining the Config table. This is a simple table which will store some metadata about our game's state. Add the following code inside the Module class in Lib.cs.
// We're using this table as a singleton, so in this table
// there will only be one element where the `id` is 0.
[Table(Name = "config", Public = true)]
public partial struct Config
{
[PrimaryKey]
public int id;
public long world_size;
}Let's break down this code. This defines a normal C# struct with two fields: id and world_size. We have added the [Table(Name = "config", Public = true)] attribute the struct. This attribute signals to SpacetimeDB that it should create a new SpacetimeDB table with the row type defined by the Config type's fields.
Although we're using
lower_snake_casefor our column names to have consistent column names across languages in this tutorial, you can also usecamelCaseorPascalCaseif you prefer. See #2168 for more information.
The Table attribute takes two parameters, a Name which is the name of the table and what you will use to query the table in SQL, and a Public visibility modifier which ensures that the rows of this table are visible to everyone.
The [PrimaryKey] attribute, specifies that the id field should be used as the primary key of the table.
The primary key of a row defines the "identity" of the row. A change to a row which doesn't modify the primary key is considered an update, but if you change the primary key, then you have deleted the old row and inserted a new one.
- Rust
- C#
You can learn more the table macro in our Rust module reference.
You can learn more the Table attribute in our C# module reference.
Creating Entities
- Rust
- C#
Next, we're going to define a new SpacetimeType called DbVector2 which we're going to use to store positions. The difference between a #[derive(SpacetimeType)] and a #[spacetimedb(table)] is that tables actually store data, whereas the deriving SpacetimeType just allows you to create a new column of that type in a SpacetimeDB table. Therefore, DbVector2 is only a type, and does not define a table.
Append to the bottom of lib.rs:
// This allows us to store 2D points in tables.
#[derive(SpacetimeType, Clone, Debug)]
pub struct DbVector2 {
pub x: f32,
pub y: f32,
}Let's create a few tables to represent entities in our game.
#[spacetimedb::table(name = entity, public)]
#[derive(Debug, Clone)]
pub struct Entity {
// The `auto_inc` attribute indicates to SpacetimeDB that
// this value should be determined by SpacetimeDB on insert.
#[auto_inc]
#[primary_key]
pub entity_id: i32,
pub position: DbVector2,
pub mass: i32,
}
#[spacetimedb::table(name = circle, public)]
pub struct Circle {
#[primary_key]
pub entity_id: i32,
#[index(btree)]
pub player_id: i32,
pub direction: DbVector2,
pub speed: f32,
pub last_split_time: Timestamp,
}
#[spacetimedb::table(name = food, public)]
pub struct Food {
#[primary_key]
pub entity_id: i32,
}Next, we're going to define a new SpacetimeType called DbVector2 which we're going to use to store positions. The difference between a [SpacetimeDB.Type] and a [SpacetimeDB.Table] is that tables actually store data, whereas the deriving SpacetimeType just allows you to create a new column of that type in a SpacetimeDB table. Therefore, DbVector2 is only a type, and does not define a table.
Append to the bottom of Lib.cs:
// This allows us to store 2D points in tables.
[SpacetimeDB.Type]
public partial struct DbVector2
{
public float x;
public float y;
public DbVector2(float x, float y)
{
this.x = x;
this.y = y;
}
}Let's create a few tables to represent entities in our game by adding the following to the end of the Module class.
[Table(Name = "entity", Public = true)]
public partial struct Entity
{
[PrimaryKey, AutoInc]
public int entity_id;
public DbVector2 position;
public int mass;
}
[Table(Name = "circle", Public = true)]
public partial struct Circle
{
[PrimaryKey]
public int entity_id;
[SpacetimeDB.Index.BTree]
public int player_id;
public DbVector2 direction;
public float speed;
public SpacetimeDB.Timestamp last_split_time;
}
[Table(Name = "food", Public = true)]
public partial struct Food
{
[PrimaryKey]
public int entity_id;
}The first table we defined is the entity table. An entity represents an object in our game world. We have decided, for convenience, that all entities in our game should share some common fields, namely position and mass.
We can create different types of entities with additional data by creating new tables with additional fields that have an entity_id which references a row in the entity table.
We've created two types of entities in our game world: Foods and Circles. Food does not have any additional fields beyond the attributes in the entity table, so the food table simply represents the set of entity_ids that we want to recognize as food.
The Circle table, however, represents an entity that is controlled by a player. We've added a few additional fields to a Circle like player_id so that we know which player that circle belongs to.
Representing Players
Next, let's create a table to store our player data.
- Rust
- C#
#[spacetimedb::table(name = player, public)]
#[derive(Debug, Clone)]
pub struct Player {
#[primary_key]
identity: Identity,
#[unique]
#[auto_inc]
player_id: i32,
name: String,
}There's a few new concepts we should touch on. First of all, we are using the #[unique] attribute on the player_id field. This attribute adds a constraint to the table that ensures that only one row in the player table has a particular player_id.
[Table(Name = "player", Public = true)]
public partial struct Player
{
[PrimaryKey]
public Identity identity;
[Unique, AutoInc]
public int player_id;
public string name;
}There are a few new concepts we should touch on. First of all, we are using the [Unique] attribute on the player_id field. This attribute adds a constraint to the table that ensures that only one row in the player table has a particular player_id. We are also using the [AutoInc] attribute on the player_id field, which indicates "this field should get automatically assigned an auto-incremented value".
We also have an identity field which uses the Identity type. The Identity type is an identifier that SpacetimeDB uses to uniquely assign and authenticate SpacetimeDB users.