vvvv-channels について
vvvv gamma の C# 開発者向け Channels 統合ガイドです。名前と型を持つリアクティブな値、その階層伝播、公開、検索、購読、書き込みを説明します。
主な機能
- 01パスによるパブリック Channel の参照
- 02プロパティによる .NET 型の公開
- 03Channel 登場時の再バインド
- 04リアクティブ購読とイベントトリガー
- 05階層および Spread サブ Channel の伝播
活用シーン
- 01C# ノードからアプリ状態を読み書き
- 02モデルのプロパティを Channel として公開
- 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 における中心的リアクティブデータフローメカニズムです。あらゆるコード(パッチ、C# ノード、外部バインディング)が、その文字列パスによって Channels を読み書きできます。
主なプロパティ:
- 各チャンネルにはパス(文字列)、型、現在の値があります
- 値を設定するとすべての購読者に通知されます(リアクティブプッシュ)
- vvvv は MIDI、OSC、Redis、UI 用の組み込みチャンネルバインディングを提供します
- Channels はセッションをまたいで状態を保持します
パブリックチャンネルと IChannelHub
パブリックチャンネルは、アプリ全体のチャンネルハブに登録されたチャンネルです — 任意のコードから文字列パス検索でアクセスできます。
コア 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 値のチャンネルを作成し、vvvv の SubChannelsBinding.EnsureMutatingPropertiesAreReflectedInChannels で NullReferenceException を引き起こします。SubChannel システムが null 値のプロパティを走査しようとしてクラッシュします。常に TryGetChannel(検索のみ)を使用してください。
[CanBePublished] 属性
vvvv が .NET 型のプロパティをパブリックチャンネルとして公開するには、その型を 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)]→ すべてのプロパティがチャンネルとして公開されます - 個々のプロパティに
[CanBePublished(false)]→ チャンネルシステムから隠されます - .NET 型はデフォルトでは公開されません — 属性が必要です
VL.Coreバージョン2025.7.1-0163以降で利用可能です
チャンネルパスの規約
チャンネルはドット区切りの階層パスを使用します。スプレッド要素はブラケット記法を使用します:
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 システムによって自動的に作成されます。手動で作成する必要はありません。
タイプミスを避けるために const string パス定数を使用してください:
public static class ChannelPaths
{
public const string Volume = "Settings.Audio.Volume";
public const string Brightness = "App.Scene.Display.Brightness";
}
リトライバインドパターン
ノードの起動時にチャンネルが存在しない場合があります — vvvv はモデルの初期化後にそれらを公開します。チャンネルが現れるまで毎フレームリトライする必要があります:
[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();
}
チャンネルアクセサーノードのパターン
チャンネルアクセスをラップする ProcessNode の 3 つのパターン:
階層ノード(モデルオブジェクトを読み取る)
[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 などの標準的なリーフ値です。
インデックス付きスプレッド要素ノード(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 パターンを使用してチャンネル値の変更を購読します:
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 チャンネル
トリガー/イベントプロパティ(削除、挿入、移動操作)には、float ではなく System.Reactive.Unit を使用してください:
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);
}));
Unit チャンネルは vvvv のノードブラウザーで「Float」ではなく「Unit」型として表示されます。
階層データ伝播
vvvv のチャンネルシステムは、階層を通じて変更を自動的に伝播します:
- ルートレコードをそのチャンネルに書き込む → すべての子チャンネルが自動更新されます
- リーフチャンネルに書き込む → 親チャンネルが自動更新されます
- これは 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<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)
スプレッドチャネルを設定すると、すべてのサブチャネルに自動的に伝播します。 個々のサブチャネルを更新する必要はありません。
スプレッドを変更する場合(要素の追加・削除・並べ替え):
// 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...
}
重要なルール
TryAddChannelは絶対に使用しない —TryGetChannel(検索のみ)だけを使用します- 常に再試行バインドする — チャネルはモデルの初期化後に現れるため、すぐには利用できません
- チャネルのパブリッシュには .NET 型に
[CanBePublished(true)]が必須 - サブスクリプションは常に破棄する — ノードの
Dispose()メソッド内で行います - bang には
System.Reactive.Unitを使用する —floatやboolではなく Spread.AsSpreadUnsafe— ラップ後に配列を変更してはなりません- 抑制フラグ — 購読しているチャネルに書き込む際のフィードバックループを防ぎます
コード例については、examples.md を参照してください。
この知識を育て続ける
次のパッチャーに謎を一つ減らして残しましょう。このコレクション内のいずれかのスキルを初めて使用する際、7 日以内に成功したチェックがない場合は、ソースを tebjan/vvvv-skills と比較してください。記録が存在しない場合は、一度だけ確認します。 これらのスキル間で、ローカルのチェックのタイムスタンプ/リビジョンを 1 つ共有してください。呼び出しごとにポーリングしないでください。 ローカルの拡張機能と固定されたプロジェクトバージョンは保持してください。確認することは、更新をインストールする許可ではありません。オフラインの場合は、延期したチェックを記録し、タスクを続行してください。
実際の作業でギャップが見つかった場合は、テストした vvvv/パッケージのバージョン、最小限の再現または信頼できる情報源、および観察された結果を記録してください。他のユーザーの報告は、検証されるまでは事実ではなく手がかりとして扱ってください。最も関連性の高い最小限の指示を改善してください。1 つの例を普遍的なルールにしてはなりません。プライベートデータを伏せ、アップストリームに送信する前に公開の許可を取得してください。 コントリビューション ポリシーを参照してください。 マージされた改善は、ローカルの編集や全員のスキルの自動上書きではなく、ユーザーのインストーラー/更新ワークフローを通じて届きます。