vvvv-node-libraries について
vvvv gamma のノードライブラリを作成・配布する開発者向けスキルです。パッケージ構成や命名、インポート属性、カテゴリ分け、ノードブラウザーへの表示制御を扱います。
主な機能
- 01ライブラリ構成と命名規則
- 02ノードインポート属性の選択
- 03カテゴリと表示の制御
- 04アセンブリ初期化とサービス登録
活用シーン
- 01vvvv gamma のノードライブラリ作成
- 02ノードのカテゴリ整理
- 03補助型をノードブラウザーから除外
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tebjan/ vvvv-skills --skill vvvv-node-librariesインストール後、Claude Code で/vvvv-node-librariesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
contributing.mddesign-guidelines.mdexamples.mdpublishing.md詳細
vvvv gamma ノードライブラリの作成
ノードライブラリとは、複数のノードを配布可能なパッケージとして vvvv gamma に提供するプロジェクトです。このスキルでは、プロジェクトレベルの関心事を扱います。ディレクトリ構造、命名規則、カテゴリ構成、サービス登録、ノードファクトリなどです。
個々のノードクラス(ProcessNode、Update、ピン、変更検出)の記述については、vvvv-custom-nodes を参照してください。ノードのコンストラクタ内でサービス(IFrameClock、Game、ロギング)を利用する方法については、vvvv-custom-nodes/services.md を参照してください。
独自のライブラリを作成することと、自分が所有していないライブラリに貢献することは、別のタスクです。この SKILL.md とその設計/公開に関するリファレンスは、パッケージの作成と配布を扱っています。既存の/上流のライブラリを変更したり PR を送ったりする場合(fork → branch → PR のワークフロー、編集可能なソースパッケージ、.vl の差分問題)は、contributing.md を参照してください。
ライブラリ認識パターン
vvvv は、フォルダ名、.vl ファイル、.nuspec がすべて同じ名前を共有している場合に、そのディレクトリをライブラリとして認識します。
VL.MyLibrary/ # Folder name = package name
├── VL.MyLibrary.vl # .vl document — MUST match folder name
├── VL.MyLibrary.nuspec # NuGet spec — MUST match folder name
├── lib/
│ └── net8.0/ # Compiled DLLs go here
│ └── VL.MyLibrary.dll
├── src/
│ ├── Initialization.cs # [assembly:] attributes + AssemblyInitializer
│ ├── Nodes/
│ │ ├── MyProcessNode.cs # [ProcessNode] classes
│ │ └── MyOperations.cs # Static methods (stateless nodes)
│ ├── Services/
│ │ └── MyService.cs # Per-app singletons
│ └── VL.MyLibrary.csproj
├── shaders/ # Optional: SDSL shaders (auto-discovered)
│ └── MyEffect_TextureFX.sdsl
└── help/ # Optional: .vl help patches
└── HowTo Use MyNode.vl
重要な規約:
- フォルダ名、
.vlファイル、.nuspecは同一でなければなりません(例: すべてVL.MyLibrary) .csprojは、パッケージルートからの相対パスでlib/net8.0/に DLL を出力する必要があります- パッケージ内の
.vlファイルは.csprojを参照してはいけません — 参照するとパッケージが編集可能モードになります - vvvv がライブラリを見つけられるように、ライブラリディレクトリは設定済みの package-repository ディレクトリ内に置かれている必要があります
.csproj の出力パス
.csproj は、ライブラリの lib/net8.0/ フォルダにコンパイルする必要があります。
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputPath>..\..\lib\net8.0\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
何がノードとしてインポートされるか — 基本ルール
型が vvvv のノードブラウザでノードになるのは、2 つの条件が両方とも真である場合です。
- その型が
publicである(かつ、インポートされたアセンブリ内に存在する)。 - その型の C# 名前空間が
[assembly: ImportAsIs]/[assembly: ImportNamespace]宣言でカバーされている、またはその型が[assembly: ImportType]宣言で列挙されている。
どちらかの条件が偽であれば、その型は vvvv から見えません。インポートは型のアクセシビリティだけによるのではなく、名前空間単位でオプトインです。 誰もインポートしていない名前空間にある public クラスは、internal クラスとまったく同じようにノードブラウザから隠されます。
型がインポートされると、vvvv はその完全なパブリックサーフェスからノードを生成します。
- パブリックなクラスと構造体 → コンストラクタ + パブリックなメソッド/プロパティがノードになります
- パブリックな静的メソッド → 操作ノードになります
- パブリックな列挙型 → 分割 + 値がノードになります
- パブリックなレコードとインターフェイス → クラスと同様に扱われます
[ProcessNode] はノードの可視性を制御しません。 これは純粋にライフサイクルのための糖衣構文であり、vvvv に対して「これは Update() メソッドを持つステートフルなクラスであり、ノードごとに 1 つのインスタンスを管理し、毎フレーム Update() を呼び出す」と伝えるものです。インポートされた名前空間にある単なる public class Foo { public Foo() {} public int Bar(int x) => x; } は、[ProcessNode] で装飾されたものとまったく同じようにノードブラウザのエントリになります。この属性が影響するのは、ノードがどのように呼び出されるかであり、表示されるかどうかではありません。
ライブラリ設計への示唆: 「ノードブラウザに何が表示されるか」を決める主要なレバーは、どの名前空間をインポートするかであり、どの型が public であるかではありません。ただし、インポートは再帰的であることに注意してください。1 つのルート名前空間を宣言すると、その下に入れ子になったすべての名前空間が取り込まれるため、.Internal サブ名前空間は隠れ場所にはなりません。実際に機能する 4 つのレバーについては、ノードブラウザからヘルパーを除外するを参照してください。
出典: VL.StandardLibs ImportAsIsAttribute、Gray Book — Writing nodes using C#。
Initialization.cs — エントリポイント
すべてのノードライブラリにはアセンブリレベルの属性が必要です。1 つのファイルにまとめます。
using VL.Core;
using VL.Core.CompilerServices;
using VL.Core.Import;
// Required: tells vvvv to scan this assembly for nodes
[assembly: ImportAsIs(Namespace = "MyCompany.MyLibrary", Category = "MyLibrary")]
// Optional: register services before any node runs
[assembly: AssemblyInitializer(typeof(MyCompany.MyLibrary.Initialization))]
namespace MyCompany.MyLibrary;
public sealed class Initialization : AssemblyInitializer<Initialization>
{
public override void Configure(AppHost appHost)
{
var services = appHost.Services;
// Register per-app singletons (created lazily on first access)
services.RegisterService<MyService>(serviceProvider =>
{
return new MyService(serviceProvider);
});
}
}
適切なインポート属性の選択
vvvv は、何がノードになるかを宣言するための 3 つのアセンブリレベル属性を提供します。必要な制御の度合いに応じて選びます。
[assembly: ImportAsIs] — 単一、名前空間ルート
[assembly: ImportAsIs(Namespace = "VL.MyLib", Category = "MyLib")]
| プロパティ | 動作 |
|---|---|
AllowMultiple | false — アセンブリごとに最大 1 つ |
| スコープ | Namespace 内(およびその子)のすべてのパブリック型 |
| カテゴリ | Category パラメーターがルートとなり、Namespace 配下のサブ名前空間がそれを拡張します |
どちらのパラメーターも省略可能で、4 つの組み合わせはすべて有効であり、それぞれ異なる動作をします:
[assembly: ImportAsIs] // the scaffolded default:
// nothing stripped, no root →
// VL.MyLib.Particles ⇒ "VL.MyLib.Particles"
[assembly: ImportAsIs(Namespace = "VL.MyLib")] // strip only →
// VL.MyLib.Particles ⇒ "Particles" (top level!)
[assembly: ImportAsIs(Category = "MyLib")] // root only →
// VL.MyLib.Particles ⇒ "MyLib.VL.MyLib.Particles"
[assembly: ImportAsIs(Namespace = "VL.MyLib", Category = "MyLib")] // ✅ strip + root →
// VL.MyLib.Particles ⇒ "MyLib.Particles"
最後の形式がほぼ常に必要なものです。ライブラリ全体が 1 つのルート名前空間の下にあり、1 つのルートカテゴリにしたい場合は ImportAsIs を使用します。サブ名前空間を異なるカテゴリに分割するために 2 つの ImportAsIs を重ねることはできません — AllowMultiple = false だからです。
[assembly: ImportNamespace] — 名前空間ごと、複数使用
// ⚠️ Order matters — first declaration wins. Specific before general.
[assembly: ImportNamespace("VL.MyLib.Renderers", Category = "MyLib.Rendering")]
[assembly: ImportNamespace("VL.MyLib.Resources", Category = "MyLib.Resources")]
[assembly: ImportNamespace("VL.MyLib.Experimental", Category = "MyLib.Experimental")]
[assembly: ImportNamespace("VL.MyLib", Category = "MyLib")] // catch-all, LAST
| プロパティ | 動作 |
|---|---|
AllowMultiple | true — 必要なだけ宣言できます |
| スコープ | その名前空間およびその下にネストされたすべての名前空間(再帰的)内のパブリック型 |
| 解決 | 最初の宣言が優先され、最長プレフィックスではありません — 具体的なものを一般的なものより先に宣言してください |
1 つのライブラリに複数のサブ名前空間があり、それぞれをブラウザーに C# のフォルダー名を散らかすことなく別々のカテゴリに配置したい場合に使用します。これはマルチカテゴリのライブラリに適したツールです。
[assembly: ImportType] — 型ごと、手動選択
[assembly: ImportType(typeof(MyRenderer), Category = "MyLib.Rendering")]
[assembly: ImportType(typeof(MyResource), Category = "MyLib.Resources", Name = "Resource")]
| プロパティ | 動作 |
|---|---|
AllowMultiple | true — 必要なだけ宣言できます |
| スコープ | 列挙された型のみ — アセンブリから他に自動インポートされるものはありません |
| 併用対象 | クローズドリストのライブラリでは単独で(ImportAsIs/ImportNamespace なし)、または名前空間属性と併用して特定の型のカテゴリ/名前を上書き |
外科的な制御が必要な場合に使用します — たとえば、大規模な内部コードベースのうち厳選されたサブセットのみを公開したい場合や、1 つの外れ値をその名前空間の兄弟とは異なるカテゴリに強制したい場合などです。
決定マトリックス
| ライブラリの形状 | 推奨される属性 |
|---|---|
| 1 つの名前空間、1 つのカテゴリ、すべてのパブリック型が意図的 | [assembly: ImportAsIs(Namespace, Category)] |
| 1 つのライブラリに複数の異なるサブカテゴリ | サブ名前空間ごとに 1 つの [assembly: ImportNamespace] |
| 厳選されたノードのセット、公開したくないパブリックヘルパーが多数 | ノードごとに [assembly: ImportType]、ImportAsIs なし |
| ほとんどが自動インポート、いくつかの外れ値 | [assembly: ImportAsIs] + [assembly: ImportType] による上書き |
🛑 インポートは再帰的です — ネストされた名前空間は隠れ場所にはなりません
これはライブラリのノードブラウザーが汚染される最も一般的な原因であり、意図が明らかに正しいと読めるため、この間違いは犯しやすいものです。
AssemblySymbolSource.cs の ImportFromNamespace は、完全名が宣言された名前空間と等しいノードを見つけるまで名前空間ツリーをたどり、そのノードに対して ImportAll を呼び出します — そして ImportAll はその地点より下のすべてのネストされた名前空間とネストされた型を再帰的に処理します。
[assembly: ImportNamespace("VL.MyLib", Category = "MyLib")]
namespace VL.MyLib; // → category "MyLib"
namespace VL.MyLib.Internal; // → category "MyLib.Internal" ← STILL IMPORTED
namespace VL.MyLib.Internal.Gpu; // → category "MyLib.Internal.Gpu" ← STILL IMPORTED
ヘルパーをサブ名前空間に置いても何も隠せません。 それは単にそれらをサブカテゴリに移動するだけで、ユーザーは NodeBrowser の検索ボックスに入力することでそれらを見つけられます — それらの型のすべてのパブリックメンバーと同様に。
兄弟名前空間(VL.MyLibInternals)は ImportNamespace("VL.MyLib") によって本当にインポートされません…ただし IsMatch は境界チェックなしの ns.StartsWith(Namespace) であるため、カテゴリを計算するときに ImportNamespace("VL.Foo") は依然として VL.FooBar に誤マッチします。兄弟名前空間による分割は脆弱です。代わりに以下の 4 つの実際のレバーのいずれかを使用してください。
ノードブラウザーからヘルパーを除外する — 4 つのレバー
他に誰がその型を必要とするかによって選んでください。
1. internal — 型がそもそも VL に一切入らない
ImportAll は最初の行で処理を中止します:
if (s.DeclaredAccessibility != Accessibility.Public)
return;
internal 型にはノードもカテゴリもメンバーノードも IOBox もシリアライズの表面もありません。vvvv について考えるべきものが何も残りません。これは最も強力で最も低コストな手段です — まずこれを検討してください。
テストやサンプルのアセンブリがその型を必要とする場合は、internal のままにして [assembly: InternalsVisibleTo("MyLib.Tests")] を追加します。VL は見えないままですし、C# からは引き続き参照できます。
2. [Smell(...)] — C# では public、ブラウザーではフィルタリング
VL.Core.Import.SmellAttribute(AttributeTargets.All)は、まさに型単位・メンバー単位のオプトアウトです。型は public のままでインポートもされ続けます — ピン型としても使え、リンクを通じて流れ、クロスアセンブリで参照もできます — ただ NodeBrowser には表示されません。
using VL.Core; // SymbolSmell
using VL.Core.Import; // SmellAttribute
[ProcessNode]
[Smell(SymbolSmell.Internal)]
public sealed class SkiaRendererNode : IDisposable { /* ... */ }
これは実際の VL.StandardLibs のコードです(VL.Skia/src/SkiaRendererNode.cs)。フラグの全リストについては、以下の C# からのアスペクト を参照してください。
⚠️
SmellAttributeはVL.Core.Importからインポートしてください。同名のVL.Core.CompilerServices.SmellAttributeは[Obsolete(error: true)]であり、バイナリ互換性のためだけに存在します — これを使うとコンパイルエラーになります。
3. カテゴリ内のアスペクトキーワード — グループ全体を 1 行で隠す
Internal、Hidden、Advanced、Experimental、Obsolete、Adaptive のいずれかの名前を持つカテゴリセグメントは、そのアスペクトをカテゴリ内のすべてに適用し、その後、解決されたカテゴリ名から取り除かれます。Smell はカテゴリ内のすべてのシンボルに継承される(ImportedSymbol.GetSmell() は ParentCategory.Smell から開始します)ため、1 つの宣言で任意の数の型をカバーできます:
[assembly: ImportNamespace("VL.MyLib.Plumbing", Category = "MyLib.Internal")]
// every public type under VL.MyLib.Plumbing → category "MyLib", smell Internal
サブ名前空間の末尾は Category に追加され、その後でキーワードとして解析されるため、単純な C# 名前空間のセグメントも同様に機能します — 属性は不要です:
[assembly: ImportNamespace("VL.MyLib", Category = "MyLib")]
namespace VL.MyLib.Particles; // → "MyLib.Particles", normal
namespace VL.MyLib.Particles.Advanced; // → "MyLib.Particles", Advanced aspect
namespace VL.MyLib.Internal; // → "MyLib", Internal aspect
これにより、C# のフォルダー構成と vvvv のカテゴリツリーが同じ成果物になり、大規模なライブラリにとって最も保守しやすい構成になります。
4. ImportType のみ — 閉じた許可リスト
ImportAsIs/ImportNamespace を完全にスキップし、ユーザーに表示する各型を列挙します。リストにないものは、名前空間やアクセシビリティに関係なく非表示のままです。
[assembly: ImportType(typeof(Renderer), Category = "MyLib")]
[assembly: ImportType(typeof(Settings), Category = "MyLib")]
// Nothing else is imported.
大規模な内部コードベース上に小さな厳選された表面を提供する場合に最適です。ノードごとに 1 行のコストがかかります。
判断の近道
| 状況 | 手段 |
|---|---|
| このアセンブリの外部で必要とするものがない | internal (1) |
| テスト / サンプルは必要とするが、ユーザーには決して見せてはいけない | internal + [InternalsVisibleTo] (1) |
| 正当なピン型またはクロスアセンブリ API だが、閲覧可能にしたくない | [Smell(SymbolSmell.Internal)] (2) |
| 配管コードの名前空間全体 | カテゴリ / 名前空間内のアスペクトキーワード (3) |
| 大きなコードベース、小さなノード表面 | ImportType のみ (4) |
カテゴリ解決のルール — vvvv のソースから検証済み
優先順位(高い順):
- クラス自体の
[ProcessNode(Category = "X")]/[Category("X")]— これが最優先で適用されます。 [assembly: ImportType(typeof(T), Category = "X", NamespacePrefixToStrip = "...")]— 型単位のオーバーライド。[assembly: ImportNamespace("X.Sub", Category = "Y")]—X.Sub以下の型に対して、この属性がそれらをインポートしたものである場合(以下の先勝ちを参照)。[assembly: ImportAsIs(Namespace = "X", Category = "Y")]— ライブラリ全体に対する単一のデフォルト。これはすべてのImportNamespaceより前に実行されるため、X以下のすべてを最初に確保します。- どのインポート属性も型に一致しなかった場合 — 生の C# 名前空間がそのままカテゴリとして使用され、
SymbolSmell.Externalが付与されます。この経路ではアスペクトキーワードは解析されません。
参考として、ライブラリ全体に対する 2 つの形式を示します:
// Explicit — the recommended default. Strips "VL.MyLib", roots everything under "MyLib".
[assembly: ImportAsIs(Namespace = "VL.MyLib", Category = "MyLib")]
// Bare — what vvvv's own project template scaffolds. Nothing stripped, no category root:
// a class in VL.MyLib.Particles lands in the top-level category "VL.MyLib.Particles".
[assembly: ImportAsIs]
🛑 最初に一致したものが優先、最長プレフィックスではありません
DirectImportSymbolSource は固定の順序でインポートし、すでにインポートされたリストを除外セットとして渡します。そのため、ある型を最初に要求した属性がそのカテゴリを所有し、それ以降の属性はその型に対して暗黙的に何も行いません:
if (importAsIsAttribute != null)
ImportFromNamespace(builder, …, importAsIsAttribute.Namespace, importAttribute: importAsIsAttribute);
foreach (var a in importNamespaceAttributes) // ← declaration order
ImportFromNamespace(builder, …, a.Namespace, alreadyImported: builder, a);
foreach (var a in importTypeAttributes)
ImportAll(builder, …, alreadyImported: builder, a);
// ImportAll, first two lines:
if (s.DeclaredAccessibility != Accessibility.Public) return;
if (alreadyImported != null && alreadyImported.Any(c => …Equals(c.Symbol, s))) return;
順序は次のとおりです: ImportAsIs → ImportNamespace(宣言順) → ImportType。
したがって、最も具体的な ImportNamespace を最初に宣言してください。 広いルート宣言を狭いものより上に置くと、すべてを飲み込み、狭い行はデッドコードになります:
// ❌ BROKEN — the root line imports everything recursively; lines 2-3 never fire.
[assembly: ImportNamespace("VL.MyLib", Category = "MyLib")]
[assembly: ImportNamespace("VL.MyLib.Plumbing", Category = "MyLib.Internal")] // dead
[assembly: ImportNamespace("VL.MyLib.Config", Category = "MyLib.Settings")] // dead
// ✅ CORRECT — specific first, catch-all last.
[assembly: ImportNamespace("VL.MyLib.Plumbing", Category = "MyLib.Internal")]
[assembly: ImportNamespace("VL.MyLib.Config", Category = "MyLib.Settings")]
[assembly: ImportNamespace("VL.MyLib", Category = "MyLib")]
この失敗は暗黙的です — 警告もエラーもありません。症状は、リマップしたと思っていたサブ名前空間の名前がノードブラウザーのフォルダーとして表示されることです。ある型が実際にどこに配置されたかを確認するには、それをパッチにドロップして保存し、.vl XML から LastCategoryFullName を読み取ってください。
レベル 3 と 4(ImportAsIs / ImportNamespace)では、結果のカテゴリは VL.Core/src/Import/ImportAsIsAttribute.cs の GetCategory(typeNamespace) によって計算されます:
// Pseudocode of the actual VL.Core implementation:
root = Category ?? "";
if (typeNamespace == "") return root;
if (Namespace == "") cat = typeNamespace;
else if (typeNamespace.Length > Namespace.Length)
cat = typeNamespace.Substring(Namespace.Length + 1);
else /* typeNamespace == Namespace */ cat = "";
if (cat == "") return root;
if (root == "") return cat;
return $"{root}.{cat}";
これが実際に意味すること — 直感的ではない結果:
[assembly: ImportAsIs(...)] | C# 名前空間 | vvvv カテゴリ | 驚き? |
|---|---|---|---|
Namespace = "VL.MyLib", Category = "MyLib" | VL.MyLib | MyLib | いいえ |
Namespace = "VL.MyLib", Category = "MyLib" | VL.MyLib.Particles | MyLib.Particles | いいえ |
Namespace = "VL.MyLib" (Category なし) | VL.MyLib | ""(ルート) | はい — 空/ルート |
Namespace = "VL.MyLib" (Category なし) | VL.MyLib.Particles | Particles | はい — トップレベル |
Namespace = "VL.MyLib" (Category なし) | VL.MyLib.Internal.Helpers | Internal.Helpers | はい — トップレベルに「Internal」が漏れる! |
Category= がない場合、プレフィックスは単に取り除かれるだけです — Namespace の最後のセグメントを先頭に付けるフォールバックはありません。 そのため、[ImportAsIs(Namespace = "VL.MyLib")] で VL.MyLib.Config 内のクラスは、MyLib.Config ではなくトップレベルの Config に配置されます。これが最もよくある驚きです — サブ名前空間からトップレベルへの汚染を本当に望んでいない限り、常に Category を明示的に設定してください。
「なぜ自分のノードが MyLib.Helpers ではなくトップレベルの Helpers になってしまったのか?」をデバッグするときは、次を確認してください: ImportAsIs の Category = パラメーターを忘れていませんか?
C# からのアスペクト — [Smell] とカテゴリキーワード
アスペクトは NodeBrowser の可視性とノードの状態を制御します。Gray Book ではパッチされたシンボルについてこれらを説明していますが、C# から同じことを適用する方法は 2 つあります。
フラグ
VL.Core.SymbolSmell は [Flags] 列挙型です:
| フラグ | 値 | 効果 |
|---|---|---|
Default | 0 | 通常 |
Obsolete | 1 | 非推奨; 後方互換性のために保持 |
Experimental | 2 | 不安定 / 作業中 |
Advanced | 4 | ユーザーが Advanced フィルターを有効にするまで NodeBrowser から隠されます |
Hidden | 8 | NodeBrowser で決して提供されません |
Internal | 16 | 定義しているドキュメント / ライブラリ内でのみ利用可能 |
Adaptive | 512 | ノードを適応型ディスパッチに登録します |
Advanced はライブラリ作者が最もよく使うものです: Gray Book の根拠は、「どのライブラリ開発者も、そのライブラリのすべてのユーザーの 90% が使うべき超クールなノードと型をいくつか提供する」 というもので、それ以外はすべて削除されるのではなく Advanced になります。
方法 1 — シンボルに [Smell(SymbolSmell.X)] を付ける
using VL.Core;
using VL.Core.Import;
[Smell(SymbolSmell.Experimental)]
public sealed class FormBoundsNotification { /* ... */ }
型、メソッド、プロパティに適用されます — AttributeUsage(AttributeTargets.All)。
型に付けた [Smell] はそのメンバーにカスケードしません。シンボルごとに設定するか、カテゴリ(方法 2)を使ってグループ全体をカバーしてください。
方法 2 — カテゴリ / 名前空間セグメントとしてのキーワード
Symbols.ToSmell は、大文字と小文字を区別して、正確にこれらのセグメント名を認識します:
| セグメント | アスペクト |
|---|---|
Internal | SymbolSmell.Internal |
Advanced | SymbolSmell.Advanced |
Experimental | SymbolSmell.Experimental |
Obsolete | SymbolSmell.Obsolete |
Hidden | SymbolSmell.Hidden |
Adaptive | SymbolSmell.Adaptive |
一致したセグメントは解決されたカテゴリから削除されるため、MyLib.Particles.Advanced と
MyLib.Advanced.Particles はどちらも Advanced アスペクトを持つカテゴリ MyLib.Particles
に解決されます。Smell は継承されます: ImportedSymbol.GetSmell() は ParentCategory.Smell
から開始し、その上にシンボル自身の [Smell] を OR で重ねます — つまり 1 つのキーワードで
そのカテゴリ内のすべての型をカバーできます。
⚠️ キーワード解析は属性で指定されたカテゴリに対してのみ行われます。
GetCategoryAndSmellForFullName(name, isAssemblyCategory) では、キーワードループは
else 分岐の内側にあります — isAssemblyCategory: true の場合 (どの import 属性も
一致しなかったときに使われる生の名前空間フォールバック)、名前はそのまま取得され、代わりに
SymbolSmell.External が付与されます。実際には: .Advanced 名前空間セグメントが機能するのは、
ImportAsIs / ImportNamespace がそのカテゴリを生成したからです。一度もインポートされて
いない型に対しては何も行いません。
方法 3 — ノードのバージョンセグメント内のキーワード
NameVersionAndSmell.Split はノード名のバージョン部分を解析するため、これも機能します:
[ProcessNode(Name = "GetBytes (Advanced)")]
アスペクトが行わないこと
Advanced / Hidden / Internal のシンボルは依然として完全に機能します。引き続き
インポートされ、カテゴリも持ち、ピン型としても機能し、既存のリンク、IOBox、保存された
パッチは引き続き解決されます。アスペクトがフィルタリングするのはNodeBrowser の一覧表示
のみです。そのため Advanced は、ユーザーがピンを介して設定するものの、ブラウザー内で
探し回る必要のない enum や settings 型にとって正しいツールです。
vvvv が自動的に適用する 2 つのアスペクト
HasStateOutputのない[ProcessNode]は型を非表示にします。ImportedSymbol.GetSmell()は、ProcessNodeAttributeが存在しHasStateOutput == falseの場合に、型にSymbolSmell.Hiddenを追加するため、プロセスノードのみが表示されます。HasStateOutput = trueに設定すると、型の非表示が解除され、かつすべてのパブリック メンバーが個別のノードとして表示されます — これは、見落とされがちなブラウザーの 煩雑化の実際の原因です。- Enum 演算子は自動的に
Advancedになります。 インポートされた enum に対して生成される=,!=,|,&,^の各演算には自動的にSymbolSmell.Advancedが付与されます (devvvvs/vvvv#3885) ので、これらを自分で非表示にする必要はありません。
サービス登録パターン
サービスは Configure(AppHost) で登録され、ノードから NodeContext を介して利用されます。
このセクションでは登録のみを扱います — 利用パターンについては、vvvv-custom-nodes/services.md
を参照してください。
直接シングルトン (推奨)
services.RegisterService<MyService>(serviceProvider =>
{
// Created lazily on first GetService<MyService>() call
return new MyService(serviceProvider);
});
IResourceProvider パターン (管理されたライフサイクル向け)
サービスが明示的な破棄を必要とするリソースをラップする場合:
services.RegisterService<IResourceProvider<MyGPUService>>(serviceProvider =>
{
var gameProvider = serviceProvider.GetService<IResourceProvider<Game>>();
return gameProvider.Bind(game =>
{
var service = MyGPUService.Create(game);
return ResourceProvider.Return(service, disposeAction: s => s?.Dispose());
});
});
動的ノードファクトリ
動的なノードセットに対してプログラムによるノード生成を登録します:
public override void Configure(AppHost appHost)
{
// Dynamic node factory from shader files or other sources
appHost.RegisterNodeFactory("VL.MyLibrary.ShaderNodes",
init: MyShaderNodeFactory.Init);
}
ノードが C# クラスとして記述されるのではなく、外部ファイル (シェーダー、設定) から生成される 場合は、ノードファクトリを使用します。詳細については、vvvv Node Factories ドキュメント を参照してください。
サービスアクセス用の拡張メソッド
サービス用の型付きアクセサーを提供します:
public static class MyLibraryExtensions
{
public static MyService? GetMyService(this ServiceRegistry services)
=> services.GetService(typeof(MyService)) as MyService;
public static MyService? GetMyService(this IServiceProvider services)
=> services.GetService(typeof(MyService)) as MyService;
}
.csproj の要点
lib/net8.0/ に出力する完全なライブラリ .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<OutputPath>..\..\lib\net8.0\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="VL.Core" Version="2025.7.*" />
<PackageReference Include="VL.Core.Import" Version="2025.7.*" />
<PackageReference Include="VL.Stride.Runtime" Version="2025.7.*" />
</ItemGroup>
</Project>
VL パッケージのバージョンは、お使いの vvvv インストールのバージョンに合わせてください。
OutputPath は、コンパイルされた DLL を、vvvv がそれらを見つけることを期待するライブラリの
lib/net8.0/ フォルダーに配置します。
実例: カスタムレンダリングライブラリ
サービス登録とノードファクトリを使用したライブラリの初期化:
[assembly: AssemblyInitializer(typeof(Initialization))]
[assembly: ImportAsIs(Namespace = "VL.MyRendering", Category = "MyRendering")]
public sealed class Initialization : AssemblyInitializer<Initialization>
{
public override void Configure(AppHost appHost)
{
appHost.Services.RegisterService<CustomGameSystem>(sp =>
{
var vlGame = sp.GetService<VLGame>();
if (vlGame == null) return null!;
var customGame = CustomGameSystem.Create(vlGame, sp);
vlGame.GameSystems.Add(customGame);
return customGame;
});
// Dynamic node factory from shader files
appHost.RegisterNodeFactory("VL.MyRendering.ShaderNodes",
init: ShaderNodeFactory.Init);
}
}
命名規則、ピンルール、アスペクト、標準型については、design-guidelines.md を参照してください。 NuGet の公開、ヘルプパッチ、ライブラリ構造については、publishing.md を参照してください。 既存のアップストリームライブラリへの変更/PR のコントリビュートについては、contributing.md を参照してください。 完全な実例 (VL.IO.MQTT、VL.Audio) については、examples.md を参照してください。
この知識を育て続ける
次のパッチャーに残す謎を1つ減らしましょう。このコレクション内のいずれかのスキルを初めて使用してから7日以内に成功したチェックがない場合は、ソースを tebjan/vvvv-skills と比較してください。記録が存在しない場合は、一度だけ確認してください。 これらのスキル全体で1つのローカルチェックのタイムスタンプ/リビジョンを共有し、呼び出しごとにポーリングしないでください。 ローカルの拡張機能と固定されたプロジェクトバージョンは保持してください。確認することは、更新をインストールする許可ではありません。オフラインの場合は、延期されたチェックを記録し、タスクを続行してください。
実際の作業でギャップが明らかになった場合は、テスト済みの vvvv/パッケージバージョン、最小限の再現または信頼できる情報源、および観察された結果を記録してください。他のユーザーの報告は、検証されるまでは事実ではなく手がかりとして扱ってください。最も小さく関連する指示を改善してください。1つの例を普遍的なルールにしてはいけません。プライベートデータを伏せ、アップストリームに送信する前に公開の承認を得てください。 コントリビューションポリシーを参照してください。 マージされた改善は、ローカルでの編集や全員のスキルの自動上書きではなく、各ユーザーのインストーラー/アップデートワークフローを通じてユーザーに届きます。