Integrate ForkTrack narrative graphs into Unity games with a clean C# event API.
- Static API: Simple, clean API via
ForkTrack.LoadGraphFromJSON(),ForkTrack.CompleteNode() - C# Events: Subscribe to
OnNodeCompleted,OnVariableChanged,OnCustomEvent, etc. - Subscriptions: Pre-register for specific node/event callbacks that validate on graph load
- Variable System: NUMBER and BOOLEAN variables with SET/ADD/SUBTRACT/TOGGLE operations
- Dependency Logic: AND/OR/NOT conditions with variable-based edge conditions
- Offline-First: Fetch once, cache locally, work offline
- Progress Persistence: Save/load slots with JSON export for cloud saves
- Secure: Encrypted token storage, SSL validation in all builds
- Schema Support: v1.0.0, v1.1.0, and v1.2.0 with auto-migration
Add to your manifest.json:
{
"dependencies": {
"com.forktrack.unity": "https://github.com/forktrack/unity-package.git"
}
}Or via Window > Package Manager > Add package from git URL.
Copy the unity-package folder to your project's Packages directory.
ForkTrack uses a static singleton pattern - no MonoBehaviour or setup required:
ForkTrack (static API) → ForkTrackRuntime (singleton) → ForkTrackGraph (data)
The runtime is created automatically on first access.
using ForkTrack;
public class GameManager : MonoBehaviour
{
public TextAsset graphFile;
void Start()
{
ForkTrack.OnGraphLoaded += OnGraphLoaded;
ForkTrack.OnNodeCompleted += OnNodeCompleted;
ForkTrack.LoadGraphFromJSON(graphFile.text);
}
void OnGraphLoaded(ForkTrackGraph graph)
{
Debug.Log($"Loaded graph with {graph.NodeCount} nodes");
}
void OnNodeCompleted(ForkTrackNode node)
{
Debug.Log($"Completed: {node.objectName} - {node.actionName}");
}
}First, configure your API token via Tools → ForkTrack → API Settings.
using ForkTrack;
using ForkTrack.API;
using System.Collections;
public class GameManager : MonoBehaviour
{
IEnumerator Start()
{
// Create client using stored settings
var client = TokenAuthenticator.CreateClient();
if (client == null)
{
Debug.LogError("No API token! Configure in Tools > ForkTrack > API Settings");
yield break;
}
// List available graphs
yield return client.ListGraphs(
onSuccess: response => {
foreach (var g in response.graphs)
Debug.Log($"Found: {g.name} ({g.id})");
},
onError: Debug.LogError
);
// Fetch and load a specific graph
yield return client.FetchGraph("your-graph-id",
onSuccess: json => ForkTrack.LoadGraphFromJSON(json),
onError: Debug.LogError
);
}
}// Node transitions from Locked → Unlocked
ForkTrack.OnNodeUnlocked += (ForkTrackNode node) => { };
// Node transitions from Unlocked → Completed
ForkTrack.OnNodeCompleted += (ForkTrackNode node) => { };
// Node reset to Locked
ForkTrack.OnNodeReset += (ForkTrackNode node) => { };
// Any state change (most flexible)
ForkTrack.OnNodeStateChanged += (ForkTrackNode node, NodeState oldState, NodeState newState) => { };// Graph loaded successfully
ForkTrack.OnGraphLoaded += (ForkTrackGraph graph) => { };
// Graph failed to load
ForkTrack.OnGraphLoadError += (string error) => { };// Variable value changed
ForkTrack.OnVariableChanged += (ForkTrackVariable var, object oldVal, object newVal) => { };Custom events defined in ForkTrack nodes are fired when nodes are unlocked/completed:
ForkTrack.OnCustomEvent += (CustomEventData evt) => {
Debug.Log($"Event: {evt.propertyId}");
Debug.Log($"Value: {evt.selectedValue}"); // The randomly selected value
Debug.Log($"All values: {string.Join(", ", evt.values)}");
Debug.Log($"From node: {evt.nodeId}");
Debug.Log($"Trigger: {evt.trigger}"); // OnUnlock or OnComplete
Debug.Log($"Weight: {evt.weight}, Delay: {evt.delay}, Amount: {evt.amount}");
};ForkTrack.OnNoteFired += (ForkTrackNode node, NodeNote note) => {
Debug.Log($"Note from {node.id}: {note.text}");
};// Get by unique ID
ForkTrackNode node = ForkTrack.GetNode("node_1234567890");
// Get by Object + Action (preferred - more readable)
ForkTrackNode node = ForkTrack.GetNode("Fred", "Interact");
// Get by title
ForkTrackNode node = ForkTrack.GetNodeByTitle("Talk to Fred");
// Get all nodes for an object
List<ForkTrackNode> fredNodes = ForkTrack.GetNodesByObject("Fred");
// Get all nodes for an action
List<ForkTrackNode> interactions = ForkTrack.GetNodesByAction("Interact");
// Get nodes by state
List<ForkTrackNode> available = ForkTrack.GetNodesInState(NodeState.Unlocked);
List<ForkTrackNode> completed = ForkTrack.GetNodesInState(NodeState.Completed);
// Get all nodes
List<ForkTrackNode> all = ForkTrack.GetAllNodes();// Complete a node (Unlocked → Completed)
// This is the most common action - when a player does something
ForkTrack.CompleteNode("Fred", "Interact");
ForkTrack.CompleteNode("node_1234567890");
// Unlock a node manually (Locked → Unlocked)
// Usually handled automatically by dependency system
ForkTrack.UnlockNode("Fred", "Interact");
// Reset a node (any state → Locked)
ForkTrack.ResetNode("node_1234567890");
// Reset entire graph to initial state
ForkTrack.ResetAll();Subscriptions let you pre-register callbacks for specific nodes/events. They're validated when a graph loads and warn you if the target doesn't exist.
// Subscribe by Object + Action
ForkTrack.SubscribeNode("Fred", "Interact",
onCompleted: node => {
Debug.Log("Fred interaction complete!");
UnlockDoor();
},
onUnlocked: node => {
Debug.Log("Can now interact with Fred");
ShowPrompt("Press E to talk");
}
);
// Subscribe by ID
ForkTrack.SubscribeNode("node_1234567890",
onCompleted: node => HandleCompletion(node)
);
// Subscribe by title
ForkTrack.SubscribeNodeByTitle("Final Boss Defeated",
onCompleted: node => RollCredits()
);
// Unsubscribe
var sub = ForkTrack.SubscribeNode("Fred", "Interact", OnComplete);
ForkTrack.UnsubscribeNode(sub);// Subscribe to any event of a type
ForkTrack.SubscribeEvent("dialogue_choice",
onTriggered: evt => {
Debug.Log($"Player chose: {evt.selectedValue}");
ProcessDialogueChoice(evt.selectedValue);
}
);
// Subscribe to a specific value only
ForkTrack.SubscribeEvent("dialogue_choice",
onTriggered: evt => Debug.Log("Player was rude!"),
value: "rude_response"
);Variables are graph-level values that can be NUMBER or BOOLEAN:
// Get values
float coins = ForkTrack.GetVariable<float>("coins");
bool hasKey = ForkTrack.GetVariable<bool>("hasKey");
// Set values (fires OnVariableChanged)
ForkTrack.SetVariable("coins", 100f);
ForkTrack.SetVariable("hasKey", true);
// Get all variable definitions
List<ForkTrackVariable> vars = ForkTrack.GetAllVariables();
foreach (var v in vars)
{
Debug.Log($"{v.name} ({v.type}) = {v.defaultValue}");
}Variables can also be modified automatically by Variable Actions on nodes (SET, ADD, SUBTRACT, TOGGLE) when nodes are unlocked/completed.
// Save current progress
ForkTrack.SaveProgress("slot1");
// Load progress (returns false if no save exists)
if (ForkTrack.HasSavedProgress("slot1"))
{
ForkTrack.LoadProgress("slot1");
}
// Delete a save
ForkTrack.DeleteProgress("slot1");// Export progress as JSON string
string progressJson = ForkTrack.ExportProgress();
SaveToCloud(progressJson);
// Import progress from JSON string
string cloudData = LoadFromCloud();
ForkTrack.ImportProgress(cloudData);Only one graph can be loaded at a time. Loading a new graph replaces the current one.
// To switch between graphs, save progress first
ForkTrack.SaveProgress("chapter1_progress");
// Load new graph
ForkTrack.LoadGraphFromJSON(chapter2Json);
// Later, return to chapter 1
ForkTrack.LoadGraphFromJSON(chapter1Json);
ForkTrack.LoadProgress("chapter1_progress");using ForkTrack.API;
// Check if token is configured
bool hasToken = TokenAuthenticator.HasToken();
// Get stored token/URL
string token = TokenAuthenticator.GetToken();
string url = TokenAuthenticator.GetAPIUrl();
// Create pre-configured client
ForkTrackAPIClient client = TokenAuthenticator.CreateClient();
// Save token programmatically (for runtime configuration)
TokenAuthenticator.SaveToken("your-api-token");
TokenAuthenticator.SaveAPIUrl("https://forktrack.pixfork.com");using ForkTrack.API;
// Create client (or use TokenAuthenticator.CreateClient())
var client = new ForkTrackAPIClient("your-token", "https://forktrack.pixfork.com");
// Validate token
yield return client.ValidateToken(isValid => {
Debug.Log(isValid ? "Token valid" : "Token invalid");
});
// List all graphs
yield return client.ListGraphs(
onSuccess: response => {
foreach (var g in response.graphs)
Debug.Log($"{g.name} (id: {g.id}, v{g.version_major}.{g.version_minor})");
},
onError: error => Debug.LogError(error)
);
// Fetch a specific graph
yield return client.FetchGraph("graph-id",
onSuccess: json => ForkTrack.LoadGraphFromJSON(json),
onError: error => Debug.LogError(error)
);Access via Tools → ForkTrack → API Settings:
| Setting | Description |
|---|---|
| API URL | ForkTrack server URL (default: https://forktrack.pixfork.com) |
| API Token | Bearer token for authentication |
Create via Assets → Create → ForkTrack → Settings, place in Resources folder:
| Setting | Description |
|---|---|
| API URL | Server URL |
| Default Graph ID | Graph to load by default |
| Cache Freshness Hours | Hours before cached graphs are stale |
| Auto Save Progress | Save on every node state change |
| Auto Load Progress | Load progress when graph loads |
| Default Save Slot | Default slot name |
| Fallback Graph | TextAsset to use when offline |
| Log Level | Debug verbosity |
ForkTrackNode node = ForkTrack.GetNode("Fred", "Interact");
// Identity
node.id // Unique ID: "node_1234567890"
node.objectId // Reference to objects list
node.actionId // Reference to actions list
node.title // Optional display title
// Resolved names (looked up from objects/actions lists)
node.objectName // "Fred"
node.actionName // "Interact"
node.categoryName // "Main Quest"
node.categoryColor // "#FF5733"
node.groupName // "Chapter 1"
// State
node.IsLocked // true if Locked
node.IsUnlocked // true if Unlocked
node.IsCompleted // true if Completed
// Content
node.description // Node description text
node.categoryId // Category reference
node.groupId // Group reference
node.autoCompleteOnUnlock // Auto-complete when unlocked
node.music // Music track reference
// Collections
node.notes // List<NodeNote>
node.customEvents // List<CustomEvent>
node.todos // List<NodeTodo>
node.variableActions // List<VariableAction>
// Display
node.GetDisplayName() // "Fred - Interact: Optional Title"Import samples via Package Manager:
- Basic Usage: Loading, events, completion, variables
- Quest System: Complete quest tracking with inventory
- Unity 2021.3 LTS or later
- No external dependencies
See LICENSE.md