by Pivec Labs
⏱ 90–120 min Racing Tutorial

Multiplayer Racing with NetCrafter

Build a complete real-time racing game — a physics car on a procedural track, with up to 16 players connected through SmartFoxServer 3 and lerp-smoothed remote cars.

Time: 90–120 minutes 14 parts C# & Java 4+ players
🧩
NetCrafter

What it is: Installed and working — you completed the Chat tutorial.

Namespace for this project: Racing

🏎
ARCADE: FREE Racing Car

What it is: A free Unity Asset Store vehicle model with proper wheel hierarchy.

How to get it
  • Search "ARCADE Free Racing Car" in the Asset Store (by Mena)
  • Import into your project
  • Go to Edit → Rendering → Materials → Convert Selected Built-in Materials to URP to fix pink materials
🖥
SmartFoxServer 3

What it is: Running locally — same setup as the Chat tutorial.

New zone: RacingZone on port 9977

☕
Maven + JDK 21

What it is: Same build tools used in the Chat tutorial.

Already detected by NetCrafter if the Chat tutorial is working.

00

Prerequisites

This tutorial assumes you have completed the NetCrafter Chat Tutorial. You should be familiar with:

  • Creating a schema and adding messages in the NetCrafter window
  • Running Generate Client Code and Generate Server Code
  • Building and deploying a JAR to SmartFoxServer 3
  • Deploying a zone config and restarting SFS3

The Racing schema is completely independent of the ChatRoom schema. All generated code goes into the Racing namespace and a separate RacingZone.

About the Examples folder

Assets/PivecLabs/Examples/Racing/ ships with a prebuilt reference scene (RacingDemoScene.unity) for comparison once you're done — it is not a shortcut. Build your own scene from scratch by following the steps below.

Placeholder note: the example's car is a plain tagged cube standing in for a real vehicle — it has no WheelColliders or steering visuals. Swap in the ARCADE Free Racing Car asset (or your own vehicle) for anything beyond a networking demo.


A1

Project Setup

Create the script folder structure. All six Racing scripts live in Assets/PivecLabs/Examples/Racing/Scripts/ — inside the NetCrafter package for export, but outside the NetCrafter/ subfolder so they compile into Assembly-CSharp alongside the generated code.

folder structure
Assets/PivecLabs/Examples/Racing/Scripts/
  TrackBuilder.cs         ← procedural track builder
  CarController.cs        ← physics car + remote lerp
  CarCamera.cs            ← smooth follow camera
  CarColorManager.cs      ← 8-colour player system
  RacingNetworkManager.cs ← all multiplayer logic
  RacingUIManager.cs      ← login / HUD panels

All six scripts are provided in this tutorial. Copy them into the folder and Unity will compile them automatically.

TextMeshPro: TextMeshPro is built into Unity 6. Before using any TMP components, import TMP Essentials once: Window → TextMeshPro → Import TMP Essentials. Click Import when prompted.

First — Enable the Racing Example Scripts

Before attaching any scripts or creating any GameObjects, add the scripting define symbol that enables the racing example scripts:

  1. Go to Edit → Project Settings → Player
  2. Find Scripting Define Symbols (under Other Settings)
  3. Add: NETCRAFTER_RACING_EXAMPLE
  4. Click Apply

This must be done BEFORE proceeding with any other steps in this tutorial.

Assembly Isolation

Generated client files compile into Assembly-CSharp (no .asmdef). Your game scripts must be in the same assembly to reference generated types like PlayerJoinedEvent. Placing scripts in Assets/PivecLabs/Examples/Racing/Scripts/ (no .asmdef ancestor) satisfies this requirement.

A2

Create a Simple Track

TrackBuilder.cs generates a rectangular oval track from Unity primitives — no external assets needed. Adjust parameters in the Inspector, then click ⋮ → Rebuild Track to regenerate.

Track layout

The track is a closed rectangular loop made from overlapping cube primitives:

PieceDescription
GroundLarge green plane (~3× track size)
North + South straightsLong dark-grey road cubes, east–west
East + West connectorsShort grey cubes on each side, running between the two corner zones
Curved cornersArc of cornerSegments small rotated cubes per corner (default 8), creating smooth 90° turns
Outer barriersWhite wall cubes along each road's outer edge
Inner barriersWhite wall cubes along each road's inner edge
Start/finish lineWhite flat marker at the north straight
Spawn pointsEmpty GameObjects on the north straight, evenly spaced

Adding the track to your scene

  1. In the Hierarchy, right-click the scene name (at the very top of the Hierarchy panel, e.g. RacingScene) → Create Empty. Name it Track.
    Important: right-click the scene name, not an existing GameObject. This ensures Track is created at world position 0,0,0.
  2. With Track selected, check the Transform component — confirm Position is 0, 0, 0. If not, click ⋮ on the Transform component → Reset.
  3. Add Component → search for TrackBuilder → add it.
  4. Click ⋮ at the top right of the TrackBuilder component → Rebuild Track. The track appears immediately.
⚠
Track corners not aligned?

If your track corners appear far from the straight sections, the Track GameObject was not at world origin when the track was built. Select the Track GameObject, click ⋮ on the Transform → Reset, then click ⋮ on TrackBuilder → Rebuild Track.

TrackBuilder only rebuilds when you explicitly click ⋮ → Rebuild Track. Changing parameter values in the Inspector does not auto-rebuild, so your scene references (including spawn point assignments) are preserved.

TrackBuilder parameters

ParameterDefaultDescription
trackWidth20Road surface width in metres
trackLength150Length of each straight section
cornerRadius30Half-distance between the two parallel straights
barrierHeight2Height of the wall barriers
cornerSegments8Arc segments per 90° corner — increase for smoother curves
numSpawnPoints4Number of spawn positions at the start line

Materials — assign in Inspector or leave null for auto-defaults

TrackBuilder exposes four material slots in the Inspector: Track Material (road surface), Barrier Material (walls), Ground Material (infield), and Start Line Material. Leave any slot empty and a plain-colour material is created automatically. Assign your own materials to customise the track appearance.

TrackBuilder.cs
// Assets/PivecLabs/Examples/Racing/Scripts/TrackBuilder.cs
// Full source — add to your project as-is.
// After attaching, click gear icon → "Rebuild Track" in the Inspector.

[ExecuteInEditMode]
public class TrackBuilder : MonoBehaviour
{
    public float trackWidth    = 20f;
    public float trackLength   = 150f;
    public float cornerRadius  = 30f;
    public float barrierHeight = 2f;
    public int   numSpawnPoints = 4;

    [ContextMenu("Rebuild Track")]
    public void Build() { /* ... builds track from primitives ... */ }
}

The SpawnPoints child contains SpawnPoint_0 through SpawnPoint_N transforms you'll assign to RacingNetworkManager in Part B.

A3

Car Controller

CarController.cs drives the ARCADE car using WheelCollider physics for the local player, and lerp-smoothing for remote players (see Part B5 for the remote mode detail).

ARCADE car hierarchy

hierarchy
ArcadeRacingCar (root)       ← add CarController here
  Body
  Wheels
    FrontLeft                ← visual mesh only
    FrontRight
    RearLeft
    RearRight
  WC_FrontLeft               ← WheelCollider (you create these)
  WC_FrontRight
  WC_RearLeft
  WC_RearRight

The visual wheel transforms under Wheels/ are pure meshes. The WC_* objects are empty GameObjects with WheelCollider components. CarController syncs visual wheel world-pose to the colliders each frame.

Setup steps

  1. Drag the ARCADE car prefab into the scene. Right-click → Prefab → Unpack Completely.
  2. Select the car root GameObject.
  3. Click Add Component and search for CarController.
  4. That’s it — CarController automatically detects the existing WheelColliders and Rigidbody. No manual assignment needed.

CarController fields

FieldDefaultDescription
motorTorque1500Torque applied to rear drive wheels (N·m)
brakeTorque3000Braking torque on Space key
maxSteerAngle30°Maximum front wheel steering angle
maxSpeed60 m/sMotor cuts out above this speed (~216 km/h)
isLocalPlayertrueSet to false for remote cars

Smooth physics — auto-configured by CarController

CarController automatically applies recommended physics settings in Awake(): linear and angular damping, lowered centre of mass, Rigidbody interpolation, and continuous collision detection. WheelCollider forward and sideways friction stiffness is also tuned for arcade handling.

If the car feels too grippy or too slidey, adjust forwardFriction.stiffness and sidewaysFriction.stiffness in ConfigureWheelFriction() inside CarController.cs.

Input System — no Input Actions asset needed

CarController reads keyboard input directly via Keyboard.current from Unity’s new Input System. No Input Actions asset or Player Input component is required.

If you see an error about the Input class switching, confirm Active Input Handling is set to Input System Package or Both in:
Edit → Project Settings → Player → Other Settings

csharp — CarController.cs (input section)
using UnityEngine.InputSystem;

private void UpdateLocal()
{
    if (Keyboard.current == null) return;

    // Steering — A/D or Left/Right arrows
    float steerInput = 0f;
    if (Keyboard.current.aKey.isPressed || Keyboard.current.leftArrowKey.isPressed)
        steerInput = -1f;
    else if (Keyboard.current.dKey.isPressed || Keyboard.current.rightArrowKey.isPressed)
        steerInput = 1f;

    // Throttle — W/Up arrow; Brake/Reverse — S/Down arrow
    float throttleInput = 0f;
    if (Keyboard.current.wKey.isPressed || Keyboard.current.upArrowKey.isPressed)
        throttleInput = 1f;
    float brakeInput = 0f;
    if (Keyboard.current.sKey.isPressed || Keyboard.current.downArrowKey.isPressed)
        brakeInput = 1f;

    // Handbrake — Space
    bool handbrake = Keyboard.current.spaceKey.isPressed;

    // W = forward (+1), S = reverse (−1)
    float combinedThrottle = throttleInput - brakeInput;

    float speed = _rb != null ? _rb.linearVelocity.magnitude : 0f;
    float appliedTorque = speed < maxSpeed ? motorTorque * combinedThrottle : 0f;

    foreach (var w in driveWheels)
    {
        w.motorTorque = appliedTorque;
        w.brakeTorque = handbrake ? brakeTorque : 0f;
    }
    foreach (var w in steerWheels)
    {
        w.steerAngle  = maxSteerAngle * steerInput;
        w.brakeTorque = handbrake ? brakeTorque : 0f;
    }
}
A4

Follow Camera

CarCamera.cs provides a smooth third-person follow camera using SmoothDamp for position and Slerp for rotation. It feels weighty without being jittery.

  1. Select the Main Camera.
  2. Add Component → CarCamera.
  3. Target: assign the car root's Transform.
  4. Offset: (0, 4, -9) — 4 m above, 9 m behind.
  5. Smooth Time: 0.18
csharp — CarCamera.cs
private void LateUpdate()
{
    if (target == null) return;

    Vector3 desiredPos = target.position + target.rotation * offset;
    transform.position = Vector3.SmoothDamp(
        transform.position, desiredPos, ref _velocity, smoothTime);

    Quaternion desiredRot = Quaternion.LookRotation(
        target.position - transform.position);
    _currentRot = Quaternion.Slerp(
        _currentRot, desiredRot, Time.deltaTime / rotationSmoothTime);
    transform.rotation = _currentRot;
}
A5

Single Player Test

Set up a minimal test scene before adding multiplayer.

  1. File → New Scene, save as Assets/Scenes/RacingScene.unity.
  2. Add a Directional Light if not present.
  3. Create a Track GameObject with TrackBuilder — click Rebuild Track.
  4. Drag the unpacked ARCADE car to the scene. Position at SpawnPoint_0.
  5. Set Main Camera → CarCamera.target = the car root's Transform.

Test controls

KeyAction
W / ↑Accelerate
S / ↓Brake / reverse
A/D / ←→Steer
SpaceHandbrake

Press Play and drive around the track. If the car flips easily, lower motorTorque to 800 or increase Rigidbody mass. If steering is sluggish, increase maxSteerAngle to 35.


B1

NetCrafter Schema

Open the NetCrafter window: Tools → Pivec Labs → NetCrafter Pro. Click New Schema and name it Racing.

SmartFox Settings tab

FieldValue
Host127.0.0.1
Zone NameRacingZone
Extension NameRacing
Extension Classcom.piveclabs.racing.RacingExtension
TCP Port9977
WebSocket Port9977
Auto-Reconnect✓

Extension Name and Extension Class are only required for server code generation (Part B2) and config generation (Part B3). The connection will work without them, but the server JAR and zone config won't be generated correctly.

Project Settings tab

FieldValue
NamespaceRacing
Overwrite Files✓

Gameplay Messages tab — ClientToServer

Create one ClientToServer message named CarUpdate with these exact settings:

  • Direction: ClientToServer
  • Transport: ExtensionRequest
  • Server Handler Name: HandleCarUpdate

This message has 9 fields. Add them one by one using the + Add Field button:

#Field NameTypeIs Array
1posXfloatNo
2posYfloatNo
3posZfloatNo
4rotXfloatNo
5rotYfloatNo
6rotZfloatNo
7rotWfloatNo
8speedfloatNo
9colorIndexintNo
⚠
Important

Set Server Handler Name to HandleCarUpdate BEFORE adding fields. The handler name links this message to the server action you will create in the next step.

Gameplay Messages tab — ServerToClient

Create three ServerToClient events:

Leave Server Handler Name blank for ServerToClient events

These events are sent by the server — they are not triggered by a client request, so no handler is needed. Setting a Handler Name on a ServerToClient event causes a validation error because no matching Server Action exists.

CarStateUpdate — Direction: ServerToClient, Transport: ExtensionEvent, Server Handler Name: (leave blank)

This event has 9 fields:

#Field NameTypeIs Array
1playerIdintNo
2posXfloatNo
3posYfloatNo
4posZfloatNo
5rotXfloatNo
6rotYfloatNo
7rotZfloatNo
8rotWfloatNo
9speedfloatNo

PlayerJoined — Direction: ServerToClient, Transport: ExtensionEvent, Server Handler Name: (leave blank)

This event has 4 fields:

#Field NameTypeIs Array
1playerIdintNo
2playerNamestringNo
3colorIndexintNo
4spawnIndexintNo

PlayerLeft — Direction: ServerToClient, Transport: ExtensionEvent, Server Handler Name: (leave blank)

This event has 1 field:

#Field NameTypeIs Array
1playerIdintNo

Server Actions tab

Only one Server Action is needed — HandleCarUpdate:

FieldValue
Handler NameHandleCarUpdate
Trigger MessageCarUpdate
Response EventCarStateUpdate
Logic TypeBroadcastAll

This handles the incoming CarUpdate message from each client and broadcasts it as CarStateUpdate to all other players. PlayerJoined and PlayerLeft are handled automatically by SFS3's room lifecycle events — the generated Java extension will handle these in Part B2.

Sync Rules tab

Create two sync rules:

#NameTargetModeInterval
1CarPositionTransform_PositionInterval50 ms
2CarRotationTransform_RotationInterval50 ms

These rules document the intended sync cadence. The actual network sending is handled by RacingNetworkManager, which bundles position and rotation together in a single CarUpdate message every 0.05 seconds — more efficient than sending them as separate messages.

Interval units

The Interval field is in milliseconds. Common values:

  • 20 ms = 0.02 s = 50 updates/second (very smooth — high bandwidth)
  • 50 ms = 0.05 s = 20 updates/second (recommended for fast-moving objects like cars)
  • 100 ms = 0.1 s = 10 updates/second (suitable for slower objects or chat presence)

Lower intervals mean smoother movement but more network traffic. 50 ms (20 Hz) is a good balance for racing games.

B2

Generate All Code

Generate client code

In the Code Generation tab, click Generate Client Code. Generated files:

generated files
Definitions/Racing/Generated/Client/
  SmartFoxConnectionManager.cs   ← connection + event dispatch
  CarUpdateRequest.cs         ← typed request sender
  CarStateUpdateEvent.cs      ← typed event receiver
  PlayerJoinedEvent.cs
  PlayerLeftEvent.cs

LocalUserId and LocalUserName

LocalUserId and LocalUserName are automatically included in the generated SmartFoxConnectionManager — no manual editing needed. RacingNetworkManager uses LocalUserId to identify the local player and assign colour/spawn index.

Enable network send/receive — add second scripting define

Now that SmartFoxConnectionManager exists, unlock the network code in the racing scripts by adding a second scripting define:

  1. Go to Edit → Project Settings → Player
  2. Find Scripting Define Symbols (under Other Settings)
  3. Add: NETCRAFTER_RACING_GENERATED
  4. Click Apply

This enables SmartFoxConnectionManager references in RacingNetworkManager.cs and RacingUIManager.cs — login, the 20 Hz position send loop, and local player ID assignment.

Generate server code

Click Generate Server Code. Generated files:

generated files
Definitions/Racing/Generated/Server/src/main/java/com/piveclabs/racing/
  RacingExtension.java
  handlers/
    HandleCarUpdate.java   ← reads position, broadcasts CarStateUpdate

Join/leave handling — auto-generated

The generated RacingExtension.java automatically handles all room lifecycle events — no manual editing needed:

  • Player join: sends PlayerJoined to all existing players, and sends a PlayerJoined for each existing player to the joining user
  • Player leave: sends PlayerLeft to all remaining players
  • Player disconnect: sends PlayerLeft to all remaining players in the disconnected user's last room

Build and deploy the JAR

Click Deploy to SFS3 in the NetCrafter window → Code → Server Code tab.

This builds RacingExtension.jar and copies it to SmartFoxServer3/server/extensions/Racing/.

JAR naming

SFS3 AdminTool only discovers JARs whose filename ends in Extension.jar. The generated pom.xml is already configured to produce RacingExtension.jar.

B3

Car Colour System

CarColorManager.cs applies a player colour to all body renderers on a car. Renderers whose gameObject.name contains "wheel" are skipped.

csharp — CarColorManager.cs
public class CarColorManager : MonoBehaviour
{
    public static readonly Color[] PlayerColors = new Color[]
    {
        Color.red,
        Color.blue,
        Color.green,
        Color.yellow,
        new Color(1f, 0.5f, 0f),  // orange
        Color.magenta,
        Color.cyan,
        Color.white,
    };

    public void SetColor(int colorIndex)
    {
        Color c = PlayerColors[colorIndex % PlayerColors.Length];
        foreach (var r in GetComponentsInChildren<Renderer>())
        {
            if (r.gameObject.name.ToLower().Contains("wheel")) continue;
            r.material.color = c;
        }
    }
}

colorIndex is assigned by the server as playerId % 8. Add CarColorManager to the car prefab so both local and remote cars can be coloured at spawn time.

B4

Scene Setup

⚠

Complete B4.1 and B4.2 before wiring any Scene Events or adding RacingNetworkManager. The NetCrafterEventDispatcher must be in the scene before Scene Events will work.

B4.1 — Create ConnectionManager Prefab

  1. Open the NetCrafter window → Scenes tab → Prefab Generator.
  2. Click Create ConnectionManager Prefab.
  3. Confirm the prefab was created at:
    Definitions/Racing/Generated/Prefabs/ConnectionManager.prefab

B4.2 — Create Multiplayer Scene

  1. Open the NetCrafter window → Scenes tab → Scene Assistant.
  2. Confirm that the ConnectionManager prefab is detected.
  3. Ensure the EventDispatcher toggle is checked.
  4. Click Create Multiplayer Scene and name it RacingScene.
    If your scene is already set up, click Add to Existing Scene instead.

The Scene Assistant adds the following GameObjects:

  • ConnectionManager (from generated prefab)
  • NetCrafterEventDispatcher
  • NetworkManager (empty GameObject — configured in B4.5)
  • Main Camera
  • SpawnPoints

B4.3 — Verify Scene Hierarchy

After Scene Assistant completes, your Hierarchy should contain:

  • ConnectionManager
  • NetCrafterEventDispatcher
  • NetworkManager (empty — RacingNetworkManager component added in B4.5)
  • SpawnPoints with children SpawnPoint_0 through SpawnPoint_3
  • Track (from Part A)
  • Car (car prefab — assigned in B4.5)
  • Main Camera

B4.4 — Wire Scene Events

Open the NetCrafter window → Scene Events tab for the Racing schema. Wire these three events:

EventTarget ObjectMethod
PlayerJoinedNetworkManagerRacingNetworkManager.OnPlayerJoined
PlayerLeftNetworkManagerRacingNetworkManager.OnPlayerLeft
CarStateUpdateNetworkManagerRacingNetworkManager.OnCarStateUpdate

The generated SmartFoxConnectionManager routes each incoming SFS3 extension event to the wired Unity method. Inside each method, the event payload is read via NetCrafterSceneEventRouter.Instance.GetLastPayload(eventName).

B4.5 — Add RacingNetworkManager

RacingNetworkManager.cs is the hub for all multiplayer logic. It:

  • Finds the existing scene car (placed in Part A) and uses it as the local player car
  • Spawns remote cars from Car Prefab on PlayerJoined
  • Destroys remote cars on PlayerLeft
  • Updates remote car target transforms on CarStateUpdate
  • Sends CarUpdate at 20 Hz via SendExtensionRequest

Scene car vs Car Prefab

The car you placed in the scene during Part A is used as your local player car. RacingNetworkManager finds it automatically via FindAnyObjectByType<CarController>() — do NOT delete it before entering multiplayer.

Remote players' cars are instantiated from the Car Prefab field. You need a prefab version of the same car so RacingNetworkManager can spawn one for each remote player who joins.

Create a Car Prefab from your scene car

  1. In the Hierarchy, select your Car GameObject.
  2. Drag it into the Project window under Assets/PivecLabs/Examples/Racing/Prefabs/ (create the folder if needed).
  3. Name the prefab CarPrefab.
  4. The original car remains in the scene — Unity creates the prefab as a copy. Do not remove the scene car.

Add RacingNetworkManager to the scene

  1. Select the NetworkManager GameObject created in B4.2.
  2. Add Component → RacingNetworkManager.
  3. Car Prefab: assign the CarPrefab you just created. This is used for remote players only — the scene car is used for the local player.
  4. Spawn Points: drag in the SpawnPoint_0 through SpawnPoint_3 transforms from the TrackBuilder's SpawnPoints child.

After adding RacingNetworkManager to the scene, drag the SpawnPoint_0 through SpawnPoint_3 GameObjects from the Hierarchy into the Spawn Points array in the Inspector. These references are saved with the scene — you only need to assign them once.

Car prefab checklist

  • ✅ CarController — all wheel references assigned, isLocalPlayer starts as true (set to false at runtime for remote cars)
  • ✅ CarColorManager — no config needed
  • ✅ Rigidbody — Mass: 1200
  • ✅ Four WheelCollider children configured

Save as Assets/PivecLabs/Examples/Racing/Prefabs/CarPrefab.prefab.

Typed events — generated after Part B2

RacingNetworkManager.cs uses the typed event classes generated in Part B2 (PlayerJoinedEvent, PlayerLeftEvent, CarStateUpdateEvent). These are only available after running Generate Client Code and adding NETCRAFTER_RACING_GENERATED. The #if NETCRAFTER_RACING_GENERATED guards in each callback ensure the scripts compile cleanly both before and after generation.

csharp — RacingNetworkManager.cs (key sections)
// Scene Event callbacks — wired in the NetCrafter Scene Events tab
// Typed events are available after Generate Client Code + NETCRAFTER_RACING_GENERATED.
public void OnPlayerJoined()
{
#if NETCRAFTER_RACING_GENERATED
    var payload = NetCrafterSceneEventRouter.Instance?
        .GetLastPayload("PlayerJoined");
    if (!(payload is PlayerJoinedEvent ev)) return;

    int localId = SmartFoxConnectionManager.Instance?.LocalUserId ?? -1;
    if (ev.playerId == localId) return;           // ignore echo of own join
    if (_remoteCars.ContainsKey(ev.playerId)) return;  // duplicate guard

    SpawnRemoteCar(ev.playerId, ev.playerName, ev.colorIndex, ev.spawnIndex);
    FindAnyObjectByType<RacingUIManager>()?.AddPlayerToList(ev.playerName);
#endif
}

public void OnPlayerLeft()
{
#if NETCRAFTER_RACING_GENERATED
    var payload = NetCrafterSceneEventRouter.Instance?
        .GetLastPayload("PlayerLeft");
    if (!(payload is PlayerLeftEvent ev)) return;

    string name = _remotePlayerNames.TryGetValue(ev.playerId, out var n) ? n : "";
    RemoveRemoteCar(ev.playerId);
    if (!string.IsNullOrEmpty(name))
        FindAnyObjectByType<RacingUIManager>()?.RemovePlayerFromList(name);
#endif
}

public void OnCarStateUpdate()
{
#if NETCRAFTER_RACING_GENERATED
    var payload = NetCrafterSceneEventRouter.Instance?
        .GetLastPayload("CarStateUpdate");
    if (!(payload is CarStateUpdateEvent ev)) return;

    if (ev.playerId == _localPlayerId) return;

    UpdateRemoteCar(
        ev.playerId,
        new Vector3(ev.posX, ev.posY, ev.posZ),
        new Quaternion(ev.rotX, ev.rotY, ev.rotZ, ev.rotW),
        ev.speed);
#endif
}
B5

Remote Car Lerp

When CarController.isLocalPlayer = false:

  • The Rigidbody is set to kinematic — no local physics simulation
  • All WheelCollider components are disabled
  • Each Update() lerps position and rotation toward the latest network values
  • Visual wheels are animated from networkSpeed (spin only — steering angle isn't part of CarStateUpdate, so remote front wheels don't visually turn)
csharp — remote update in CarController.cs
private void UpdateRemote()
{
    transform.position = Vector3.Lerp(
        transform.position,
        networkTargetPosition,
        Time.deltaTime * 10f);

    transform.rotation = Quaternion.Slerp(
        transform.rotation,
        networkTargetRotation,
        Time.deltaTime * 10f);

    // Accumulate wheel spin angle from received speed
    _wheelSpinAngle += networkSpeed * Time.deltaTime * (360f / (2f * Mathf.PI * 0.35f));
}

A lerp factor of 10 means the car visually reaches its target within ~100 ms. At 20 updates per second, remote cars look smooth even on a typical LAN with <50 ms ping.

B6

Login and Lobby UI

The UI has three panels on a single Canvas. Only one panel is visible at a time.

Create the Canvas and panels

  1. GameObject → UI → Canvas. Set Render Mode to Screen Space — Overlay.
  2. Inside the Canvas, create three empty child GameObjects: LoginPanel, WaitingPanel, RacePanel.

LoginPanel layout

ElementTypeNotes
TitleTMP_Text"Racing Multiplayer"
UsernameInputTMP_InputFieldPlaceholder: "Enter your name..."
LoginButtonButtononClick → RacingUIManager.OnLoginClicked

WaitingPanel layout

ElementTypeNotes
TitleTMP_Text"Waiting for players..."
PlayerListTextTMP_TextUpdated by AddPlayerToList()
StartRaceButtonButtononClick → OnStartRaceClicked; hidden until 2+ players

RacePanel layout

ElementTypeNotes
SpeedTextTMP_TextShows "120 km/h"
PositionTextTMP_TextShows "1st", "2nd"…
LapTextTMP_TextShows "Lap 1/3"

Wire up RacingUIManager

  1. Create an empty GameObject, name it UIManager.
  2. Add Component → RacingUIManager.
  3. Assign all panel GameObjects and UI element references in the Inspector.
  4. Set LoginButton.onClick → RacingUIManager.OnLoginClicked.
  5. Set StartRaceButton.onClick → RacingUIManager.OnStartRaceClicked.
B7

Scene Events Wiring

Open the NetCrafter window → Scene Events tab for the Racing schema. Wire these three events:

EventTarget ObjectMethod
PlayerJoinedNetworkManagerRacingNetworkManager.OnPlayerJoined
PlayerLeftNetworkManagerRacingNetworkManager.OnPlayerLeft
CarStateUpdateNetworkManagerRacingNetworkManager.OnCarStateUpdate

The generated SmartFoxConnectionManager routes each incoming SFS3 extension event to the wired Unity method. Inside each method, the event payload is read via NetCrafterSceneEventRouter.Instance.GetLastPayload(eventName).

B8

SFS3 Zone Config

The zone file RacingZone.zone.xml must be in SmartFoxServer3/server/zones/. If you followed this tutorial, it was created automatically. The key values:

xml — RacingZone.zone.xml
<zone>
  <name>RacingZone</name>
  <allowGuestUsers>true</allowGuestUsers>
  <guestUserNamePrefix>Guest#</guestUserNamePrefix>
  <maxUsers>100</maxUsers>
  <wordsFilter active="false"/>
  <floodFilter active="false"/>
  <rooms>
    <room>
      <name>GameRoom</name>
      <maxUsers>16</maxUsers>
      <isGame>false</isGame>
      <!-- ... full room structure with mmoSettings, roomVariables stubs ... -->
    </room>
  </rooms>
  <privilegeManager active="false"/>
  <extension>
    <name>Racing</name>
    <type>JAVA</type>
    <file>com.piveclabs.racing.RacingExtension</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>

⚠️ Every stub element is required. SFS3 3.0.2 throws NullPointerException on startup if wordsFilter, floodFilter, privilegeManager, buddyList, databaseManager, or udpSettings are absent — even when disabled.

B9

Full Test Sequence

1. Start SFS3

Launch sfs3-standalone.exe. Wait for the console to show the extension loaded. In AdminTool, Zones → RacingZone → Extensions should show Racing in green.

2. Player 1 (Unity Editor)

  1. Press Play.
  2. The LoginPanel appears. Enter a name (e.g., Driver1) and click Login.
  3. The WaitingPanel appears. "Waiting for players..."

3. Player 2 (Build or second instance)

Option A — File → Build and Run a standalone build, then run the .exe alongside Unity.

Option B — run a second Unity instance from the same project on the same machine.

Enable Run In Background

When testing two instances on the same machine, the Unity Editor pauses network processing when it loses focus. Enable Run In Background so both instances keep sending and receiving while you switch windows:
Edit → Project Settings → Player → Resolution and Presentation → Run In Background ✓

  1. Enter a different name (e.g., Driver2) and click Login.
  2. Both WaitingPanels now show Start Race.

4. Expected results

What to checkExpected
Car coloursDriver1 = red (index 0), Driver2 = blue (index 1)
Remote car visibleEach player sees the other's car at the correct spawn point
Remote car movementMoves with smooth lerp — no teleporting
DisconnectClosing one client removes its car from the other's scene

Debugging tips

  • Car doesn't appear: Confirm PlayerJoined is wired in Scene Events and NetworkManager is active in the scene hierarchy.
  • Remote car teleports: The lerp factor is applied per-frame — ensure Time.deltaTime is being used (it is in the provided code).
  • Build failed: Check the Tool Detection panel in the Server Code tab — it shows the Maven and JDK paths that will be used, detected live each time the tab is drawn.
  • AdminTool shows extension in red: Check SFS3 console for Java stack trace — usually a missing import or wrong FQN in the zone XML.
+

What's Next

The multiplayer foundation is in place. Here are natural next features:

Lap Timing

Add a trigger collider at the finish line. When a car crosses it, send a LapComplete message to the server. The extension records and broadcasts lap times.

Race Positions

Track each player's lap count and distance to next checkpoint on the server. Broadcast an ordered leaderboard event every second to drive the position HUD.

Power-ups

Place collectable GameObjects on the track. On collision, send a PowerUpCollected message. The server validates and broadcasts a boost or penalty to the collecting player.

Mobile Input

Replace Input.GetAxis in CarController with UI Toolkit on-screen buttons or Unity's new Input System touch actions — no code changes needed in the networking layer.

AI Opponents

Add non-networked waypoint-following AI cars using the same CarController with isLocalPlayer = false and a separate CarAI component that feeds network target values.

Leaderboard

Use SFS3's databaseManager to persist best lap times. Add a GetLeaderboard client message; the extension queries the DB and returns results as a LeaderboardData event.

🛣️

Custom Track Shapes

Extend TrackBuilder to support figure-8 tracks, L-shaped circuits, or import custom track meshes. The multiplayer sync system works with any track layout.

Car Skins (URP)

Replace the runtime material.color tint in CarColorManager with per-player texture swaps using URP material variants. Assign a Texture2D[] array of body textures and set _BaseMap on the car's material at spawn time — results in sharper, more distinct looks without batching overhead.