Get SpatialOS

Sites

Menu

Add your first feature: A Health Pick-up tutorial for the FPS Starter Project

In-game view of the health pickup prefab

Before starting this tutorial, make sure you have followed the Get started guide which sets up the FPS Starter Project. This tutorial follows on from that guide.

What does the tutorial cover?

You will add health pack pick-ups to the game. These pick-ups grant health to players who walk over them.

To implement this feature you will:

Open the FPS Starter Project in your Unity Editor

  1. Launch your Unity Editor.
  2. It should automatically detect the project but if it doesn’t, select Open and then select gdk-for-unity-fps-starter-project/workers/unity.

Define a new SpatialOS entity

In this section we’re going to define a new SpatialOS entity called HealthPickup. SpatialOS entities are made up of SpatialOS components, which store the properties associated with that entity. Components are defined in your project’s schema.

Define a new SpatialOS component

The first step in defining a new SpatialOS entity is defining its components and their properties. Let’s do that now:

  1. Using your file manager, navigate to gdk-for-unity-fps-starter-project/schema, then create a pickups directory.
  2. Inside the gdk-for-unity-fps-starter-project/schema/pickups/ directory, use a text editor of your choice to create a new file called health_pickup.schema.
  3. Paste in the following definition and save the file:

    package pickups;
    
    component HealthPickup {
    id = 21001;
    bool is_active = 1;
    uint32 health_value = 2;
    }
    

    This defines a new SpatialOS component called HealthPickup, and adds two properties:

    • is_active: A flag indicating whether the health pack is active, or has been consumed.
    • health_value: An integer value, indicating the amount of health the pack will grant to a player.
      You may also notice the id property, this is a Component ID, you can ignore it for now.
  4. Any time you modify your schema files you must then run code generation. To do this, select Generate code from the SpatialOS menu in your Unity Editor.

    Code generation creates C# helper classes based on the components and properties defined in the schemalang snippet above. It therefore must be run in order to make use of your newly defined HealthPickup component within your game logic.
    Note: When writing schema files, your properties must use snake case (for example, “health_value”), but the code generation process will create the helper classes in PascalCase (for example, “HealthValue”).

Define a new SpatialOS entity

Now that we’ve defined and generated the HealthPickup component and its properties, let’s deifine the HealthPickup entity we’re going to attach that component to.

All SpatialOS GDK projects contain a C# file that, once for each type of entity in your project, declares a function that defines which components should be instantiated when a new type of that entity is added to a SpatialOS World. The object that these functions return is an entity template.

HealthPickup is a new type of entity, so we must create a new entity template. To do this, we’ll need to add a new function within the FpsEntityTemplates class:

  1. In your Unity Editor, locate Assets/Fps/Scripts/Config/FpsEntityTemplates.cs and open it in your code editor.
  2. Ensure your code can reference the Pickups namespace by adding using Pickups; to the top of the file.
  3. Define HealthPickup, an entirely new type of entity, by adding this new function within the FpsEntityTemplates class:

    public static EntityTemplate HealthPickup(Vector3f position, uint healthValue)
    {
    var gameLogic = WorkerUtils.UnityGameLogic;
    
    var healthPickupComponent = new Pickups.HealthPickup.Snapshot { IsActive = true, HealthValue = healthValue };
    
    var entityTemplate = new EntityTemplate();
    entityTemplate.AddComponent(new Position.Snapshot { Coords = new Coordinates(position.X, position.Y, position.Z) }, gameLogic);
    entityTemplate.AddComponent(new Metadata.Snapshot { EntityType = "HealthPickup"}, gameLogic);
    entityTemplate.AddComponent(new Persistence.Snapshot(), gameLogic);
    entityTemplate.AddComponent(healthPickupComponent, gameLogic);
    entityTemplate.SetReadAccess(gameLogic, WorkerUtils.UnityClient);
    entityTemplate.SetComponentWriteAccess(EntityAcl.ComponentId, gameLogic);
    
    return entityTemplate;
    }
    

    Let’s break down what the above snippet does:

    • The struct Pickups.HealthPickup.Snapshot was generated from the HealthPickup component you previously defined in schemalang.
    • The line entityTemplate.AddComponent(healthPickupComponent, gameLogic); adds an instance of this struct to the HealthPickup entity.
    • The line entityTemplate.SetReadAccess(gameLogic, WorkerUtils.UnityClient); states that both server-workers (gameLogic) and client-workers (UnityClient) have read access to this entity (that they can see health packs).
    • The line entityTemplate.SetComponentWriteAccess(EntityAcl.ComponentId, gameLogic); states that only server-workers have write access to the healthPickupComponent.
      We state this because we don’t want clients to be able to alter how much health is in a health pack, that would be cheating.
    • You may also notice Position, Metadata and Persistence, these are standard library components that you can ignore for now.

    Add your new entity to the snapshot

    In this section we’re going to add a health pack entity to the SpatialOS world. There are two ways to do this:

    • At runtime, by passing an EntityTemplate object to an entity creation function.
    • At start-up, by adding a health pack entity to the Snapshot, so it’s already in the world when the game begins.

    We will do the latter, so that when the game begins there will already be a health pack in a pre-defined location.

    Edit the snapshot generation script.

    The SpatialOS menu in your Unity Editor contains a “Generate FPS Snapshot” option. This option runs Assets/Fps/Scripts/Editor/SnapshotGenerator/SnapshotMenu.cs. We will now modify this script to add a HealthPack entity to our snapshot:

    1. In your Unity Editor, locate Assets/Fps/Scripts/Editor/SnapshotGenerator/SnapshotMenu.cs and open it in your code editor.
    2. Ensure your code can reference the Vector3f namespace by adding using Improbable; to the top of the file.
    3. The function below contains logic for adding a health pack entity to the snapshot object, and sets the amount of health the packs restore to 100. Paste it inside the SnapshotMenu class.
    private static void AddHealthPacks(Snapshot snapshot)
    {
    var healthPack = FpsEntityTemplates.HealthPickup(new Vector3f(5, 0, 0), 100);
    snapshot.AddEntity(healthPack);
    }
    
  4. Call your new function by pasting the below snippet inside GenerateFpsSnapshot(). Be sure to paste this below the GenerateSnapshot lines and above the SaveSnapshot lines, so that it’s run during snapshot generation.

    AddHealthPacks(localSnapshot);
    AddHealthPacks(cloudSnapshot);
    

    In your own game may want to consider moving default values (such as health pack positions, and health values) into a settings file. But for now, we will keep this example simple.

    Update the snapshot

    All SpatialOS GDK projects contain a directory named snapshots in the root of the project. If you have updated the snapshot generation script SnapshotMenu.cs, as we did in the step above, or if you’ve altered components in an entity template, then your snapshot will be out of date, and must be regenerated.

    1. Regenerate the default.snapshot file from the SpatialOS menu in your Unity Editor, by running “Generate FPS Snapshot”.
    2. If you launch a local deployment (Ctrl + L in your Unity Editor), you should be able to see one HealthPickup entity in the Inspector.
      World view in the Inspector showing the `HealthPickup` entity
      If we were to test the game at this point, the health pack entity would appear in the inspector but not in-game. This is because we have not yet defined how to represent the entity on your client or server-workers. We’ll do this in the next section.
    3. Before you move on, in the terminal window that’s running the SpatialOS process, enter Ctrl+C to stop the process.

    Plan your entity representations

    In this section we’re going to decide how to represent the HealthPickup entity in our client-workers and in our server-workers. First we must think about how each of the workers should represent the entity, so let’s quickly review how we want this game mechanic to play out:

    • Players should see a health pack hovering just above the ground.
    • When a player collides with the health pack. it is consumed and disappears.
    • A health pack should only be consumed if the player is not already at full health.
    • Consumed health packs re-appear after a cool-down, and are ready for use again.

    We can neatly separate this logic between the client-side and server-side representations:

    • The UnityClient client-worker should display a visual representation for each health pack in the world. It should only display health packs that are currently “active”.
    • The UnityGameLogic server-worker should, when a player collides with an active health pack, check whether that player is injured and allow the player to consume the health pack if they are.

    The FPS Starter Project uses the SpatialOS GDK’s MonoBehaviour workflow. In this workflow SpatialOS entities are represented by Unity prefabs. Crucially, you can use different prefabs to represent the same type of entity on different types of workers. This allows you to separate client-side and server-side entity representation, as we planned above.

    Implement client-side entity representation

    The client-side logic we want to capture for this game mechanic is:

    • Visualise active health packs hovering just above the ground.
    • Do not visualise inactive health packs.
    1. In your Unity Editor, locate Assets/Fps/Prefabs/HealthPickup.prefab.
    2. Select this prefab and press Ctrl+D to duplicate it.
    3. Move this duplicated prefab to Assets/Fps/Resources/Prefabs/UnityClient.
    4. Rename the duplicated prefab to HealthPickup (the process of duplication will have appended an unnecessary 1 to the file name).
    5. Select the duplicated prefab to open it.
    6. Still in your Unity Editor, add a new script component to the root of your duplicated HealthPickup prefab by selecting Add Component > New Script in the Inspector window.
    7. Name this script HealthPickupClientVisibility, and replace its contents with the following code snippet:
    using Improbable.Gdk.Subscriptions;
    using Pickups;
    using UnityEngine;
    
    namespace Fps
    {
    [WorkerType(WorkerUtils.UnityClient)]
    public class HealthPickupClientVisibility : MonoBehaviour
    {
        [Require] private HealthPickupReader healthPickupReader;
    
        private MeshRenderer cubeMeshRenderer;
    
        private void OnEnable()
        {
            cubeMeshRenderer = GetComponentInChildren<MeshRenderer>();
            healthPickupReader.OnUpdate += OnHealthPickupComponentUpdated;
            UpdateVisibility();
        }
    
        private void UpdateVisibility()
        {
            cubeMeshRenderer.enabled = healthPickupReader.Data.IsActive;
        }
    
        private void OnHealthPickupComponentUpdated(HealthPickup.Update update)
        {
            UpdateVisibility();
        }
    }
    }
    

This script is mostly standard C# code that you could find in any game built with Unity Engine. There are a few annotations which are specific to the SpatialOS GDK though, let’s break those down:

  • using Pickups;
    The class HealthPickupClientVisibility relies upon the is_active property that we previously defined as a property of the HealthPickup component. For this reason the package Pickups, that we declared in the health_pickup.schema file, appears in a using statement at the top of the script.

  • [WorkerType(WorkerUtils.UnityClient)]
    This WorkerType annotation decorates the class HealthPickupClientVisibility. It tells SpatialOS to only enable this class on UnityClient client-workers, ensuring that it will never run on your server-workers.

  • [Require] private HealthPickup.Requirable.Reader healthPickupReader;
    This is an instruction to the client-worker running the HealthPickupClientVisibility class. It tells the client-worker to only enable this script on entities that have a HealthPickup component and the client-worker has read-access to.

  • cubeMeshRenderer.enabled = healthPickupReader.Data.IsActive; uses the is_active bool and that we previously defined to determine if the visual client-side representation of the health pack entity should appear in the game world.

Test your changes

  1. In your Unity Editor, launch a local deployment of your game by selecting SpatialOS > “Local launch” or using the shortcut Ctrl + L.
  2. Open the FPS-Development Scene in your Unity Editor. The Scene file is located in Assets/Fps/Scene.
  3. Press Play in your Unity Editor to play the game.

    In-game view of the health pickup prefab

    You’ll know that your previous changes have worked if you can see a HealthPickup entity in the inspector, and find a floating health pack when running around in-game. Currently it just sits there, inert. If you walk into it, nothing happens. Let’s fix that!

    Our next step will be to add some game logic to the health pack so that it reacts to player collisions and grants them health.

  4. Before you move on, in the terminal window that’s running the SpatialOS process, enter Ctrl+C to stop the process.

Implement server-side entity representation

The server-side logic we want to capture for this game mechanic is:

  • Detect player collisions with the health pack.
  • Check two conditions:
    • The player must be injured.
    • The health pack must be active.
  • Grant health to the player when both conditions are met.

To achieve this we need to:

  • Create a server-side representation of the health pack.
  • Add a script to the health pack which can:
    • Read its own component data (to check if the health pack is active).
    • Read the component data of player entities (to check if they are injured).
    • Write to its own component data (to set itself to “inactive”), and to the player’s (to grant them health).
  1. In your Unity Editor, locate Assets/Fps/Prefabs/HealthPickup.prefab.
  2. Select this prefab and press Ctrl+D to duplicate it.
  3. Move this duplicated prefab to Assets/Fps/Resources/Prefabs/UnityGameLogic.
  4. Rename the duplicated prefab to HealthPickup (the process of duplication will have appended an unnecessary 1 to the file name).
  5. Select the duplicated prefab to open it.
  6. Still in your Unity Editor, add a new script component to the root of your duplicated HealthPickup prefab by selecting Add Component > New Script in the Inspector window.
  7. Name this script HealthPickupServerBehaviour, and replace its contents with the following code snippet:
using System.Collections;
using Improbable.Gdk.Core;
using Improbable.Gdk.Health;
using Improbable.Gdk.Subscriptions;
using Pickups;
using UnityEngine;

namespace Fps
{
    [WorkerType(WorkerUtils.UnityGameLogic)]
    public class HealthPickupServerBehaviour : MonoBehaviour
    {
        [Require] private HealthPickupWriter healthPickupWriter;
        [Require] private HealthComponentCommandSender healthCommandRequestSender;

        private Coroutine respawnCoroutine;

        private void OnEnable()
        {
            // If the pickup is inactive on initial checkout - turn off collisions and start the respawning process.
            if (!healthPickupWriter.Data.IsActive)
            {
                respawnCoroutine = StartCoroutine(RespawnCubeRoutine());
            }
        }

        private void OnDisable()
        {
            if (respawnCoroutine != null)
            {
                StopCoroutine(respawnCoroutine);
            }
        }

        private void OnTriggerEnter(Collider other)
        {
            // OnTriggerEnter is fired regardless of whether the MonoBehaviour is enabled/disabled.
            if (healthPickupWriter == null)
            {
                return;
            }

            if (!other.CompareTag("Player"))
            {
                return;
            }

            HandleCollisionWithPlayer(other.gameObject);
        }

        private void SetIsActive(bool isActive)
        {
            healthPickupWriter?.SendUpdate(new HealthPickup.Update
                {
                    IsActive = new Option<BlittableBool>(isActive)
                });
        }

        private void HandleCollisionWithPlayer(GameObject player)
        {
            var playerSpatialOsComponent = player.GetComponent<LinkedEntityComponent>();

            if (playerSpatialOsComponent == null)
            {
                return;
            }

            healthCommandRequestSender.SendModifyHealthCommand(playerSpatialOsComponent.EntityId, new HealthModifier
            {
                Amount = healthPickupWriter.Data.HealthValue
            });

            // Toggle health pack to its "consumed" state
            SetIsActive(false);

            // Begin cool-down period before re-activating health pack
            respawnCoroutine = StartCoroutine(RespawnCubeRoutine());
        }

        private IEnumerator RespawnCubeRoutine()
        {
            yield return new WaitForSeconds(15f);
            SetIsActive(true);
        }
    }
}

Let’s break down what the above snippet does:

  • [WorkerType(WorkerUtils.UnityGameLogic)]
    This WorkerType annotation decorates the class HealthPickupServerBehaviour. It tells SpatialOS to only enable this class on UnityGameLogic server-workers, ensuring that it will never run on your client-workers.
  • [Require] private HealthPickupWriter healthPickupWriter;
    This is an instruction to the server-worker running the HealthPickupServerBehaviour class. It tells the server-worker to only enable this script on the worker with write-access to the HealthPickup component.
  • private void OnTriggerEnter(Collider other)
    Most functions will only be called if the script component’s enabled property is true, but OnTriggerEnter is called even when this is false. It is unusual in this sense. For this reason, scripts which use OnTriggerEnter must check whether the Writer is null (indicating a lack of authority) before using functions on the writer.
  • private void SetIsActive(bool isActive)
    This function performs a component update. It sets the value of IsActive, the compoment property we defined earlier, to a given value.
  • healthPickupWriter?.SendUpdate(new HealthPickup.Update
    When sent to SpatialOS, the HealthPickup.Update object updates the HealthPickup component.
  • private void HandleCollisionWithPlayer(GameObject player)
    This function will be called any time a player walks through a health pack. It handles cross-worker interaction using commands. When you send a command it acts as a request, which SpatialOS delivers to the single worker that has write-access for the component that the command is intended for.
    Cross-worker interactions can be necessary when your game has multiple UnityGameLogic server-workers, because the worker with write-access for the HealthPack entity may not be the same worker that has write-access to the Player entity who has collided with that health pack.
  • private IEnumerator RespawnCubeRoutine()
    This coroutine re-activates consumed health packs after a cool-down period. It starts at the end of the HandleCollisionWithPlayer function as well as in OnEnable for any health pack entities which are inactive. Running coroutines are stopped in OnDisable.

Optional: Ignore healthy players

This section is intended to reinforce what you’ve learned but is entirely optional. If you don’t want to implement this, you can move onto the next section.

The HandleCollisionWithPlayer function in your HealthPickupServerBehaviour.cs script currently attempts to heal any colliding player. If the player is already on full health we might want to ignore them so that the health pack is not consumed and the health modifier command is never sent.

  1. At the beginning of the HandleCollisionWithPlayer function add an if-statement which reads the player’s current health from a Monobehaviour (that you will need to write yourself) and returns early if their health is at the maximum.

Test your changes in a local deployment

The distributed game logic is now in place, and we can test if it is working correctly. To test this feature, you can follow these steps:

If you implemented the respawn coroutine then you should also see the health pack reappear after a short time. Here’s how it should look:



That’s it! Well done, and welcome to the GDK!

We’d love to know what you think, and invite you to join our community on our forums, or on Discord.

Search results

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums