Under Construction

Prometheus Lab

Portfolio & Resources

Back to Blog
Unity25 min4 views

Photon Quantum

Author

Minh Khoa

Author

1. Photon Quantum là gì?

image.pngPhoton Quantum là một multiplayer engine deterministic cho Unity, dùng kiến trúc ECS và predict/rollback networking. Nói gọn:

  • Unity lo render, animation view, UI, audio, input device.
  • Quantum lo simulation: game state, physics deterministic, rules, damage, spawn, win/lose.
  • Network không sync transform từng object như kiểu truyền thống. Các client chủ yếu trao đổi input; vì simulation deterministic nên cùng input sẽ ra cùng kết quả.

Nếu Fusion/PUN thường khiến bạn nghĩ về NetworkObject, RPC, state replication, interpolation, authority, thì Quantum bắt bạn nghĩ theo kiểu: "Toàn bộ game là một máy mô phỏng theo tick. Tất cả client chạy cùng một máy. Server giúp xác nhận input và điều phối thời gian."

Quantum hợp với game cần phản hồi nhanh và fairness cao:

  • Fighting game, sports, racing, arena action.
  • Top-down shooter, brawler, MOBA nhỏ, tactical realtime.
  • Game physics/network phức tạp mà bạn không muốn tự viết rollback netcode.

Quantum không phải lựa chọn đầu tiên cho game idle/offline thuần, UI-heavy social app, hoặc multiplayer đơn giản chỉ cần sync vài biến.

2. Ý tưởng cốt lõi: deterministic + rollback

Trong multiplayer truyền thống, client A nói "tôi đang ở vị trí X", client B nhận và nội suy. Dễ hiểu nhưng dễ lệch, dễ cheat, và physics sync thường đau.

Trong Quantum:

  1. Mỗi client gửi input theo tick.
  2. Mỗi client tự chạy simulation cục bộ.
  3. Nếu input thật từ server đến khác với input đã đoán, Quantum rollback về frame cũ rồi re-simulate.
  4. Vì simulation deterministic, kết quả sau re-simulate sẽ giống nhau giữa các client.

Điều này tạo cảm giác ít lag hơn lockstep cổ điển vì client không phải chờ tất cả người chơi trước khi mô phỏng frame kế tiếp. Cái giá phải trả là code gameplay phải tuyệt đối deterministic.

3. Mental model: Quantum là game engine nhỏ nằm bên trong Unity

Hãy chia não thành 2 vùng:

Quantum Simulation

Đây là "sự thật" của gameplay:

  • Entity, Component, System.
  • Player input, command, event.
  • Transform2D/3D deterministic.
  • Physics 2D/3D deterministic, KCC, navigation.
  • Fixed-point math: FPFPVector2FPVector3.
  • Không phụ thuộc MonoBehaviourGameObjectTime.timeUnityEngine.Random, float physics của Unity.

Unity View

Đây là phần trình diễn:

  • Model, sprite, animator, particle, sound.
  • UI health bar, floating text, camera.
  • Input polling từ keyboard/gamepad/mobile.
  • Subscribe event từ Quantum để bật VFX/SFX.
  • Đọc state từ Quantum để hiển thị.

Luật vàng: Quantum quyết định chuyện gì xảy ra; Unity chỉ làm nó trông hay hơn.

4. Cấu trúc project cơ bản

Sau khi cài Quantum 3, project thường có:

Assets/
  Photon/          # SDK Photon/Quantum, thường không sửa trực tiếp
  QuantumUser/     # code của game
    Simulation/    # logic deterministic, .qtn, systems
    View/          # Unity-side scripts: input, UI, view glue
    Resources/     # config, assets runtime

Khi upgrade SDK, Assets/Photon có thể bị thay thế. Code game nên sống trong Assets/QuantumUser.

5. ECS trong Quantum

Quantum dùng Entity Component System:

  • Entity: ID của object trong simulation.
  • Component: dữ liệu thuần, ví dụ HealthPlayerLinkWeaponInventory.
  • System: logic chạy mỗi tick, ví dụ MovementSystemCombatSystem.
  • Frame: snapshot game state hiện tại. Mọi đọc/ghi simulation đi qua Frame.

Ví dụ component:

component Health {
  FP Current;
  FP Max;
}

component PlayerLink {
  player_ref PlayerRef;
}

Ví dụ system:

namespace Quantum {
  using Photon.Deterministic;
  using UnityEngine.Scripting;

  [Preserve]
  public unsafe class RegenSystem : SystemMainThreadFilter<RegenSystem.Filter> {
    public struct Filter {
      public EntityRef Entity;
      public Health* Health;
    }

    public override void Update(Frame frame, ref Filter filter) {
      filter.Health->Current = FPMath.Min(
        filter.Health->Max,
        filter.Health->Current + FP._1 * frame.DeltaTime
      );
    }
  }
}

[Preserve] quan trọng để Unity stripping không loại bỏ system.

6. DSL .qtn: nơi khai báo game state

Quantum dùng DSL riêng, thường lưu trong file .qtn, để khai báo data deterministic. Từ đó Quantum codegen ra C#.

Bạn có thể khai báo:

  • component
  • struct
  • input
  • signal
  • event
  • asset
  • enumflagsunion
  • dynamic collections như list<T>dictionary<K,V>hash_set<T>

Ví dụ một file Player.qtn:

input {
  FPVector2 Move;
  button Dash;
  button Attack;
}

component PlayerLink {
  player_ref PlayerRef;
}

component Fighter {
  FP MoveSpeed;
  FP DashCooldown;
  FP AttackCooldown;
}

event PlayerAttacked {
  entity_ref Attacker;
  FPVector2 Position;
}

signal OnDamage(FP amount, entity_ref target);

Điểm quan trọng: game state trong Quantum cần blittable/deterministic. Đừng nhét object C#, reference Unity, GameObjectTransformDateTimefloat tùy tiện vào simulation.

7. Input: thứ được gửi mỗi tick

Input là dữ liệu nhỏ, lặp lại liên tục. Dùng cho hành động realtime:

  • Move direction.
  • Jump/dash/fire button.
  • Aim direction.
  • Skill slot đang giữ.

Định nghĩa input:

input {
  FPVector2 Move;
  button Fire;
}

Unity poll input rồi đưa cho Quantum:

using Photon.Deterministic;
using Quantum;
using UnityEngine;

public class LocalQuantumInput : MonoBehaviour {
  private void OnEnable() {
    QuantumCallback.Subscribe(this, (CallbackPollInput callback) => PollInput(callback));
  }

  private void PollInput(CallbackPollInput callback) {
    var input = new Quantum.Input();

    var move = new Vector2(
      UnityEngine.Input.GetAxisRaw("Horizontal"),
      UnityEngine.Input.GetAxisRaw("Vertical")
    );

    input.Move = move.normalized.ToFPVector2();
    input.Fire = UnityEngine.Input.GetKey(KeyCode.Space);

    callback.SetInput(input, DeterministicInputFlags.Repeatable);
  }
}

Với button, hãy poll trạng thái hiện tại bằng GetKey, không dùng GetKeyDown/GetKeyUp. Quantum tự tính WasPressedIsDownWasReleased trong simulation theo tick.

Trong simulation:

namespace Quantum {
  using Photon.Deterministic;
  using UnityEngine.Scripting;

  [Preserve]
  public unsafe class PlayerMoveSystem : SystemMainThreadFilter<PlayerMoveSystem.Filter> {
    public struct Filter {
      public EntityRef Entity;
      public Transform2D* Transform;
      public PlayerLink* PlayerLink;
      public Fighter* Fighter;
    }

    public override void Update(Frame frame, ref Filter filter) {
      Input* input = frame.GetPlayerInput(filter.PlayerLink->PlayerRef);
      if (input == null) {
        return;
      }

      filter.Transform->Position +=
        input->Move * filter.Fighter->MoveSpeed * frame.DeltaTime;

      if (input->Fire.WasPressed) {
        frame.Events.PlayerAttacked(filter.Entity, filter.Transform->Position);
      }
    }
  }
}

Tối ưu input: giữ input càng nhỏ càng tốt. Ví dụ nhiều game encode direction từ FPVector2 sang Byte để giảm bandwidth.

8. Command: hành động không cần gửi mỗi tick

Command giống input nhưng dùng cho hành động thỉnh thoảng mới xảy ra:

  • Mua item.
  • Chọn hero.
  • Vote surrender.
  • Spawn enemy từ debug UI.
  • Teleport admin/debug.

Ví dụ command:

namespace Quantum {
  using Photon.Deterministic;

  public class CommandBuyItem : DeterministicCommand {
    public int ItemId;

    public override void Serialize(BitStream stream) {
      stream.Serialize(ref ItemId);
    }
  }
}

Gửi từ Unity:

QuantumRunner.Default.Game.SendCommand(new Quantum.CommandBuyItem {
  ItemId = 1001
});

Đọc trong simulation:

namespace Quantum {
  using UnityEngine.Scripting;

  [Preserve]
  public unsafe class PlayerCommandSystem : SystemMainThread {
    public override void Update(Frame frame) {
      for (int i = 0; i < frame.PlayerCount; i++) {
        var command = frame.GetPlayerCommand(i) as CommandBuyItem;
        if (command == null) {
          continue;
        }

        // Validate trong simulation: tiền đủ không, item hợp lệ không, cooldown không...
        // Sau đó mới mutate game state.
      }
    }
  }
}

Quy tắc: input cho điều khiển realtime; command cho quyết định hiếm, dữ liệu to hơn, không cần gửi liên tục.

9. Events và Callbacks: nói chuyện từ simulation ra Unity

Event dùng để báo "một chuyện vừa xảy ra" cho view:

  • Player bị hit -> bật VFX, camera shake.
  • Pickup coin -> bật âm thanh.
  • Match ended -> mở result UI.
  • Bullet fired -> spawn muzzle flash.

Định nghĩa event:

event DamageTaken {
  entity_ref Target;
  FP Amount;
  FPVector2 Position;
}

Trigger trong simulation:

frame.Events.DamageTaken(target, damage, hitPosition);

Subscribe trong Unity:

using Quantum;
using UnityEngine;

public class DamageView : MonoBehaviour {
  private void OnEnable() {
    QuantumEvent.Subscribe<EventDamageTaken>(this, OnDamageTaken);
  }

  private void OnDamageTaken(EventDamageTaken e) {
    Debug.Log($"Damage {e.Amount} at tick {e.Tick}");
    // Spawn VFX/SFX/UI here.
  }
}

Event không nên dùng để sửa game state. Muốn hệ thống simulation nói chuyện với nhau, dùng signal.

Do rollback, event có vài sắc thái:

  • Event thường có thể phát sinh trong predicted frame.
  • Quantum có cơ chế tránh gọi trùng event ở view.
  • synced event chỉ được dispatch khi frame đã được server xác nhận, ít sai hơn nhưng có delay.
  • Event data nên tự đủ dùng; đừng phụ thuộc quá nhiều vào việc sau này resolve lại entity/component vì frame gốc có thể không còn.

10. Signals: giao tiếp giữa các system trong simulation

Signal là callback deterministic bên trong Quantum. Dùng để tách hệ thống:

signal OnDamage(FP amount, entity_ref target);

System phát signal:

frame.Signals.OnDamage(FP._10, enemyEntity);

System nghe signal:

namespace Quantum {
  using Photon.Deterministic;
  using UnityEngine.Scripting;

  [Preserve]
  public unsafe class DamageSystem : SystemSignalsOnly, ISignalOnDamage {
    public void OnDamage(Frame frame, FP amount, EntityRef target) {
      if (frame.Unsafe.TryGetPointer(target, out Health* health)) {
        health->Current = FPMath.Max(FP._0, health->Current - amount);
      }
    }
  }
}

Event là simulation -> Unity view. Signal là simulation -> simulation.

11. Entity Prototype và Asset

Quantum khuyến khích data-driven workflow:

  • QuantumEntityPrototype: prefab/entity data được author trong Unity.
  • AssetObject: data asset dùng trong simulation, gần giống ScriptableObject nhưng có AssetRef.
  • RuntimePlayer: dữ liệu player gửi vào simulation, ví dụ avatar, nickname, loadout.

Ví dụ spawn avatar khi player vào game:

namespace Quantum {
  using UnityEngine.Scripting;

  [Preserve]
  public unsafe class PlayerSpawnSystem : SystemSignalsOnly, ISignalOnPlayerAdded {
    public void OnPlayerAdded(Frame frame, PlayerRef player, bool firstTime) {
      RuntimePlayer data = frame.GetPlayerData(player);
      var prototype = frame.FindAsset<EntityPrototype>(data.PlayerAvatar);

      EntityRef entity = frame.Create(prototype);
      frame.Add(entity, new PlayerLink { PlayerRef = player });
    }
  }
}

Tư duy đúng: designer chỉnh prototype/asset trong Unity; programmer viết system đọc data đó một cách deterministic.

12. Frame: nơi chứa toàn bộ game state

Frame là object quan trọng nhất trong Quantum simulation. Nó cho bạn:

  • Tick hiện tại.
  • DeltaTime deterministic.
  • Entity/component API.
  • Input của player.
  • Asset database.
  • Events/signals.
  • Random deterministic nếu dùng API của Quantum.
  • Trạng thái predicted/verified.

Quantum phân biệt:

  • Predicted frame: frame client tự đoán để giảm latency.
  • Verified frame: frame đã có input được server xác nhận, đáng tin cậy giữa các client.

Bạn thường viết gameplay không cần tự rollback. Quantum lo. Nhưng bạn phải viết code deterministic để rollback/re-simulate ra cùng kết quả.

13. Physics, KCC, navigation

Quantum có bộ thư viện deterministic riêng:

  • Fixed-point math.
  • 2D/3D physics.
  • Collider/body/callback/joint.
  • Kinematic Character Controller 2D/3D.
  • NavMesh/pathfinding/steering/avoidance.

Đừng dùng Unity Physics để quyết định gameplay multiplayer deterministic. Unity Physics/Transform có thể dùng cho view, nhưng sự thật gameplay nên nằm trong Quantum physics/state.

Ví dụ:

  • Hitbox của đòn đánh: Quantum physics query.
  • Player position thật: Transform2D/Transform3D trong Quantum.
  • Model chạy animation: Unity view đọc theo entity view.

14. Ví dụ thực tế: mini arena "Sushi Duel"

Giả sử bạn làm mode PvP nhỏ trong game sushi:

  • 2 người chơi tranh nguyên liệu rơi trên map.
  • Nhặt cá, dash, đẩy đối thủ, nấu combo.
  • Sau 90 giây ai nhiều điểm hơn thắng.

Thiết kế Quantum:

input {
  FPVector2 Move;
  button Dash;
  button Interact;
}

component PlayerLink {
  player_ref PlayerRef;
}

component Chef {
  FP Speed;
  FP DashCooldown;
  Int32 Score;
}

component Ingredient {
  Int32 TypeId;
  Int32 Point;
}

event IngredientPicked {
  entity_ref Player;
  Int32 TypeId;
  FPVector2 Position;
}

Luồng gameplay:

  1. Unity poll move/dash/interact.
  2. ChefMoveSystem di chuyển chef bằng Quantum transform/physics.
  3. PickupSystem kiểm tra overlap giữa chef và ingredient.
  4. Khi pickup hợp lệ, tăng Chef.Score, destroy ingredient entity.
  5. Trigger IngredientPicked event.
  6. Unity nhận event, bật sound, particle, floating score.
  7. MatchTimerSystem kết thúc trận khi timer về 0, trigger result event.

Nhờ deterministic, cả 2 máy cùng nhận input theo tick và cùng ra kết quả: ai nhặt trước, điểm bao nhiêu, ingredient biến mất lúc nào.

15. Checklist sống còn khi viết Quantum

  • Không dùng float cho gameplay simulation. Dùng FP.
  • Không dùng UnityEngine.Random; dùng random deterministic từ Quantum/frame.
  • Không đọc Time.timeDateTime.Now, framerate, local clock trong simulation.
  • Không mutate GameObject/Transform trong simulation system.
  • Không gửi mọi thứ qua event/command. Giữ game state trong component.
  • Input phải nhỏ và gửi mỗi tick; command chỉ cho hành động hiếm.
  • Poll button bằng trạng thái hiện tại, không dùng GetKeyDown/GetKeyUp.
  • Event cho VFX/UI/SFX; signal cho logic simulation.
  • Data event nên đủ tự mô tả.
  • Dynamic collection phải allocate/free đúng cách, rồi set default sau khi free.
  • System order quan trọng. Đăng ký system trong SystemConfig có chủ đích.
  • Test bằng local multiplayer, high latency simulation, replay/checksum.
  • Khi có desync, nghi ngờ đầu tiên: non-deterministic code, random sai, float, collection lifecycle, data asset khác nhau giữa client.

16. Khi nào nên dùng Quantum, khi nào không?

Nên dùng Quantum nếu:

  • Gameplay cần cạnh tranh realtime và phản hồi nhanh.
  • Physics/game state phức tạp, khó sync bằng transform replication.
  • Bạn muốn rollback netcode nhưng không muốn tự xây engine.
  • Team chấp nhận học ECS, fixed-point, codegen, deterministic discipline.

Cân nhắc Fusion/PUN hoặc giải pháp khác nếu:

  • Game chỉ co-op nhẹ, ít object, không cần rollback.
  • Game thiên về room/chat/social hơn là simulation.
  • Bạn cần server-authoritative state sync kiểu truyền thống.
  • Team chưa sẵn sàng tách gameplay khỏi Unity MonoBehaviour.

17. Cách học nhanh nhất

Lộ trình 7 ngày:

  1. Chạy sample Asteroids của Quantum.
  2. Đọc và sửa Input.qtn, thêm một button mới.
  3. Viết một component Health.
  4. Viết một system trừ máu khi bắn trúng.
  5. Trigger event để Unity bật VFX.
  6. Thêm command BuyItem hoặc SpawnEnemy.
  7. Chạy 2 local players, bật latency giả lập, nhìn rollback hoạt động.

Sau đó mới học sâu physics, KCC, asset pipeline, replay, checksum, custom server plugin.

18. Một câu chốt

Photon Quantum không phải là "Photon nhưng sync mượt hơn". Nó là cách viết game multiplayer theo kiểu deterministic simulation: bạn xây một thế giới thuần dữ liệu, chạy theo tick, cùng input cho cùng kết quả; Unity chỉ là lớp nhìn/nghe/chạm bên ngoài.

Khi bạn hiểu ranh giới đó, Quantum trở nên rất sáng: gameplay nằm trong Frame, data nằm trong .qtn, logic nằm trong System, input đi vào mỗi tick, event đi ra cho view. Thế là bộ xương của một game multiplayer rollback đã đứng được.