In the previous lessons you created enemy pirates that move around the world, and learned about sending updates to a component property. In this lesson, you'll:
- learn about events: another thing that can be part of a component
- create a new event that can be synchronised across workers
- fire cannonballs when the event is triggered
- make sure those cannonballs are fired on all clients
We haven't mentioned it yet, but you might have discovered some extra controls: your player's ship can fire cannonballs,
using the E
and Q
keys. Try it out now:
-
If you haven't still got the game running locally from the previous lesson:
-
In the SpatialOS window, click
Build
, then underRun SpatialOS locally
, clickRun
. -
When SpatialOS is ready, run a client (open the scene
UnityClient.unity
, then click Play ▶), and clickCONNECT
.
-
-
Press
E
andQ
to fire cannonballs.These cannonballs are local GameObjects: they're not SpatialOS entities. They're fired using the following Unity code:
if (Input.GetKeyDown(KeyCode.Q)) { if (cannon != null) { cannon.Fire(-transform.right); } }
You can take a look at that in
PlayerInputController.cs
, which we'll be editing later. (To locate the script; in the Unity Editor's project panel, navigate togamelogic/Pirates/Behaviours
.)
But the fact that these are local GameObjects causes a problem. You'll find this out if you connect another Unity client to your game:
-
Open a new terminal window, and navigate to the root directory of your project.
-
Run
spatial local worker launch UnityClient default
.This will launch another Unity client, connecting to the same game.
-
Make sure you can see both game windows at the same time.
-
In one of the game windows, move the ship so you can see the other ship clearly.
-
Press
E
orQ
to fire a cannon.
Now you should be able to see the problem: only the player's own client visualizes the cannonball firing. Because they're local GameObjects, they're not visible to other clients.
In this lesson, you'll fix that.
Once you've seen that the cannonballs are only displayed on the player's own client, stop spatial local launch
by running Ctrl + C
in the terminal, and close the second Unity client. You'll run both clients again later, after making some code changes.
In a SpatialOS game, you have to decide what things you want to synchronise to SpatialOS and what things you don't.
In order to synchronise something, it needs to be expressed as an aspect of a component on an entity.
If something's part of a component on an entity, SpatialOS makes sure that other workers and clients have access to it.
If it's not, no other worker or client will have access to it.
Keeping some things local is fine. For example, your player might be able to change the colour of their UI. Nobody but that player needs that information. But you can see, in the case of firing cannonballs, that some information needs to be shared, so that all clients know that a cannonball has been fired.
One way of solving the problem is to make the cannonballs into entities. This would make them visible to other clients in the same way your ship is. But entities make most sense when they're long-lived objects, especially ones that move around the world.
There's actually a simpler (and cheaper in terms of bandwidth overhead) way to get the effect you want: you can use a feature of components called events.
In the previous lesson, you used properties, which are persistent values stored in a component. Events are also part of a component, but they're transient. You send an event update in the same way you do a property update. And just like a property change, any worker that can "see" an entity will receive the event update.
So in this case, you can rewrite PlayerInputController.cs
so that when a player presses E
or Q
, this fires an
event. You can then write another script that runs on every worker that can see your ship, which, whenever it
receives the event, fires the cannons locally:
There are a few different steps to join everything up to make it work.
In this lesson, you'll:
- add a new event to a component
- trigger the event on the authoritative Unity client (the Unity client with write access to the component)
- fire the cannons on all workers when they receive the event
In the few lessons after that, you'll:
- (lesson 5) actually detect the cannonball collision
- (lesson 6) give ships a
Health
value, and reduce that health when a cannonball hits them - (lesson 7) show an animation of the ships sinking when their health reaches 0
In lesson 3, you used the properties on the ShipControls
component to move pirate ships around. In this lesson,
you're going to add something new to ShipControls
.
Components are defined in a project's schema. SpatialOS uses this schema to generate code which workers use to read and
write to components. It's written in schemalang (SpatialOS documentation),
and it's located in the schema
directory of the project.
-
From the project root directory, navigate to
schema/improbable/ship/ShipControls.schema
.This schema file is where
ShipControls
is defined. -
Look at this file. At the moment, it contains two properties:
target_steering
andtarget_speed
. -
ShipControls
needs to record the "fire cannonballs" key presses as events. To implement this, addFireLeft
andFireRight
component events to this definition, like this:package improbable.ship; type FireLeft {} type FireRight {} component ShipControls { // ... CODE ... // // The component event for triggering firing cannonballs left. event FireLeft fire_left; // The component event for triggering firing cannonballs right. event FireRight fire_right; }
The declarations for the
FireLeft
andFireRight
types don't have any fields: they are empty objects. They could store data that is synchronized when the events are triggered. For example, they could store the time at which the key press was made. We'll leave them empty for now. -
You've changed the schema. Whenever you change the schema, you need to regenerate the generated code.
In the SpatialOS window, click
Build
, then underGenerate from schema
, clickBuild
.This generates code that workers can use to read and modify components, and allows SpatialOS to synchronize components across the system.
When the player presses E
or Q
, you want to trigger the Fire
component events you just created.
You'll do this from a script that exists on the PlayerShip
GameObject on all UnityClients.
But when a player hits E
or Q
, you only want their local instance of PlayerShip to trigger the FireLeft
/
FireRight
event.
So it's important to make sure that this script will only be enabled on the PlayerShip
on which the UnityClient
has write access: that is, the ship that belongs to the player.
-
Open the project in the Unity Editor.
-
In the
EntityPrefabs
folder, double-click on thePlayerShip
prefab. -
On the
PlayerShip
prefab is aPlayerInputController.cs
script. Double-click on it to open it in your C# IDE. -
Take a look at this line:
[Require] private ShipControls.Writer ShipControlsWriter;
You came across
[Require]
in the previous lesson. Requiring a component writer means that this script will only be enabled on the worker with write access toShipControls
.This means
PlayerInputController.cs
only runs on the UnityClient with write access toShipControls
. In this case, the worker with write access will be the player's Unity Client, which is associated with this entity. (We'll look at how this is set up in a later lesson.)It won't run on other UnityClients, or on a
UnityWorker
. This is important, because it means only the player can control their own ship. -
The script reads the user input on each frame, and updates the
ShipControls
component with the new values. The steering and speed values are taken from the Vertical and Horizontal axes, as defined in the Unity input manager. -
It also checks for the
Q
andE
keys being pressed, but it just creates local GameObjects. You want to replace this with code that sends an event update to SpatialOS.You sent an update in the previous lesson using
ShipControlsWriter.Send(new ShipControls.Update()
. But instead of using.Set<name of property>
, you trigger an event using.Add<name of event>
.Replace the two
if
clauses with the following:if (Input.GetKeyDown(KeyCode.Q)) { ShipControlsWriter.Send(new ShipControls.Update().AddFireLeft(new FireLeft())); } if (Input.GetKeyDown(KeyCode.E)) { ShipControlsWriter.Send(new ShipControls.Update().AddFireRight(new FireRight())); }
When you're finished, PlayerInputController
should look like this:
using Assets.Gamelogic.Core;
using Improbable.Ship;
using Improbable.Unity;
using Improbable.Unity.Visualizer;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Assets.Gamelogic.Pirates.Behaviours
{
// Add this MonoBehaviour on client workers only
[WorkerType(WorkerPlatform.UnityClient)]
public class PlayerInputController : MonoBehaviour
{
/*
* Client will only have write access for their own designated PlayerShip entity's ShipControls component,
* so this MonoBehaviour will be enabled on the client's designated PlayerShip GameObject only and not on
* the GameObject of other players' ships.
*/
[Require]
private ShipControls.Writer ShipControlsWriter;
void OnEnable()
{
SceneManager.UnloadSceneAsync(BuildSettings.SplashScreenScene);
}
void Update()
{
ShipControlsWriter.Send(new ShipControls.Update()
.SetTargetSpeed(Mathf.Clamp01(Input.GetAxis("Vertical")))
.SetTargetSteering(Input.GetAxis("Horizontal")));
if (Input.GetKeyDown(KeyCode.Q))
{
ShipControlsWriter.Send(new ShipControls.Update().AddFireLeft(new FireLeft()));
}
if (Input.GetKeyDown(KeyCode.E))
{
ShipControlsWriter.Send(new ShipControls.Update().AddFireRight(new FireRight()));
}
}
}
}
You've removed the code that actually fired the cannons, and instead triggered an event.
Now, there is no code to fire the cannons. You want to fire them using a script that runs on all workers: the problem you saw earlier was that other clients weren't firing cannonballs.
CannonFirer.cs
is used to fire the cannons. You'll extend this script to watch for Fire
events being triggered,
and respond by firing the cannon (creating the local cannonball GameObject).
-
In the Unity Editor, select the
PlayerShip
prefab. -
Open the script
CannonFirer.cs
. -
At the top, add the import statements
using Improbable.Ship;
(for theShipControls
generated code that you updated in step 2.4 above). andusing Improbable.Unity.Visualizer;
(for theRequire
syntax). -
Add a
[Require]
statement, but not for a component writer: for aReader
.This means that this script will be enabled on workers with read access to this component:
public class CannonFirer : MonoBehaviour { [Require] private ShipControls.Reader ShipControlsReader;
In previous steps, you've used a component writer. This object is a component reader, which has a subset of a writer's functionality.
Among other things, you can use it to register a callback for when events and properties change.
In this step, you'll register callbacks to run a function when the FireLeft
and FireRight
events
on the ShipControls
component are triggered.
-
Still in
CannonFirer.cs
, add the followingOnFireLeft
function to fire the left cannon:private void OnFireLeft(FireLeft fireLeft) { // Respond to FireLeft event AttemptToFireCannons(-transform.right); }
-
Add a similar
OnFireRight
function for firing the right cannon as well. Make sure to exclude the minus sign so the cannonballs travel in the correct direction.private void OnFireRight(FireRight fireRight) { // Respond to FireRight event AttemptToFireCannons(transform.right); }
-
This script needs to watch for
FireLeft
andFireRight
events. To do this, register the following callbacks, using theShipControlsReader
:private void OnEnable() { ShipControlsReader.FireLeftTriggered.Add(OnFireLeft); ShipControlsReader.FireRightTriggered.Add(OnFireRight); }
This uses
FireLeftTriggered
andFireRightTriggered
as synchronous callbacks.OnFireLeft()
andOnFireRight()
will run every time the events are triggered. -
Deregister the callbacks in
OnDisable()
, to prevent unexpected behaviour:private void OnDisable() { ShipControlsReader.FireLeftTriggered.Remove(OnFireLeft); ShipControlsReader.FireRightTriggered.Remove(OnFireRight); }
You must register all callbacks in
OnEnable()
, a standard function in the Unity lifecycle. It runs when the MonoBehaviour is enabled - which includes when an entity is first created, and when it crosses worker boundaries.
Similarly, you must de-register all callbacks in
OnDisable()
.
The finished script should look like something this:
using UnityEngine;
using Improbable.Ship;
using Improbable.Unity.Visualizer;
namespace Assets.Gamelogic.Pirates.Cannons
{
// This MonoBehaviour will be enabled on both client and server-side workers
public class CannonFirer : MonoBehaviour
{
[Require] private ShipControls.Reader ShipControlsReader;
private Cannon cannon;
private void OnEnable()
{
ShipControlsReader.FireLeftTriggered.Add(OnFireLeft);
ShipControlsReader.FireRightTriggered.Add(OnFireRight);
}
private void OnDisable()
{
ShipControlsReader.FireLeftTriggered.Remove(OnFireLeft);
ShipControlsReader.FireRightTriggered.Remove(OnFireRight);
}
private void OnFireLeft(FireLeft fireLeft)
{
// Respond to FireLeft event
AttemptToFireCannons(-transform.right);
}
private void OnFireRight(FireRight fireRight)
{
// Respond to FireRight event
AttemptToFireCannons(transform.right);
}
private void Start()
{
// Cache entity's cannon gameobject
cannon = gameObject.GetComponent<Cannon>();
}
public void AttemptToFireCannons(Vector3 direction)
{
if (cannon != null)
{
cannon.Fire(direction);
}
}
}
}
- Refresh the Unity Editor (
Ctrl+R
) to register the code changes you've made. (Unity should refresh automatically, but this makes sure.) - Rebuild the worker code: In the SpatialOS window, click
Build
, then underWorkers
, clickBuild
.
You don't always have to build everything. For a handy reference, see What to build when.
To test the changes, run the game locally:
-
In the SpatialOS window, click
Build
, then underRun SpatialOS locally
, clickRun
. -
When the terminal window says SpatialOS is ready, run a client (open the scene
UnityClient.unity
, then click Play ▶), and clickCONNECT
. -
As you did at the start of this lesson, run another client:
- Open a new terminal window, and navigate to the root directory of your project.
- Run
spatial local worker launch UnityClient default
.
-
As before, make sure you can see both game windows at the same time, and move one of the ships so you can see the other ship clearly.
-
Press
E
orQ
to fire a cannon.
It's done when: You can see ship's cannons fire in both clients:
To stop spatial local launch
running, switch to that terminal window and use Ctrl + C
.
In this lesson, you:
- learned what an event (SpatialOS documentation) is
- learned what schema (SpatialOS documentation) is
- added a new event to a component (SpatialOS documentation) in schema
- ran codegen (SpatialOS documentation)
- triggered an event
- used a component
Reader
(SpatialOS documentation) - registered callbacks to watch for the event
- responded to the event by firing cannons on all workers
This fired cannonballs, but also made sure those cannonballs are fired on all clients.
Your cannonballs are firing, but at the moment, if they hit an enemy, nothing will happen.
In the next lesson you'll detect the collision between cannonballs and enemy ships on the UnityWorker.