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.
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
PlayerLinkcomponent; - spawning a player entity with
ISignalOnPlayerAdded; - reading input through a custom Input struct and Unity's Input System;
- building a
SystemFilterto 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
- Go to
photonengine.comand create or sign in to a Photon account. - Click
Documentation > Quantum > Getting Started > Preview SDK 3.1 > Download. - In the Photon Dashboard, click
Create New App. - Leave Application Type as
Multiplayer Game. - Set Photon SDK to
Quantum. - Name the application, for example
Simple Brawler, and clickCreate. - 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
- Create a new Unity project on Unity 6.3 LTS using the URP pipeline.
- Import the downloaded Quantum
.unitypackage. - 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.
- 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.
Writing the PlayerLink Component (QTN)
Components, asset references, signals, and events are defined using Quantum's own DSL, written in .qtn files.
- In
QuantumUser/Simulation, create a folder calledQTN. - Inside it, create a file called
Player.qtn. - 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:
- In the
Simulationfolder, create aSystemsfolder. - Right-click and select
Quantum > SystemSignalsOnly. - Name it
PlayerJoinedSystem. - Implement
ISignalOnPlayerAdded, which provides three parameters: aFrame, thePlayerRefof the joining player, and abool FirstTimeindicating 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:
- Expand its
Local Playersarray to size1. Quantum defaults to spectator mode, so this step is required to have a controlled player. - Assign
CharacterAEntityPrototypeto the first element'sPlayerAvatarfield — a pre-configured entity prototype with basic components such asTransform3Dand its graphics.
Registering the System & First Play Test
Systems only run if they are registered in the simulation config:
- Search for
Config Systemin the project window. - Open the
QuantumDefaultConfigasset's Default Config System. - Add
PlayerJoinedSystemto 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:
- In
OnEnable, enable the movement and jump input actions. - Inside
PollInput, create a new input struct instance. - Read the move action's
Vector2value and convert it withToFpVector2()— safe to do on the Unity side, since it does not affect determinism. - Check whether the jump action is pressed and assign it to
input.Jump. - 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
- Add
PlayerMovementSystemto the systems list inQuantumDefaultConfig. - Link the movement and jump input actions to
QuantumDebugInput. - 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:
- Go to
Tools > Quantum > Setupand add the Start UI to the current scene. - Remove
QuantumLocalDebugRunnerfrom the scene. - In
Canvas/StartUI, expand thePlayersarray and assignCharacterAEntityPrototype. - Build a standalone build, for example windowed at
1024x768.
Testing Online: Editor + Standalone Build
- Select the appropriate region (for example
EU). - Press Play in the editor.
- 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
.qtnfiles rather than hand-writing generated structs. - Use
SystemSignalsOnlyfor logic that only needs to react to events, andSystemMainThreadFilterfor logic that runs every frame. - Represent all simulation-side vectors with Quantum's
FPVector2/FPVector3, never Unity'sVector2/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- Overview
- Video Timeline
- Downloading Quantum 3.1 & Creating an App Id
- Unity Project Setup & Quantum Hub
- Quantum's ECS: Components, Systems, Signals
- Writing the PlayerLink Component (QTN)
- Codegen in Action
- Creating the PlayerJoinedSystem
- Spawning the Player (ISignalOnPlayerAdded)
- Setting Up QuantumLocalDebugRunner
- Registering the System & First Play Test
- Defining Input in Quantum
- Reading Input From Unity's Input System
- Building the Movement & Jump System
- Testing Movement, Adding Start UI & Building Standalone
- Testing Online: Editor + Standalone Build
- Key Concepts
- Best Practices
- Common Pitfalls
- Summary