Pirates 2 - Create new entities
At the moment, the world is a bit empty. So we’ll start by showing you how to add NPCs. In this lesson you’ll:
- create a template for a new entity type: enemy pirate ships
- learn about snapshots: the starting state of the game world
- write code that uses your template to add entities to a snapshot
- populate the default snapshot with these pirate ships
1. Create simple enemy pirates
In the first lesson, you came across the idea of entities: the building blocks of a SpatialOS world. Pretty much everything in your game world is an entity. So adding some NPCs means adding new entities.
Note that there are some things in the world which shouldn’t be entities: for example, projectiles are sometimes better modelled as local GameObjects. We’ll discuss these design decisions in detail in lesson 4.
The steps to create an entity are:
- Write an entity template, which lists all of the components an entity has.
- Use that template to spawn the entity in the game world.
So to write a template for pirate entities, you need to decide: What components should a pirate ship entity have?
They’re pretty similar to the ones that the player’s ship should have, so let’s start by looking at those.
1.1. Look at the PlayerShip template
In the Unity Editor’s project panel, navigate to Assets/Gamelogic/EntityTemplates/
. This contains the script
EntityTemplateFactory.cs
, which defines templates for all the entities in the game so far.
One of the methods here, CreatePlayerShipTemplate()
, defines the template for the player’s ship:
public static Entity CreatePlayerShipTemplate(string clientWorkerId, Vector3 initialPosition)
{
var playerEntityTemplate = EntityBuilder.Begin()
// Add components to the entity, then set the access permissions for the component on the entity relative to the client or server worker ids.
.AddPositionComponent(initialPosition, CommonRequirementSets.SpecificClientOnly(clientWorkerId))
.AddMetadataComponent(SimulationSettings.PlayerShipPrefabName)
.SetPersistence(false)
.SetReadAcl(CommonRequirementSets.PhysicsOrVisual)
.AddComponent(new Rotation.Data(0), CommonRequirementSets.SpecificClientOnly(clientWorkerId))
.AddComponent(new ClientConnection.Data(SimulationSettings.TotalHeartbeatsBeforeTimeout), CommonRequirementSets.PhysicsOnly)
.AddComponent(new ShipControls.Data(0, 0), CommonRequirementSets.SpecificClientOnly(clientWorkerId))
.AddComponent(new ClientAuthorityCheck.Data(), CommonRequirementSets.SpecificClientOnly(clientWorkerId))
.Build();
return playerEntityTemplate;
}
What’s going on here?
Take a look at the lines that begin with
.Add
. These lines add components to the specified entity template for the player’s ship:
- Position: defines the position of an entity in the world. Every entity needs a
Position
component. There is a specialAddPositionComponent
method because Position is a required component for all entities.- Metadata: defines the entity type of an entity which is used internally by the Unity SDK to map entities to Unity prefabs.
- ClientConnection: used to maintain a connection with clients. This is only needed by entities associated with a player client, so you don’t need this one for the pirate ships.
- ShipControls: used to steer the ship. You can re-use this one, as a pirate ship will need to move around too.
- ClientAuthorityCheck: another component only needed by entities associated with a player client. You don’t need this one either.
Every entity also has an ACL (access control list) component, which determines which workers can read from and write to its components. You’ll learn about and edit ACLs in later lessons. For now, it’s enough to know that
CommonRequirementSets.SpecificClientOnly(clientWorkerId)
makes sure that only the player client has write access to the player ship components.We set the Persistence component of this entity to
false
, because we want it to have the same lifecycle as the player that controls it.
1.2. Create a Pirate entity template
You’ve looked at CreatePlayerShipTemplate()
. Now, you need to write a similar method that creates a template for a
pirate ship:
In
EntityTemplateFactory.cs
, add a new method to create a Pirate Entity at a given position:public static Entity CreatePirateEntityTemplate(Vector3 initialPosition, uint initialRotation) { }
- You create an entity using the builder pattern. Add the following lines to initialize an
EntityBuilder
with a Position component, which every entity needs:
var pirateEntityTemplate = EntityBuilder.Begin() .AddPositionComponent(initialPosition, CommonRequirementSets.PhysicsOnly)
The second argument also adds an entry to the ACL, making the Position component available only to workers with the ‘physics’ attribute - ie UnityWorkers.
- You create an entity using the builder pattern. Add the following lines to initialize an
The pirate ships also need a Unity prefab associated with them, so that Unity can spawn a GameObject for them. That prefab is already in the project, and its name is in the
PirateShipPrefabName
setting.Add a line that sets the prefab name for this entity:
... // Set name of Unity prefab associated with this entity .AddMetadataComponent(SimulationSettings.PirateShipPrefabName)
- We want NPC ships to be persistent, that is, to be included in snapshots (we’ll talk about snapshots in the next section). Add the following line:
... .SetPersistence(true)
When you create an entity template, as mentioned above, you create an ACL to determine which workers can read and write to the entity. Add the following line that gives any worker read access to the pirate entity:
... // Grant any worker read access to this entity. .SetReadAcl(CommonRequirementSets.PhysicsOrVisual)
- As discussed in step 1.1. above, the pirate ship entity needs two components,
and they both already exist:
Rotation
andShipControls
.
Add the components to the template in the same way as in
CreatePlayerShipTemplate()
method:... // Add components to the entity .AddComponent(new Rotation.Data(initialRotation), CommonRequirementSets.PhysicsOnly) .AddComponent(new ShipControls.Data(0, 0), CommonRequirementSets.PhysicsOnly)
Both of these components should be controlled by a
UnityWorker
, henceCommonRequirementSets.PhysicsOnly
.- As discussed in step 1.1. above, the pirate ship entity needs two components,
and they both already exist:
Finally, the method needs to build the entity and return it to the caller:
... .Build(); return pirateEntityTemplate;
The finished method should look like this:
public static Entity CreatePirateEntityTemplate(Vector3 initialPosition, uint initialRotation) { var pirateEntityTemplate = EntityBuilder.Begin() .AddPositionComponent(initialPosition, CommonRequirementSets.PhysicsOnly) .AddMetadataComponent(SimulationSettings.PirateShipPrefabName) .SetPersistence(true) .SetReadAcl(CommonRequirementSets.PhysicsOrVisual) .AddComponent(new Rotation.Data(initialRotation), CommonRequirementSets.PhysicsOnly) .AddComponent(new ShipControls.Data(0, 0), CommonRequirementSets.PhysicsOnly) .Build(); return pirateEntityTemplate; }
1.3. What’s a snapshot?
There’s a difference between how you’ll use CreatePlayerShipTemplate()
and CreatePirateEntityTemplate()
. Why is
there a difference? It depends on when you create the entities.
CreatePlayerShipTemplate()
is called at runtime: the entity is only created when a player connects.
In contrast, pirate ships will exist in the world from the beginning: they’ll be part of the starting state of the game world. That starting state is defined in a snapshot.
Snapshots
A snapshot is a representation of the state of a game world. For each entity in the world, a snapshot lists all of that entity’s components, and the values of their properties (in fact, this is the same as the ‘snapshot entity’ you created above). It also has the entity’s unique ID, assigned when the entity was created.
All game worlds need an initial state to load from when you start the game running; a snapshot provides this. When SpatialOS starts up, it populates the contents of the game world based on a particular snapshot.
2. Add pirate ships to the snapshot
The Pirates game world starts from the snapshot snapshots/default.snapshot
. So in order to add pirates to your world, you
need to edit this snapshot.
The easiest way to do this uses a customization of the Unity editor:
the custom Unity menu item Improbable > Snapshots > Generate Default Snapshot
.
To add pirate entities to the default snapshot:
- In Unity’s Editor’s project panel, navigate to
Assets/Editor/
and open the scriptSnapshotMenu.cs
. Snapshots are created as a
Dictionary
, which maps anEntityId
to aSnapshotEntity
.There are already functions for creating fish and island entities.
Write a similar method in the
SnapshotMenu
class which repeatedly adds PirateShip entities (using the entity template method you wrote in step 1.2) to the dictionary.Your new function could look like this:
public static void PopulateSnapshotWithPirateEntities(ref Dictionary<EntityId, Entity> snapshotEntities, ref int nextAvailableId) { for (var i = 0; i < SimulationSettings.TotalPirates; i++) { // Choose a starting position for this pirate entity var pirateCoordinates = new Vector3((Random.value - 0.5f) * SimulationSettings.PiratesSpawnDiameter, 0, (Random.value - 0.5f) * SimulationSettings.PiratesSpawnDiameter); var pirateRotation = System.Convert.ToUInt32(Random.value * 360); snapshotEntities.Add(new EntityId(nextAvailableId++), EntityTemplateFactory.CreatePirateEntityTemplate(pirateCoordinates, pirateRotation)); } }
2. Find the functionGenerateDefaultSnapshot()
.This is the function that runs when you click
Improbable > Snapshots > Generate Default Snapshot
. It constructs the dictionary for a newly generated snapshot.Extend this function so that it uses your new
CreatePirateShipTemplate()
function to populate the map with pirates.Once you’ve modified the function, it should look something like this:
// ... CODE ... // [MenuItem("Improbable/Snapshots/Generate Default Snapshot")] private static void GenerateDefaultSnapshot() { var snapshotEntities = new Dictionary<EntityId, Entity>(); var currentEntityId = 1; snapshotEntities.Add(new EntityId(currentEntityId++), EntityTemplateFactory.CreatePlayerCreatorTemplate()); PopulateSnapshotWithIslandTerrainEntities(ref snapshotEntities, ref currentEntityId); PopulateSnapshotWithSmallFishGroups(ref snapshotEntities, ref currentEntityId); PopulateSnapshotWithLargeFish(ref snapshotEntities, ref currentEntityId); // Add pirate ships PopulateSnapshotWithPirateEntities(ref snapshotEntities, ref currentEntityId); SaveSnapshot(snapshotEntities); } // ... CODE ... //
You edited the script that generates the default snapshot. Now you actually need to run the script. To do this, use the menu
Improbable > Snapshots > Generate Default Snapshot
.
3. Check it worked
To test these changes, run the game locally:
Open a terminal, and navigate to the root directory in your project.
As you did in step 2.4 of the last lesson, run the game locally by running
spatial local launch
.Wait until you see
SpatialOS ready. Access the inspector at http://localhost:21000/inspector
.As you did in step 2.5 of the last lesson, back in the Unity Editor, run a client by opening the scene
UnityClient.unity
, then clicking Play ▶.In the client window, click
CONNECT
.
It’s done when: The world is full of pirates:
You can also take a look at the pirate ship entities in the Inspector:
Stop the game
When you’re done, stop the game running:
- In Unity, click the Play icon again to stop your client.
- In your terminal, stop
spatial local launch
usingCtrl + C
.
Lesson summary
In this lesson you:
- learnt what an entity template is
- created a new entity template for a pirate ship, and added components to it
- learnt what a snapshot is
- wrote code to edit what’s in the default snapshot
- changed the world’s default snapshot to contain pirate ships
At the moment, your enemies are staying still, which isn’t very interesting. In the next lesson, you’ll move the pirate ships around the world, by sending updates to a component.