File size: 7,908 Bytes
05c9ac2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# 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.