This document is about: FUSION 2
SWITCH TO

此頁面正在修正中,可能有待更新。

Features

Topology
SHARED AUTHORITY

This Addon is for Fusion 2.1.x. It does not support Fusion 2.0.x.

This page is the Fusion Plugin SDK feature reference.

See Setup for installation and a first-run walkthrough.

Code and Data Sharing

Your Unity project and the plugin execute in different environments — the plugin has no direct access to your code, prefabs, or scenes, and no concept of UnityEngine.

The tools below bridge that gap: code is exported into the plugin's compilation unit, and asset data is captured into a snapshot the plugin loads at startup.

The FusionPluginProjectSettings asset is where you configure the export pipeline — export buttons, auto-export toggles, code-include paths, and per-asset postprocess callbacks all live in its inspector.
Each subsection below covers either a control on this asset or a code-side annotation that influences what it exports.

FusionPluginProjectSettings inspector — tour of the controls covered in this section
`FusionPluginProjectSettings` — the inspector hub for the plugin export pipeline.

Note: Create the asset in your Unity project via the right-click menu Create > Fusion > Plugin Project Settings.

Exporting

When you press Export in the FusionPluginProjectSettings inspector, two outputs are produced:

  1. Types are generated. All your NetworkBehaviours, [Networked] properties, networked structs, [NetworkInput] types, and types derived from NetworkObject are mirrored into the plugin project under Fusion.Plugin.Types/Generated/UserTypes.<AssemblyName>.cs. The mirrors are declared partial, so plugin code can extend them.
  2. Asset data is captured. Prefab hierarchies, scene NetworkObject layouts, and networked ScriptableObject state are written to embedded JSON databases (DB.Prefab.*.json, DB.Scene.*.json, DB.ScriptableObject.*.json) inside the same Generated/ folder. These are loaded server-side at runtime so the plugin can spawn and resolve the same objects clients use.

The export is a snapshot. After any Unity-side change that the plugin should see, re-export.

Auto-Exporting

The FusionPluginProjectSettings asset has an AutoExport field that controls when the plugin export runs automatically in response to project changes. Each asset type can be enabled independently.

AutoExport field on FusionPluginProjectSettings
`AutoExport` toggles — enable per asset type to have the plugin re-export automatically as you work in Unity.

Note: Auto-export aims to cover every workflow but may miss something. If it does, run Export manually and please file a bug report.

Code Annotations

The PluginCodeExportSettings(PluginExportOptions opts) attribute can be used to include a type or field that would be otherwise skipped or to skip something that would otherwise be included from the export. This can be achieved via the PluginExportOptions.Skip and PluginExportOptions.Export flags.

C#

    // Fields are skipped by default, but can be exported via the attribute
    [PluginCodeExportSettings(PluginExportOptions.Export)]
    int MaxHealth = 100;

Intercepting Exports

If you need to inject custom logic into the export pipeline (e.g. strip a field, transform a value, attach extra metadata) the FusionPluginExportCallbacks UnityEvents on the settings asset fire once per captured asset:

  • PostprocessPrefabData(...)
  • PostprocessSceneData(...)
  • PostprocessScriptableObjectData(...)

Code Sharing

FusionPluginProjectSettings.IncludePaths is a list of folder/file patterns inside the Unity project. Anything matching is included in the plugin project (csproj) at export time, becoming part of the plugin's compilation unit.

IncludePaths field on FusionPluginProjectSettings
`IncludePaths` — folder/file patterns whose contents get pulled into the plugin's compilation unit at export time.
Fusion Plugin Project .csproj updated after Export
After `Export` the Fusion Plugin Project will be updated.

There's also a Unity asset label, FusionPluginInclude, that achieves the same per-file — apply the label to a script and it'll be picked up even if it lives outside an IncludePaths folder.

This is the right tool for:

  • Shared types (enums, plain data classes) that both Unity and the plugin need to agree on.
  • Helper / utility code you want to reuse server-side.

Press Export after changing IncludePaths for the changes to land in the plugin project.

Note: The plugin does not depend on UnityEngine. We provide our own stand-ins for the most common UnityEngine types, but coverage isn't exhaustive — keep UnityEngine dependencies in shared files to a minimum to avoid plugin-side compile errors.

Network Behaviours

By default all Networked properties and their owning NetworkBehaviours are exported for extension.

Networked Properties - Basics

For example a simple Health NetworkBehaviour declared unity side

C#

// Unity Side
using Fusion;

namespace Foo {
  public class Health : NetworkBehaviour {
    [PluginCodeExportSettings(PluginExportOptions.Export)]
    int MaxHealth = 100;

    [Networked]
    public int CurrentHealth { get; set; }
  }
}

Would generate a partial class in Fusion.Plugin.Types, that you can extend as follows

C#

// Plugin Side - Extend by placing a partial in Fusion.Plugin.Types
using Fusion;
namespace Foo;

public partial class Health {
  public override void FixedUpdateNetwork() {
    if (CurrentHealth > MaxHealth) {
      Log.Info($"{CurrentHealth} exceeds {MaxHealth}");
    }
  }
}

This is an example of the plugin identifying users setting their health to values outside the expected range — log it, flag the user, or kick them.

The above example also demonstrates

  • Usage of the [PluginCodeExportSettings(PluginExportOptions.Export)] attribute to mark a field to be included in the export, fields are skipped by default.
  • Overriding FixedUpdateNetwork in the plugin, you are also able to override the Spawned and Despawned lifetime methods

C#

public partial class Health {
  public override void Spawned() {
    base.Spawned();
  }

  public override void Despawned(NetworkRunner runner, bool hasState) {
    base.Despawned(runner, hasState);
  }

  public override void FixedUpdateNetwork() {
  }
}

Networked Properties - Plugin Authority

The [Networked] attribute has two parameters that work together to make a property server-authoritative:

C#

public class Player : NetworkBehaviour {
  [Networked(PluginAuthority = true, AllowPrediction = false)]
  public int Level { get; set; }

  [Networked(PluginAuthority = true, AllowPrediction = false)]
  public int Xp { get; set; }
}
Parameter Effect
PluginAuthority = true When a custom plugin is running, the plugin owns the property. Even the client that holds the object's state authority sees the property as a proxy — it can read but its writes don't stick. Without a plugin, this parameter has no effect.
AllowPrediction = false (default is true.) Disables local prediction. Only state authority can write. input authority and proxy writes are silently ignored.

Combined, [Networked(PluginAuthority = true, AllowPrediction = false)] gives you a property that only the plugin can change — clients see plugin authoritative values. This is the recommended pattern for anti-cheat-sensitive state (XP, currency, match score).

RPCs

Intercepting RPCs

To force RPCs through the plugin first, add InvokeLocalMode = RpcInvokeLocalMode.ForwardToPlugin:

C#

// Unity side
public class Chest : NetworkBehaviour {
  [Rpc(RpcSources.All, RpcTargets.StateAuthority, InvokeLocalMode = RpcInvokeLocalMode.ForwardToPlugin)]
  public void RPC_Open() {
    // Default client behaviour: spawn loot. Only runs if the plugin
    // doesn't cancel the RPC.
    SpawnLoot();
  }
}

C#

// Plugin side - Fusion.Plugin.Types
partial class Chest {
  partial void RPC_Open(ref RpcInfo info) {
    if (NotAllowedToOpen()) {
      info.Cancel();  // Suppresses delivery - clients never run RPC_Open.
      return;
    }

    // Or do server-authoritative work directly and prevent the client
    // version from running:
    SpawnLootServerSide();
    info.Cancel();
  }
}

Mechanism:

  • The exporter emits a matching partial void on the plugin side for every RPC method. Fill it in.
  • The first parameter is ref RpcInfo info. Calling info.Cancel() marks the RPC for suppression so it isn't forwarded to its declared targets.
  • To modify an RPC's payload rather than just block it, cancel the original and call a fresh RPC with the corrected arguments.

Writing plugin-aware client code

Runner.HasCustomPlugin lets the same Unity code branch on whether a custom plugin is running this session:

C#

// Unity Side
[Rpc(RpcSources.All, RpcTargets.StateAuthority, InvokeLocalMode = RpcInvokeLocalMode.ForwardToPlugin)]
public void RPC_TakeHit(int damage) {
  if (Runner.HasCustomPlugin == false) {
    // No plugin: client state auth applies the damage itself.
    CurrentHealth = Mathf.Max(0, CurrentHealth - damage);
  }

  // Either way, fire the cosmetic event.
  HitReceived.Invoke();
}

C#

// Plugin Side - Fusion.Plugin.Types
partial void RPC_TakeHit(int damage, ref RpcInfo info) {
    // Note we do not want to cancel the RPC as we want remaining
    // clients to receive it to trigger the HitReceived effect
    CurrentHealth = Mathf.Max(0, CurrentHealth - damage);
}

This is what lets you ship one codebase that runs both against the plugin and without it (e.g. for local testing or sessions that don't need server-authoritative logic).

Baked per-object data

IPluginBakedDataProvider<T> lets a NetworkBehaviour ship arbitrary custom data into the plugin export, captured from Unity at export time. The exporter calls Bake(...) on each instance and writes the returned object alongside the rest of the prefab/scene data; on the plugin side it's surfaced as a BakedPluginData field.

C#

// Unity side
public class Chest : NetworkBehaviour, IPluginBakedDataProvider<Chest.PluginData> {

  PluginData IPluginBakedDataProvider<PluginData>.Bake(in PluginBakedDataContext ctx) {
    transform.GetPositionAndRotation(out var pos, out var rot);
    return new PluginData { Position = pos, Rotation = rot };
  }

    // The data type needs to be visible to the plugin too. Either define
    // it in a shared file (see "Code Sharing" above) or annotate it
    // with [PluginCodeExportSettings(...)] so the exporter emits a mirror.
    [PluginCodeExportSettings(PluginExportOptions.Export)]
    public sealed class PluginData
    {
        [PluginCodeExportSettings(PluginExportOptions.Export)]
        public Vector3 Position;
        [PluginCodeExportSettings(PluginExportOptions.Export)]
        public Quaternion Rotation;
    }
}

C#

// Plugin side
partial class Chest {
  public void SpawnItem() {
    var spawnPos = BakedPluginData.Position
                 + BakedPluginData.Rotation * Vector3.forward;
    // ... spawn an item at spawnPos
  }
}

Key points:

  • Bake(in PluginBakedDataContext context) is called once per object at export time. Return your data instance.
  • A public T BakedPluginData; field is exported on the plugin-side class — you access it directly
  • The data type must be visible to the plugin. Easiest approaches: put the type in a shared file (FusionPluginProjectSettings.IncludePaths, see Code Sharing) or annotate it with [PluginCodeExportSettings(...)] so the exporter emits a mirror.

Plugin entry points

Fusion.Plugin.Custom/Core/ contains three partial classes that own the plugin lifecycle. The SDK ships baseline implementations; you can substitute custom behaviour via partial methods:

Class Partial method Use to
CustomPlugin CreateServerUser(ref FusionServer result) Provide a custom FusionServer subclass instance.
CustomPlugin HasCustomLogicUser(ref bool value) Override the Runner.HasCustomPlugin flag broadcast to clients.
CustomPluginFactory CreatePluginUser(IPluginHost, string, Dictionary<string,string>, ref IGamePlugin) Provide a fully custom plugin instance, or read dashboard config before plugin creation.
CustomServer CreateRunnerUser(NetworkProjectConfig, NetAddress, INetSocket, ref NetworkRunner) Provide a custom NetworkRunner instance (e.g. with non-default initialization).

Most customers won't need to touch these — the defaults are fine. Use them only when the default FusionServer / NetworkRunner plumbing doesn't fit your scenario.

Server-side hooks

The runner inside the plugin is the same NetworkRunner the Unity client uses, running in SimulationModes.Server. The plugin gets all the standard INetworkRunnerCallbacksFusionServer provides empty virtual implementations of each, and CustomServer (the project's subclass) is where you override the callbacks you need.

The recommended pattern is to add a CustomServer.User.cs (or similar) file alongside CustomServer.cs:

C#

namespace Fusion.Plugin;

partial class CustomServer {
  public override void OnPlayerJoined(NetworkRunner runner, PlayerRef player) {
    base.OnPlayerJoined(runner, player);   // keep the actor mapping intact
    Log.Info($"player joined: {player}");
  }

  public override void OnPlayerLeft(NetworkRunner runner, PlayerRef player) {
    Log.Info($"player left: {player}");
    base.OnPlayerLeft(runner, player);
  }
}

Authority & validation

Three callbacks on FusionServer gate client actions. All return bool — return false to deny.

Callback Used for
CanPlayerCreateObject(runner, player, ref readonly NetworkObjectHeader header) Whether player may spawn the object described by header. Default: allow.
CanPlayerDestroyObject(runner, player, NetworkObjectMeta meta) Whether player may destroy an object. Default: allow.
CanPlayerRequestStateAuthorityChange(runner, player, NetworkObjectMeta meta, bool acquire) Whether player may acquire/release state authority on an object. Default: allow.

These run for every spawn / destroy / authority request — keep them cheap.

Plugin Side Spawning

It's possible for the plugin to spawn NetworkObjects using the familiar Runner.Spawn methods.

C#

partial class CustomServer {
  public override void OnPlayerJoined(NetworkRunner runner, PlayerRef player) {
    base.OnPlayerJoined(runner, player);

    Runner.Spawn("SomeObject");
  }
}

Where "SomeObject" is the name of a Unity prefab that has a NetworkObject behaviour associated with it

Plugin Logging

Use the Log API for all plugin logging — it's wired into log4net via the SDK's log4net.config. Levels: Log.Debug, Log.Info, Log.Warn, Log.Error.

CustomPluginFactory.GetLoggerName() returns "Custom", so entries from your plugin appear under [Plugin.Custom] in the log — convenient for searching.

Plugin Threading

The plugin runs in Photon Server's multi-threaded hosting environment. Multiple game sessions can be updated on different threads concurrently.

Practical implications:

  • Avoid mutable static state. If two sessions in the same plugin process both touch the same static, you'll run into concurrency issues. Use instance fields on CustomServer / CustomPlugin instead.
  • Within a single session's tick, callbacks are serialized — you don't need to lock state owned by a single CustomServer instance against itself.

Common patterns

  • Cheat detection. Check Networked state values against plausible bounds, flag users whose values exceed them.
  • Right-size your cheat-proofing. 100% cheat-proof gameplay is the wrong goal — chasing it leads to false positives that punish honest players. Detect cheaters silently. Flag users based on repeated patterns, not a single event. Segregate them into a separate matchmaking pool, and let them play against each other.
  • Plugin-authoritative critical state. Use [Networked(PluginAuthority = true, AllowPrediction = false)] for anything anti-cheat-sensitive (XP, currency, score). No client can write it.
  • Different logic per session type. Branch on dashboard config or session properties to run different gameplay rules (casual vs ranked, region-specific).
Back to top