This document is about: QUANTUM 3
SWITCH TO

Simple Brawler Tutorial Part 1: Build a Multiplayer Brawler From Scratch

This page accompanies the Build a Multiplayer Brawler From Scratch (Part 1) - Photon Quantum 3.1 Simple Brawler Tutorial video and explains how to go from a blank Unity 6 project to a moving, jumping, networked character using Photon Quantum 3.1.

Use this page as a reference for the first entry in the Simple Brawler series. It assumes you already know what Photon Quantum is; if not, watch the Quantum introduction video linked below first. Future parts of the series cover animations, life/death cycles, and proper spawning.

Build a Multiplayer Brawler From Scratch (Part 1) - Photon Quantum 3.1 Simple Brawler Tutorial

Overview

Simple Brawler is a hands-on series that builds a small multiplayer party game with Photon Quantum, step by step, using a downloadable sample project.

The end goal of the series is a character with a basic attack skill that knocks other characters and objects off a platform, where the last character standing wins. Part 1 covers only the foundation: downloading Quantum, setting up a Unity project, and getting a networked character that can move and jump.

The video covers:

  • downloading Quantum 3.1 and creating a Photon App Id;
  • setting up a Unity 6 (URP) project with the Quantum Hub;
  • the basics of Quantum's ECS: Components, Systems, and Signals;
  • writing a QTN file and a PlayerLink component;
  • spawning a player entity with ISignalOnPlayerAdded;
  • reading input through a custom Input struct and Unity's Input System;
  • building a SystemFilter to move and jump the character;
  • registering systems in the default config;
  • running a local build online with the Start UI.

Video Timeline

Time Section
00:00 Introduction & What We're Building
01:30 Quick Quantum Recap
01:40 Downloading Quantum 3.1 & Creating an App Id
02:10 Unity Project Setup & Quantum Hub
03:00 Quantum's ECS: Components, Systems, Signals
03:20 Writing the PlayerLink Component (QTN)
04:25 Codegen in Action
04:50 Creating the PlayerJoinedSystem
05:20 Spawning the Player (ISignalOnPlayerAdded)
06:40 Setting Up QuantumLocalDebugRunner
07:45 Registering the System & First Play Test
08:30 Defining Input in Quantum
09:00 Reading Input From Unity's Input System
09:45 Building the Movement & Jump System
10:45 Testing Movement, Adding Start UI & Building Standalone
11:45 Testing Online: Editor + Standalone Build
12:05 Wrap-Up & What's Next

Downloading Quantum 3.1 & Creating an App Id

  1. Go to photonengine.com and create or sign in to a Photon account.
  2. Click Documentation > Quantum > Getting Started > Preview SDK 3.1 > Download.
  3. In the Photon Dashboard, click Create New App.
  4. Leave Application Type as Multiplayer Game.
  5. Set Photon SDK to Quantum.
  6. Name the application, for example Simple Brawler, and click Create.
  7. Copy the App Id shown above the application name — it is needed during Unity project setup.

Quantum 3.1 was in preview at the time of recording, so the exact downloaded version may differ, but the APIs used in the series are stable across releases.

Unity Project Setup & Quantum Hub

  1. Create a new Unity project on Unity 6.3 LTS using the URP pipeline.
  2. Import the downloaded Quantum .unitypackage.
  3. The Photon Quantum Hub window opens automatically. Work through its steps:
  • install the Quantum User files, which store all Unity and Quantum scripts and prefabs;
  • paste the App Id copied from the dashboard;
  • skip the Quantum Asteroids sample and mark the step as completed;
  • leave the Quantum Log Level at Info.
  1. Close the Quantum Hub. It can be reopened at any time via Tools > Quantum > Quantum Hub.

Completing the Hub's first step opens the main scene, QuantumGameScene. Third-party assets for the map, characters, and props are imported under QuantumUser/Resources.

Quantum's ECS: Components, Systems, Signals

Quantum uses an ECS (Entity Component System) design:

Concept Role
Entity An ID with no behavior or data of its own.
Component A pure data container attached to an entity.
System Logic that reads and modifies component data.
Signal A way for systems to react to simulation events, such as a player joining.

Because components cannot modify their own data, a system is required whenever component data needs to change.

Components, asset references, signals, and events are defined using Quantum's own DSL, written in .qtn files.

  1. In QuantumUser/Simulation, create a folder called QTN.
  2. Inside it, create a file called Player.qtn.
  3. Define a component that stores which player an entity belongs to:

component PlayerLink
{
    PlayerRef Player;
}

Codegen in Action

After saving the .qtn file, Quantum runs code generation automatically in the background.

The generated PlayerLink struct can be found under Quantum.CodeGen.Core.cs. Writing the component in the .qtn file generates the full struct automatically, avoiding the need to hand-write this boilerplate.

Creating the PlayerJoinedSystem

Quantum offers several system base types, including:

System Type Provides
SystemMainThreadFilter An Update method that runs every frame against a filter of components.
SystemSignalsOnly No Update method; used purely to react to signals.

To create a player-spawning system, only signal handling is needed:

  1. In the Simulation folder, create a Systems folder.
  2. Right-click and select Quantum > SystemSignalsOnly.
  3. Name it PlayerJoinedSystem.
  4. Implement ISignalOnPlayerAdded, which provides three parameters: a Frame, the PlayerRef of the joining player, and a bool FirstTime indicating whether the player is reconnecting.

Spawning the Player (ISignalOnPlayerAdded)

Inside OnPlayerAdded, retrieve the entity prototype the player brought with them, spawn it, attach PlayerLink, and set its starting position:

C#

public void OnPlayerAdded(Frame frame, PlayerRef player, bool firstTime)
{
    var entityPrototype = frame.GetPlayerData(player).PlayerAvatar;
    var entity = frame.Create(entityPrototype);

    frame.Add(entity, new PlayerLink() { Player = player });

    var transform3D = frame.Unsafe.GetPointer<Transform3D>(entity);
    transform3D->Position = FPVector3.Zero;
}

Because Transform3D and other components are structs, modifying them requires retrieving a pointer to their memory address through frame.Unsafe.GetPointer<T>.

Setting Up QuantumLocalDebugRunner

QuantumLocalDebugRunner in the scene is used to run the Quantum simulation locally:

  1. Expand its Local Players array to size 1. Quantum defaults to spectator mode, so this step is required to have a controlled player.
  2. Assign CharacterAEntityPrototype to the first element's PlayerAvatar field — a pre-configured entity prototype with basic components such as Transform3D and its graphics.

Registering the System & First Play Test

Systems only run if they are registered in the simulation config:

  1. Search for Config System in the project window.
  2. Open the QuantumDefaultConfig asset's Default Config System.
  3. Add PlayerJoinedSystem to the systems list.

At this point, pressing Play spawns the player entity, but the character remains static since no input is being read yet.

Defining Input in Quantum

Input is defined in the same Player.qtn file using Quantum's own types, since Unity's Vector2/Vector3 are float-based and not deterministic:

input
{
    FPVector2 Movement;
    button Jump;
}

button is a special input type in Quantum that tracks when it was pressed or released.

Reading Input From Unity's Input System

Input is read and sent to the simulation from the QuantumDebugInput object in the scene, inside its PollInput method:

  1. In OnEnable, enable the movement and jump input actions.
  2. Inside PollInput, create a new input struct instance.
  3. Read the move action's Vector2 value and convert it with ToFpVector2() — safe to do on the Unity side, since it does not affect determinism.
  4. Check whether the jump action is pressed and assign it to input.Jump.
  5. Send the input to the simulation:

C#

callback.SetInput(input, DeterministicInputFlags.Repeatable);

Building the Movement & Jump System

Create a new SystemFilter system called PlayerMovementSystem.

A filter defines which entities the system runs against. Besides EntityRef, at least one component pointer field is required:

C#

public struct Filter
{
    public EntityRef Entity;
    public PlayerLink* PlayerLink;
    public PhysicsBody3D* PhysicsBody;
}

This system runs on every entity that has at least PlayerLink and PhysicsBody3D — additional components on the entity do not exclude it.

Define movement and jump tuning values, then read input and apply forces:

C#

public static readonly FP MoveForce = 40;
public static readonly FP JumpImpulse = 10;

public override void Update(Frame frame, ref Filter filter)
{
    Input* input = frame.GetPlayerInput(filter.PlayerLink->Player);

    FPVector3 moveDirection = new FPVector3(input->Movement.X, FP._0, input->Movement.Y).Normalized;
    filter.PhysicsBody->AddForce(moveDirection * MoveForce);

    if (input->Jump.WasPressed)
    {
        filter.PhysicsBody->AddLinearImpulse(new FPVector3(FP._0, JumpImpulse, FP._0));
    }
}

PhysicsBody3D behaves similarly to Unity's Rigidbody.

Testing Movement, Adding Start UI & Building Standalone

  1. Add PlayerMovementSystem to the systems list in QuantumDefaultConfig.
  2. Link the movement and jump input actions to QuantumDebugInput.
  3. Press Play to confirm the character moves and jumps. Movement may feel floaty at this stage — this is addressed later in the series.

To test online instead of through the local debug runner:

  1. Go to Tools > Quantum > Setup and add the Start UI to the current scene.
  2. Remove QuantumLocalDebugRunner from the scene.
  3. In Canvas/StartUI, expand the Players array and assign CharacterAEntityPrototype.
  4. Build a standalone build, for example windowed at 1024x768.

Testing Online: Editor + Standalone Build

  1. Select the appropriate region (for example EU).
  2. Press Play in the editor.
  3. Launch the standalone build and join the same session.

With no custom netcode written, both the editor and standalone build connect into a working online session, and the character can be controlled with either a keyboard or a controller through Unity's Input System.

Key Concepts

Concept Description
QTN file A file using Quantum's DSL to define components, inputs, signals, and events.
PlayerLink Component storing the PlayerRef an entity belongs to.
SystemSignalsOnly A system type used purely to react to signals, with no Update method.
SystemMainThreadFilter A system type with an Update method that runs against a filter each frame.
ISignalOnPlayerAdded Signal interface used to spawn an entity when a player joins.
frame.GetPlayerData Retrieves the data a joining player brought with them, including their avatar prototype.
frame.Unsafe.GetPointer<T> Retrieves a writable pointer to a component's memory for a given entity.
Input struct Deterministic, Quantum-native representation of player input.
button A Quantum input type that tracks press/release state.
QuantumLocalDebugRunner Runs the Quantum simulation locally for one or more debug players.
SystemFilter Defines which component combination a system's Update runs against.
Start UI Prefab used to connect to an online Quantum session instead of the local debug runner.

Best Practices

Use this checklist when setting up a new Quantum project:

  • Create a dedicated Photon App Id per project rather than reusing one across unrelated projects.
  • Install the Quantum User files during Hub setup, since Unity and Quantum scripts and prefabs live there.
  • Define components, inputs, signals, and events in .qtn files rather than hand-writing generated structs.
  • Use SystemSignalsOnly for logic that only needs to react to events, and SystemMainThreadFilter for logic that runs every frame.
  • Represent all simulation-side vectors with Quantum's FPVector2/FPVector3, never Unity's Vector2/Vector3.
  • Convert Unity input to Quantum's fixed-point types (for example ToFpVector2()) only on the Unity side, before sending it to the simulation.
  • Expand QuantumLocalDebugRunner's Local Players array when testing locally, since Quantum defaults to spectator mode.
  • Switch to the Start UI and a standalone build to validate an actual online session, not just the local debug runner.

Common Pitfalls

Pitfall Why It Is a Problem
Forgetting to register a new system in the Default Config The system's logic never runs, even though it compiles correctly.
Using Unity's Vector2/Vector3 inside the simulation Float-based Unity vectors are not deterministic and break Quantum's determinism model.
Modifying a component by value instead of by pointer Structs are copies; changes made without frame.Unsafe.GetPointer<T> are lost.
Leaving QuantumLocalDebugRunner's Local Players array empty Quantum defaults to spectator mode, so no controllable player is spawned.
Adding a SystemFilter with only an EntityRef field At least one component pointer is required for the filter to match entities.
Forgetting to link input actions to QuantumDebugInput No input reaches the simulation, so the character appears static.

Summary

Part 1 of the Simple Brawler series takes a blank Unity 6 (URP) project to a networked, controllable character using Photon Quantum 3.1. After downloading Quantum and creating a Photon App Id, the Quantum Hub sets up the project and imports the Quantum User files.

A PlayerLink component, written in a .qtn file, tracks which player owns each entity. PlayerJoinedSystem reacts to ISignalOnPlayerAdded to spawn the player's entity prototype and attach PlayerLink. A Quantum-native Input struct carries movement and jump input from Unity's Input System into the simulation, and PlayerMovementSystem — a SystemFilter over PlayerLink and PhysicsBody3D — applies force and impulse to move and jump the character.

Once the system is registered in the Default Config and the Start UI replaces the local debug runner, the same project runs as a working online session between the editor and a standalone build, with no custom netcode required. Later parts of the series add animations, life/death cycles, and proper spawning.

Last updated on

Back to top