|
| 1 | +--- |
| 2 | +title: OSC |
| 3 | +description: 在 Basis 脚本中发布和接收 OSC 数据 |
| 4 | +icon: Activity |
| 5 | +--- |
| 6 | + |
| 7 | +import { Callout } from 'fumadocs-ui/components/callout'; |
| 8 | +import { Card, Cards } from 'fumadocs-ui/components/card'; |
| 9 | + |
| 10 | +## 概览 |
| 11 | + |
| 12 | +Basis 为需要接收 OSC 消息、订阅实时地址范围,或将数值重新发布回 OSC bridge 运行时的创作者脚本提供了 `BasisOsc` shim。 |
| 13 | + |
| 14 | +该组件会感知作用域。同一个相对地址的解析结果可能不同,取决于对象运行在本地 avatar、远程 avatar、prop 还是场景下。 |
| 15 | + |
| 16 | +<Callout type="info"> |
| 17 | +在 prop、avatar 和场景中的创作者 OSC 工作流里使用 `BasisOsc`。bridge 会将活动订阅和已发布的值反映到 OSC Query,而数值型已发布值也可以更新 Vixxy avatar 菜单变量。 |
| 18 | +</Callout> |
| 19 | + |
| 20 | +## 设置 |
| 21 | + |
| 22 | +1. 将 `BasisOsc` 组件添加到与脚本相同的 GameObject 上,或者添加到你可以引用的附近对象上。调用 `GetComponent<BasisOsc>()` 时,Cilbox 可以自动添加这个 shim。 |
| 23 | +2. 将该对象放在一个受支持的 Basis GameObject 下,并带有以下组件,从而处于其作用域内: |
| 24 | + - `Basis.Scripts.BasisSdk.BasisAvatar` |
| 25 | + - `Basis.Scripts.BasisSdk.BasisProp` |
| 26 | + - `Basis.Scripts.BasisSdk.BasisScene` |
| 27 | +3. 在订阅或发布之前,先通过 `GetComponent<BasisOsc>()` 读取组件。 |
| 28 | +4. 当对象被禁用或销毁时,移除订阅。 |
| 29 | + |
| 30 | +在 Unity 编辑器中,`BasisOsc` 检视器会在运行时显示已解析的作用域、发布前缀、默认订阅前缀、`ReceiveAll` 状态,以及当前的精确订阅和前缀订阅注册情况。 |
| 31 | + |
| 32 | +```csharp title="获取 BasisOsc 引用" |
| 33 | +using Basis.Shims; |
| 34 | +using UnityEngine; |
| 35 | + |
| 36 | +[Cilboxable] |
| 37 | +public class OscConsumer : MonoBehaviour |
| 38 | +{ |
| 39 | + private BasisOsc osc; |
| 40 | + |
| 41 | + private void Start() |
| 42 | + { |
| 43 | + osc = GetComponent<BasisOsc>(); |
| 44 | + if (osc == null) |
| 45 | + { |
| 46 | + Debug.LogError("OscConsumer requires a BasisOsc component."); |
| 47 | + } |
| 48 | + } |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +<Callout type="warn"> |
| 53 | +如果无法解析 `BasisAvatar`、`BasisProp` 或 `BasisScene` 作用域,相对订阅会默认回退到 `/avatar/parameters/...`。只有当组件处于受支持的作用域下时,发布才可用。 |
| 54 | +</Callout> |
| 55 | + |
| 56 | +## 订阅 OSC |
| 57 | + |
| 58 | +当你只想要一个地址时,使用精确订阅。 |
| 59 | + |
| 60 | +```csharp title="精确订阅" |
| 61 | +osc.Subscribe("/avatar/parameters/TestToggle", OnToggleMessage); |
| 62 | + |
| 63 | +private void OnToggleMessage(OscMessage message, OscData[] arguments) |
| 64 | +{ |
| 65 | + Debug.Log($"Received {message.Path} with {arguments.Length} value(s)."); |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +允许使用相对地址。在本地 avatar 上,`"Face/Smile"` 会解析为 `/avatar/parameters/Face/Smile`。 |
| 70 | + |
| 71 | +```csharp title="相对精确订阅" |
| 72 | +osc.Subscribe("Face/Smile", OnSmileChanged); |
| 73 | +``` |
| 74 | + |
| 75 | +如果你需要知道最终注册的规范化地址,请使用带 `out` 变量的重载。 |
| 76 | + |
| 77 | +```csharp title="获取精确订阅的解析地址" |
| 78 | +osc.Subscribe("Face/Smile", OnSmileChanged, out string resolvedAddress); |
| 79 | +Debug.Log($"Subscribed to {resolvedAddress}"); |
| 80 | +``` |
| 81 | + |
| 82 | +如果你只想让订阅存在于本地实例上,请使用 `localOnly` 重载。在远程 avatar 上不会注册任何内容。 |
| 83 | + |
| 84 | +```csharp title="仅在本地实例上订阅" |
| 85 | +osc.Subscribe("Face/Smile", OnSmileChanged, localOnly: true, out string resolvedAddress); |
| 86 | +if (resolvedAddress != null) |
| 87 | +{ |
| 88 | + Debug.Log($"Local-only subscription resolved to {resolvedAddress}"); |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +当你想要所有匹配的子地址时,使用前缀订阅。 |
| 93 | + |
| 94 | +```csharp title="前缀订阅" |
| 95 | +osc.SubscribePrefix("FT/", OnFaceTrackingMessage); |
| 96 | + |
| 97 | +private void OnFaceTrackingMessage(OscMessage message, OscData[] arguments) |
| 98 | +{ |
| 99 | + Debug.Log($"Matched prefix for {message.Path}"); |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +```csharp title="获取解析后的前缀地址" |
| 104 | +osc.SubscribePrefix("FT/", OnFaceTrackingMessage, out string resolvedPrefix); |
| 105 | +Debug.Log($"Watching prefix {resolvedPrefix}"); |
| 106 | +``` |
| 107 | + |
| 108 | +前缀匹配使用路径段边界。像 `/avatar/parameters` 这样的前缀会匹配 `/avatar/parameters/Face/Smile`,但不会匹配 `/avatar/parametersExtra`。 |
| 109 | + |
| 110 | +### 值回调 |
| 111 | + |
| 112 | +当你只关心第一个参数时,使用值回调。 |
| 113 | + |
| 114 | +```csharp title="精确值回调" |
| 115 | +osc.SubscribeValue("Speed", value => |
| 116 | +{ |
| 117 | + if (value.Kind == OscDataKind.Float32) |
| 118 | + { |
| 119 | + Debug.Log($"Speed is now {value.FloatValue}"); |
| 120 | + } |
| 121 | +}); |
| 122 | +``` |
| 123 | + |
| 124 | +```csharp title="仅本地的值回调" |
| 125 | +osc.SubscribeValue("Speed", value => |
| 126 | +{ |
| 127 | + Debug.Log($"Only the local instance will receive this callback: {value.Kind}"); |
| 128 | +}, localOnly: true, out string resolvedAddress); |
| 129 | +``` |
| 130 | + |
| 131 | +```csharp title="前缀值回调" |
| 132 | +osc.SubscribePrefixValue("/avatar/parameters/Hands/", value => |
| 133 | +{ |
| 134 | + Debug.Log($"First value kind: {value.Kind}"); |
| 135 | +}); |
| 136 | +``` |
| 137 | + |
| 138 | +`SubscribeValue` 和 `SubscribePrefixValue` 只有在消息至少包含一个参数时才会触发。 |
| 139 | + |
| 140 | +### 移除订阅 |
| 141 | + |
| 142 | +当脚本结束时,移除你添加的处理器。这样可以让运行时路由器和 OSC Query 表面与活动脚本保持同步。 |
| 143 | + |
| 144 | +```csharp title="移除精确订阅" |
| 145 | +osc.Unsubscribe("Face/Smile", OnSmileChanged); |
| 146 | +osc.UnsubscribeValue("Speed", OnSpeedChanged); |
| 147 | +``` |
| 148 | + |
| 149 | +```csharp title="移除前缀订阅" |
| 150 | +osc.UnsubscribePrefix("FT/", OnFaceTrackingMessage); |
| 151 | +osc.UnsubscribePrefixValue("FT/", OnFaceTrackingValue); |
| 152 | +``` |
| 153 | + |
| 154 | +当你想移除某个规范化地址或前缀下注册的所有回调时,请使用仅地址版本。 |
| 155 | + |
| 156 | +```csharp title="移除某个地址或前缀的所有回调" |
| 157 | +osc.Unsubscribe("Face/Smile"); |
| 158 | +osc.UnsubscribePrefix("FT/"); |
| 159 | +``` |
| 160 | + |
| 161 | +当组件需要清除自己拥有的所有精确和前缀订阅时,使用 `ClearSubscriptions()`。 |
| 162 | + |
| 163 | +### 接收所有消息 |
| 164 | + |
| 165 | +当你希望组件观察其允许范围内的每一条路由 OSC 消息时,设置 `ReceiveAll`。 |
| 166 | + |
| 167 | +```csharp title="接收所有消息" |
| 168 | +osc.ReceiveAll = true; |
| 169 | +osc.OnMessage += (message, arguments) => |
| 170 | +{ |
| 171 | + Debug.Log($"Saw {message.Path}"); |
| 172 | +}; |
| 173 | +``` |
| 174 | + |
| 175 | +`ReceiveAll` 故意受到安全限制。它不会绕过正常的作用域边界。 |
| 176 | + |
| 177 | +- 本地 avatar 的 `BasisOsc` 只会看到 `/avatar/parameters/*` |
| 178 | +- 远程 avatar 的 `BasisOsc` 只会看到 `/avatar/public/*` |
| 179 | +- prop 和场景的 `BasisOsc` 看到的默认接收作用域与相对订阅相同,即 `/avatar/public/*` |
| 180 | + |
| 181 | +这意味着 `ReceiveAll` 更像一个带作用域的通配符,而不是整个进程范围的 OSC tap。 |
| 182 | + |
| 183 | +## 发布 OSC |
| 184 | + |
| 185 | +单个参数使用 `PublishValue`,多个参数使用 `PublishValues`。 |
| 186 | + |
| 187 | +```csharp title="发布一个值" |
| 188 | +osc.PublishValue("Status", OscData.String("ready")); |
| 189 | +``` |
| 190 | + |
| 191 | +```csharp title="发布多个值" |
| 192 | +osc.PublishValues("Blend", new[] |
| 193 | +{ |
| 194 | + OscData.Float32(0.5f), |
| 195 | + OscData.String("armed"), |
| 196 | +}); |
| 197 | +``` |
| 198 | + |
| 199 | +如果你需要知道最终发布到的绝对地址,请使用带 `out` 变量的重载。 |
| 200 | + |
| 201 | +```csharp title="获取解析后的发布地址" |
| 202 | +osc.PublishValue("Status", OscData.String("ready"), out string resolvedAddress); |
| 203 | +Debug.Log($"Published to {resolvedAddress}"); |
| 204 | +``` |
| 205 | + |
| 206 | +```csharp title="获取多值发布的解析地址" |
| 207 | +osc.PublishValues("Blend", new[] |
| 208 | +{ |
| 209 | + OscData.Float32(0.5f), |
| 210 | + OscData.String("armed"), |
| 211 | +}, out string resolvedAddress); |
| 212 | + |
| 213 | +Debug.Log($"Published blend packet to {resolvedAddress}"); |
| 214 | +``` |
| 215 | + |
| 216 | +你可以发布多种 OSC 数据类型,包括布尔值、整数、浮点数、字符串、符号、MIDI 数据、数组和 blob。 |
| 217 | + |
| 218 | +本地 avatar 也可以直接发布到 `/avatar/public/...`,如果这正是你要公开的地址。 |
| 219 | + |
| 220 | +```csharp title="本地 avatar 发布到 avatar public" |
| 221 | +osc.PublishValue("/avatar/public/Status", OscData.String("shareable")); |
| 222 | +``` |
| 223 | + |
| 224 | +<Callout type="warn"> |
| 225 | +远程 avatar 的 `BasisOsc` 组件不能发布。不要假设远程 avatar 的发布行为与本地 avatar、prop 或场景相同。 |
| 226 | +</Callout> |
| 227 | + |
| 228 | +### Vixxy avatar 菜单支持 |
| 229 | + |
| 230 | +当 `BasisOsc` 能通过 avatar 通信或采集服务解析到变量时,发布的数值也会提交到 Vixxy 变量存储。 |
| 231 | + |
| 232 | +支持的 Vixxy 值类型如下: |
| 233 | + |
| 234 | +- `OscData.Boolean` |
| 235 | +- `OscData.Int32` |
| 236 | +- `OscData.Int64` |
| 237 | +- `OscData.Float32` |
| 238 | +- `OscData.Float64` |
| 239 | + |
| 240 | +对于 `PublishValues`,只有第一个值会提交给 Vixxy。非数值 OSC 值仍会发布到 OSC,但不会作为 Vixxy 变量提交。 |
| 241 | + |
| 242 | +Vixxy 变量地址来源于解析后的 OSC 地址。Basis 会在提交数值前,从受作用域限制的 prop 和 scene 地址中移除 `/avatar/parameters/`、`/avatar/public/`,或第一个 `/parameters/` 段。 |
| 243 | + |
| 244 | +```csharp title="发布到 OSC 并更新 Vixxy 变量" |
| 245 | +osc.PublishValue("Expression/Smile", OscData.Float32(1.0f)); |
| 246 | +``` |
| 247 | + |
| 248 | +在本地 avatar 上,这会发布到 `/avatar/parameters/Expression/Smile`,并提交 Vixxy 变量 `Expression/Smile`。 |
| 249 | + |
| 250 | +## 地址与作用域规则 |
| 251 | + |
| 252 | +`BasisOsc` 在路由前会规范化地址。这对订阅和发布都很重要。 |
| 253 | + |
| 254 | +### 本地 avatar |
| 255 | + |
| 256 | +- 相对订阅默认解析到 `/avatar/parameters/...`。 |
| 257 | +- 相对发布地址会发布到 `/avatar/parameters/...`。 |
| 258 | +- 也允许绝对 `/avatar/public/...` 发布地址。 |
| 259 | + |
| 260 | +### 远程 avatar |
| 261 | + |
| 262 | +- 相对订阅默认解析到 `/avatar/public/...`。 |
| 263 | +- 绝对 `/avatar/parameters/...` 订阅会重写为 `/avatar/public/...`。 |
| 264 | +- 不支持发布。 |
| 265 | + |
| 266 | +### Prop |
| 267 | + |
| 268 | +- 相对订阅默认解析到 `/avatar/public/...`。 |
| 269 | +- 绝对 avatar 订阅仅限于 `/avatar/public/...`。 |
| 270 | +- 相对发布地址会作用于 prop 实例: |
| 271 | + |
| 272 | +```text |
| 273 | +/prop/<prop-id>/parameters/... |
| 274 | +``` |
| 275 | + |
| 276 | +### 场景 |
| 277 | + |
| 278 | +- 相对订阅默认解析到 `/avatar/public/...`。 |
| 279 | +- 绝对 avatar 订阅仅限于 `/avatar/public/...`。 |
| 280 | +- 相对发布地址会作用于 scene 实例: |
| 281 | + |
| 282 | +```text |
| 283 | +/scene/<scene-id>/parameters/... |
| 284 | +``` |
| 285 | + |
| 286 | +<Callout type="warn"> |
| 287 | +不要假设原始路径会原样使用。如果组件具有作用域,`BasisOsc` 可能会重写相对地址,或拒绝受限的绝对 avatar 路径。 |
| 288 | +</Callout> |
| 289 | + |
| 290 | +## 地址报告变体 |
| 291 | + |
| 292 | +有些 `BasisOsc` 方法提供重载,会通过 `out string resolvedAddress` 参数返回规范化后的绝对路径。 |
| 293 | + |
| 294 | +通常最常用的变体如下: |
| 295 | + |
| 296 | +- `Subscribe(string, OscMessageEvent, out string resolvedAddress)` |
| 297 | +- `Subscribe(string, OscMessageEvent, bool localOnly)` |
| 298 | +- `Subscribe(string, OscMessageEvent, bool localOnly, out string resolvedAddress)` |
| 299 | +- `SubscribeValue(string, OscValueEvent, out string resolvedAddress)` |
| 300 | +- `SubscribeValue(string, OscValueEvent, bool localOnly)` |
| 301 | +- `SubscribeValue(string, OscValueEvent, bool localOnly, out string resolvedAddress)` |
| 302 | +- `SubscribePrefix(string, OscMessageEvent, out string resolvedAddress)` |
| 303 | +- `SubscribePrefixValue(string, OscValueEvent, out string resolvedAddress)` |
| 304 | +- `PublishValue(string, OscData, out string resolvedAddress)` |
| 305 | +- `PublishValues(string, OscData[], out string resolvedAddress)` |
| 306 | + |
| 307 | +这些重载在以下情况下很有用: |
| 308 | + |
| 309 | +- 你想记录相对地址最终解析到了哪里 |
| 310 | +- 你想保存规范化地址供后续 `unsubscribe` 使用 |
| 311 | +- 你想让脚本明确知道一次发布实际发送到了哪里 |
| 312 | + |
| 313 | +对于 `Subscribe(...)` 和 `SubscribeValue(...)`,`localOnly: true` 表示回调只会注册到本地实例。在远程 avatar 上,解析地址为 `null`,不会添加订阅。 |
| 314 | + |
| 315 | +没有被动式的 `Subscribe(...)` 或 `SubscribePrefix(...)` 重载。`BasisOsc` 中的订阅都是回调驱动的,`osc.OnMessage` 只有在某个基于回调的订阅或 `ReceiveAll` 匹配时才会触发。 |
| 316 | + |
| 317 | +## OSC 数据类型 |
| 318 | + |
| 319 | +`OscData` 封装了解码后的 OSC 参数,并通过 `BoolValue`、`IntValue`、`FloatValue`、`StringValue`、`BlobValue`、`Elements` 以及 MIDI 或颜色字段等属性暴露类型化的值。 |
| 320 | + |
| 321 | +发布时请使用这些工厂方法: |
| 322 | + |
| 323 | +- `OscData.Boolean(bool)` |
| 324 | +- `OscData.Nil()` |
| 325 | +- `OscData.Impulse()` |
| 326 | +- `OscData.Int32(int)` |
| 327 | +- `OscData.Float32(float)` |
| 328 | +- `OscData.TimeTag(uint seconds, uint nanoseconds)` |
| 329 | +- `OscData.String(string)` |
| 330 | +- `OscData.Symbol(string)` |
| 331 | +- `OscData.Blob(byte[])` |
| 332 | +- `OscData.Color(byte r, byte g, byte b, byte a)` |
| 333 | +- `OscData.Int64(long)` |
| 334 | +- `OscData.Float64(double)` |
| 335 | +- `OscData.Char(uint)` |
| 336 | +- `OscData.Midi(byte port, byte status, byte data1, byte data2)` |
| 337 | +- `OscData.ArrayValue(params OscData[] elements)` |
| 338 | + |
| 339 | +## OSC Query 行为 |
| 340 | + |
| 341 | +OSC bridge 会根据运行时的实时状态更新 OSC Query。 |
| 342 | + |
| 343 | +- 精确订阅会为订阅地址创建 query 节点。 |
| 344 | +- 前缀订阅会为订阅前缀创建 query 分支。 |
| 345 | +- 已发布的值会创建带有最新负载的 query 节点。 |
| 346 | +- 随着订阅变化,query 节点会动态创建和移除。 |
| 347 | + |
| 348 | +这意味着 OSC Query 表面不是固定静态 schema。它取决于当前活动的 `BasisOsc` 组件,以及它们此刻拥有的订阅或已发布值。 |
| 349 | + |
| 350 | +<Callout type="info"> |
| 351 | +由于接收器状态按实体 ID 集中管理,清理非常重要。请在 `OnDisable`、`OnDestroy` 中移除订阅,或者在脚本完成时调用 `ClearSubscriptions()`。 |
| 352 | +</Callout> |
| 353 | + |
| 354 | +## 示例 |
| 355 | + |
| 356 | +该分支添加了一个名为 `OSC Subscription Example` 的示例,使用 `BasisOscCilboxSubscriptionExample.cs`。它展示了显式和隐式地址订阅,并在销毁时注销处理器。 |
| 357 | + |
| 358 | +<Cards> |
| 359 | + <Card |
| 360 | + title="脚本概览" |
| 361 | + description="返回主脚本指南,了解 Cilbox 和脚本设置的更广泛背景。" |
| 362 | + href="/docs/scripting" |
| 363 | + /> |
| 364 | +</Cards> |
| 365 | + |
| 366 | +## 故障排除 |
| 367 | + |
| 368 | +- 如果相对地址的解析结果出乎意料,先检查最近的 `BasisAvatar`、`BasisProp` 或 `BasisScene` 祖先。 |
| 369 | +- 如果远程 avatar 没有收到 `/avatar/parameters/...`,请改为订阅 `/avatar/public/...`,或者使用相对路径。 |
| 370 | +- 如果值回调没有触发,请确认传入消息至少包含一个参数。 |
| 371 | +- 如果发布的数据没有出现在预期位置,请检查组件是否位于本地 avatar、prop 或 scene 的作用域下。 |
| 372 | +- 如果旧的 query 节点或监听器仍然存在,请确保脚本在关闭时解除订阅。 |
| 373 | + |
0 commit comments