PUN Classic (v1)、PUN 2 和 Bolt 處於維護模式。 PUN 2 將支援 Unity 2019 至 2022,但不會添加新功能。 當然,您所有的 PUN & Bolt 專案可以用已知性能繼續運行使用。 對於任何即將開始或新的專案:請切換到 Photon Fusion 或 Quantum。

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.

Create new Event
Create new Event.

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.

Configure the new Event
Configure the new Event.

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.

Configure Event Senders
Configure Event Senders.

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 Connectedand 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:

  1. Add a new variable to your class of type List<string> and call it logMessages. Make sure you have using System.Collections.Generic; at the top of your file so you have access to the List<T> class;
  2. Inside OnEvent we need to just add the value of the evnt.Message property to the logMessages 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:

Event logs on Gameplay
Event logs on Gameplay.

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.

Create and Configure the FlashColorEvent
Create and Configure the FlashColorEvent.

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.

FlashColor Event on Gameplay
FlashColor Event on Gameplay.

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