diff --git a/docs/deploying/hosted.md b/docs/deploying/hosted.md new file mode 100644 index 00000000..0898a267 --- /dev/null +++ b/docs/deploying/hosted.md @@ -0,0 +1,74 @@ +# Deploying - Hosted + +This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI. Via CLI, we will then: + +1. Ensure our hosted server named `testnet` exists as the default. +1. Create an `Identity`. +1. `Publish` your app. + +💡 This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI and that you already have `testnet` server added (exists by default). If you accidentally removed `testnet`, add it back via CLI: + +```bash +spacetime server add "https://testnet.spacetimedb.com" testnet +``` + +## SpacetimeDB Cloud (Hosted) Deployment + +Currently, for hosted deployment, only the `testnet` server is available for SpacetimeDB cloud, which is subject to wipes. + +📢 Stay tuned (such as [via Discord](https://discord.com/invite/SpacetimeDB)) for `mainnet` coming soon! + +## Set the Server Default + +To make CLI commands easier so that we don't need to keep specifying `testnet` as the target server, let's set it as default: + +```bash +spacetime server set-default testnet +``` + +## Creating an Identity + +By default, there are no identities created. Let's create a new one via CLI: +```bash +spacetime identity new --name {Nickname} --email {Email} +``` + +💡If you already created an identity but forgot to attach an email, add it via CLI: +```bash +spacetime identity set-email {Email} +``` + +## Create and Publish a Module + +Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: + +```bash +cd ~ +spacetime init --lang rust HelloSpacetimeDB +cd HelloSpacetimeDB +spacetime publish HelloSpacetimeDB +``` + +## Hosted Web Dashboard + +By earlier associating an email with your CLI identity, you can now view your published modules on the web dashboard. For multiple identities, first list them and copy the hash you want to use: + +```bash +spacetime identity list +``` + +1. Open the SpacetimeDB [login page](https://spacetimedb.com/login) using the same email above. +1. Choose your identity from the dropdown menu. + - \[For multiple identities\] `CTRL+F` to highlight the correct identity you copied earlier. +1. Check your email for a validation link. + +You should now be able to see your published modules on the web dashboard! + +--- + +## Summary + +- We ensured the hosted `testnet` server existed, then set it as the default. +- We added an `identity` to bind with our hosted `testnet` server, ensuring it contained both a Nickname and Email. +- We then logged in the web dashboard via an email `one-time password (OTP)` and were then able to view our published apps. +- With SpacetimeDB Cloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment. diff --git a/docs/deploying/index.md b/docs/deploying/index.md new file mode 100644 index 00000000..415841f5 --- /dev/null +++ b/docs/deploying/index.md @@ -0,0 +1,48 @@ +# Deploying Overview + +SpacetimeDB supports both hosted and self-hosted publishing in multiple ways. Below, we will: + +1. Generally introduce Identities. +1. Generally introduce Servers. +1. Choose to proceed with either a [Hosted](/docs/deploying/hosted.md) or [Self-Hosted](/docs/deploying/self-hosted.md) deployment. + +💡 This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI. + +## About Identities + +An `Identity` is a hash attached to a `Nickname` and `Email`, allowing you to manage your app (such as `Publishing` your app). + +Each `Identity` is bound to one, single `Server`: Unlike GitHub, for example, you would require one identity per server. + +By default, there are no identities created. While the next tutorial will go more in-depth, you may create a new one via CLI: +```bash +spacetime identity new --name {Nickname} --email {Email} +``` + +See the verbose [overview identity explanation](https://spacetimedb.com/docs#identities), [API reference](/docs/http/identity.md) or CLI help (below) for further managing `Identities`: +```bash +spacetime identity --help +``` + +## About Servers + +You `publish` your app to a target `Server` database: While we recommend to **host** your SpacetimeDB app with us for simplicity and scalability, you may also **self-host** (such as locally). + +By default, there are already two default servers added ([testnet](/docs/deploying/hosted.md) and [local](/docs/deploying/self-hosted.md)). While the next tutorial will go more in-depth, you may list your servers via CLI: +```bash +spacetime server list +``` + +See the [API reference](/docs/http/database.md) or CLI help (below) for further managing `Servers`: +```bash +spacetime server --help +``` + +--- + +## Deploying via CLI + +Choose a server for your hosting tutorial path to set a server as default, create an identity, and deploy (`publish`) your app: + +1. [testnet](/docs/deploying/hosted.md) (hosted) +2. [local](/docs/deploying/self-hosted.md) (self-hosted) diff --git a/docs/deploying/self-hosted.md b/docs/deploying/self-hosted.md new file mode 100644 index 00000000..2886b5b8 --- /dev/null +++ b/docs/deploying/self-hosted.md @@ -0,0 +1,60 @@ +# Deploying - Self-Hosted + +This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI. Via CLI, we will then: + +1. Ensure our localhost server named `local` exists as the default. +1. Start our localhost server in a separate terminal window. +1. Create an `Identity` with at least a Nickname. +1. `Publish` your app. + +💡 This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI and that you already have `local` server added (exists by default). If you accidentally removed `local`, add it back via CLI with the `--no-fingerprint` flag (since our server is not yet running): + +```bash +spacetime server add "http://127.0.0.1:3000" local --no-fingerprint +``` + +## Set the Server Default + +To make CLI commands easier so that we don't need to keep specifying `local` as the target server, let's set it as default: + +```bash +spacetime server set-default local +``` + +## Start the Local Server + +In a **separate** terminal window, start the local listen server in the foreground: +```bash +spacetime start +``` + +## Creating an Identity + +By default, there are no identities created. Let's create a new one via CLI: +```bash +spacetime identity new --name {Nickname} +``` + +💡We could optionally add `--email {Email}` to the above command, but is currently unnecessary for local deployment since there's no web dashboard. If you already created an identity but forgot to attach a Nickname, add it via CLI to easier identify your modules: +```bash +spacetime identity set-name {Nickname} +``` + +## Create and Publish a Module + +Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: + +```bash +cd ~ +spacetime init --lang rust HelloSpacetimeDB +cd HelloSpacetimeDB +spacetime publish HelloSpacetimeDB +``` + +--- + +## Summary + +- We ensured the self-hosted `local` server existed, then set it as the default. +- We then opened a separate terminal to run the self-hosted `local` server in the foreground. +- We added an `identity` to bind with our self-hosted `local` server set to default, ensuring it contained a Nickname. diff --git a/docs/deploying/testnet.md b/docs/deploying/testnet.md deleted file mode 100644 index ce648043..00000000 --- a/docs/deploying/testnet.md +++ /dev/null @@ -1,34 +0,0 @@ -# SpacetimeDB Cloud Deployment - -The SpacetimeDB Cloud is a managed cloud service that provides developers an easy way to deploy their SpacetimeDB apps to the cloud. - -Currently only the `testnet` is available for SpacetimeDB cloud which is subject to wipes. The `mainnet` will be available soon. - -## Deploy via CLI - -1. [Install](/install) the SpacetimeDB CLI. -1. Configure your CLI to use the SpacetimeDB Cloud. To do this, run the `spacetime server` command: - -```bash -spacetime server add --default "https://testnet.spacetimedb.com" testnet -``` - -## Connecting your Identity to the Web Dashboard - -By associating an email with your CLI identity, you can view your published modules on the web dashboard. - -1. Get your identity using the `spacetime identity list` command. Copy it to your clipboard. -1. Connect your email address to your identity using the `spacetime identity set-email` command: - -```bash -spacetime identity set-email -``` - -1. Open the SpacetimeDB website and log in using your email address. -1. Choose your identity from the dropdown menu. -1. Validate your email address by clicking the link in the email you receive. -1. You should now be able to see your published modules on the web dashboard. - ---- - -With SpacetimeDB Cloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment. diff --git a/docs/index.md b/docs/index.md index 7a95f4f8..738f9a50 100644 --- a/docs/index.md +++ b/docs/index.md @@ -94,29 +94,27 @@ SpacetimeDB was designed first and foremost as the backend for multiplayer Unity 1. What is SpacetimeDB? It's a whole cloud platform within a database that's fast enough to run real-time games. -1. How do I use SpacetimeDB? +2. How do I use SpacetimeDB? Install the `spacetime` command line tool, choose your favorite language, import the SpacetimeDB library, write your application, compile it to WebAssembly, and upload it to the SpacetimeDB cloud platform. Once it's uploaded you can call functions directly on your application and subscribe to changes in application state. -1. How do I get/install SpacetimeDB? +3. How do I get/install SpacetimeDB? Just install our command line tool and then upload your application to the cloud. -1. How do I create a new database with SpacetimeDB? +4. How do I create a new database with SpacetimeDB? Follow our [Quick Start](/docs/getting-started) guide! -TL;DR in an empty directory: +TL;DR in an empty directory, init and publish a barebones app named HelloWorld. ```bash spacetime init --lang=rust -spacetime publish +spacetime publish HelloWorld ``` 5. How do I create a Unity game with SpacetimeDB? Follow our [Unity Project](/docs/unity-project) guide! -TL;DR in an empty directory: +TL;DR after already initializing and publishing (see FAQ #5), generate the SDK: ```bash -spacetime init --lang=rust -spacetime publish spacetime generate --out-dir --lang=csharp ``` diff --git a/docs/modules/c-sharp/index.md b/docs/modules/c-sharp/index.md index 31ebd1d4..fc641402 100644 --- a/docs/modules/c-sharp/index.md +++ b/docs/modules/c-sharp/index.md @@ -4,6 +4,36 @@ You can use the [C# SpacetimeDB library](https://github.com/clockworklabs/Spacet It uses [Roslyn incremental generators](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) to add extra static methods to types, tables and reducers marked with special attributes and registers them with the database runtime. +## C# Module Limitations & Nuances + +Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), it's important to be aware of the following: + +1. No DateTime-like types in Types or Tables: + - Use `long` for microsecond unix epoch timestamps + - See example usage and converts at the [_TimeConvert_ module demo class](https://github.com/clockworklabs/zeke-demo-project/blob/3fa1c94e75819a191bd785faa7a7d15ea4dc260c/Server-Csharp/src/Utils.cs#L19) + + +2. No Timers or async/await, such as those to create repeating loops: + - For repeating invokers, instead **re**schedule it from within a fired [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) function. + + +3. Using `Debug` advanced option in the `Publisher` Unity editor tool will add callstack symbols for easier debugging: + - However, avoid using `Debug` mode when publishing outside a `localhost` server: + - Due to WASM buffer size limitations, this may cause publish failure. + + +4. If you `throw` a new `Exception`, no error logs will appear. Instead, use either: + 1. Use `Log(message, LogLevel.Error);` before you throw. + 2. Use the demo's static [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs) class to `Utils.Throw()` to wrap the error log before throwing. + + +5. `[AutoIncrement]` or `[PrimaryKeyAuto]` will never equal 0: + - Inserting a new row with an Auto key equaling 0 will always return a unique, non-0 value. + + +6. Enums cannot declare values out of the default order: + - For example, `{ Foo = 0, Bar = 3 }` will fail to compile. + ## Example Let's start with a heavily commented version of the default example from the landing page: diff --git a/docs/nav.js b/docs/nav.js index cb8d22f1..bdf49e5d 100644 --- a/docs/nav.js +++ b/docs/nav.js @@ -9,14 +9,22 @@ function section(title) { const nav = { items: [ section("Intro"), - page("Overview", "index", "index.md"), + page("Overview", "index", "index.md"), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? + page("Getting Started", "getting-started", "getting-started.md"), + page("Overview", "index", "index.md"), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? page("Getting Started", "getting-started", "getting-started.md"), section("Deploying"), - page("Testnet", "deploying/testnet", "deploying/testnet.md"), - section("Unity Tutorial"), - page("Part 1 - Basic Multiplayer", "unity/part-1", "unity/part-1.md"), - page("Part 2 - Resources And Scheduling", "unity/part-2", "unity/part-2.md"), - page("Part 3 - BitCraft Mini", "unity/part-3", "unity/part-3.md"), + page("Overview", "deploying", "deploying/index.md"), + page("Hosted", "deploying/hosted", "deploying/hosted.md"), + page("Self-Hosted", "deploying/hosted", "deploying/self-hosted.md"), + section("Unity Tutorial - Basic Multiplayer"), + page("Overview", "unity-tutorial", "unity/index.md"), + page("1 - Project Setup", "unity/part-1", "unity/part-1.md"), + page("2 - Server (C# Module)", "unity/part-2", "unity/part-2a-rust.md"), + page("3 - Client (Unity)", "unity/part-2", "unity/part-2.md"), + section("Unity Tutorial - Advanced"), + page("4 - Resources & Scheduling", "unity/part-4", "unity/part-4.md"), + page("5 - BitCraft Mini", "unity/part-5", "unity/part-5.md"), section("Server Module Languages"), page("Overview", "modules", "modules/index.md"), page("Rust Quickstart", "modules/rust/quickstart", "modules/rust/quickstart.md"), diff --git a/docs/sdks/c-sharp/index.md b/docs/sdks/c-sharp/index.md index 7c920cf5..f02bc764 100644 --- a/docs/sdks/c-sharp/index.md +++ b/docs/sdks/c-sharp/index.md @@ -65,11 +65,12 @@ dotnet add package spacetimedbsdk ### Using Unity -To install the SpacetimeDB SDK into a Unity project, [download the SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest), packaged as a `.unitypackage`. - -In Unity navigate to the `Assets > Import Package > Custom Package` menu in the menu bar. Select your `SpacetimeDB.Unity.Comprehensive.Tutorial.unitypackage` file and leave all folders checked. +To install the SpacetimeDB SDK into a Unity project, simply add the following line to your `Packages/manifest.json`: +```json +"com.clockworklabs.spacetimedbsdk": "https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git" +``` -(See also the [Unity Tutorial](/docs/unity/part-1)) +💡 See also the [Unity Tutorial](/docs/unity/index.md) ## Generate module bindings diff --git a/docs/unity/index.md b/docs/unity/index.md index 2b8e6d67..3ae82550 100644 --- a/docs/unity/index.md +++ b/docs/unity/index.md @@ -1,23 +1,24 @@ # Unity Tutorial Overview -Need help with the tutorial or CLI commands? [Join our Discord server](https://discord.gg/spacetimedb)! +> [!IMPORTANT] +> TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. -The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, using a command line terminal and coding. +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! + +The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, Git, using a commandline terminal and coding. We'll give you some CLI commands to execute. If you are using Windows, we recommend using Git Bash or PowerShell. For Mac, we recommend Terminal. -Tested with UnityEngine `2022.3.20f1 LTS` (and may also work on newer versions). +Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll be opening .cs files in an IDE like _Visual Studio_ or _Rider_. ## Unity Tutorial - Basic Multiplayer Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](/docs/modules/rust) or [C#](/docs/modules/c-sharp): -- [Part 1 - Setup](/docs/unity/part-1.md) -- [Part 2a - Server (Rust)](/docs/unity/part-2a-rust.md) -- [Part 2b - Server (C#)](/docs/unity/part-2b-csharp.md) -- [Part 3 - Client](/docs/unity/part-3.md) +![Core Architecture](/images/unity-tutorial/overview/core-architecture.png) + -## Unity Tutorial - Advanced -By this point, you should already have a basic understanding of SpacetimeDB client, server and CLI: +1. [Setup](/docs/unity/part-1.md) +2. [Server (C#)](/docs/unity/part-2.md) ☼ +3. [Client (Unity)](/docs/unity/part-3.md) -- [Part 4 - Resources & Scheduling](/docs/unity/part-4.md) -- [Part 5 - BitCraft Mini](/docs/unity/part-5.md) +☼ While the tutorial uses C#, the repo cloned in [Part 1](/docs/unity/part-1.md) does include a legacy Rust example to optionally use, instead. diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index b8b8c3c0..fc95cdcf 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -1,122 +1,63 @@ -# Unity Tutorial - Basic Multiplayer - Part 1 - Setup +# Unity Multiplayer Tutorial - Part 1 -![UnityTutorial-HeroImage](/images/unity-tutorial/UnityTutorial-HeroImage.JPG) +> [!IMPORTANT] +> TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! +## Quickstart Project Setup -## Prepare Project Structure +This progressive tutorial will guide you to: -This project is separated into two sub-projects; +1. Quickly setup up a multiplayer game project demo, using Unity and SpacetimeDB. +1. Publish your demo SpacetimeDB C# server module to `testnet`. -1. Server (module) code -2. Client code +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! -First, we'll create a project root directory (you can choose the name): +## 1. Clone the Project +Let's name it `SpacetimeDBUnityTutorial` for reference: ```bash -mkdir SpacetimeDBUnityTutorial -cd SpacetimeDBUnityTutorial +git clone https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade SpacetimeDBUnityTutorial ``` -We'll start by populating the client directory. +This project repo is separated into two sub-projects: -## Setting up the Tutorial Unity Project +1. [Server](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp) (SpacetimeDB Module) +1. [Client](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Client) (Unity project) -In this section, we will guide you through the process of setting up a Unity Project that will serve as the starting point for our tutorial. By the end of this section, you will have a basic Unity project and be ready to implement the server functionality. +> [!TIP] +> You may optionally _update_ the [SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk) via the Package Manager in Unity -### Step 1: Create a Blank Unity Project +## 2. Publishing the Project -Open Unity and create a new project by selecting "New" from the Unity Hub or going to **File -> New Project**. +From Unity, you don't need CLI commands for common functionality: There's a Unity editor tool for that! -![UnityHub-NewProject](/images/unity-tutorial/UnityHub-NewProject.JPG) +![Unity Publisher Editor Tool GIF](/images/unity-tutorial/part-1/unity-publisher-editor-tool-animated.gif) + -**⚠️ Important: Ensure `3D (URP)` is selected** to properly render the materials in the scene! +1. Open the _Publisher_ editor tool: `ALT+SHIFT+P` (or `Window/SpacetimeDB/Publisher` in the top menu) +1. Create an identity -> Select `testnet` for the server +1. Browse to your repo root `Server-Csharp` dir -> **Publish** -> **Generate** Unity files -For Project Name use `client`. For Project Location make sure that you use your `SpacetimeDBUnityTutorial` directory. This is the directory that we created in a previous step. +💡For the next section, we'll use the selected `Server` and publish result `Host` -![UnityHub-3DURP](/images/unity-tutorial/UnityHub-3DURP.JPG) +## 3. Connecting the Project -Click "Create" to generate the blank project. +1. Open `Scenes/Main` in Unity -> select the `GameManager` GameObject in the inspector. +1. Matching the earlier Publish setup: + 1. For the GameManager `Db Name or Address`, input `testnet` + 1. For the GameManager `Host`, input `https://testnet.spacetimedb.com +1. Save your scene -### Step 2: Adding Required Packages +## 4. Running the Project -To work with SpacetimeDB and ensure compatibility, we need to add some essential packages to our Unity project. Follow these steps: +With the same `Main` scene open, press play! -1. Open the Unity Package Manager by going to **Window -> Package Manager**. -2. In the Package Manager window, select the "Unity Registry" tab to view unity packages. -3. Search for and install the following package: - - **Input System**: Enables the use of Unity's new Input system used by this project. + -![PackageManager-InputSystem](/images/unity-tutorial/PackageManager-InputSystem.JPG) +You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. -4. You may need to restart the Unity Editor to switch to the new Input system. +## Conclusion -![PackageManager-Restart](/images/unity-tutorial/PackageManager-Restart.JPG) +Congratulations! You have successfully set up your multiplayer game project. -### Step 3: Importing the Tutorial Package - -In this step, we will import the provided Unity tutorial package that contains the basic single-player game setup. Follow these instructions: - -1. Download the tutorial package from the releases page on GitHub: [https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest) -2. In Unity, go to **Assets -> Import Package -> Custom Package**. - -![Unity-ImportCustomPackageB](/images/unity-tutorial/Unity-ImportCustomPackageB.JPG) - -3. Browse and select the downloaded tutorial package file. -4. Unity will prompt you with an import settings dialog. Ensure that all the files are selected and click "Import" to import the package into your project. -5. At this point in the project, you shouldn't have any errors. - -![Unity-ImportCustomPackage2](/images/unity-tutorial/Unity-ImportCustomPackage2.JPG) - -### Step 4: Running the Project - -Now that we have everything set up, let's run the project and see it in action: - -1. Open the scene named "Main" in the Scenes folder provided in the project hierarchy by double-clicking it. - -![Unity-OpenSceneMain](/images/unity-tutorial/Unity-OpenSceneMain.JPG) - -**NOTE:** When you open the scene you may get a message saying you need to import TMP Essentials. When it appears, click the "Import TMP Essentials" button. - -🧹 Clear any false-positive TMPro errors that may show. - -![Unity Import TMP Essentials](/images/unity-tutorial/Unity-ImportTMPEssentials.JPG) - -2. Press the **Play** button located at the top of the Unity Editor. - -![Unity-Play](/images/unity-tutorial/Unity-Play.JPG) - -3. Enter any name and click "Continue" - -4. You should see a character loaded in the scene, and you can use the keyboard or mouse controls to move the character around. - -Congratulations! You have successfully set up the basic single-player game project. In the next section, we will start integrating SpacetimeDB functionality to enable multiplayer features. - -## Writing our SpacetimeDB Server Module - -At this point you should have the single player game working. In your CLI, your current working directory should be within your `SpacetimeDBUnityTutorial` directory that we created in a previous step. - -### Create the Module - -1. It is important that you already have the SpacetimeDB CLI tool [installed](/install). - -2. Run SpacetimeDB locally using the installed CLI. In a **new** terminal or command window, run the following command: - -```bash -spacetime start -``` - -💡 Standalone mode will run in the foreground. -💡 Below examples Rust language, [but you may also use C#](../modules/c-sharp/index.md). - -### The Entity Component Systems (ECS) - -Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). - -We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. - -### Create the Server Module - -From here, the tutorial continues with your favorite server module language of choice: - - [Rust](part-2a-rust.md) - - [C#](part-2b-csharp.md) +In the next section, we will break down how Server Modules work and analyze the demo code. In a later section, we'll also analyze the Unity client demo. \ No newline at end of file diff --git a/docs/unity/part-3.md b/docs/unity/part-3.md index c80000e1..66918cac 100644 --- a/docs/unity/part-3.md +++ b/docs/unity/part-3.md @@ -1,479 +1,253 @@ -# Unity Tutorial - Basic Multiplayer - Part 3 - Client +# Unity Multiplayer Tutorial - Part 3 -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! +> [!IMPORTANT] +> TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. -This progressive tutorial is continued from one of the Part 2 tutorials: -- [Rust Server Module](/docs/unity/part-2a-rust.md) -- [C# Server Module](/docs/unity/part-2b-c-sharp.md) +## Prerequisites -## Updating our Unity Project Client to use SpacetimeDB +- This progressive tutorial is continued from [Part 2](/docs/unity/part-2.md). +- Or [start from the beginning](/docs/unity/index.md). -Now we are ready to connect our _BitCraft Mini_ project to SpacetimeDB. +## Analyzing the Unity Client Demo -### Import the SDK and Generate Module Files +In this part of the tutorial, we will: -1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. +1. Setup your `GameManager` connection properties. +1. Use the _Publisher_ editor tool to deploy your module. +1. Inspect high-level client initialization. +1. Press Play -> Guided breakdown of game features: + 1. Chat + 1. Resource Gathering + 1. Inventory + 1. Store + 1. Unlockables -```bash -https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git -``` - -![Unity-PackageManager](/images/unity-tutorial/Unity-PackageManager.JPG) - -3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. - -```bash -mkdir -p ../client/Assets/module_bindings -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -``` - -### Connect to Your SpacetimeDB Module +Start by opening `Scenes/Main` in the Unity project from the repo `/Client` dir. -The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. +## GameManager Connection Setup -![Unity-AddNetworkManager](/images/unity-tutorial/Unity-AddNetworkManager.JPG) +![GameManager Inspector (+host name variations)](/images/unity-tutorial/part-3/) + -Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: +Select the `GameManager` in the scene hierarchy: -**Append to the top of TutorialGameManager.cs** - -```csharp -using SpacetimeDB; -using SpacetimeDB.Types; -using System.Linq; -``` +1. Set **Db Name Or Address** to: `unity-demo`. +2. Set the **Host Name** to: `testnet`. +3. Save your scene. -At the top of the class definition add the following members: +## High-Level Client Initialization -**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** +Open the **GameManager.cs** script we were just inspecting and jump to `Start()`: ```csharp -// These are connection variables that are exposed on the GameManager -// inspector. -[SerializeField] private string moduleAddress = "unity-tutorial"; -[SerializeField] private string hostName = "localhost:3000"; - -// This is the identity for this player that is automatically generated -// the first time you log in. We set this variable when the -// onIdentityReceived callback is triggered by the SDK after connecting -private Identity local_identity; +/// Register callbacks -> Connect to SpacetimeDB +private void Start() +{ + Application.runInBackground = true; + + initSubscribeToEvents(); + connectToSpacetimeDb(); +} ``` -The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. - -Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. - -**REPLACE the Start() function in TutorialGameManager.cs** +1. Once connected, we subscribe to all tables, then unregister the callback: ```csharp -// Start is called before the first frame update -void Start() +/// When we connect to SpacetimeDB we send our subscription queries +/// to tell SpacetimeDB which tables we want to get updates for. +/// After called, we'll unsub from onConnect +private void initOnceOnConnect() { - instance = this; - SpacetimeDBClient.instance.onConnect += () => { Debug.Log("Connected."); - // Request all tables - SpacetimeDBClient.instance.Subscribe(new List() + SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM *", }); + + SpacetimeDBClient.instance.onConnect -= connectToSpacetimeDb; }; - - // Called when we have an error connecting to SpacetimeDB - SpacetimeDBClient.instance.onConnectError += (error, message) => - { - Debug.LogError($"Connection error: " + message); - }; - - // Called when we are disconnected from SpacetimeDB - SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => - { - Debug.Log("Disconnected."); - }; - - // Called when we receive the client identity from SpacetimeDB - SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { - AuthToken.SaveToken(token); - local_identity = identity; - }; - - // Called after our local cache is populated from a Subscribe call - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; - - // Now that we’ve registered all our callbacks, lets connect to spacetimedb - SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); } ``` -In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. - -Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. - ---- - -**Local Client Cache** - -The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. - ---- - -Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. +> [!TIP] +> In a production environment, you'd instead subscribe to limited, local scopes and resubscribe with different parameters as you move through different zones. -**Append after the Start() function in TutorialGameManager.cs** +2. We then subscribe to database callbacks such as connection, disconnection, errors, inventory, player, etc. + - This includes an **identity received** callback. + - This is important since your identity what we will 1st check for _nearly all_ callbacks to determine if it's the local player: ```csharp -void OnSubscriptionApplied() +private void onIdentityReceived(string token, Identity identity, Address address) { - // If we don't have any data for our player, then we are creating a - // new one. Let's show the username dialog, which will then call the - // create player reducer - var player = PlayerComponent.FilterByOwnerId(local_identity); - if (player == null) - { - // Show username selection - UIUsernameChooser.instance.Show(); - } - - // Show the Message of the Day in our Config table of the Client Cache - UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); - - // Now that we've done this work we can unregister this callback - SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; -} -``` - -### Adding the Multiplayer Functionality - -Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. - -**Append to the top of UIUsernameChooser.cs** - -```csharp -using SpacetimeDB.Types; -``` - -Then we're doing a modification to the `ButtonPressed()` function: - -**Modify the ButtonPressed function in UIUsernameChooser.cs** - -```csharp -public void ButtonPressed() -{ - CameraController.RemoveDisabler(GetHashCode()); - _panel.SetActive(false); - - // Call the SpacetimeDB CreatePlayer reducer - Reducer.CreatePlayer(_usernameField.text); -} -``` - -We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. - -First append this using to the top of `RemotePlayer.cs` - -**Create file RemotePlayer.cs, then replace its contents:** - -```csharp -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using SpacetimeDB.Types; -using TMPro; - -public class RemotePlayer : MonoBehaviour -{ - public ulong EntityId; - - public TMP_Text UsernameElement; - - public string Username { set { UsernameElement.text = value; } } - - void Start() - { - // Initialize overhead name - UsernameElement = GetComponentInChildren(); - var canvas = GetComponentInChildren(); - canvas.worldCamera = Camera.main; - - // Get the username from the PlayerComponent for this object and set it in the UI - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - if (playerComp is null) - { - string inputUsername = UsernameElement.Text; - Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); - Reducer.CreatePlayer(inputUsername); - - // Try again, optimistically assuming success for simplicity - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - } + // Cache the "last connected" dbNameOrAddress to later associate with the cached AuthToken + // This is so that if we change servers, we know to clear the token to prevent mismatch + PlayerPrefs.SetString(DB_NAME_ADDRESS_KEY, dbNameOrAddress); + AuthToken.SaveToken(token); - Username = playerComp.Username; - - // Get the last location for this player and set the initial position - EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); - transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - - // Register for a callback that is called when the client gets an - // update for a row in the EntityComponent table - EntityComponent.OnUpdate += EntityComponent_OnUpdate; - } + // Cache the player identity for later to compare against component ownerIds + // to see if it's the local player + _localIdentity = identity; } ``` -We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. - -**Append to bottom of RemotePlayer class in RemotePlayer.cs:** - +3. Finally, we connect via a token, host and database name: ```csharp -private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) +/// On success => +/// 1. initOnceOnConnect() -> Subscribe to tables +/// 2. onIdentityReceived() -> Cache identity, token +/// On fail => onConnectError() +private void connectToSpacetimeDb() { - // If the update was made to this object - if(obj.EntityId == EntityId) - { - var movementController = GetComponent(); - - // Update target position, rotation, etc. - movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); - movementController.RemoteTargetRotation = obj.Direction; - movementController.SetMoving(obj.Moving); - } + string token = getConnectAuthToken(); + string normalizedHostName = getNormalizedHostName(); + + SpacetimeDBClient.instance.Connect( + token, + normalizedHostName, + dbNameOrAddress); } ``` -Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. +## Play the Demo Game +![Gameplay Actions<>UI GIF](/images/unity-tutorial/part-3/action-ui-animation.gif) + -**Append to bottom of Start() function in TutorialGameManager.cs:** +Notice at the bottom-right, you have some tips: -```csharp -PlayerComponent.OnInsert += PlayerComponent_OnInsert; -``` +1. **Enter** = Chat +2. **Tab** = Inventory +3. Collect resources +4. Spend at the shop -Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. +✅ From here, you can either explore the game's features on your own or continue reading for a guided breakdown below. Try triggering in-game features, then noting the log callstacks for entry point and flow hints. -**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** +___ -```csharp -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } -} -``` +## Features Breakdown -Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` (Rust) or `UpdatePlayerPosition(...)` (C#) is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. +### Feature: Chat -**Append to the top of LocalPlayer.cs** +![Chat<>Reducer Tool](/images/unity-tutorial/part-3/chat-reducer-tool.min.png) + -```csharp -using SpacetimeDB.Types; -using SpacetimeDB; -``` +Note the message of the day, directing you to emulate a third-party with the _Reducers_ editor tool: -**Append to the bottom of LocalPlayer class in LocalPlayer.cs** +> Try the 'Reducers' tool **SHIFT+ALT+D** -```csharp -private float? lastUpdateTime; -private void FixedUpdate() -{ - float? deltaTime = Time.time - lastUpdateTime; - bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; - bool isConnected = SpacetimeDBClient.instance.IsConnected(); +💡 Alternately accessed from the top menu: `Window/SpacetimeDB/Reducers` - if (hasUpdatedRecently || !isConnected) - { - return; - } +1. CreatePlayer - lastUpdateTime = Time.time; - var p = PlayerMovementController.Local.GetModelPosition(); +![Create Player via Reducer Tool](/images/unity-tutorial/part-3/create-player-via-reducer-tool.min.png) + + +2. Repeat with `SendChatMessage` to see it appear in chat from your created "third-party" player. - Reducer.UpdatePlayerPosition(new StdbVector3 - { - X = p.x, - Y = p.y, - Z = p.z, - }, - PlayerMovementController.Local.GetModelRotation(), - PlayerMovementController.Local.IsMoving()); -} -``` +💡 Dive into `UIChatController.cs` to best further discover how client-side chat messages work. -Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. +### Feature: Resource Gathering -![GameManager-Inspector2](/images/unity-tutorial/GameManager-Inspector2.JPG) +![Resource Gathering](/images/unity-tutorial/part-3/resource-gathering.min.png) + -### Play the Game! +Thanks to our scheduler set by the server via the `Init()`, resources will spawn every 5~10 seconds (with the specified max cap): -Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. +1. Extract (harvest) `Iron` by left-clicking a nearby blue node. +1. Once you unlock `Strawberries` in the shop later, you can see and extract those, too. + - 💡`Strawberries` are likely already spawned on the server, but the unlockable requirement prevents you from seeing it (client-authoritative) or extracting it (server-authoritative). -![Unity-AddOpenScenes](/images/unity-tutorial/Unity-AddOpenScenes.JPG) +Extracting a resource will trigger the following flows: -When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. +![initSubscribeToEvents-Resource-Extraction-Events](/images/unity-tutorial/part-3/initSubscribeToEvents-resource-extraction.min.png) + -### Implement Player Logout +1. **[Client]** Call `Reducer.Extract()` from `PlayerInputReceiver.cs` via `OnActionButton()`. -So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. -**Append to the bottom of Start() function in TutorialGameManager.cs** -```csharp -PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; -``` +2. **[Server]** `Extract()` from `Resource.cs` will: + 1. Validate player, identity, config, inventory resource, and animator (which handles extraction speed). + 1. If valid, validate unlockables (eg: for `Strawberries`), consider items like `Pickaxe` for increased extraction speed and finally extract: + 1. InventoryComponent will be updated to add the resource and lower the `ResourceNodeComponent` hp by 1. + 1. If 0 hp, it will be destroyed. + 1. The resource will be inserted into to the player's `InventoryComponent` -We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. -Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. +3. **[Client]** Remember when [GameManager at Start()](#high-level-client-initialization) declared `initSubscribeToEvents()`? + - **Inventory:** + 1. `onInventoryComponentUpdate()` -> `PlayerInventoryController.Local.InventoryUpdate(newValue)` chain will be called. + 1. `InventoryUpdate()` will clear `PlayerInventoryController.Local._pockets` and resync with the server's `newValue`. + - **Resource:** `onResourceNodeComponentDelete` will `GameResources.RemoveAll()` those matching `oldValue.EntityId`. + - **Animation:** `OnAnimationComponentUpdate` will show animations from a RemotePlayer extracting a resource. -**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** -```csharp -private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(newValue); -} +💡 Dive into `GameResource.cs` to best further discover how client-side extractions work. -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(obj); -} +### Feature: Inventory -private void OnPlayerComponentChanged(PlayerComponent obj) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it - var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); - if (obj.LoggedIn) - { - // Only spawn remote players who aren't already spawned - if (existingPlayer == null) - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } - } - else - { - if (existingPlayer != null) - { - Destroy(existingPlayer.gameObject); - } - } - } -} -``` +![Player Inventory](/images/unity-tutorial/part-3/player-inventory.min.png) + -Now you when you play the game you should see remote players disappear when they log out. +- On server [Player.cs::CreatePlayer()](./part-2.md#db-initialization), this chained to `createPlayerInventory()`, creating the default inventory `Pockets` and an empty set of `UnlockIds`. + - When the `Pocket` was created, we started the new Player with 5x `Iron`. + - Since the `Strawberry` unlock in the store only requires 5x `Iron`, we may _unlock_ it right away to see and extract `Strawberries`. -Before updating the client, let's generate the client files and update publish our module. +See the [Store](#feature-store) section below for example items being consumed and replaced for a store purchase. -**Execute commands in the server/ directory** -```bash -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -spacetime publish -c unity-tutorial -``` +💡 Dive into `UIInventoryWindow.cs` to best further discover how client-side inventory works. -On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. +### Feature: Unlockables -**Append to the top of UIChatController.cs:** -```csharp -using SpacetimeDB.Types; -``` +![Unlockables](/images/unity-tutorial/part-3/unlockables.min.png) + -**REPLACE the OnChatButtonPress function in UIChatController.cs:** +See the [Store](#feature-store) section below for an example unlock store purchase. -```csharp -public void OnChatButtonPress() -{ - Reducer.SendChatMessage(_chatInput.text); - _chatInput.text = ""; -} -``` +💡 Dive into `UIUnlocks.cs` to best further discover how client-side unlocks work. -Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: +### Feature: Shop -**Append to the bottom of the Start() function in TutorialGameManager.cs:** -```csharp -Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; -``` +![Store Purchase (+Logs)](/images/unity-tutorial/part-3/shop-purchase-wLogsanimated.gif) + -Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. +Open **ShopRow.cs** to find the `Purchase()` function: -**Append after the Start() function in TutorialGameManager.cs** ```csharp -private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) +/// Success will trigger: OnInventoryComponentUpdate +public void Purchase() { - var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); - if (player != null) - { - UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); - } + Debug.Log($"Purchasing shopId:{_shopId}, shopSaleIdx:{_shopSaleIdx}"); + Reducer.Purchase( + LocalPlayer.instance.EntityId, + _shopId, + (uint)_shopSaleIdx); + + tooltip.Clear(); } ``` -Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. - -## Conclusion - -This concludes the SpacetimeDB basic multiplayer tutorial, where we learned how to create a multiplayer game. In the next Unity tutorial, we will add resource nodes to the game and learn about _scheduled_ reducers: - -From here, the tutorial continues with more-advanced topics: The [next tutorial](/docs/unity/part-4.md) introduces Resources & Scheduling. +Purchasing from the store will trigger the following flows: ---- +1. **[Server]** `Purchase()` from `Shop.cs` will: + 1. Validate player, shop, sale, inventory, and required unlocks. + 1. If valid, process the sale: + 1. **Delete** required items from the player's `InventoryComponent`. + 1. **Update** `InventoryComponent` for qualifying `.GiveItems` and/or `.GivePlayerUnlocks` + 1. If any `InventoryComponent.GiveGlobalUnlocks`, **Update** `Config` by adding to its `GlobalUnlockIds`. -### Troubleshooting -- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` +2. **[Client]** Remember when [GameManager at Start()](#high-level-client-initialization) declared `initSubscribeToEvents()`? + - **Inventory:** + 1. `onInventoryComponentUpdate()` -> `PlayerInventoryController.Local.InventoryUpdate(newValue)` chain will be called. + 1. `InventoryUpdate()` will clear `PlayerInventoryController.Local._pockets` and resync with the server's `newValue`. + - **Config:** `onConfigComponentUpdate()` -> `PlayerInventoryController.Local.ConfigUpdate(newValue)` chain will be called. -- If you get this exception when running the project: - -``` -NullReferenceException: Object reference not set to an instance of an object -TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) -``` - -Check to see if your GameManager object in the Scene has the NetworkManager component attached. +## Conclusion -- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. +✅ You've successfully finished the SpacetimeDB client-server multiplayer tutorial series! **From here:** -``` -Connection error: Unable to connect to the remote server -``` +- Check the [C# Reference Doc](../modules/c-sharp/index.md) +- Explore the CLI tool by opening a terminal and typing `spacetime`, which expands upon the _Publisher_ and _Reducer_ Unity editor tools. + - 💡Try starting with `spacetime sql -h` to learn how to query your database [via SQL queries](../sql/index.md)! +- Ask questions in our [Discord server](https://discord.gg/spacetimedb) diff --git a/docs/unity/part-4.md b/docs/unity/part-4.md index a87f27a2..b51d7f4f 100644 --- a/docs/unity/part-4.md +++ b/docs/unity/part-4.md @@ -1,261 +1,297 @@ -# Unity Tutorial - Advanced - Part 4 - Resources and Scheduling +# Unity Multiplayer Tutorial - Part 2 -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! +> [!IMPORTANT] +> TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. -This progressive tutorial is continued from the [Part 3](/docs/unity/part-3.md) Tutorial. +### Prerequisites -**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** +- This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md): +- Or [start from the beginning](/docs/unity/index.md). -In this second part of the lesson, we'll add resource nodes to our project and learn about scheduled reducers. Then we will spawn the nodes on the client so they are visible to the player. +## Analyzing the C# Server Module -## Add Resource Node Spawner +This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md). -In this section we will add functionality to our server to spawn the resource nodes. +In this part of the tutorial, we will: -### Step 1: Add the SpacetimeDB Tables for Resource Nodes +1. Learn core concepts of the C# server module. +1. Review limitations and common practices. +1. Breakdown high-level concepts like Types, Tables, and Reducers. +1. Breakdown the initialization reducer (entry point) and chat demo features. -1. Before we start adding code to the server, we need to add the ability to use the rand crate in our SpacetimeDB module so we can generate random numbers. Open the `Cargo.toml` file in the `Server` directory and add the following line to the `[dependencies]` section. +The server module will handle the game logic and data management for the game. -```toml -rand = "0.8.5" -``` +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! -We also need to add the `getrandom` feature to our SpacetimeDB crate. Update the `spacetimedb` line to: +## The Entity Component Systems (ECS) -```toml -spacetimedb = { "0.5", features = ["getrandom"] } -``` +Before we continue to creating the server module, it's important to understand the basics of the ECS. +This is a game development architecture that separates game objects into components for better flexibility and performance. +You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). -2. The first entity component we are adding, `ResourceNodeComponent`, stores the resource type. We'll define an enum to describe a `ResourceNodeComponent`'s type. For now, we'll just have one resource type: Iron. In the future, though, we'll add more resources by adding variants to the `ResourceNodeType` enum. Since we are using a custom enum, we need to mark it with the `SpacetimeType` attribute. Add the following code to lib.rs. +![ECS Flow, Wikipedia Creative Commons CC0 1.0](/images/unity-tutorial/part-2/ecs-flow-wiki-creative-commons-cc0-v1.min.png) + -```rust -#[derive(SpacetimeType, Clone)] -pub enum ResourceNodeType { - Iron, -} +We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, +making it ideal for building multiplayer games with SpacetimeDB. -#[spacetimedb(table)] -#[derive(Clone)] -pub struct ResourceNodeComponent { - #[primarykey] - pub entity_id: u64, +## C# Module Limitations & Nuances - // Resource type of this resource node - pub resource_type: ResourceNodeType, -} -``` +Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), it's important to be aware of the following: -Because resource nodes never move, the `MobileEntityComponent` is overkill. Instead, we will add a new entity component named `StaticLocationComponent` that only stores the position and rotation. +1. No DateTime-like types in Types or Tables: + - Use `long` for microsecond unix epoch timestamps + - See example usage and converts at the [_TimeConvert_ module demo class](https://github.com/clockworklabs/zeke-demo-project/blob/3fa1c94e75819a191bd785faa7a7d15ea4dc260c/Server-Csharp/src/Utils.cs#L19) -```rust -#[spacetimedb(table)] -#[derive(Clone)] -pub struct StaticLocationComponent { - #[primarykey] - pub entity_id: u64, - pub location: StdbVector2, - pub rotation: f32, -} -``` +2. No Timers or async/await, such as those to create repeating loops: + - For repeating invokers, instead **re**schedule it from within a fired [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) function. -3. We are also going to add a couple of additional column to our Config table. `map_extents` let's our spawner know where it can spawn the nodes. `num_resource_nodes` is the maximum number of nodes to spawn on the map. Update the config table in lib.rs. -```rust -#[spacetimedb(table)] -pub struct Config { - // Config is a global table with a single row. This table will be used to - // store configuration or global variables +3. Using `Debug` advanced option in the `Publisher` Unity editor tool will add callstack symbols for easier debugging: + - However, avoid using `Debug` mode when publishing outside a `localhost` server: + - Due to WASM buffer size limitations, this may cause publish failure. - #[primarykey] - // always 0 - // having a table with a primarykey field which is always zero is a way to store singleton global state - pub version: u32, - pub message_of_the_day: String, +4. If you `throw` a new `Exception`, no error logs will appear. Instead, use either: + 1. Use `Log(message, LogLevel.Error);` before you throw. + 2. Use the demo's static [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs) class to `Utils.Throw()` to wrap the error log before throwing. - // new variables for resource node spawner - // X and Z range of the map (-map_extents to map_extents) - pub map_extents: u32, - // maximum number of resource nodes to spawn on the map - pub num_resource_nodes: u32, -} -``` -4. In the `init` reducer, we need to set the initial values of our two new variables. Update the following code: +5. `[AutoIncrement]` or `[PrimaryKeyAuto]` will never equal 0: + - Inserting a new row with an Auto key equaling 0 will always return a unique, non-0 value. -```rust - Config::insert(Config { - version: 0, - message_of_the_day: "Hello, World!".to_string(), - // new variables for resource node spawner - map_extents: 25, - num_resource_nodes: 10, - }) - .expect("Failed to insert config."); -``` +6. Enums cannot declare values out of the default order: + - For example, `{ Foo = 0, Bar = 3 }` will fail to compile. -### Step 2: Write our Resource Spawner Repeating Reducer +## Namespaces -1. Add the following code to lib.rs. We are using a special attribute argument called repeat which will automatically schedule the reducer to run every 1000ms. +Common `using` statements include: -```rust -#[spacetimedb(reducer, repeat = 1000ms)] -pub fn resource_spawner_agent(_ctx: ReducerContext, _prev_time: Timestamp) -> Result<(), String> { - let config = Config::filter_by_version(&0).unwrap(); +```csharp +using SpacetimeDB; // Contains class|func|struct attributes like [Table], [Type], [Reducer] +using static SpacetimeDB.Runtime; // Contains Identity DbEventArgs, Log() +using SpacetimeDB.Module; // Contains prop attributes like [Column] +using static Module.Utils; // Helper to workaround the `throw` and `DateTime` limitations noted above +``` - // Retrieve the maximum number of nodes we want to spawn from the Config table - let num_resource_nodes = config.num_resource_nodes as usize; +- You will mostly see `SpacetimeDB.Module` in [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs) for schema definitions +- `SpacetimeDB` and `SpacetimeDB.Runtime` can be found in most all SpacetimeDB scripts +- `Module.Utils` parse DateTimeOffset into a timestamp string and wraps `throw` with error logs - // Count the number of nodes currently spawned and exit if we have reached num_resource_nodes - let num_resource_nodes_spawned = ResourceNodeComponent::iter().count(); - if num_resource_nodes_spawned >= num_resource_nodes { - log::info!("All resource nodes spawned. Skipping."); - return Ok(()); - } +## Partial Classes & Structs - // Pick a random X and Z based off the map_extents - let mut rng = rand::thread_rng(); - let map_extents = config.map_extents as f32; - let location = StdbVector2 { - x: rng.gen_range(-map_extents..map_extents), - z: rng.gen_range(-map_extents..map_extents), - }; - // Pick a random Y rotation in degrees - let rotation = rng.gen_range(0.0..360.0); - - // Insert our SpawnableEntityComponent which assigns us our entity_id - let entity_id = SpawnableEntityComponent::insert(SpawnableEntityComponent { entity_id: 0 }) - .expect("Failed to create resource spawnable entity component.") - .entity_id; - - // Insert our static location with the random position and rotation we selected - StaticLocationComponent::insert(StaticLocationComponent { - entity_id, - location: location.clone(), - rotation, - }) - .expect("Failed to insert resource static location component."); - - // Insert our resource node component, so far we only have iron - ResourceNodeComponent::insert(ResourceNodeComponent { - entity_id, - resource_type: ResourceNodeType::Iron, - }) - .expect("Failed to insert resource node component."); - - // Log that we spawned a node with the entity_id and location - log::info!( - "Resource node spawned: {} at ({}, {})", - entity_id, - location.x, - location.z, - ); - - Ok(()) -} -``` +- Throughout the demo, you will notice most classes or structs with a SpacetimeDB [Attribute] such as `[Table]` or `[Reducer]` will be defined with the `partial` keyword. -2. Since this reducer uses `rand::Rng` we need add include it. Add this `use` statement to the top of lib.rs. +- This allows the _Roslyn Compiler_ to [incrementally generate](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) additions to the SpacetimeDB SDK, such as adding helper functions and utilities. This means SpacetimeDB takes care of all the low-level tooling for you, such as inserting, updating or querying the DB. + - This further allows you to separate your models from logic within the same class. -```rust -use rand::Rng; -``` +* Notice that the module class, itself, is also a `static partial class`. -3. Even though our reducer is set to repeat, we still need to schedule it the first time. Add the following code to the end of the `init` reducer. You can use this `schedule!` macro to schedule any reducer to run in the future after a certain amount of time. +## Types & Tables -```rust - // Start our resource spawner repeating reducer - spacetimedb::schedule!("1000ms", resource_spawner_agent(_, Timestamp::now())); -``` +`[Table]` attributes are database columns, while `[Type]` attributes are define a schema. -4. Next we need to generate our client code and publish the module. Since we changed the schema we need to make sure we include the `--clear-database` flag. Run the following commands from your Server directory: +### Types -```bash -spacetime generate --out-dir ../Assets/autogen --lang=csharp +`[Type]` attributes attach to properties containing `[Table]` attributes when you want to use a custom Type that's not [SpacetimeDB natively-supported](../modules/c-sharp#supported-types). These are generally defined as a `partial struct` or `partial class` -spacetime publish -c yourname/bitcraftmini -``` +Let's inspect a real example `Type`; open [Server-cs/Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): -Your resource node spawner will start as soon as you publish since we scheduled it to run in our init reducer. You can watch the log output by using the `--follow` flag on the logs CLI command. +In Unity, you are likely familiar with the `Vector2` type. In SpacetimeDB, let's inspect the `StdbVector2` type to store 2D positions in the database: -```bash -spacetime logs -f yourname/bitcraftmini +```csharp +/// A spacetime type which can be used in tables & reducers to represent a 2D position (such as movement) +[Type] +public partial class StdbVector2 +{ + public float X; + public float Z; + + // This allows us to use StdbVector2::ZERO in reducers + public static readonly StdbVector2 ZERO = new() + { + X = 0, + Z = 0, + }; +} ``` -### Step 3: Spawn the Resource Nodes on the Client +- Since `Types` are used in `Tables`, we can now use a custom SpacetimeDB `StdbVector3` `Type` in a `[Table]`. -1. First we need to update the `GameResource` component in Unity to work for multiplayer. Open GameResource.cs and add `using SpacetimeDB.Types;` to the top of the file. Then change the variable `Type` to be of type `ResourceNodeType` instead of `int`. Also add a new variable called `EntityId` of type `ulong`. +We may optionally include `static readonly` property "helper" functions such as the above-exampled `ZERO`. -```csharp - public ulong EntityId; +### Tables - public ResourceNodeType Type = ResourceNodeType.Iron; -``` +`[Table]` attributes use `[Type]`s - either custom (like `StdbVector2` above) or [SpacetimeDB natively-supported types](../modules/c-sharp#supported-types). +These are generally defined as a `struct` or `class`. -2. Now that we've changed the `Type` variable, we need to update the code in the `PlayerAnimator` component that references it. Open PlayerAnimator.cs and update the following section of code. We need to add `using SpacetimeDB.Types;` to this file as well. This fixes the compile errors that result from changing the type of the `Type` variable to our new server generated enum. +Let's inspect a real example `Table`, looking again at [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): ```csharp - var resourceType = res?.Type ?? ResourceNodeType.Iron; - switch (resourceType) - { - case ResourceNodeType.Iron: - _animator.SetTrigger("Mine"); - Interacting = true; - break; - default: - Interacting = false; - break; - } - for (int i = 0; i < _tools.Length; i++) - { - _tools[i].SetActive(((int)resourceType) == i); - } - _target = res; +/// Represents chat messages within the game, including the sender and message content +[Table] +public partial class ChatMessage +{ + /// Primary key, automatically incremented + [Column(ColumnAttrs.PrimaryKeyAuto)] + public ulong ChatEntityId; + + /// The entity id of the player (or NPC) that sent the message + public ulong SourceEntityId; + + /// Message contents + public string? ChatText; + + /// Microseconds Timestamp of when the message was sent + /// Convert between long and DateTimeOffset via Utils.TimeConvert + public long Timestamp; + + /// Microseconds DateTimeOffset representing Timestamp microseconds of when the action ended + /// Convert between long and DateTimeOffset via Utils.TimeConvert + public DateTimeOffset TimestampOffset => + TimeConvert.FromMicrosecondsTimestamp(Timestamp); +} ``` -3. Now that our `GameResource` is ready to be spawned, lets update the `BitcraftMiniGameManager` component to actually create them. First, we need to add the new tables to our SpacetimeDB subscription. Open BitcraftMiniGameManager.cs and update the following code: +- The `Id` vars are `ulong` types, commonly used for SpacetimeDB unique identifiers +- Notice how `Timestamp` field is a `long` type instead of `DateTimeOffset` (a [limitation](#limitations) mentioned earlier): + - Take note the `TimestampOffset` helper, currying the function to a `TimeConvert` utils class. + - This ensures we preserve the time to the microsecond via unix epoch timestamps. ```csharp - SpacetimeDBClient.instance.Subscribe(new List() - { - "SELECT * FROM Config", - "SELECT * FROM SpawnableEntityComponent", - "SELECT * FROM PlayerComponent", - "SELECT * FROM MobileEntityComponent", - // Our new tables for part 2 of the tutorial - "SELECT * FROM ResourceNodeComponent", - "SELECT * FROM StaticLocationComponent" - }); +/// This component will be created for all world objects that can move smoothly throughout the world, keeping track +/// of position, the last time the component was updated & the direction the mobile object is currently moving. +[Table] +public partial class MobileEntityComponent +{ + /// Primary key for the mobile entity + [Column(ColumnAttrs.PrimaryKey)] + public ulong EntityId; + + /// The last known location of this entity + public StdbVector2? Location; + + /// Movement direction, {0,0} if not moving at all. + public StdbVector2? Direction; + + /// Microseconds Timestamp of when the movement started + /// Convert between long and DateTimeOffset via Utils.TimeConvert + public long MoveStartTimestamp; + + /// Microseconds DateTimeOffset representing Timestamp microseconds of when the action ended + /// Convert between long and DateTimeOffset via Utils.TimeConvert + public DateTimeOffset MoveStartTimestampOffset => + TimeConvert.FromMicrosecondsTimestamp(MoveStartTimestamp); +} ``` -4. Next let's add an `OnInsert` handler for the `ResourceNodeComponent`. Add the following line to the `Start` function. +- `EntityId` is the unique identifier for the table, declared as a `ulong` +- Location and Direction are both `StdbVector2` types discussed above -```csharp - ResourceNodeComponent.OnInsert += ResourceNodeComponent_OnInsert; +## Reducers + +Reducers are static cloud functions with a `[Reducer]` attribute that run on the server. +These called from the client, always returning `void`. They are defined with the `[Reducer]` attribute and take +a `DbEventArgs` object as the first argument, followed by any number of arguments that you want to pass to the reducer. + +While there are some premade Reducers, such as for init and [dis]connection, you may also create your own. + +> [!NOTE] +> In a fully developed game, the server would typically perform server-side validation on client-requested actions to ensure +they comply with game boundaries, rules, and mechanics. + +### Overview + +Looking at the most straight-forward example, open [Chat.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Chat.cs): + +```c# +/// Add a chat entry to the ChatMessage table +[Reducer] +public static void SendChatMessage(DbEventArgs dbEvent, string text) +{ + // Get the player component based on the sender identity + PlayerComponent player = PlayerComponent.FindByOwnerId(dbEvent.Sender) ?? + Throw($"{nameof(SendChatMessage)} Error: Player not found"); + + // Now that we have the player we can insert the chat message + // using the player entity id. + new ChatMessage + { + ChatEntityId = 0, // This column auto-increments, so we can set it to 0 + SourceEntityId = player.EntityId, + ChatText = text, + Timestamp = Utils.TimeConvert.ToMicrosecondsTimestamp(dbEvent.Time), + }.Insert(); +} ``` -5. Finally we add the new function to handle the insert event. This function will be called whenever a new `ResourceNodeComponent` is inserted into our local client cache. We can use this to spawn the resource node in the world. Add the following code to the `BitcraftMiniGameManager` class. +- Every reducer starts with `[Reducer] public static void Foo(DbEventArgs dbEvent, ...)`. +- Every reducer contains a `DbEventArgs` as the 1st arg. + - Contains the sender's `Identity`, `DateTimeOffset` sent, and a semi-anonymous `Address` to compare sender vs others. +- The `PlayerComponent` was found by passing the sender's `Identity` to `PlayerComponent.FindByOwnerId()`. +- `Throw()` is the helper function (workaround for one of the [limitations](#limitations) mentioned earlier) that logs an error before throwing. +- This timestamp utilized `Utils.TimeConvert.ToMicrosecondsTimestamp()` for consistent time parsing. +- Since `ChatEntityId` is tagged with the `[Column(ColumnAttrs.PrimaryKeyAuto)]` attribute, setting it to 0 will auto-increment. + +### DB Initialization -To get the position and the rotation of the node, we look up the `StaticLocationComponent` for this entity by using the EntityId. +Let's find the entry point that gets called every time we **publish** or **clear** the database: +Open [Lib.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Lib.cs): + +> [!TIP] +> Not to be confused with the `Connect` ReducerKind for **player** initialization! ```csharp - private void ResourceNodeComponent_OnInsert(ResourceNodeComponent insertedValue, ReducerEvent callInfo) +/// Initial configuration for the game world. +/// Called when the module is initially published. +[Reducer(ReducerKind.Init)] +public static void Init() +{ + try + { + Config config = initConfig(); // Create our global config table. + + spawnRocks(config, spawnCount: 10); + List unlocks = initUnlocks(); + ShopComponent shopComponent = getInitShopComponent(unlocks); + initItemCatalog(); + SpawnShop(shopComponent, requiredUnlock: null); + initResourceSpawners(); + } + catch (Exception e) { - switch(insertedValue.ResourceType) - { - case ResourceNodeType.Iron: - var iron = Instantiate(IronPrefab); - StaticLocationComponent loc = StaticLocationComponent.FilterByEntityId(insertedValue.EntityId); - Vector3 nodePos = new Vector3(loc.Location.X, 0.0f, loc.Location.Z); - iron.transform.position = new Vector3(nodePos.x, MathUtil.GetTerrainHeight(nodePos), nodePos.z); - iron.transform.rotation = Quaternion.Euler(0.0f, loc.Rotation, 0.0f); - break; - } + Throw($"{nameof(Init)} Error: {e.Message}"); } +} ``` -### Step 4: Play the Game! +While this is very high-level, **this is what's happening:** + +1. We init the Config table, treating it as a singleton (there's only 1 Config at row `0`). +2. We spawn rocks into the world. +3. We create an unlockable and init our shop with these in-mind. +4. We init the item catalog, containing static metadata for the items, such as names<>ids. +5. We init our resource spawners, using the [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) to spawn our common and uncommon resources. + - For the common ones, we initially schedule them to fire almost-immediately. + - When the scheduler function fires, we **re**schedule them to infinitely call upon itself in intervals. + +### Other Premade Reducers + +- `[Reducer(ReducerKind.Connect)]` - Their `Identity` can be found in the `Sender` value of the `DbEventArgs`. +- `[Reducer(ReducerKind.Disconnect)]` +- `[Reducer(ReducerKind.Update)]` - Not to be confused with Unity-style Update loops, this calls when a `[Table]` row is updated. + +## Publishing the Module + +To deploy outside of Unity, we'd normally use the `spacetime publish` CLI command. + +However, Unity has an integrated **Publisher** editor tool that will be introduced in the next section! + +## Conclusion + +You have now learned the core concepts of the C# server module, reviewed limitations/common practices +and broke down high-level concepts like Types, Tables, and Reducers - with real examples from the demo. -6. Hit Play in the Unity Editor and you should now see your resource nodes spawning in the world! +In the next section, we'll publish our module, break down the **client-side** Unity code and analyze client-server flows. \ No newline at end of file diff --git a/nav.ts b/nav.ts index 8f463ad7..8d3367fb 100644 --- a/nav.ts +++ b/nav.ts @@ -25,22 +25,25 @@ function section(title: string): NavSection { const nav: Nav = { items: [ section("Intro"), - page("Overview", "index", "index.md"), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? - page("Getting Started", "getting-started", "getting-started.md"), + page("Overview", "index", "index.md"), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? + page("Getting Started", "getting-started", "getting-started.md"), + page("Overview", "index", "index.md"), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? + page("Getting Started", "getting-started", "getting-started.md"), section("Deploying"), - page("Testnet", "deploying/testnet", "deploying/testnet.md"), + page("Overview", "deploying", "deploying/index.md"), + page("Hosted", "deploying/hosted", "deploying/hosted.md"), + page("Self-Hosted", "deploying/hosted", "deploying/self-hosted.md"), section("Unity Tutorial - Basic Multiplayer"), - page("Overview", "unity-tutorial", "unity/index.md"), - page("1 - Setup", "unity/part-1", "unity/part-1.md"), - page("2a - Server (Rust)", "unity/part-2a-rust", "unity/part-2a-rust.md"), - page("2b - Server (C#)", "unity/part-2b-c-sharp", "unity/part-2a-c-sharp.md"), - page("3 - Client", "unity/part-3", "unity/part-3.md"), + page("Overview", "unity-tutorial", "unity/index.md"), + page("1 - Project Setup", "unity/part-1", "unity/part-1.md"), + page("2 - Server (C# Module)", "unity/part-2", "unity/part-2a-rust.md"), + page("3 - Client (Unity)", "unity/part-2", "unity/part-2.md"), section("Unity Tutorial - Advanced"), - page("4 - Resources And Scheduling", "unity/part-4", "unity/part-4.md"), - page("5 - BitCraft Mini", "unity/part-5", "unity/part-5.md"), + page("4 - Resources & Scheduling", "unity/part-4", "unity/part-4.md"), + page("5 - BitCraft Mini", "unity/part-5", "unity/part-5.md"), section("Server Module Languages"), page("Overview", "modules", "modules/index.md"), @@ -50,7 +53,7 @@ const nav: Nav = { page("C# Reference", "modules/c-sharp", "modules/c-sharp/index.md"), section("Client SDK Languages"), - page("Overview", "sdks", "sdks/index.md"), + page("Overview", "sdks", "sdks/index.md"), page("Typescript Quickstart", "sdks/typescript/quickstart", "sdks/typescript/quickstart.md"), page("Typescript Reference", "sdks/typescript", "sdks/typescript/index.md"), page("Rust Quickstart", "sdks/rust/quickstart", "sdks/rust/quickstart.md"), @@ -61,7 +64,7 @@ const nav: Nav = { page("C# Reference", "sdks/c-sharp", "sdks/c-sharp/index.md"), section("WebAssembly ABI"), - page("Module ABI Reference", "webassembly-abi", "webassembly-abi/index.md"), + page("Module ABI Reference", "webassembly-abi", "webassembly-abi/index.md"), section("HTTP API"), page("HTTP", "http", "http/index.md"), @@ -70,14 +73,14 @@ const nav: Nav = { page("`/energy`", "http/energy", "http/energy.md"), section("WebSocket API Reference"), - page("WebSocket", "ws", "ws/index.md"), + page("WebSocket", "ws", "ws/index.md"), section("Data Format"), - page("SATN", "satn", "satn.md"), - page("BSATN", "bsatn", "bsatn.md"), + page("SATN", "satn", "satn.md"), + page("BSATN", "bsatn", "bsatn.md"), section("SQL"), - page("SQL Reference", "sql", "sql/index.md"), + page("SQL Reference", "sql", "sql/index.md"), ], };