Bolt 104 - Events
In Bolt 102
and Bolt 103
we learned the basics of getting Bolt
running and how get some properties and game objects replicating over the network. In this section we will look at events in Bolt
, what they are and how you use them.
The first thing we need to do is to create a new Event
, this is done in the same way we created a new State
previously. Open up the Bolt Assets
window and right-click in an empty area and select New Event
from the drop-down menu.
When you create the event, Bolt
will open it in the Bolt Editor
window, the first functionality we're creating is going to be a simple game log which tells everyone in the game when someone joins and leaves.
Rename the event to LogEvent
and then click New Property
next to the name field. Name our new property Message
and set the type to String
. Set the 'Encoding & Length' settings to UTF8
and 64
.
Before we continue lets examine the two settings on the event asset itself, namely Global Senders
and Entity Senders
.
An event in Bolt can be sent in two different ways: Globally or to a specific Entity.
A global event will be received in the classes which inherits from Bolt.GlobalEventListener
and can be sent freely without the need to have an actual Bolt Entity as the target.
An entity event will be received only on the scripts which the entity is sent to and that inherit from Bolt.EntityEventListener
.
The options for Global Senders
and Entity Senders
controls who can send the event. Since our LogEvent
is going to be a global event, but we only want the server to send it we should switch Global Senders
to Only Server
and since we never want to send this event as an Entity Event
set Entity Senders
to None
.
Important: To make Bolt aware of our event we need to compile again, either using the green 'arrow' button in the Bolt Assets
window or through the Bolt/Compile Assembly
menu.
After Bolt is compiled, we are going to create a new C# script in Tutorial/Scripts
and call it ServerCallbacks
.
We want it to inherit from Bolt.GlobalEventListener
and also decorate it with the [BoltGlobalBehaviour(BoltNetworkModes.Server)]
attribute which tells Bolt
to only create an instance of this class on the server. For more information on global callbacks see: In Depth - Global Callbacks.
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;
[BoltGlobalBehaviour(BoltNetworkModes.Server)]
public class ServerCallbacks : Bolt.GlobalEventListener
{
// Your code here...
}
We also want to implement the Connected
and Disconnected
callbacks.
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;
[BoltGlobalBehaviour(BoltNetworkModes.Server)]
public class ServerCallbacks : Bolt.GlobalEventListener
{
public override void Connected(BoltConnection connection)
{
}
public override void Disconnected(BoltConnection connection)
{
}
}
The code inside Connected
and Disconnected
is identical other than the string we will send in the Message
property on the LogEvent
.
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;
[BoltGlobalBehaviour(BoltNetworkModes.Server)]
public class ServerCallbacks : Bolt.GlobalEventListener
{
public override void Connected(BoltConnection connection)
{
var log = LogEvent.Create();
log.Message = string.Format("{0} connected", connection.RemoteEndPoint);
log.Send();
}
public override void Disconnected(BoltConnection connection)
{
var log = LogEvent.Create();
log.Message = string.Format("{0} disconnected", connection.RemoteEndPoint);
log.Send();
}
}
In Bolt you use EventName.Create();
to create a new event, you then assign the properties you want and call eventObject.Send();
to send it on it's way. The Create
method has several overloads with different parameters that lets you specify who the event should go to, how it should be delivered, etc.
The last piece missing now for us is to listen to the event, open up the NetworkCallbacks
script from the previous chapters. There is now a new method on it called void OnEvent(LogEvent evnt)
that you can implement like this:
- Add a new variable to your class of type
List<string>
and call itlogMessages
. Make sure you haveusing System.Collections.Generic;
at the top of your file so you have access to theList<T>
class; - Inside
OnEvent
we need to just add the value of theevnt.Message
property to thelogMessages
list.
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;
[BoltGlobalBehaviour]
public class NetworkCallbacks : GlobalEventListener
{
List<string> logMessages = new List<string>();
public override void SceneLoadLocalDone(string scene)
{
// randomize a position
var spawnPosition = new Vector3(Random.Range(-8, 8), 0, Random.Range(-8, 8));
// instantiate cube
BoltNetwork.Instantiate(BoltPrefabs.Cube, spawnPosition, Quaternion.identity);
}
public override void OnEvent(LogEvent evnt)
{
logMessages.Insert(0, evnt.Message);
}
}
The last thing we need to do is to just display our log, we are going to use the standard unity OnGUI
method for this.
C#
void OnGUI()
{
// only display max the 5 latest log messages
int maxMessages = Mathf.Min(5, logMessages.Count);
GUILayout.BeginArea(new Rect(Screen.width / 2 - 200, Screen.height - 100, 400, 100), GUI.skin.box);
for (int i = 0; i < maxMessages; ++i)
{
GUILayout.Label(logMessages[i]);
}
GUILayout.EndArea();
}
We're rendering a box in the bottom center of our screen that is 400 x 100 pixels. And then we just print the max 5 latest messages that arrived in the log. The entire NetworkCallbacks
script now looks like this.
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;
[BoltGlobalBehaviour]
public class NetworkCallbacks : GlobalEventListener
{
List<string> logMessages = new List<string>();
void OnGUI()
{
// only display max the 5 latest log messages
int maxMessages = Mathf.Min(5, logMessages.Count);
GUILayout.BeginArea(new Rect(Screen.width / 2 - 200, Screen.height - 100, 400, 100), GUI.skin.box);
for (int i = 0; i < maxMessages; ++i)
{
GUILayout.Label(logMessages[i]);
}
GUILayout.EndArea();
}
public override void SceneLoadLocalDone(string scene)
{
// randomize a position
var spawnPosition = new Vector3(Random.Range(-8, 8), 0, Random.Range(-8, 8));
// instantiate cube
BoltNetwork.Instantiate(BoltPrefabs.Cube, spawnPosition, Quaternion.identity);
}
public override void OnEvent(LogEvent evnt)
{
logMessages.Insert(0, evnt.Message);
}
}
And if we run our game and connect a couple of clients, it should look like this:
We're going to implement one more event just to show how Entity events
differ from the Global events
we just sent.
Create a new event in the Bolt Assets
window and call it FlashColorEvent
. Set Global Senders
to None
and Entity Senders
to Only Owner
. Create a new property and call it FlashColor
, set the type of it to Color
.
We are going to use this event to allow you to flash your own cube in a red color to signal that we are taking some (pretend) damage.
Important: Remember to compile Bolt after you created your event.
Open up our CubeBehaviour
script in Tutorial/Scripts
, we are going to change the baseclass from Bolt.EntityBehaviour<ICubeState>
to Bolt.EntityEventListener<ICubeState>
, like this:
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;
public class CubeBehaviour : Bolt.EntityEventListener<ICubeState>
{
// ...
}
The Bolt.EntityEventListener<T>
class actually inherits from Bolt.EntityBehaviour<T>
so all the same methods are available on it as Bolt.EntityBehaviour<T>
.
At the end of the SimulateOwner
method we are going to add a couple of new lines that lets us send our FlashColorEvent
when we press the F
key.
C#
// ...
if (Input.GetKeyDown(KeyCode.F))
{
var flash = FlashColorEvent.Create(entity);
flash.FlashColor = Color.red;
flash.Send();
}
// ...
The way you send an entity event is almost identical to a global event, except for one detail: You pass the entity you want to send the event on into the Create
method.
We're going to add a few more things to our CubeBehaviour
class: first we need to keep track of when to stop flashing the color from the FlashColorEvent
, to do so we will add a variable called resetColorTime
.
We will also add a variable called renderer
and use it to save a reference to the GameObject's Renderer component in the Attached
method. This way we don't have to call GetComponent<Renderer>
over and over again.
C#
// ...
private float _resetColorTime;
private Renderer _renderer;
public override void Attached()
{
_renderer = GetComponent<Renderer>();
// ...
}
// ...
When we compiled Bolt with our new FlashColorEvent
, a new method was created on the Bolt.EntityEventListener<T>
class that is called OnEvent(FlashColorEvent evnt)
, we are going to implement this in our CubeBehaviour
.
C#
public override void OnEvent(FlashColorEvent evnt)
{
_resetColorTime = Time.time + 0.2f;
_renderer.material.color = evnt.FlashColor;
}
Here we set the reset-time of the flash to be 0.2 seconds into the future, and we change our material color to the color we received in the event. The last thing we need to do is to just implement the standard Unity Update
method to reset our color when the time has passed.
C#
void Update()
{
if (_resetColorTime < Time.time)
{
_renderer.material.color = state.CubeColor;
}
}
The entire CubeBehaviour
script now looks like this.
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;
public class CubeBehaviour : Bolt.EntityEventListener<ICubeState>
{
private float _resetColorTime;
private Renderer _renderer;
public override void Attached()
{
_renderer = GetComponent<Renderer>();
state.SetTransforms(state.CubeTransform, transform);
if (entity.IsOwner)
{
state.CubeColor = new Color(Random.value, Random.value, Random.value);
}
state.AddCallback("CubeColor", ColorChanged);
}
public override void SimulateOwner()
{
var speed = 4f;
var movement = Vector3.zero;
if (Input.GetKey(KeyCode.W)) { movement.z += 1; }
if (Input.GetKey(KeyCode.S)) { movement.z -= 1; }
if (Input.GetKey(KeyCode.A)) { movement.x -= 1; }
if (Input.GetKey(KeyCode.D)) { movement.x += 1; }
if (movement != Vector3.zero)
{
transform.position = transform.position + (movement.normalized * speed * BoltNetwork.FrameDeltaTime);
}
if (Input.GetKeyDown(KeyCode.F))
{
var flash = FlashColorEvent.Create(entity);
flash.FlashColor = Color.red;
flash.Send();
}
}
public override void OnEvent(FlashColorEvent evnt)
{
_resetColorTime = Time.time + 0.2f;
_renderer.material.color = evnt.FlashColor;
}
void Update()
{
if (_resetColorTime < Time.time)
{
_renderer.material.color = state.CubeColor;
}
}
void OnGUI()
{
if (entity.IsOwner)
{
GUI.color = state.CubeColor;
GUILayout.Label("@@@");
GUI.color = Color.white;
}
}
void ColorChanged()
{
GetComponent<Renderer>().material.color = state.CubeColor;
}
}
And if we play our game and hit F
on any of the instances the cube that is controlled by that instance will breefly flash red on all screens.
Why two types of events?
This is a question which comes up a lot, why are there two types of events in Bolt? Why not just one like how RPC's work in PUN or Unity Networking?
Global events can be both Reliable and Unreliable by passing in different parameters to the Create
method.
In general most global events will be Reliable and that is also the default if you don't pass in anything to change it.
Global events are intended for things that exist around the game like dealing with authentication or handling stuff like player inventories, etc.
Entity events are always Unreliable, they are intended for small one-off effects like showing a damage indicator or maybe playing an explosion, things that are ephemeral and if one player misses it it doesn't matter.
Continue in next chapter >>.
Back to top