mcpField
V
vvvv-channels

vvvv-channels

github.com/tebjan开发3492 次安装2 天前更新
代码仓库

关于 vvvv-channels

面向 vvvv gamma C# 开发者的 Channels 集成指南,介绍具名、类型化的响应式值容器及其层级传播,涵盖公开、读取、订阅和写入模式。

主要功能

  1. 01按路径访问公开 Channel
  2. 02使用属性发布 .NET 类型
  3. 03延迟绑定与重试查找
  4. 04响应式订阅和事件触发
  5. 05层级与 Spread 子 Channel 传播

适用场景

  1. 01从 C# 节点读取或写入应用状态
  2. 02将模型属性公开为 Channel
  3. 03订阅值变化并响应事件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tebjan/vvvv-skills --skill vvvv-channels

安装后在 Claude Code 中输入/vvvv-channels确认已加载。

附带文件examples.md

详细介绍

vvvv gamma Channels — C# 集成

什么是 Channels

Channels 是命名的、类型化的、可观察的值容器——vvvv gamma 中的核心响应式数据流机制。任何代码(patch、C# 节点、外部绑定)都可以通过字符串路径读取和写入 channels。

主要特性:

  • 每个 channel 都有一个路径(字符串)、一个类型和一个当前值
  • 设置值会触发所有订阅者(响应式推送)
  • vvvv 提供 MIDI、OSC、Redis 和 UI 的内置 channel 绑定
  • Channels 会在会话之间持久保存状态

Public Channels 和 IChannelHub

Public channels 是注册在应用级 channel hub 中的 channels——任何代码都可以通过字符串路径查找来访问它们。

核心 API(VL.Core.Reactive)
using VL.Core.Reactive;

// Get the app-wide channel hub (singleton)
var hub = IChannelHub.HubForApp;

// Safe lookup — returns null if channel doesn't exist yet
IChannel<object>? ch = hub.TryGetChannel("MyApp.Settings.Volume");

// Read the current value
object? value = ch.Object;

// Write a new value (fires all subscribers)
ch.Object = newValue;

重要:切勿使用 hub.TryAddChannel()——它会创建值为 null 的 channels,导致 vvvv 的 SubChannelsBinding.EnsureMutatingPropertiesAreReflectedInChannels 中出现 NullReferenceException。SubChannel 系统会尝试遍历该空值的属性并崩溃。始终使用 TryGetChannel(仅查找)。

[CanBePublished] 特性

要让 vvvv 将 .NET 类型属性公开为 public channels,必须使用来自 VL.Core.EditorAttributes 的 [CanBePublished(true)] 装饰该类型。

using VL.Core.EditorAttributes;

// All public properties become channels when this type is published
[CanBePublished(true)]
public class MyModel
{
    // Standard .NET types work directly — float, bool, string, Vector3, etc.
    public float Volume { get; set; } = 0.5f;
    public bool Muted { get; set; } = false;
    public string Label { get; set; } = "Default";

    // Hidden from the channel system entirely
    [CanBePublished(false)]
    public string InternalId { get; } = Guid.NewGuid().ToString();
}

规则:

  • 在类/结构体上使用 [CanBePublished(true)] → 所有属性都会作为 channels 发布
  • 在单个属性上使用 [CanBePublished(false)] → 在 channel 系统中隐藏该属性
  • 默认情况下不会发布 .NET 类型——必须使用该特性
  • 从 VL.Core 版本 2025.7.1-0163+ 开始可用

Channel 路径约定

Channels 使用以点分隔的层级路径。Spread 元素使用方括号表示法:

Root.Page.Zone.Group.Parameter          — leaf parameter
Root.Page.Zone                          — hierarchy node (model object)
Root.Page.Items[0].PropertyName         — spread element sub-channel
Root.Page.Items[2].DeleteInstance        — indexed bang channel

当发布带有 [CanBePublished(true)] 的类型时,vvvv 的 SubChannel 系统会自动创建子 channels。无需手动创建。

使用 const string 路径常量以避免拼写错误:

public static class ChannelPaths
{
    public const string Volume = "Settings.Audio.Volume";
    public const string Brightness = "App.Scene.Display.Brightness";
}

重试绑定模式

节点启动时,channels 可能尚不存在——vvvv 会在模型初始化后发布它们。必须每帧重试,直到 channel 出现:

[ProcessNode]
public class MyChannelReader : IDisposable
{
    private IChannel<object>? _channel;

    public void Update(out float value)
    {
        // Retry until channel exists
        if (_channel == null)
        {
            var hub = IChannelHub.HubForApp;
            if (hub != null)
                _channel = hub.TryGetChannel("Settings.Audio.Volume");
        }

        // Read value (with safe cast)
        value = _channel?.Object is float f ? f : 0f;
    }

    public void Dispose() { _channel = null; }
}

绑定后,缓存的引用在节点的生命周期内都有效——无需重新查找。

PublicChannelHelper — 可复用工具类

一个封装了重试绑定和可选订阅模式的辅助类。可在任何项目中复用:

public class PublicChannelHelper : IDisposable
{
    private IChannel<object>? _channel;
    private IDisposable? _subscription;
    private readonly Action<object?>? _onNext;

    public PublicChannelHelper(Action<object?>? onNext = null)
    {
        _onNext = onNext;
    }

    public IChannel<object>? Channel => _channel;
    public bool IsBound => _channel != null;

    public bool TryBind(IChannelHub hub, string path)
    {
        if (_channel != null) return true;

        var ch = hub.TryGetChannel(path);
        if (ch == null) return false;

        _channel = ch;
        if (_onNext != null)
            _subscription = ch.Subscribe(new CallbackObserver(_onNext));
        return true;
    }

    public void Dispose()
    {
        _subscription?.Dispose();
        _subscription = null;
        _channel = null;
    }

    private sealed class CallbackObserver : IObserver<object?>
    {
        private readonly Action<object?> _onNext;
        public CallbackObserver(Action<object?> onNext) => _onNext = onNext;
        public void OnNext(object? value) => _onNext(value);
        public void OnError(Exception error) { }
        public void OnCompleted() { }
    }
}

在 ProcessNode 中的用法:

[ProcessNode]
public class VolumeReader : IDisposable
{
    private readonly PublicChannelHelper _ch = new();

    public void Update(out float value, out IChannel<object>? channel)
    {
        if (!_ch.IsBound)
        {
            var hub = IChannelHub.HubForApp;
            if (hub != null) _ch.TryBind(hub, "Settings.Audio.Volume");
        }
        value = _ch.Channel?.Object is float f ? f : 0f;
        channel = _ch.Channel;
    }

    public void Dispose() => _ch.Dispose();
}

Channel 访问器节点模式

用于封装 channel 访问的 ProcessNode 有三种模式:

层级节点(读取模型对象)
[ProcessNode]
public class Camera : IDisposable
{
    private readonly PublicChannelHelper _ch = new();

    public void Update(out CameraSettings? value, out IChannel<object>? channel)
    {
        if (!_ch.IsBound)
        {
            var hub = IChannelHub.HubForApp;
            if (hub != null) _ch.TryBind(hub, "App.Scene.Camera");
        }
        value = _ch.Channel?.Object as CameraSettings;
        channel = _ch.Channel;
    }

    public void Dispose() => _ch.Dispose();
}
叶参数节点(读取类型化值)

与层级节点的模式相同,但输出类型是 float、bool 或 string 等标准叶值。

索引 Spread 元素节点(接受 int index)
[ProcessNode]
public class SceneItemAccessor : IDisposable
{
    private PublicChannelHelper _ch = new();
    private int _boundIndex = -1;

    public void Update(out SceneItem? value, out IChannel<object>? channel, int index = 0)
    {
        // Rebind when index changes
        if (index != _boundIndex)
        {
            _ch.Dispose();
            _ch = new();
            _boundIndex = index;
        }
        if (!_ch.IsBound)
        {
            var hub = IChannelHub.HubForApp;
            if (hub != null) _ch.TryBind(hub, $"App.Effects[{index}]");
        }
        value = _ch.Channel?.Object as SceneItem;
        channel = _ch.Channel;
    }

    public void Dispose() => _ch.Dispose();
}

响应式订阅

使用标准的 IObservable 模式订阅 channel 值变化:

IChannel<object>? ch = hub.TryGetChannel("Settings.Audio.Volume");
if (ch != null)
{
    IDisposable subscription = ch.Subscribe(new CallbackObserver(value =>
    {
        // Called whenever the channel value changes
        if (value is float f)
            ApplyVolume(f);
    }));

    // ALWAYS dispose when done (in node's Dispose method)
}

Bang Channels

对于触发器/事件属性(删除、插入、移动操作),使用 System.Reactive.Unit——不要使用 float:

using System.Reactive;

[CanBePublished(true)]
public class MyInstance
{
    public Unit DeleteInstance { get; set; }     // Bang channel
    public Unit InsertAfterInstance { get; set; } // Bang channel
}

事件本身就是 bang——值无关紧要。订阅并在回调中执行操作:

var ch = hub.TryGetChannel("Project.Items[0].DeleteInstance");
ch?.Subscribe(new BangObserver(() =>
{
    // Triggered when the bang fires — queue the action
    _pendingDeletions.Add(0);
}));

在 vvvv 的节点浏览器中,Unit channels 显示为 “Unit” 类型,而不是 “Float”。

层级数据传播

vvvv 的 channel 系统会自动在层级结构中传播更改:

  • 向根记录写入其 channel → 所有子 channels 自动更新
  • 向叶 channel 写入 → 父 channels 自动更新
  • 这是 vvvv 的 SubChannel 系统内置的功能——无需手动传播

这支持高效的批量操作:

// Save: read the root channel → serialize the entire hierarchy
var model = rootChannel.Object as AppModel;
string json = JsonSerializer.Serialize(model);

// Load: deserialize → write to root channel → ALL children update
var loaded = JsonSerializer.Deserialize<AppModel>(json);
rootChannel.Object = loaded;  // Every sub-channel updates automatically

Spread 通道和子通道

发布 Spread<T> 属性时(其中 T 带有 [CanBePublished(true)]),vvvv 会自动为每个元素创建子通道:

Project.Items           → Spread<ItemModel>
Project.Items[0]        → ItemModel (auto-created)
Project.Items[0].Name   → string (auto-created)
Project.Items[1].Name   → string (auto-created)

设置 Spread 通道会自动传播到所有子通道。 无需单独更新各个子通道。

修改 Spread(添加/移除/重新排序元素)时:

// Build new spread
var newSpread = modifiedList.ToArray().AsSpreadUnsafe();

// Set on spread channel — sub-channels update automatically
spreadChannel.Object = newSpread;

警告:Spread.AsSpreadUnsafe(array) 会在不复制的情况下包装数组——包装后不得修改该数组。

防止反馈循环:向自己也订阅的通道写入时,请使用抑制标志:

_suppressCallback = true;
_channel.Object = newValue;
_suppressCallback = false;

// In the subscription callback:
void OnChanged(object? value)
{
    if (_suppressCallback) return;
    // Process the change...
}

关键规则

  1. 绝不要使用 TryAddChannel——只能使用 TryGetChannel(仅查找)
  2. 始终重试绑定——通道会在模型初始化后出现,而不是立即出现
  3. .NET 类型必须带有 [CanBePublished(true)],才能发布通道
  4. 始终释放订阅——在节点的 Dispose() 方法中执行
  5. bang 使用 System.Reactive.Unit——不要使用 float 或 bool
  6. Spread.AsSpreadUnsafe——包装后不得修改数组
  7. 抑制标志——向已订阅的通道写入时,用于防止反馈循环

代码示例请参阅 examples.md。

持续积累这些知识

让下一个 patcher 少遇到一个谜题。首次使用本集合中的任何技能时,如果过去七天内没有成功检查记录,请将源内容与 tebjan/vvvv-skills 进行比对。如果没有记录,请检查一次。在这些技能之间共享一条本地检查时间戳/修订版本;不要在每次调用时都轮询。保留本地扩展和固定的项目版本;检查不代表获准安装更新。如果处于离线状态,请记录延期检查,并继续任务。

当实际工作暴露出缺漏时,请记录经过测试的 vvvv/包版本、最小复现示例或权威来源,以及观察到的结果。将其他用户的报告视为线索,未经验证前不要当作事实。改进最相关的最小范围说明;不要把一个示例变成普遍规则。提交上游之前,请删除私人数据并取得发布授权。请参阅贡献政策。合并后的改进会通过安装器/更新工作流送达用户,而不是通过本地编辑或自动覆盖每个人的技能。

评论

支持 Markdown