Improbable Icon

SpatialOS Discourse Forums

Recommended approach for placing entities in the Unity editor?

I am wondering if there is a recommended approach for pre-placing entities in the editor. The workflow in the example projects is to make things spawn dynamically based on data.

What I’d like to do is place “Resource nodes” (basically spawn points for various types of things) in my level scene, and then in some way make the snapshot generate the matching spatialOS automatically, but I’m not entirely sure what’s the best approach to do that?

I guess the snapshot would have to read the level data somehow to find all the interesting entities. Is there any way SpatialOS already handles that, or a recommended way of doing it?

Hi @Bubbline

Welcome to the SpatialOS forums. :slight_smile:

The easiest approach to do this would be to create a new editor script that iterates over all instances of a specific Monobehaviour in a scene and calls a method on each that allows the GameObject to then write itself to the snapshot in whatever way you choose.

Unity has something similar with the entity conversion flow in DOTS if you wanted an example.

Thanks for the reply. Is there any place where I can see the code for the example in DOTS? I haven’t found it when looking around Github.

Hey @Bubbline

If you’re unfamiliar with the ECS then this might risk over-complicating things, but the functionality you’d want to take a look at would probably be in the Unity API docs here:
https://docs.unity3d.com/Packages/com.unity.entities@0.0/api/Unity.Entities.ConvertToEntity.html

It sounds like you’ve found a way to achieve what you wanted already though, is that right?

Yep, I got it to work fine. Here’s some detail on how I did it, just in case it might be useful for anyone else coming to this thread:

How it works

  1. There is a Spawner entity that gets created on resource nodes, and takes care of spawning new entities/respawning them.
  2. In the UnityGameLogic level prefab, we place prefabs that have a SpawnNodeLocation component to indicate that those gameObjects should trigger the creation of a Spawner in the snapshot
  3. During snapshot generation, we read the level prefab, find all the SpawnNodeLocation and create matching Spawner entities.

SpatialOS spawner entity

To start with, you need an entity that can be created for each of your spawn points. This entity will have a SpawnerComponent that takes care of tracking spawns and generating new entities. The Spawner entity is what we will place in our level and add to the snapshot.

I won’t go too much into details on this one, but basically you want a SpatialOS component that keeps track of spawn timers to handle respawning. For example:

component Spawner {
    id = 66001;
    bool is_spawned = 1;
    uint32 spawner_type = 2; // Assumes your game knows about different types of spawners
    option<float> last_spawn = 3; // Keep time of last spawn, so if a new game logic worker takes authority on the node, it can continue the respawn timer
}

Game logic Spawner gameObject

Then you can have your game logic Spawner prefab which uses the previous component. This new prefab should have its own MonoBehaviour to handle doing the entity spawn logic.

Make sure to place this prefab in the Resources/Prefabs/UnityGameLogic folder so that standard object creation can spawn it, if using default object creation.

The new SpawnNodeComponent MonoBehaviour component needs to read data from the spatialOS SpawnerComponent, and handle sending world commands to spawn entities for your game. For example:

public class SpawnNodeComponent : MonoBehaviour
{
    [Require] SpawnerReader reader;
    [Require] SpawnerWriter writer;
    [Require] WorldCommandSender worldCommands;

    private float respawnTime = 1;

    private void OnEnable()
    {
        var alreadySpawned = reader.Data.IsSpawned;
        if (!alreadySpawned)
        {
            StartRespawnTimer();
        }
    }
    // Used the stored data to calculate when to respawn your entity 
    private async void StartRespawnTimer()
    {
        var delay = respawnTime;
        var lastSpawn = reader.Data.LastSpawn;
        if (lastSpawn != null)
        {
            delay -= Time.time - lastSpawn.Value;
        }
        delay = Mathf.Clamp(delay, 0, respawnTime);
        await new WaitForSeconds(delay);
        Respawn();
    }
    // Spawn a new entity
    private void Respawn()
    {
        // Replace this with game-specific logic for handling whatever you want to spawn
        var template = EntityTemplates.SpawnNode(reader.Data.SpawnerType, transform.position);
        var request = new WorldCommands.CreateEntity.Request(template);
        worldCommands.SendCreateEntityCommand(request, OnCreateEntityResponse);
        writer.Update {
          IsSpawned = true,
          LastSpawn = Time.time,
       };
       // At this point you also want some game-specific logic to track the "death" of your entity in some way, to start the respawn timer again.
    }

    private void OnCreateEntityResponse(WorldCommands.CreateEntity.ReceivedResponse response)
    {
        Debug.Log("Spawned a thing");
    }
}

Spawners placement

The next step is to enable placing spawn nodes in your game level.

Create a prefab for your resource nodes, with a specific component (in my case, SpawnNodeLocation). This component is used to mark entity sawn points.

public enum NodeType
{
    ResourceNode = 1,
    SomethingElse = 2,
}
public class SpawnNodeLocation : MonoBehaviour
{
    // We can use this field to customise which type of entity this spawner should create
    [SerializeField] public NodeType nodeType;
}

You can add data to this component so you can tweak in the inspector which specific type of node it is, depending on what your game does. This component will be passed to the entity creation function later, so it can setup your spawner with the right data.

Then, create a gameObject in the level that will hold all the spawner locations, and place some in it.

Spawner entity creation function

Now we can create a new entity template that will use the data from SpawnNodeLocation to generate a Spawner entity:

    public static EntityTemplate CreateSpawner(SpawnNodeLocation node)
    {
        var entityType = "Spawner";
        var position = node.transform.position;
        var template = new EntityTemplate();
        var spawnerComponent = new Spawner.Snapshot
        {
            IsSpawned = false,
            SpawnerType = node.nodeType,
            LastSpawn = 0,
        };
        template.AddComponent(spawnerComponent, WorkerUtils.UnityGameLogic);
        template.AddComponent(new Metadata.Snapshot { EntityType = entityType }, WorkerUtils.UnityGameLogic);
        template.AddComponent(new Position.Snapshot { Coords = Coordinates.FromUnityVector(position) }, WorkerUtils.UnityGameLogic);
        template.AddComponent(new Persistence.Snapshot(), WorkerUtils.UnityGameLogic);
        template.SetComponentWriteAccess(EntityAcl.ComponentId, WorkerUtils.UnityGameLogic);
        // Note: Don't give read access to the client if you don't want people to be able to read your spawn data
        template.SetReadAccess(UnityGameLogicConnector.WorkerType);
        return template;
    }

Snapshot generation

The last step is to make the snapshot generator loop through
In the snapshot generator, add a function for generating spawn node entities:

    private static SpawnNodeLocation[] AddSpawners(Snapshot snapshot)
    {
        // Load your level prefab
        var level = Resources.Load<GameObject>(LevelPath);
        // Get to your gameObject containing all resource nodes
        var resourcesContainer = level.transform.Find("ResourceNodes");
       // Find all the node spawners in children (I didn't make this recursive, so only top-level children will work. You could make it recursive)
        var spawners = resourcesContainer.GetComponentsInChildren<SpawnNodeLocation>();
        for (var i = 0; i < spawners.Length; i++)
        {
            // Create an entity based on the spawner info
            var spawner = spawners[i];
            var entity = EntityTemplates.CreateSpawner(spawner);
            snapshot.AddEntity(entity);
        }
        return spawners;
    }

Then if you make sure this function is called, you snapshot generator should read all the spawn locations and create the spawners, which will automatically start to do their spawning work.

1 Like

Hey @Bubbline

Glad to here you got it working, and thanks a lot for that very detailed explanation!