Game Creator 2 — Multiplayer Character
Build a full multiplayer third-person game using Game Creator 2 and NetCrafter. GC2's character system handles all locomotion locally; NetCrafter handles the server protocol. Only the Unity-side scripts differ from the standard Character tutorial — the schema, server extension, and zone config are identical.
Compatibility note
- This tutorial demonstrates NetCrafter's compatibility with Game Creator 2.
- The network schema, server extension, and zone config are identical to the Character tutorial.
- Only the Unity-side C# scripts differ — they use GC2's API instead of Starter Assets.
- If you completed the Character tutorial first, you can skip B1–B2 and re-use the generated code.
Prerequisites
Unity Editor with URP (Universal Render Pipeline) template.
✓ Required — use URP for correct material colour support.
GC2 Core + Characters package installed via Package Manager.
✓ Required — this tutorial uses GC2's Character and locomotion system.
NetCrafter 1.1.0+ imported into the Unity project.
✓ Required — open the NetCrafter window via Tools → Pivec Labs.
SFS3 running locally on port 9977. Java 21 + Maven installed.
✓ See Deployment Guide for setup steps.
About the Examples folder
Assets/PivecLabs/Examples/GC2/ ships with a prebuilt reference scene (GC2DemoScene.unity) for comparison once you're done — it is not a shortcut. Build your own scene from scratch by following the steps below.
The example's scripts are wrapped in NETCRAFTER_GC2_EXAMPLE/NETCRAFTER_GC2_GENERATED compile guards, which are off by default — they only compile once Game Creator 2 is actually installed and you've added those define symbols yourself (Edit → Project Settings → Player → Scripting Define Symbols). Until then, components referencing them will show as "missing script" in the Inspector — that's expected.
Placeholder note: the example's RemoteGC2CharacterPrefab uses a plain capsule, not a skinned GC2 model, so GC2CharacterColorManager's per-player tinting (which only affects SkinnedMeshRenderers) won't visibly do anything until you swap in a real rigged character.
Project Setup §
Before writing any scripts, configure the project so GC2 and NetCrafter example scripts can coexist without compile errors.
Step 1Add the compile define
Go to Edit → Project Settings → Player → Other Settings → Scripting Define Symbols and add:
NETCRAFTER_GC2_EXAMPLE
This guard wraps all GC2 example scripts in the source project. In this tutorial project the scripts are clean — no guard needed.
PivecLabs.Examples.asmdef to the Examples/GC2/ folder. Example scripts must compile into Assembly-CSharp so they can see the generated types (SmartFoxConnectionManager, PlayerJoinedEvent, etc.).Use the GC2 Sample Scene §
Step 1Open the GC2 character example scene
In the Project window, navigate to:
Assets/Plugins/GameCreator/Installs/GameCreator.Examples@1.11.28/1 - Characters/1_PlayerMovement.unity
Open it, then save a working copy via File → Save As:
Assets/Scenes/GC2CharacterScene.unity
Step 2Verify single-player movement
Press Play. The GC2 character should:
- Move with WASD / left stick
- Jump with Space
- Have the camera following from behind (Cinemachine or GC2's follow camera)
If movement works, single-player setup is confirmed. Stop Play and continue.
GC2 Components to Sync §
Before writing network code, understand what GC2 exposes for multiplayer use.
Player hierarchy
Player ← root; has Character (GC2) component
└── Mannequin ← GC2 mannequin pivot (y = -1 offset)
└── Mannequin ← has Animator (CompleteLocomotion controller)
├── Armature ← bone hierarchy root (Hips → Spine → …)
└── Character ← SkinnedMeshRenderer (mesh + materials)
Key GC2 API for networking
| Expression | Type | Purpose |
|---|---|---|
character.IsPlayer | bool | Set false on remote clones — disables ALL GC2 player-input units |
character.Player.IsControllable | bool | Secondary input gate (not needed for remote clones) |
character.Animim.Animator | Animator | The Unity Animator on the inner Mannequin |
character.Driver.IsGrounded | bool | Whether the character is touching the ground |
character.Driver.WorldMoveDirection | Vector3 | Current world-space velocity direction |
character.Motion.IsJumping | bool | Whether a jump is active |
CompleteLocomotion animator parameters
All 17 parameters in the CompleteLocomotion.controller are Float — there are no Bool parameters. Key ones for network sync:
| Parameter | Default | Usage |
|---|---|---|
Speed | 0 | Overall movement speed — set on remote characters to drive blend tree |
Speed-XZ | 0 | Horizontal speed — drive idle/walk/run blend on remote characters |
Grounded | 1 | 1 = on ground, 0 = airborne (float, not bool) |
Intent-X/Y/Z | 0 | Input intent direction |
Stand | 1 | Stand level (1 = standing) |
Phase-0…3 | 0 | Animation phase channels |
No CharacterController to disable
GC2 uses its own UnitDriverController (inside the Character component) for physics. There is no separate CharacterController, ThirdPersonController, or StarterAssetsInputs to disable on remote clones. Setting character.IsPlayer = false is the single required step.
NetCrafter Schema §
Create a new schema named GC2Character using the Definition Tools:
- Go to Tools → Pivec Labs → Definition Tools.
- Click Create Definition.
- Name it:
GC2Character. - Click Create.
Then configure the schema fields as described below.
SmartFox Settings
Go to NetCrafter → Settings → SmartFox Settings and configure:
| Field | Value |
|---|---|
| Host | 127.0.0.1 |
| Zone | GC2Zone |
| Extension Name | GC2Character |
| Extension Class | com.piveclabs.gc2.GC2CharacterExtension |
| TCP Port | 9977 |
| WS Port | 9977 |
Click Save Settings after making changes.
Project Settings
| Field | Value |
|---|---|
| Namespace | GC2Character |
| Overwrite Files | true |
ClientToServer Message
Create one message named PlayerUpdate with transport ExtensionRequest and handler HandlePlayerUpdate:
| Field | Type | Description |
|---|---|---|
posX | Float | World X position |
posY | Float | World Y position |
posZ | Float | World Z position |
rotY | Float | Y-axis rotation (euler degrees) |
speed | Float | Animator "Speed" parameter |
motionSpeed | Float | Animator "Speed-XZ" parameter |
animState | Int | Discrete state (0=idle, 1=moving, 2=airborne), mirroring Character's stateHash / VR's animState |
grounded | Float | GC2's "Grounded" animator blend value |
speedY | Float | Vertical velocity, for jump/fall blending |
stand | Float | GC2's "Stand" animator blend value (crouch/stand) |
colorIndex | Int | Player colour (0–7) |
ServerToClient Events
Create three events (handler name: leave blank — the server broadcasts these):
PlayerStateUpdate
| Field | Type |
|---|---|
playerId | Int |
posX | Float |
posY | Float |
posZ | Float |
rotY | Float |
speed | Float |
motionSpeed | Float |
animState | Int |
grounded | Float |
speedY | Float |
stand | Float |
PlayerJoined
| Field | Type |
|---|---|
playerId | Int |
playerName | String |
colorIndex | Int |
spawnIndex | Int |
PlayerLeft
| Field | Type |
|---|---|
playerId | Int |
Server Action
| Field | Value |
|---|---|
| Name | HandlePlayerUpdate |
| Trigger | PlayerUpdate |
| Response Event | PlayerStateUpdate |
| Logic | BroadcastAll |
Generate All Code §
GC2Character — not Character. If it shows Character, change it to GC2Character and save before generating. This prevents a namespace conflict with Game Creator 2's Character component.Step 1Generate client code
In the NetCrafter window → Code tab → click Generate Client Code. This creates SmartFoxConnectionManager.cs, PlayerJoinedEvent.cs, PlayerStateUpdateEvent.cs, PlayerLeftEvent.cs, and related types under Definitions/GC2Character/Generated/Client/.
Step 2Generate and build server code
Click Generate Server Code then Deploy JAR. The extension JAR is named GC2CharacterExtension.jar and is copied to your SFS3 extensions/GC2Character/ folder.
Step 3Add the generated define
In Edit → Project Settings → Player → Scripting Define Symbols, add:
NETCRAFTER_GC2_GENERATED
This unlocks the #if NETCRAFTER_GC2_GENERATED blocks in GC2NetworkManager.cs that call SmartFoxConnectionManager and the generated event types.
After generating client code you MUST add a second scripting define symbol or the multiplayer scripts will not compile correctly and Login() will never be called:
- Go to Edit → Project Settings → Player → Other Settings
- Find Scripting Define Symbols
- Add:
NETCRAFTER_GC2_GENERATED - Click Apply
⚠ Without this define, the connection will appear to work (you will see "Connected — waiting for Login()") but Login() will never be called and your game will be stuck forever.
B2 Checklist — before continuing
- ☐ Client code generated ✓
- ☐
NETCRAFTER_GC2_EXAMPLEdefined ✓ - ☐
NETCRAFTER_GC2_GENERATEDdefined ✓ ← Easy to miss! - ☐ Zero compile errors ✓
Create the Remote GC2 Character Prefab §
Remote GC2 characters are instances of the same GC2 Player prefab, stripped of camera-follow components and extended with network sync scripts.
Step 1 — Locate the GC2 Player prefab
In the Project window, find the Player prefab added by the GC2 Characters package. It is typically at:
Assets/Plugins/GameCreator/Installs/GameCreator.Characters@<version>/Player.prefab
Alternatively, use the Player GameObject already in your scene from Part A.
Step 2 — Duplicate and rename
In the Hierarchy, right-click Player → Duplicate. Name the duplicate RemoteGC2Character.
Step 3 — Remove camera components
Unlike the Starter Assets character, there are no input scripts to remove manually. RemoteGC2CharacterController calls character.IsPlayer = false in Awake(), which disables all GC2 input in one call.
You do need to remove camera-follow components so the scene camera does not try to track this remote character:
- Remove or disable any GC2 Camera Motor child GameObject
- Remove or disable any Cinemachine Virtual Camera that follows this character
- Remove any camera target components that reference the main follow camera
Step 4 — Add network components
On RemoteGC2Character, use Add Component to attach:
- RemoteGC2CharacterController
- GC2CharacterColorManager
- RemoteAnimationEventReceiver
NameCanvas child — see Step 5. Adding it to the root causes LookAt() to rotate the entire character toward the camera.Step 5 — Set up the name display
5.1 Create the Canvas
- Right-click RemoteGC2Character in the Hierarchy → UI → Canvas.
- Name it
NameCanvas. - On the Canvas component: set Render Mode to World Space.
- On the NameCanvas Transform:
- Position:
X=0, Y=2.2, Z=0 - Rotation:
X=0, Y=0, Z=0 - Scale:
X=0.01, Y=0.01, Z=0.01
⚠️The default Canvas scale is 100× too large for world space. Setting scale to0.01makes it the right size relative to the character. - Position:
- On the Rect Transform: Width =
200, Height =50.
5.2 Add the Text
- Right-click NameCanvas → UI → Text - TextMeshPro.
- Name it
NameText. - If prompted to import TMP Essentials, click Import TMP Essentials.
- On the Rect Transform: set Anchor to stretch/stretch (fill the canvas); set Left, Right, Top, Bottom offsets to
0. - On the TMP_Text component:
- Alignment: Center Middle
- Font Size:
36 - Color: White
5.3 Add and wire PlayerNameDisplay
- Select the NameCanvas child of RemoteGC2Character.
- Click Add Component and add PlayerNameDisplay.
⚠️PlayerNameDisplay must be on NameCanvas, not on the root RemoteGC2Character. The component calls
transform.LookAt()inLateUpdateto billboard the label toward the camera. When placed on the root, this rotates the entire character. OnNameCanvasit only rotates the name label. - On the PlayerNameDisplay component:
- Name Text: drag the
NameTextTMP child from the Hierarchy into this field. - Camera Transform: leave empty —
GC2NetworkManagerassigns this at runtime from the main camera.
- Name Text: drag the
Step 6 — Save as a prefab
Drag RemoteGC2Character from the Hierarchy into:
Assets/PivecLabs/Examples/GC2/Prefabs/RemoteGC2Character.prefab
(Create the Prefabs folder first if it does not exist.)
Step 7 — Remove from the Hierarchy
Delete RemoteGC2Character from the Hierarchy — it is now a prefab asset and no longer needs to live in the scene. You will assign it to GC2NetworkManager in Part B4.
Components on the final RemoteGC2Character prefab
| Component | Status |
|---|---|
| Transform | ✓ Kept |
| Character (GC2) | ✓ Kept — IsPlayer = false is set at runtime to disable all GC2 input |
| Animator (on inner Mannequin) | ✓ Kept |
| RemoteGC2CharacterController | ✓ Added |
| GC2CharacterColorManager | ✓ Added |
PlayerNameDisplay (on NameCanvas child) | ✓ Added to NameCanvas |
| RemoteAnimationEventReceiver | ✓ Added |
| GC2 Camera Motor / Cinemachine target | ✗ Remove |
ThirdPersonController, StarterAssetsInputs, or PlayerInput — there are no such components to remove. All GC2 player input is blocked by setting character.IsPlayer = false at runtime.Scene Setup §
B4.1Create the NetworkManager
- In the Hierarchy, right-click the scene name → Create Empty.
- Name it
NetworkManager. - Confirm its Transform Position is
0, 0, 0(click ⋮ → Reset if not). - With NetworkManager selected, click Add Component, search for GC2NetworkManager, and add it.
B4.2Add GC2UIManager
- Create a second empty GameObject named
UIManager. - Add the GC2UIManager component to it.
- (Or add both components to the same
NetworkManagerGameObject — either approach works.)
B4.3Assign the remote character prefab
- Select NetworkManager → GC2NetworkManager component.
- In the Remote Players section, drag
RemoteGC2Character.prefabfrom:
Assets/PivecLabs/Examples/GC2/Prefabs/RemoteGC2Character.prefab
into the Remote Character Prefab field. Confirm it showsRemoteGC2Character(not None).
B4.4Add the ConnectionManager prefab
- In the NetCrafter window, go to Scenes → Prefab Generator → Create ConnectionManager Prefab.
B4.5Add to existing scene
- In the NetCrafter window, click Scene Assistant → Add to Existing Scene.
- This places
ConnectionManager,NetCrafterSceneEventRouter, andNetCrafterEventDispatcherin the scene.
B4.6Wire Scene Events
In the NetCrafter window → Scenes → Scene Events, wire the three network events to GC2NetworkManager:
| Event | Target Method |
|---|---|
PlayerJoined | GC2NetworkManager.OnPlayerJoined() |
PlayerLeft | GC2NetworkManager.OnPlayerLeft() |
PlayerStateUpdate | GC2NetworkManager.OnPlayerStateUpdate() |
GC2NetworkManager automatically subscribes to the RoomJoined event in Start() via NetCrafterEventDispatcher — no manual wiring needed for room join.
B4.7Run In Background
To keep the connection alive when the Unity Editor loses focus during multiplayer testing:
- Go to Edit → Project Settings → Player → Resolution and Presentation
- Enable Run In Background
B4.8Add GC2CharacterColorManager to the local Player
Select the Player GameObject in the scene and add the GC2CharacterColorManager component. GC2NetworkManager uses this at runtime to tint the local character with the player’s assigned colour.
GC2NetworkManager §
GC2NetworkManager.cs is the top-level coordinator. It handles the connection lifecycle, spawns and removes remote GC2 characters, runs the 20 Hz send loop, and forwards typed events from the NetCrafter event router.
Key responsibilities
- Awake() — finds the GC2
Characterin the scene; setscharacter.IsPlayer = falseto block all input during login - Connect() — called by the login button; passes the player name to
SmartFoxConnectionManager - OnRoomJoined() — sets
character.IsPlayer = true(re-enables GC2 input); gets theAnimatorviacharacter.Animim.Animator; starts the send loop - SendPlayerUpdate() — runs at 20 Hz (50 Hz while jumping); reads position, rotation, and GC2 animator params; sends via
SmartFoxConnectionManager - OnPlayerJoined() / OnPlayerLeft() — spawn/destroy remote character GameObjects
- OnPlayerStateUpdate() — forwards state to the matching
RemoteGC2CharacterController
private void Awake()
{
// Find the GC2 Character in the scene (local Player has IsPlayer = true by default)
_localCharacter = FindAnyObjectByType<Character>();
if (_localCharacter != null)
_localCharacter.IsPlayer = false; // block all GC2 input until room join
}
public void OnRoomJoined()
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
if (_localCharacter != null)
{
_localCharacter.IsPlayer = true; // re-enable all GC2 input
_localAnimator = _localCharacter.Animim.Animator; // Animator lives on inner Mannequin
}
#if NETCRAFTER_GC2_GENERATED
int uid = SmartFoxConnectionManager.Instance?.LocalUserId ?? 0;
_localPlayerId = uid >= 0 ? uid : 0;
#endif
_localColorIndex = _localPlayerId % GC2CharacterColorManager.PlayerColors.Length;
if (_localCharacter == null)
{
Debug.LogError("[GC2NetworkManager] No GC2 Character found in scene.");
return;
}
_localCharacter.GetComponent<GC2CharacterColorManager>()?.SetColor(_localColorIndex);
_localCharacter.GetComponentInChildren<PlayerNameDisplay>(true)?.Hide();
_isConnected = true;
uiManager?.ShowGamePanel();
StartCoroutine(SendUpdateLoop());
}
public void OnPlayerStateUpdate()
{
#if NETCRAFTER_GC2_GENERATED
var payload = NetCrafterSceneEventRouter.Instance?.GetLastPayload("PlayerStateUpdate");
if (!(payload is PlayerStateUpdateEvent ev)) return;
if (!_remoteCharacters.TryGetValue(ev.playerId, out var remoteGo)) return;
var ctrl = remoteGo?.GetComponent<RemoteGC2CharacterController>();
ctrl?.SetNetworkState(
new Vector3(ev.posX, ev.posY, ev.posZ),
ev.rotY, ev.speed, ev.motionSpeed, ev.animState,
ev.speedY, ev.grounded, ev.stand);
#endif
}
Key differences from CharacterNetworkManager
| Feature | CharacterNetworkManager | GC2NetworkManager |
|---|---|---|
| Character lookup | FindAnyObjectByType<ThirdPersonController>() | FindAnyObjectByType<Character>() |
| Disable input | Disable ThirdPersonController, StarterAssetsInputs, PlayerInput | character.IsPlayer = false |
| Re-enable input | N/A (remote prefab lacks those components) | character.IsPlayer = true |
| Animator access | character.GetComponent<Animator>() | character.Animim.Animator (nested inside Mannequin) |
| Velocity source | ThirdPersonController speed | character.Driver.WorldMoveDirection |
| Jump state | Internal TPC field | character.Motion.IsJumping |
| MotionSpeed param | "MotionSpeed" | "Speed-XZ" (GC2 CompleteLocomotion) |
| Send rate (jumping) | 20 Hz constant | 50 Hz while jumping |
Character type alias
using Character = GameCreator.Runtime.Characters.Character; is declared at the top of GC2NetworkManager.cs. This resolves the naming conflict that arises when using GameCreator.Runtime.Characters; makes Character ambiguous (GC2 uses it as both a class name and a sub-namespace prefix). The namespace for generated types is set to GC2Character (not Character) in Project Settings to avoid the same conflict.
Login UI §
GC2UIManager.cs manages two panels: a login screen before the player connects, and a game panel showing the connected-players list. It is structurally identical to CharacterUIManager.
B6.1Create the Canvas
- Right-click the Hierarchy → UI → Canvas — Render Mode: Screen Space - Overlay
- Name it
UICanvas
B6.2Create the Login Panel
- Right-click
UICanvas→ UI → Panel — name itLoginPanel - Add a TMP Text child — title:
GC2 Character Demo, font size 36, centre-aligned - Add a TMP InputField — name it
PlayerNameInput, placeholder:Enter your name... - Add a TMP Button — name it
ConnectButton, label:Connect - Add a TMP Text — name it
StatusText, small font, for connection messages
B6.3Create the Game Panel
- Right-click
UICanvas→ UI → Panel — name itGamePanel, set inactive in the Inspector - Resize to a small corner panel (e.g. top-right, 200×200 px)
- Add a TMP Text child — name it
PlayerListText, set alignment to top-left
B6.4Attach and wire GC2UIManager
- Select the
UIManagerGameObject (created in B4.2); confirm GC2UIManager is attached - In the Inspector, wire all fields:
- Login Panel →
LoginPanel - Player Name Input →
PlayerNameInput - Connect Button →
ConnectButton - Status Text →
StatusText - Game Panel →
GamePanel - Player List Text →
PlayerListText
- Login Panel →
- Wire
ConnectButton’s On Click event →GC2NetworkManager.Connect()
GC2UIManager.ShowLoginPanel() unlocks the cursor so the player can use the login form. GC2NetworkManager.OnRoomJoined() locks it again for gameplay. No changes to GC2’s internal scripts are needed.SFS3 Zone Config §
In the NetCrafter window → Code → Config Generator → click Generate Zone Config. Copy the generated GC2Zone.zone.xml to your SFS3 server/zones/ folder and restart SFS3.
The generated config will match this structure (required stubs included to prevent SFS3 NullPointerExceptions):
<zone>
<name>GC2Zone</name>
<allowGuestUsers>true</allowGuestUsers>
<guestUserNamePrefix>Guest#</guestUserNamePrefix>
<maxUsers>100</maxUsers>
<wordsFilter active="false"/>
<floodFilter active="false"/>
<rooms>
<room>
<name>GameRoom</name>
<groupId>default</groupId>
<password></password>
<maxUsers>50</maxUsers>
<maxSpectators>0</maxSpectators>
<isDynamic>false</isDynamic>
<isGame>false</isGame>
<isHidden>false</isHidden>
<autoRemoveMode>DEFAULT</autoRemoveMode>
<permissions>
<flags>PUBLIC_MESSAGES,ROOM_NAME_CHANGE</flags>
<maxRoomVariablesAllowed>30</maxRoomVariablesAllowed>
</permissions>
<events>USER_ENTER_EVENT,USER_EXIT_EVENT</events>
<badWordsFilter isActive="false"/>
<roomVariables/>
<extension>
<name></name><type>JAVA</type><file></file>
<propertiesFile></propertiesFile><reloadMode>AUTO</reloadMode>
</extension>
<mmoSettings>
<isActive>false</isActive>
<defaultAOI>100,100,0</defaultAOI>
<lowerMapLimit></lowerMapLimit>
<higherMapLimit></higherMapLimit>
<forceFloats>false</forceFloats>
<userMaxLimboSeconds>50</userMaxLimboSeconds>
<proximityListUpdateMillis>500</proximityListUpdateMillis>
<sendAOIEntryPoint>true</sendAOIEntryPoint>
</mmoSettings>
<allowOwnerInvitation>true</allowOwnerInvitation>
<suppressUserList>false</suppressUserList>
<allowAudioStreaming>false</allowAudioStreaming>
</room>
</rooms>
<privilegeManager active="false"/>
<extension>
<name>GC2Character</name>
<type>JAVA</type>
<file>com.piveclabs.gc2.GC2CharacterExtension</file>
<propertiesFile></propertiesFile>
<reloadMode>AUTO</reloadMode>
</extension>
<buddyList active="false"/>
<databaseManager active="false"/>
<udpSettings>
<keepAlive>true</keepAlive>
<rdpQualitySetting>BEST_EFFORT</rdpQualitySetting>
<rdpReliableChannelType>2</rdpReliableChannelType>
</udpSettings>
</zone>
Test §
Step 1Start SFS3
Launch SFS3 and confirm the GC2Zone appears in the AdminTool. The GC2CharacterExtension should be listed under Extensions.
Step 2First client
- Press Play in Unity Editor.
- The Login Panel appears. Enter a name (e.g. Alice) and press Connect.
- The Login Panel hides, cursor locks, and the GC2 character starts responding to WASD + Space.
- Alice's name should appear in the player list.
Step 3Second client
- Build and run a standalone player, or open a second Unity Editor instance.
- Enter a different name (e.g. Bob) and connect.
- In Alice's editor: Bob's GC2 character appears, tinted a different colour, with Bob's name tag above their head.
- Move Bob — Alice should see the animation playing correctly (idle → walk → run).
- Jump Bob — Alice should see the jump animation replicate.
Step 4Disconnect test
Stop Bob's client. Alice should see Bob's character disappear and the player list update automatically.
SmartFoxConnectionManager.Instance?.SendExtensionRequest is being called (add a Debug.Log inside SendPlayerUpdate). Also confirm that the Scene Events are correctly wired on the NetCrafterSceneEventRouter component in the scene.RemoteGC2CharacterController is finding the Character component on Awake(). The Character component must be on the same root GameObject as the RemoteGC2CharacterController.What's Next §
The GC2 tutorial establishes the same multiplayer foundation as the Character tutorial using GC2's API. Here are logical extensions:
This tutorial demonstrates NetCrafter's compatibility with Game Creator 2. The network schema, server extension, and zone config are identical to the Character Tutorial — only the Unity-side scripts differ. Any NetCrafter schema update applies equally to both tutorials without changes to the server layer.