| # Custom Side Channels | |
| You can create your own side channel in C# and Python and use it to communicate | |
| custom data structures between the two. This can be useful for situations in | |
| which the data to be sent is too complex or structured for the built-in | |
| `EnvironmentParameters`, or is not related to any specific agent, and therefore | |
| inappropriate as an agent observation. | |
| ## Overview | |
| In order to use a side channel, it must be implemented as both Unity and Python | |
| classes. | |
| ### Unity side | |
| The side channel will have to implement the `SideChannel` abstract class and the | |
| following method. | |
| - `OnMessageReceived(IncomingMessage msg)` : You must implement this method and | |
| read the data from IncomingMessage. The data must be read in the order that it | |
| was written. | |
| The side channel must also assign a `ChannelId` property in the constructor. The | |
| `ChannelId` is a Guid (or UUID in Python) used to uniquely identify a side | |
| channel. This Guid must be the same on C# and Python. There can only be one side | |
| channel of a certain id during communication. | |
| To send data from C# to Python, create an `OutgoingMessage` instance, add data | |
| to it, call the `base.QueueMessageToSend(msg)` method inside the side channel, | |
| and call the `OutgoingMessage.Dispose()` method. | |
| To register a side channel on the Unity side, call | |
| `SideChannelManager.RegisterSideChannel` with the side channel as only argument. | |
| ### Python side | |
| The side channel will have to implement the `SideChannel` abstract class. You | |
| must implement : | |
| - `on_message_received(self, msg: "IncomingMessage") -> None` : You must | |
| implement this method and read the data from IncomingMessage. The data must be | |
| read in the order that it was written. | |
| The side channel must also assign a `channel_id` property in the constructor. | |
| The `channel_id` is a UUID (referred in C# as Guid) used to uniquely identify a | |
| side channel. This number must be the same on C# and Python. There can only be | |
| one side channel of a certain id during communication. | |
| To assign the `channel_id` call the abstract class constructor with the | |
| appropriate `channel_id` as follows: | |
| ```python | |
| super().__init__(my_channel_id) | |
| ``` | |
| To send a byte array from Python to C#, create an `OutgoingMessage` instance, | |
| add data to it, and call the `super().queue_message_to_send(msg)` method inside | |
| the side channel. | |
| To register a side channel on the Python side, pass the side channel as argument | |
| when creating the `UnityEnvironment` object. One of the arguments of the | |
| constructor (`side_channels`) is a list of side channels. | |
| ## Example implementation | |
| Below is a simple implementation of a side channel that will exchange ASCII | |
| encoded strings between a Unity environment and Python. | |
| ### Example Unity C# code | |
| The first step is to create the `StringLogSideChannel` class within the Unity | |
| project. Here is an implementation of a `StringLogSideChannel` that will listen | |
| for messages from python and print them to the Unity debug log, as well as send | |
| error messages from Unity to python. | |
| ```csharp | |
| using UnityEngine; | |
| using Unity.MLAgents; | |
| using Unity.MLAgents.SideChannels; | |
| using System.Text; | |
| using System; | |
| public class StringLogSideChannel : SideChannel | |
| { | |
| public StringLogSideChannel() | |
| { | |
| ChannelId = new Guid("621f0a70-4f87-11ea-a6bf-784f4387d1f7"); | |
| } | |
| protected override void OnMessageReceived(IncomingMessage msg) | |
| { | |
| var receivedString = msg.ReadString(); | |
| Debug.Log("From Python : " + receivedString); | |
| } | |
| public void SendDebugStatementToPython(string logString, string stackTrace, LogType type) | |
| { | |
| if (type == LogType.Error) | |
| { | |
| var stringToSend = type.ToString() + ": " + logString + "\n" + stackTrace; | |
| using (var msgOut = new OutgoingMessage()) | |
| { | |
| msgOut.WriteString(stringToSend); | |
| QueueMessageToSend(msgOut); | |
| } | |
| } | |
| } | |
| } | |
| ``` | |
| Once we have defined our custom side channel class, we need to ensure that it is | |
| instantiated and registered. This can typically be done wherever the logic of | |
| the side channel makes sense to be associated, for example on a MonoBehaviour | |
| object that might need to access data from the side channel. Here we show a | |
| simple MonoBehaviour object which instantiates and registers the new side | |
| channel. If you have not done it already, make sure that the MonoBehaviour which | |
| registers the side channel is attached to a GameObject which will be live in | |
| your Unity scene. | |
| ```csharp | |
| using UnityEngine; | |
| using Unity.MLAgents; | |
| public class RegisterStringLogSideChannel : MonoBehaviour | |
| { | |
| StringLogSideChannel stringChannel; | |
| public void Awake() | |
| { | |
| // We create the Side Channel | |
| stringChannel = new StringLogSideChannel(); | |
| // When a Debug.Log message is created, we send it to the stringChannel | |
| Application.logMessageReceived += stringChannel.SendDebugStatementToPython; | |
| // The channel must be registered with the SideChannelManager class | |
| SideChannelManager.RegisterSideChannel(stringChannel); | |
| } | |
| public void OnDestroy() | |
| { | |
| // De-register the Debug.Log callback | |
| Application.logMessageReceived -= stringChannel.SendDebugStatementToPython; | |
| if (Academy.IsInitialized){ | |
| SideChannelManager.UnregisterSideChannel(stringChannel); | |
| } | |
| } | |
| public void Update() | |
| { | |
| // Optional : If the space bar is pressed, raise an error ! | |
| if (Input.GetKeyDown(KeyCode.Space)) | |
| { | |
| Debug.LogError("This is a fake error. Space bar was pressed in Unity."); | |
| } | |
| } | |
| } | |
| ``` | |
| ### Example Python code | |
| Now that we have created the necessary Unity C# classes, we can create their | |
| Python counterparts. | |
| ```python | |
| from mlagents_envs.environment import UnityEnvironment | |
| from mlagents_envs.side_channel.side_channel import ( | |
| SideChannel, | |
| IncomingMessage, | |
| OutgoingMessage, | |
| ) | |
| import numpy as np | |
| import uuid | |
| # Create the StringLogChannel class | |
| class StringLogChannel(SideChannel): | |
| def __init__(self) -> None: | |
| super().__init__(uuid.UUID("621f0a70-4f87-11ea-a6bf-784f4387d1f7")) | |
| def on_message_received(self, msg: IncomingMessage) -> None: | |
| """ | |
| Note: We must implement this method of the SideChannel interface to | |
| receive messages from Unity | |
| """ | |
| # We simply read a string from the message and print it. | |
| print(msg.read_string()) | |
| def send_string(self, data: str) -> None: | |
| # Add the string to an OutgoingMessage | |
| msg = OutgoingMessage() | |
| msg.write_string(data) | |
| # We call this method to queue the data we want to send | |
| super().queue_message_to_send(msg) | |
| ``` | |
| We can then instantiate the new side channel, launch a `UnityEnvironment` with | |
| that side channel active, and send a series of messages to the Unity environment | |
| from Python using it. | |
| ```python | |
| # Create the channel | |
| string_log = StringLogChannel() | |
| # We start the communication with the Unity Editor and pass the string_log side channel as input | |
| env = UnityEnvironment(side_channels=[string_log]) | |
| env.reset() | |
| string_log.send_string("The environment was reset") | |
| group_name = list(env.behavior_specs.keys())[0] # Get the first group_name | |
| group_spec = env.behavior_specs[group_name] | |
| for i in range(1000): | |
| decision_steps, terminal_steps = env.get_steps(group_name) | |
| # We send data to Unity : A string with the number of Agent at each | |
| string_log.send_string( | |
| f"Step {i} occurred with {len(decision_steps)} deciding agents and " | |
| f"{len(terminal_steps)} terminal agents" | |
| ) | |
| env.step() # Move the simulation forward | |
| env.close() | |
| ``` | |
| Now, if you run this script and press `Play` the Unity Editor when prompted, the | |
| console in the Unity Editor will display a message at every Python step. | |
| Additionally, if you press the Space Bar in the Unity Engine, a message will | |
| appear in the terminal. | |