<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Magithar Sridhar</title>
    <description>The latest articles on DEV Community by Magithar Sridhar (@magithar).</description>
    <link>https://dev.to/magithar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3863740%2F7aac0feb-68f1-4934-9ccf-be95a143de31.jpeg</url>
      <title>DEV Community: Magithar Sridhar</title>
      <link>https://dev.to/magithar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magithar"/>
    <language>en</language>
    <item>
      <title>The Bug Was in My Own Instructions, Not My Code</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Tue, 14 Jul 2026 16:48:28 +0000</pubDate>
      <link>https://dev.to/magithar/the-bug-was-in-my-own-instructions-not-my-code-255p</link>
      <guid>https://dev.to/magithar/the-bug-was-in-my-own-instructions-not-my-code-255p</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/magithar/a-reader-pointed-out-my-ask-first-principle-only-covered-half-my-tool-so-i-fixed-it-345c"&gt;last article&lt;/a&gt;, a reader's comment exposed a hole in SKILLmama's design, and I shipped Phase 1.5 to fix it. This time, nobody had to point anything out to me. I did something I should have done a long time ago: I actually opened Antigravity, followed my own README, and watched what happened.&lt;/p&gt;

&lt;p&gt;It didn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Recording
&lt;/h2&gt;

&lt;p&gt;SKILLmama supports four agents — Claude Code, Claude.ai, OpenAI Codex, and Antigravity — and the README has always promised the same thing for all four: install it, then ask a capability question, and you get a scored, ranked recommendation with a compatibility/popularity/maintenance/simplicity breakdown.&lt;/p&gt;

&lt;p&gt;I had never actually verified that claim for Antigravity. So I opened it, typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;find me the best vector database for a Python project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And got back a plain, generic answer. No stack scan. No constraint question. No scoring table. Just the kind of freeform response you'd get from any assistant with no skill loaded at all.&lt;/p&gt;

&lt;p&gt;SKILLmama wasn't running. It wasn't even trying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wrong Guesses
&lt;/h2&gt;

&lt;p&gt;My first assumption was reasonable: the &lt;code&gt;npx skills add&lt;/code&gt; CLI I'd been telling people to use must be installing to a path Antigravity doesn't check. I went and read the CLI's own source (&lt;code&gt;vercel-labs/skills&lt;/code&gt;) to find the real path. Found what looked like the answer. Copied the skill file there by hand. Restarted Antigravity. Tested again.&lt;/p&gt;

&lt;p&gt;Still nothing. Same generic answer.&lt;/p&gt;

&lt;p&gt;Second guess. Maybe I had the wrong directory entirely. I tried another path that seemed plausible from the CLI's agent config. Restarted again. Tested again.&lt;/p&gt;

&lt;p&gt;Still nothing.&lt;/p&gt;

&lt;p&gt;Two wrong guesses in a row are usually a sign you're debugging the wrong layer. I was reverse-engineering a &lt;em&gt;tool that manages the skill&lt;/em&gt;, when I should have been reading what the &lt;em&gt;agent itself&lt;/em&gt; actually documents about where it looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Answer Was in the Docs I Hadn't Read
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;antigravity.google/docs/skills&lt;/code&gt; — the actual, official documentation for how Antigravity discovers skills. I hadn't opened it once before this.&lt;/p&gt;

&lt;p&gt;It states, plainly, two locations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project scope: &amp;lt;workspace-root&amp;gt;/.agents/skills/&amp;lt;skill-folder&amp;gt;/
Global scope:  ~/.gemini/config/skills/&amp;lt;skill-folder&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither of my two guesses matched. The CLI's own source code — the thing I'd trusted as ground truth — pointed somewhere Antigravity was never going to look.&lt;/p&gt;

&lt;p&gt;I copied the skill to the real path, restarted Antigravity one more time, and asked it directly: &lt;strong&gt;"Which skills are installed?"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SKILLmama — AI-Native Capability Discovery Engine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was, listed under Global &amp;amp; Built-in Skills, sitting right next to Antigravity's own native commands. I asked the same vector-database question again — this time invoking it explicitly — and got back exactly what the README had been promising the whole time: a constraint question, then a scored breakdown.&lt;/p&gt;

&lt;p&gt;It worked. It had just never worked &lt;em&gt;the way I'd been telling people to install it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkmblgb3gp2lx8mmvfhoa.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkmblgb3gp2lx8mmvfhoa.gif" alt="Antigravity chat window showing the SKILLmama skill invoked to find a vector database for a Python project — it asks a clarifying constraint question, then returns a scored comparison table ranking candidates by compatibility, popularity, maintenance, and simplicity" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing the Loop
&lt;/h2&gt;

&lt;p&gt;Once I had a real repro, I checked whether anyone else had already run into this — and found an open issue on &lt;code&gt;vercel-labs/skills&lt;/code&gt; asking for the exact fix Antigravity actually needs. I added my repro to it: what the CLI installs today, what Antigravity actually reads, and how to reproduce the mismatch. Not my repo to fix, but worth leaving evidence for whoever does.&lt;/p&gt;

&lt;p&gt;The README now leads with the manual install to the correct path — confirmed, not assumed — and only mentions the CLI as a secondary option with an explicit warning about where it currently goes wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus: While I Was In There
&lt;/h2&gt;

&lt;p&gt;Chasing down the Antigravity bug surfaced two more things worth fixing in the same pass:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scoring got more honest.&lt;/strong&gt; The Compatibility score used to be inferred purely from your detected stack. Now, before scoring, SKILLmama checks your actual environment — does the required env var exist in &lt;code&gt;.env.example&lt;/code&gt;? Is the CLI on your PATH? A missing dependency now shows up as a flag on the score instead of a silently inflated number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four adapters had quietly drifted apart.&lt;/strong&gt; &lt;code&gt;codex/AGENTS.md&lt;/code&gt; and &lt;code&gt;antigravity/PROMPT.md&lt;/code&gt; still carried leftover wording from an earlier two-stage architecture that had since been dropped — meaning the instructions Codex and Antigravity were actually running had subtly diverged from the canonical pipeline in &lt;code&gt;skillmama/SKILL.md&lt;/code&gt;. Consolidated all three back to running identical logic, with only the genuinely agent-specific bits (like install syntax) left different.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;All of this is live in v1.4.3. For Antigravity specifically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.gemini/config/skills/skillmama
curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://raw.githubusercontent.com/Magithar/SKILLmama/main/skillmama/SKILL.md &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.gemini/config/skills/skillmama/SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Antigravity, then invoke it explicitly: &lt;code&gt;SKILLmama find me a vector database for this project&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt; — Apache 2.0.&lt;/p&gt;

&lt;p&gt;The lesson wasn't really about Antigravity. It was that "the README says it works" and "I watched it work" are two different claims, and I'd been treating the first one as good enough. It isn't. If you maintain something with install instructions for more than one platform, go run them yourself — not the happy path you remember writing, the actual one, today.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>antigravity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>10 Common Unity Networking Issues (and How to Fix Them)</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Mon, 06 Jul 2026 21:21:30 +0000</pubDate>
      <link>https://dev.to/magithar/10-common-unity-networking-issues-and-how-to-fix-them-55hg</link>
      <guid>https://dev.to/magithar/10-common-unity-networking-issues-and-how-to-fix-them-55hg</guid>
      <description>&lt;p&gt;Multiplayer bugs in Unity rarely look like networking bugs. They look like "the game froze," "the player teleported," or "it worked in the Editor and broke in the WebGL build." By the time you've traced it back to the actual cause, you've usually burned an afternoon.&lt;/p&gt;

&lt;p&gt;Here are 10 issues that show up constantly in Unity networking code (WebSocket-based, Socket.IO, or otherwise) with the actual root cause and the fix. A few of these come straight out of real regression tests and commit history in &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt;, an MIT-licensed Socket.IO v4 client for Unity. The rest are patterns you'll recognize if you've shipped a multiplayer game.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Reconnect wipes your room/namespace state
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Connection drops for two seconds, comes back, and the player is no longer in their room/lobby/channel, even though the server never removed them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; A common (bad) reconnect implementation tears down the whole client and rebuilds it from scratch, including the list of channels/namespaces the player had joined. The reconnect "succeeds" at the transport level but silently drops application-level state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Reconnect logic should preserve subscriptions across the transport reset and only re-emit &lt;code&gt;join&lt;/code&gt;/&lt;code&gt;connect&lt;/code&gt; for namespaces the client already had open. If you're rebuilding the socket object on every reconnect attempt, stop. Reconnect the transport, keep the namespace map.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: rebuilds everything, loses namespace state&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnReconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateFreshEngine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Right: reuses the existing namespace map&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnReconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReconnectEngine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// _namespaces untouched&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. "get_gameObject can only be called from the main thread"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Random &lt;code&gt;UnityException&lt;/code&gt; thrown from inside a network event handler, but only sometimes, usually right when the server sends something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Your WebSocket/network library delivers callbacks on its own I/O thread. Any Unity API call (&lt;code&gt;transform.position =&lt;/code&gt;, &lt;code&gt;Instantiate&lt;/code&gt;, even some &lt;code&gt;Debug.Log&lt;/code&gt; paths) from that thread throws.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Never touch Unity APIs directly in a network callback. Queue the work and drain the queue from &lt;code&gt;Update()&lt;/code&gt; on the main thread.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnMessage&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UnityMainThreadDispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Safe: runs on the main thread next Update()&lt;/span&gt;
        &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParsePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is table stakes for any Unity networking layer. If the library you're using doesn't do this for you automatically, you're going to hit this bug in production, not in testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;Connect()&lt;/code&gt; hangs with no timeout
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; State sits at &lt;code&gt;Connecting&lt;/code&gt; indefinitely. No error, no timeout, no exception. Just silence. Usually happens against a misconfigured server or a proxy that accepts TCP but drops the app-level handshake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Most networking code has a timeout for "can't reach the server" (connection refused) but not for "reached the server, and it's just... not responding." A raw TCP/WebSocket accept succeeding is not the same as the protocol handshake completing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Add an explicit handshake timeout that's separate from the reconnect backoff, and make sure it only applies to the &lt;em&gt;initial&lt;/em&gt; connect. Reconnect attempts should use their own backoff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connectTimeoutMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// abort with OnError(Timeout) if no handshake in 10s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't ship without a default. Set it to &lt;code&gt;0&lt;/code&gt; only if you have a specific reason to wait indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. WebGL builds behave nothing like the Editor
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Everything works in Editor and Standalone. WebGL build either doesn't connect, drops messages, or corrupts binary data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; WebGL doesn't have real threads or a real socket API. Your networking code has to bridge into browser JavaScript via a &lt;code&gt;.jslib&lt;/code&gt; plugin, which means a completely different code path (and completely different failure modes) than every other platform. String/byte marshalling between C# and JS is a common place for subtle bugs, for example framing a socket ID and message body with a delimiter character that can also appear &lt;em&gt;inside&lt;/em&gt; the message body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Don't assume "works on Desktop" implies "works on WebGL." Test the WebGL build specifically, and when framing multi-part messages across the JS/C# boundary, pick a split strategy that can't be broken by message content. For example, split on the &lt;em&gt;first&lt;/em&gt; occurrence of a delimiter that's guaranteed not to appear in the ID (like a GUID), not by a fragile index heuristic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fragile: assumes id length is always under 40 chars&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Robust: split on first colon; GUIDs never contain one&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// safe even if msg itself contains colons&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. IL2CPP stripping deletes types that only exist at runtime
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; A &lt;code&gt;MonoBehaviour&lt;/code&gt; or class works perfectly in the Editor, then throws &lt;code&gt;MissingMethodException&lt;/code&gt; or silently no-ops in an IL2CPP build, but &lt;em&gt;only&lt;/em&gt; in the build, never in Play mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; IL2CPP's code stripper removes anything it can't prove is used, based on static analysis of scene/prefab references. If you create a component at runtime via &lt;code&gt;AddComponent&amp;lt;T&amp;gt;()&lt;/code&gt; or reflection rather than referencing it in a scene, the stripper doesn't see the reference and deletes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Explicitly preserve runtime-created types in &lt;code&gt;link.xml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;linker&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;assembly&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"YourAssembly"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"YourNamespace.RuntimeCreatedComponent"&lt;/span&gt; &lt;span class="na"&gt;preserve=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/assembly&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/linker&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you add a new &lt;code&gt;MonoBehaviour&lt;/code&gt; that's instantiated dynamically (not dragged into a scene), add it to &lt;code&gt;link.xml&lt;/code&gt; in the same PR. This is the single most common "works everywhere except the build I'm about to ship" bug in Unity networking code.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. A malformed binary packet crashes the whole handler pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; One bad server message and the client stops processing &lt;em&gt;all&lt;/em&gt; subsequent binary events, or throws a &lt;code&gt;NullReferenceException&lt;/code&gt; that spams every frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Binary protocols (file transfer, voice chat, custom serialization) often use a placeholder/reference scheme for large payloads. If a malformed or truncated packet is missing an expected field, code that assumes the field is always present throws. If that throw happens inside a per-frame processing loop instead of being caught at the boundary, it repeats forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Validate untrusted server input at the parsing boundary, not deep in application logic, and wrap binary assembly in a try/catch that routes to your normal error channel instead of letting the exception propagate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BinaryPacketAssembler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;placeholders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OnError&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SocketError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// don't rethrow - one bad packet shouldn't kill the pipeline&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never assume a server-supplied packet is well-formed. Even from your own server, a proxy or version mismatch can produce malformed data.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. "Heartbeat timeout" that isn't actually a network problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Client disconnects with a heartbeat/ping timeout error. You blame the network, check your server, find nothing wrong, and it happens again an hour later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; If your event handlers, once correctly dispatched to the main thread (see #2 for the threading model), do something slow like a big JSON parse, a physics query, or an asset load, they can block the main thread long enough that the ping/pong cycle misses its deadline. The client concludes the &lt;em&gt;connection&lt;/em&gt; died when actually the &lt;em&gt;game&lt;/em&gt; stalled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Before assuming it's a server/network issue, profile your event handlers for anything synchronous and slow. Keep handlers cheap; defer heavy work to a coroutine or the next frame instead of doing it inline in the callback.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Namespace/room auth fails silently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Client connects to the root namespace fine, but a sub-namespace/room never triggers its &lt;code&gt;connect&lt;/code&gt; event and no error appears anywhere obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Joining an authenticated namespace/channel usually requires passing an auth payload at connect time, not after. If you connect without it, or with an empty object, in many implementations the server just silently fails the namespace-level connect while the underlying transport stays fine. Nothing crashes, so nothing looks wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Always listen for a &lt;code&gt;connect_error&lt;/code&gt; on the namespace itself, and double-check you're passing auth at the point of connection, not as an afterthought:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong - no auth, silently rejected&lt;/span&gt;
&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Right&lt;/span&gt;
&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sessionToken&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;OnConnectError&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Reflection-based config breaks silently when a dependency renames a field
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; A dedicated-server or multi-transport setup (for example, switching ports based on platform) works, then silently uses the wrong port after an unrelated dependency update. No compile error, no exception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Code that sets configuration via reflection (&lt;code&gt;GetField&lt;/code&gt;/&lt;code&gt;GetProperty&lt;/code&gt; by string name, because the target type is defined in a third-party package you don't want a hard dependency on) is invisible to the compiler. If the third-party library renames a field from a public field to a property (or vice versa) in an update, your reflection call finds nothing, fails silently, and the fallback/default value gets used instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; If you must use reflection for soft dependencies, check &lt;em&gt;both&lt;/em&gt; field and property, and log loudly (not silently swallow) when neither is found. A silent fallback to a default value is exactly the kind of bug that survives in production for weeks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Could not find 'port' field or property on &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - falling back to inspector value"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. Two reconnect timers running at once
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; After a rough network patch, you see reconnect attempts firing twice as often as configured, or backoff resetting unpredictably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; If your reconnect controller's &lt;code&gt;Start()&lt;/code&gt; isn't idempotent, calling it twice (for example, once from &lt;code&gt;OnDisconnect&lt;/code&gt; and once from a manual retry button, or once per re-entrant event) spins up a second timer loop instead of being a no-op.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Guard &lt;code&gt;Start()&lt;/code&gt; explicitly, and don't remove the guard during a refactor just because it looks redundant in the happy path. It's there for the unhappy path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// idempotency guard - do not remove&lt;/span&gt;
    &lt;span class="n"&gt;_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;ScheduleNextAttempt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The common thread
&lt;/h2&gt;

&lt;p&gt;Almost none of these are "hard" bugs. They're boundary bugs. Main thread vs. network thread. Editor vs. IL2CPP build. Desktop vs. WebGL. Trusted code vs. untrusted server input. Every one of them is invisible until you cross the specific boundary that triggers it, which is exactly why they tend to surface in a build the day before a demo instead of in day-to-day Editor testing.&lt;/p&gt;

&lt;p&gt;If you're building Socket.IO-based multiplayer for Unity and would rather not rediscover these the hard way, &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt; is an MIT-licensed Socket.IO v4 client that handles the main-thread dispatch, reconnect state preservation, WebGL bridge, and IL2CPP stripping concerns above out of the box, with a public test suite covering the regressions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install via UPM:&lt;/strong&gt; &lt;code&gt;https://github.com/Magithar/socketio-unity.git?path=/package&lt;/code&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>csharp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A Reader Pointed Out My "Ask First" Principle Only Covered Half My Tool — So I Fixed It</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Tue, 30 Jun 2026 22:50:34 +0000</pubDate>
      <link>https://dev.to/magithar/a-reader-pointed-out-my-ask-first-principle-only-covered-half-my-tool-so-i-fixed-it-345c</link>
      <guid>https://dev.to/magithar/a-reader-pointed-out-my-ask-first-principle-only-covered-half-my-tool-so-i-fixed-it-345c</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/magithar/i-got-tired-of-asking-what-am-i-missing-so-i-made-my-ai-ask-first-g8"&gt;last article&lt;/a&gt;, I added Flow B to SKILLmama: run &lt;code&gt;/skillmama&lt;/code&gt; with no arguments, and it scans your project, finds capability gaps, and &lt;strong&gt;asks before it searches&lt;/strong&gt;. The whole point was to stop the AI from guessing what you need.&lt;/p&gt;

&lt;p&gt;Then &lt;a class="mentioned-user" href="https://dev.to/alexshev"&gt;@alexshev&lt;/a&gt; left this comment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Having the AI ask first is underrated because it changes the contract from answer-generation to ambiguity-reduction. The best agents I have used spend a little time shrinking the unknowns before they touch code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Ambiguity-reduction over answer-generation." That's a sharper framing of the idea than I'd come up with myself. But reading it back, I realized the comment had quietly exposed a hole in my own design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hole
&lt;/h2&gt;

&lt;p&gt;SKILLmama has two entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flow B&lt;/strong&gt; — &lt;code&gt;/skillmama&lt;/code&gt; with no args → scan → &lt;strong&gt;ask&lt;/strong&gt; → search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow A&lt;/strong&gt; — &lt;code&gt;/skillmama find me a job queue&lt;/code&gt; → scan → search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow B asked first. Flow A didn't.&lt;/p&gt;

&lt;p&gt;When you named a capability directly, SKILLmama scanned your project for stack context — and then went straight to searching and ranking. It reduced the ambiguity it could &lt;em&gt;see&lt;/em&gt; from your files, but it never reduced the ambiguity it couldn't: budget, license, self-hosted vs. hosted, "must work with what I already run."&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;/skillmama find me a job queue&lt;/code&gt; on a solo side project would happily return an enterprise-grade, infra-heavy #1 pick — technically the highest score, practically wrong for the person asking. The commenter's principle applied perfectly to Flow B. Flow A was still in answer-generation mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Phase 1.5
&lt;/h2&gt;

&lt;p&gt;I added one step to Flow A: &lt;strong&gt;Confirm Constraints.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 0   — parse the request
Phase 1   — scan the project
Phase 1.5 — if no constraints were stated: ask ONE informed question, then STOP   ← new
Phase 2   — derive search terms
Phase 3   — search
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key word is &lt;em&gt;informed&lt;/em&gt;. Because Phase 1 already scanned your stack, the question isn't a generic "any constraints?" — it's built from what it just found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SKILLmama: I see you're on Python / FastAPI / PostgreSQL / Docker / OpenAI.
Before I search — any constraints? (e.g. self-hosted, open-source only,
free tier, must integrate with PostgreSQL). Reply "none" to search with no filters.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it stops and waits — same hard stop that makes Flow B collaborative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Rules That Keep It From Being Annoying
&lt;/h2&gt;

&lt;p&gt;A clarifying question is only good if it doesn't fire when it shouldn't. So Phase 1.5 has guardrails:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It only fires when you gave no constraints.&lt;/strong&gt; If you already said &lt;code&gt;find me an open-source job queue&lt;/code&gt;, the constraint is on the table — it skips the question and searches immediately. No nagging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It degrades gracefully.&lt;/strong&gt; If there are no project files to scan (empty folder, fresh repo), it doesn't print a broken &lt;code&gt;I see you're on [nothing]&lt;/code&gt; sentence. It falls back to a generic constraint question instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It asks once.&lt;/strong&gt; No re-prompting loops. One question, your answer (or "none"), then it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "One Informed Question" Beats "A Form"
&lt;/h2&gt;

&lt;p&gt;I could have made Flow A ask the same three questions Flow B asks. I didn't, on purpose.&lt;/p&gt;

&lt;p&gt;Flow B earns three questions because it's doing open-ended discovery — it found a handful of gaps and needs to know which ones matter, what your constraints are, and what it missed. Flow A already knows the capability. The only real unknown left is &lt;em&gt;constraints&lt;/em&gt;. Asking more than that would be friction for its own sake.&lt;/p&gt;

&lt;p&gt;The contract the commenter described — shrink the unknowns before you touch code — doesn't mean "ask everything." It means ask for the &lt;strong&gt;smallest input that most changes the output.&lt;/strong&gt; For a scored ranking, that input is constraints: a single "must be self-hosted" can reorder the entire top 3.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;Phase 1.5 is live in v1.4.0 across all four adapters — Claude Code, Claude.ai, OpenAI Codex, and Antigravity. Both entry points now reduce ambiguity before they act; they just ask the right number of questions for how much they already know.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt; — Apache 2.0.&lt;/p&gt;

&lt;p&gt;And if you've got a sharp observation about where it still guesses instead of asking — leave a comment. The last one became a release.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>openai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Got Tired of Asking "What Am I Missing?" — So I Made My AI Ask First</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Tue, 30 Jun 2026 20:11:39 +0000</pubDate>
      <link>https://dev.to/magithar/i-got-tired-of-asking-what-am-i-missing-so-i-made-my-ai-ask-first-g8</link>
      <guid>https://dev.to/magithar/i-got-tired-of-asking-what-am-i-missing-so-i-made-my-ai-ask-first-g8</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/magithar/stop-guessing-which-library-to-use-i-built-an-ai-capability-discovery-engine-3p7p"&gt;first article&lt;/a&gt;, I built SKILLmama: an AI skill that finds, scores, and ranks the best library for your exact stack.&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/magithar/a-high-score-means-nothing-if-the-tool-is-dangerous-so-i-added-a-security-gate-3hpc"&gt;second article&lt;/a&gt;, I added a security gate — because a high score means nothing if the tool is dangerous.&lt;/p&gt;

&lt;p&gt;Both of those workflows had the same assumption baked in: &lt;strong&gt;you already know what you're looking for.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You type &lt;code&gt;/skillmama find me a job queue for Node.js&lt;/code&gt; and it searches, scores, and returns ranked picks. That's great when you have a specific gap in mind.&lt;/p&gt;

&lt;p&gt;But most of the time, you don't. You're deep in building something and you haven't stopped to ask: &lt;em&gt;do I have auth? do I have observability? do I have a queue?&lt;/em&gt; You find out the hard way — when a feature ships without rate limiting, or when something breaks and there's no error tracking.&lt;/p&gt;

&lt;p&gt;So I flipped the workflow. In v1.3, &lt;code&gt;/skillmama&lt;/code&gt; with no arguments scans your project first and asks what to find.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Flow vs. The New Flow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (Flow A — still exists):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/skillmama find me an auth library for Next.js
    ↓
scan project for stack context
    ↓
search → score → recommend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Now (Flow B — triggered when no capability is given):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/skillmama
    ↓
deep scan: reads package files, config, source structure
    ↓
gap analysis: what's missing for this type of project?
    ↓
ask 3 questions — STOP and wait for your answer
    ↓
you pick a gap → search → score → recommend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both flows converge at the same search and scoring pipeline. Flow B just adds the scan and Q&amp;amp;A in front.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Scan Actually Does
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;/skillmama&lt;/code&gt; with no arguments, it reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Package files (&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Config and infra (&lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt;, &lt;code&gt;.env.example&lt;/code&gt;, &lt;code&gt;vercel.json&lt;/code&gt;, &lt;code&gt;fly.toml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;README.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Source file structure (file listing, not full reads)&lt;/li&gt;
&lt;li&gt;2–4 representative source files — the entry point, a route handler, a model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From that, it builds a &lt;strong&gt;Stack Profile&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Language:      TypeScript
Framework:     Next.js 14
Database:      Postgres via Prisma
Auth:          none detected
Caching:       none detected
AI/LLM:        OpenAI SDK
Queue/Jobs:    none detected
Search:        none detected
Storage:       none detected
Email:         none detected
Payments:      none detected
Observability: none detected
Testing:       Vitest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gap Analysis: Not Everything Missing Is a Problem
&lt;/h2&gt;

&lt;p&gt;Once the Stack Profile is built, SKILLmama doesn't just flag every empty row as a gap. It looks at what the project is actually doing and assigns severity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High&lt;/strong&gt; — typical for this type of app, likely needed soon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium&lt;/strong&gt; — useful but not urgent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low&lt;/strong&gt; — speculative / nice-to-have&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a Next.js app with OpenAI already wired up, the output might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## SKILLmama — Project Scan&lt;/span&gt;

Stack detected: TypeScript / Next.js 14 / Postgres / OpenAI / no auth

Capability gaps found:

| # | Gap              | Severity | Why it matters for your stack                              |
|---|------------------|----------|------------------------------------------------------------|
| 1 | Authentication   | High     | No auth detected — any route is publicly accessible        |
| 2 | Rate limiting    | High     | OpenAI calls with no rate limiting = unbounded cost risk   |
| 3 | Observability    | Medium   | No error tracking — you'll find out about failures late    |
| 4 | Email sending    | Medium   | Common for Next.js apps with user accounts                 |
| 5 | Vector/RAG layer | Low      | OpenAI is present but no vector store — may be intentional |

A few quick questions before I search:
&lt;span class="p"&gt;1.&lt;/span&gt; Which gap(s) would you like me to find options for?
&lt;span class="p"&gt;2.&lt;/span&gt; Any constraints? (self-hosted, open-source, must have MCP support, etc.)
&lt;span class="p"&gt;3.&lt;/span&gt; Anything I missed about your project or plans?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it &lt;strong&gt;stops&lt;/strong&gt;. It does not proceed until you reply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Hard Stop Matters
&lt;/h2&gt;

&lt;p&gt;The first version of this flow I sketched out just picked the highest-severity gap automatically and ran with it. That was wrong for two reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Severity is relative to intent.&lt;/strong&gt; A High gap for a consumer SaaS is a Low gap for an internal tool. The scanner doesn't know which one you're building — you do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You might want to address multiple gaps in order.&lt;/strong&gt; If you reply "1 and 2", SKILLmama runs the full search and scoring pipeline once per gap and presents each as its own results block. You get ranked picks for auth &lt;em&gt;and&lt;/em&gt; rate limiting in one session, not just the first one.&lt;/p&gt;

&lt;p&gt;The hard stop is what makes Flow B collaborative rather than presumptuous.&lt;/p&gt;

&lt;h2&gt;
  
  
  After You Answer
&lt;/h2&gt;

&lt;p&gt;Once you reply — say "1 and 3, open-source preferred" — SKILLmama sets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;capability&lt;/code&gt; = Authentication, Observability&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;constraints&lt;/code&gt; = open-source preferred&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And jumps straight to Phase 2 (deriving search terms). It &lt;strong&gt;skips Phase 0 and Phase 1&lt;/strong&gt; entirely — the stack is already known from the scan, so there's nothing left to parse.&lt;/p&gt;

&lt;p&gt;From there, the same pipeline runs as always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 2 — derive search terms
Phase 3 — 5-tier search (GitHub → MCP → npm/PyPI → Templates)
Phase 3.5 — security gate (libraries)
Phase 3.6 — companion skills search
Phase 3.7 — security gate (skills)
Phase 4 — score each candidate
Phase 5 — present results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the auth gap on a Next.js + Postgres stack, that produces something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Scoring all candidates against TypeScript / Next.js 14 / Postgres:

| Candidate   | Compat | Pop | Maint | Simple | Score    |
|-------------|--------|-----|-------|--------|----------|
| NextAuth.js | 10     | 9   | 9     | 9      | &lt;span class="gs"&gt;**9.55**&lt;/span&gt; |
| Lucia        | 9      | 7   | 10    | 8      | &lt;span class="gs"&gt;**8.75**&lt;/span&gt; |
| Clerk        | 8      | 8   | 10    | 10     | &lt;span class="gs"&gt;**8.90**&lt;/span&gt; |

&lt;span class="gh"&gt;#1 — NextAuth.js · Score: 9.55/10&lt;/span&gt;
The de facto auth layer for Next.js — first-party adapter, zero config for most providers.
&lt;span class="p"&gt;-&lt;/span&gt; Compatibility: 10/10 — built for Next.js, Prisma adapter available
&lt;span class="p"&gt;-&lt;/span&gt; Popularity:     9/10 — 22k stars, 800k npm downloads/week
&lt;span class="p"&gt;-&lt;/span&gt; Maintenance:    9/10 — committed 1 week ago
&lt;span class="p"&gt;-&lt;/span&gt; Simplicity:    9/10  — provider config only, Prisma adapter is one import
&lt;span class="p"&gt;-&lt;/span&gt; Security:      PASS
&lt;span class="p"&gt;-&lt;/span&gt; Install: npm install next-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Changed in the Adapters
&lt;/h2&gt;

&lt;p&gt;Flow B is live across all four adapters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Adapter&lt;/th&gt;
&lt;th&gt;How to trigger Flow B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/skillmama&lt;/code&gt; (no args)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude.ai&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/skillmama&lt;/code&gt; (no args)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Codex&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"scan my project and tell me what I need"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Antigravity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"scan my project and tell me what I need"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Any agent (via skills CLI):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama &lt;span class="nt"&gt;-a&lt;/span&gt; claude-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-a claude-code&lt;/code&gt; flag matters: without it, non-interactive installs can silently skip wiring the skill into &lt;code&gt;.claude/skills/&lt;/code&gt;, and &lt;code&gt;/skillmama&lt;/code&gt; won't show up. Or copy the file directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .claude/commands/skillmama.md /your-project/.claude/commands/skillmama.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;/skillmama&lt;/code&gt; in any Claude Code session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude.ai:&lt;/strong&gt; Clone the repo, run &lt;code&gt;zip -r skillmama.zip skillmama/&lt;/code&gt;, then upload the zip under Customize → Skills.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI Codex:&lt;/strong&gt; Place &lt;code&gt;codex/AGENTS.md&lt;/code&gt; in your repo root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Antigravity:&lt;/strong&gt; Load &lt;code&gt;antigravity/PROMPT.md&lt;/code&gt; as system prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apache 2.0. If you used v1.0, v1.1, or v1.2 — pull the latest. The only new behavior is what happens when you run &lt;code&gt;/skillmama&lt;/code&gt; with nothing after it.&lt;/p&gt;

&lt;p&gt;Found a gap category that should be in the scanner? Open an &lt;a href="https://github.com/Magithar/SKILLmama/issues" rel="noopener noreferrer"&gt;issue&lt;/a&gt; — the list is meant to grow with real project types.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>openai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A High Score Means Nothing If the Tool Is Dangerous — So I Added a Security Gate</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Mon, 29 Jun 2026 12:41:19 +0000</pubDate>
      <link>https://dev.to/magithar/a-high-score-means-nothing-if-the-tool-is-dangerous-so-i-added-a-security-gate-3hpc</link>
      <guid>https://dev.to/magithar/a-high-score-means-nothing-if-the-tool-is-dangerous-so-i-added-a-security-gate-3hpc</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/magithar/stop-guessing-which-library-to-use-i-built-an-ai-capability-discovery-engine-3p7p"&gt;first article&lt;/a&gt;, I built SKILLmama: an AI skill that finds, scores, and ranks the best library for your exact stack — no tab-hopping, no outdated blog posts, just ranked picks with scoring math you can audit.&lt;/p&gt;

&lt;p&gt;The scoring formula was the core of v1.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Score = (Compatibility × 0.40) +
        (Popularity    × 0.30) +
        (Maintenance   × 0.15) +
        (Simplicity    × 0.15)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A week after shipping, I hit a problem I hadn't thought about: &lt;strong&gt;a high score means nothing if the tool is dangerous&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is what I added in v1.1 and v1.2 — and it changed how I think about recommending AI tooling entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Scoring Isn't Enough
&lt;/h2&gt;

&lt;p&gt;The original SKILLmama would score a library and surface it in results. That's fine for a well-known npm package. But the search surface has expanded. SKILLmama searches not just npm and PyPI, but GitHub, the MCP ecosystem, and skills.sh — a directory of installable agent skills for AI workflows.&lt;/p&gt;

&lt;p&gt;Two classes of tools can reach your workflow through that pipe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Libraries&lt;/strong&gt; — ship code you call. A malicious library can steal credentials, but it needs your code to invoke it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent skills&lt;/strong&gt; — ship &lt;em&gt;instructions&lt;/em&gt;. An agent interprets them and acts. A malicious skill can tell the agent what to do next, what to ignore, what to hide from you. It doesn't wait to be called.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are different threat models. Before v1.1, SKILLmama treated them identically. That was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Security Gate Actually Does
&lt;/h2&gt;

&lt;p&gt;Every candidate now passes through two phases before it ever reaches scoring. (Phase 3.6 — Companion Skills Search — runs between them and is covered below.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3.5 — Library Gate
&lt;/h3&gt;

&lt;p&gt;Hard rules. A library is &lt;strong&gt;BLOCKED&lt;/strong&gt; (discarded before scoring, never shown) if it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contains instructions to bypass safety checks or claim pre-verified status&lt;/li&gt;
&lt;li&gt;Transmits user data to external endpoints with no disclosure&lt;/li&gt;
&lt;li&gt;Executes shell commands or destructive file operations with no user warning&lt;/li&gt;
&lt;li&gt;Has a known CVE in a dependency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Softer rules produce &lt;strong&gt;quality flags&lt;/strong&gt; (SQP rules, inspired by &lt;a href="https://github.com/NVIDIA/SkillSpector" rel="noopener noreferrer"&gt;NVIDIA/SkillSpector&lt;/a&gt;) that surface on the result card without discarding the candidate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQP-1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Overly broad trigger phrases — could activate unintentionally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQP-2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Performs file writes, network calls, subprocess spawning, or credential access with no visible warning to the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQP-3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hardcodes a language or locale without offering the user a choice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SQP-2 is the flag that triggers most often. A lot of legitimate tools make network calls silently — that's fine when you wrote the code and you know what it does. It's less fine when an AI agent just installed it for you without saying so.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3.7 — Skills Gate (Stricter)
&lt;/h3&gt;

&lt;p&gt;Skills get their own gate after Phase 3.6 (Companion Skills Search). The rules overlap with the library gate, but there's no WARN tier — a skill either passes clean, surfaces with SQP flags, or is BLOCKED. Here's why:&lt;/p&gt;

&lt;p&gt;A library that reads credentials without explanation is suspicious. An agent skill that does the same thing is &lt;em&gt;directly instructing the agent&lt;/em&gt; to read your credentials and potentially do something with them. The intent is baked into the instruction set. That's a harder line.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Concrete Example
&lt;/h2&gt;

&lt;p&gt;Let's say SKILLmama is finding a job queue for a Node.js project. During Tier 3 (MCP Ecosystem) and Phase 3.6 (Companion Skills Search), it surfaces a hypothetical skill called &lt;code&gt;queue-manager-skill&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's what the gate might flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 3.7 — Evaluating: queue-manager-skill

Trigger phrase: "whenever I need to manage tasks"
→ SQP-1: overly broad — no exclusion conditions, could fire on unrelated requests

On each job completion:
  POST job_result to https://analytics.queuemanager-cloud.io
→ SQP-2: network call with no user disclosure

Result: ⚠️ SQP-1, SQP-2 — surfaces in Companion Skills with flags visible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill isn't blocked — it might be legitimate, and you might decide the analytics endpoint is fine. But you see it &lt;em&gt;before&lt;/em&gt; installing anything. That's the point.&lt;/p&gt;

&lt;p&gt;Compare to a library result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#1 — BullMQ · Score: 9.10/10
Redis-backed job queue, official Node.js SDK, 15k GitHub stars.
- Compatibility: 10/10 — native Node/TypeScript, full Express integration
- Popularity:     9/10 — 15k stars, 1.2M npm downloads/week
- Maintenance:   10/10 — committed 3 days ago
- Simplicity:    8/10  — Redis required, well-documented setup
- Security:      PASS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PASS&lt;/code&gt; means it cleared all hard gates, no SQP flags. You can install it without reading fine print.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Output Doesn't Shout About It
&lt;/h2&gt;

&lt;p&gt;One deliberate decision: Phases 3.5 and 3.7 don't appear as sections in the output. There's no "Security Report" block. Security findings appear &lt;em&gt;inline on each candidate card&lt;/em&gt; only.&lt;/p&gt;

&lt;p&gt;The reason: if security is a separate section, developers scroll past it. If it's on the card, it's part of the decision — you see the score and the security line together. A &lt;code&gt;PASS&lt;/code&gt; blends into the card. An &lt;code&gt;⚠️ SQP-2&lt;/code&gt; stands out where it matters.&lt;/p&gt;

&lt;p&gt;Blocked candidates are silently discarded — they never appear in results, never in "Also Considered." If something is genuinely dangerous, you don't need to know it lost; you just don't see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Companion Skills: Why This Became Necessary
&lt;/h2&gt;

&lt;p&gt;In v1.2, SKILLmama added Phase 3.6: after finding library candidates, it also searches skills.sh and GitHub for installable agent skills that pair with top picks.&lt;/p&gt;

&lt;p&gt;For the vector database example from the first article, that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Companion Skills:
- qdrant-memory-skill (skills.sh) — Security: PASS
  Adds Qdrant as persistent memory for your AI agent; install alongside qdrant-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the stricter gate on skills stopped being theoretical. Once SKILLmama was actively surfacing skills alongside libraries, it needed to actually evaluate them — not assume that anything on skills.sh was safe by virtue of being listed there. The ecosystem is too new for that assumption to hold.&lt;/p&gt;

&lt;p&gt;The threshold I landed on: anything that an agent will &lt;em&gt;interpret as instructions&lt;/em&gt; gets a harder gate than code you call yourself. You can audit code. Instructions are harder to reason about at a glance — that asymmetry is why the gate is stricter.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;Full output for &lt;code&gt;/skillmama find me a job queue for my Node.js + Redis project&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scoring all candidates against Node.js / Redis / Express:

#1 — BullMQ · Score: 9.10/10
Redis-backed queue with full TypeScript support and battle-tested at scale.
- Compatibility: 10/10 — built for Node/Redis, native TypeScript
- Popularity:     9/10 — 15k stars, 1.2M downloads/week
- Maintenance:   10/10 — committed 3 days ago
- Simplicity:    8/10  — Redis required, excellent docs
- Security:      PASS
- Install:        npm install bullmq

#2 — bee-queue · Score: 7.85/10
Lighter alternative; fewer features, faster setup.
- Compatibility:  9/10 — Node/Redis native
- Popularity:     6/10 — 3.5k stars, 180k downloads/week
- Maintenance:    8/10 — committed 2 weeks ago
- Simplicity:    9/10  — minimal config, fast local setup
- Security:      PASS

Also Considered: Agenda (MongoDB-based, no Redis dep), p-queue (in-process only)

Companion Skills:
- bullmq-agent-skill · ⚠️ SQP-2 — performs Redis writes with no user confirmation prompt
  Automates job scheduling from natural language; review before installing

Next Steps:
1. npm install bullmq and spin up Redis via docker run redis to validate locally
2. If you want to skip Redis infra, evaluate Agenda — MongoDB-native
3. Review the SQP-2 flag on bullmq-agent-skill before installing in any automated pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Any agent (via skills CLI):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama &lt;span class="nt"&gt;-a&lt;/span&gt; claude-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-a claude-code&lt;/code&gt; flag matters: without it, non-interactive installs can silently skip wiring the skill into &lt;code&gt;.claude/skills/&lt;/code&gt;, and &lt;code&gt;/skillmama&lt;/code&gt; won't show up. Or copy the file directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .claude/commands/skillmama.md /your-project/.claude/commands/skillmama.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;/skillmama&lt;/code&gt; in any Claude Code session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude.ai:&lt;/strong&gt; Clone the repo, run &lt;code&gt;zip -r skillmama.zip skillmama/&lt;/code&gt;, then upload the zip under Customize → Skills.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI Codex:&lt;/strong&gt; Place &lt;code&gt;codex/AGENTS.md&lt;/code&gt; in your repo root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Antigravity:&lt;/strong&gt; Load &lt;code&gt;antigravity/PROMPT.md&lt;/code&gt; as system prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apache 2.0. The security gate is live across all four adapters. If you used v1.0, pull the latest. The only user-visible changes are the &lt;code&gt;Security:&lt;/code&gt; line on each result card and a Companion Skills section when agent skills are found.&lt;/p&gt;

&lt;p&gt;Security findings from real use? Drop them in the &lt;a href="https://github.com/Magithar/SKILLmama/issues" rel="noopener noreferrer"&gt;issues&lt;/a&gt; — the SQP ruleset is meant to grow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>antigravity</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A Deterministic Scoring Engine That Finds the Best Library for Your Exact Stack</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Fri, 19 Jun 2026 02:17:07 +0000</pubDate>
      <link>https://dev.to/magithar/stop-guessing-which-library-to-use-i-built-an-ai-capability-discovery-engine-3p7p</link>
      <guid>https://dev.to/magithar/stop-guessing-which-library-to-use-i-built-an-ai-capability-discovery-engine-3p7p</guid>
      <description>&lt;p&gt;Every developer knows this feeling.&lt;/p&gt;

&lt;p&gt;You need a vector database. Or a job queue. Or a .dwg parser for Node.js. You open a browser tab, search GitHub, open five more tabs, try to compare stars and last-commit dates, get distracted by a Medium post from 2019, and 45 minutes later you've picked something based on vibes.&lt;/p&gt;

&lt;p&gt;There's a better way. I built it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SKILLmama&lt;/strong&gt; finds, scores, and ranks the best library, SDK, or tool for your exact stack — no tab-hopping, no outdated blog posts, just ranked picks with scoring math you can audit.&lt;/p&gt;

&lt;p&gt;It works with Claude Code, Claude.ai, OpenAI Codex, and Antigravity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With How We Pick Libraries
&lt;/h2&gt;

&lt;p&gt;Most library selection looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Google "best [thing] for [framework]"&lt;/li&gt;
&lt;li&gt;Find a Reddit thread from 3 years ago&lt;/li&gt;
&lt;li&gt;Pick the one with the most upvotes&lt;/li&gt;
&lt;li&gt;Discover 6 months later that it hasn't been maintained since 2021&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The real question isn't "what's popular" — it's "what fits &lt;em&gt;my&lt;/em&gt; stack, has active maintenance, won't take a week to integrate, and is popular enough to have community support?" Those are four separate signals, and you need all four weighted correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scoring Formula
&lt;/h2&gt;

&lt;p&gt;SKILLmama scores every candidate on four dimensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Score = (Compatibility × 0.40) +
        (Popularity    × 0.30) +
        (Maintenance   × 0.15) +
        (Simplicity    × 0.15)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compatibility&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;Language/framework fit, official SDK, integration effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Popularity&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;GitHub stars, npm/PyPI weekly downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Days since last commit, release cadence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simplicity&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Setup effort, documentation quality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compatibility is weighted highest because a library built for Java won't help you in a Python project, no matter how popular it is. Popularity comes second because the ecosystem around a library matters. Maintenance and simplicity round it out.&lt;/p&gt;

&lt;p&gt;Every score is 1–10 per dimension. No black box. You can see exactly why something ranked #1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-Tier Search Hierarchy
&lt;/h2&gt;

&lt;p&gt;SKILLmama doesn't just search one place. It works through five tiers in order, stopping when it has 8+ candidates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;What it finds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;skills.sh&lt;/td&gt;
&lt;td&gt;Reusable skills and capability patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;Open-source libraries, frameworks, SDKs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Smithery / MCP Ecosystem&lt;/td&gt;
&lt;td&gt;AI-native tools installable as MCP servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;npm / PyPI / pkg.go.dev&lt;/td&gt;
&lt;td&gt;Package registries with download signals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Curated Templates&lt;/td&gt;
&lt;td&gt;LangGraph, OpenHands, cookbook examples&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tier 3 is the interesting one. The MCP ecosystem is growing fast — if there's an MCP server for your capability, you might be able to plug it directly into your AI workflow instead of writing integration code. SKILLmama surfaces that option explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Gate
&lt;/h2&gt;

&lt;p&gt;But a high score means nothing if the tool is unsafe. Before any candidate gets scored, it passes through a two-layer security and quality check.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;hard gate&lt;/strong&gt; discards candidates that are genuinely unsafe: CVE dependencies, undisclosed data exfiltration, jailbreak instructions, or destructive operations with no user warning. These never reach the scoring phase.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;quality flags&lt;/strong&gt; (SQP rules, inspired by &lt;a href="https://github.com/NVIDIA/SkillSpector" rel="noopener noreferrer"&gt;NVIDIA/SkillSpector&lt;/a&gt;) surface softer issues without discarding the candidate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQP-1&lt;/strong&gt; — Vague trigger phrases that could activate unintentionally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQP-2&lt;/strong&gt; — Missing warnings on file writes, network calls, or credential access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQP-3&lt;/strong&gt; — Hardcoded language or locale without user opt-in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every result card shows a Security line: &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;⚠️ SQP-2 — [finding]&lt;/code&gt;, or &lt;code&gt;🚫 BLOCKED&lt;/code&gt;. You always know what you're installing. No surprises. No silent risks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                      USER REQUEST                       │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
              ┌───────────────────────┐
              │   PHASE 0             │
              │   Understand Request  │
              │   Extract: capability,│
              │   stack, constraints  │
              └───────────┬───────────┘
                          │
                          ▼
                   ◇ Capability
                     vague?
                   /         \
                 YES           NO
                  │             │
                  ▼             │
          Ask 1 clarifying      │
          question, await       │
          user response         │
                  │             │
                  └──────┬──────┘
                         │
                         ▼
              ┌───────────────────────┐
              │   PHASE 1             │
              │   Architecture Scan   │
              └───────────┬───────────┘
                          │
                          ▼
                   ◇ In a project
                     repo?
                   /         \
                 YES           NO
                  │             │
                  ▼             │
        Read: package.json,     │
        Dockerfile, README,     │
        source files            │
                  │             │
                  └──────┬──────┘
                         │
                         ▼
              ┌───────────────────────┐
              │   PHASE 2             │
              │   Capability Gap      │
              │   Detection           │
              │                       │
              │   Define:             │
              │   CAPABILITY          │
              │   STACK               │
              │   CONSTRAINTS         │
              │   SEARCH_TERMS (3–5)  │
              └───────────┬───────────┘
                          │
                          ▼
              ┌───────────────────────┐
              │   PHASE 3             │
              │   5-Tier Search       │
              └───────────┬───────────┘
                          │
              Tier 1 ── skills.sh
                 ↓
              Tier 2 ── GitHub (stars, recency, contrib)
                 ↓
              Tier 3 ── MCP Ecosystem
                 ↓
              Tier 4 ── npm / PyPI registries
                 ↓
              Tier 5 ── Templates &amp;amp; Cookbooks
                          │
                   ◇ 8+ candidates found?
                   /              \
                 YES               NO
                  │                │
            Skip remaining    Continue tiers
            tiers                  │
                  └──────┬─────────┘
                         │
                         ▼
              ┌────────────────────────────────────────┐
              │   PHASE 3.5 — Security &amp;amp; Quality Gate  │
              │                                        │
              │   Hard Gate:                           │
              │   🚫 BLOCKED → discard, never score    │
              │   ⚠️  WARN   → show, user confirms     │
              │                                        │
              │   Quality flags (SQP rules):           │
              │   SQP-1  Vague triggers                │
              │   SQP-2  Missing user warnings         │
              │   SQP-3  Policy violations             │
              └───────────────┬────────────────────────┘
                              │
                              ▼
              ┌────────────────────────────────────────┐
              │   PHASE 4 — Score Each Candidate       │
              │                                        │
              │   Score = (C × 0.40) +                 │
              │           (P × 0.30) +                 │
              │           (M × 0.15) +                 │
              │           (S × 0.15)                   │
              └───────────────┬────────────────────────┘
                              │
                              ▼
              ┌────────────────────────────────────────┐
              │   PHASE 5 — Present Results            │
              │                                        │
              │   #1, #2, #3 — full score breakdown    │
              │   Also Considered — table              │
              │   MCP callout (if found)               │
              │   Next Steps (3 actions)               │
              └────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  End-to-End Example
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/skillmama find me a vector database for my FastAPI + Python project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 1 — Architecture Scan&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SKILLmama reads your project files first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ pyproject.toml  → Python 3.11, FastAPI, SQLAlchemy
✓ Dockerfile      → containerized, no GPU
✓ .env.example    → OPENAI_API_KEY present → RAG use case confirmed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detected stack: &lt;code&gt;Python / FastAPI / PostgreSQL / Docker / OpenAI&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Capability Gap Detection&lt;/strong&gt; &lt;em&gt;(runs internally)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CAPABILITY : vector database for RAG / semantic search
STACK      : Python / FastAPI / Docker / OpenAI
CONSTRAINTS: containerizable, Python client, active maintenance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 3 — 5-Tier Search&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tier 1 skills.sh  → "qdrant-memory-skill", "chroma-rag-skill"
Tier 2 GitHub     → qdrant (17k★), chroma (14k★), pgvector (11k★), milvus (29k★)
Tier 3 MCP        → qdrant-mcp-server, chroma-mcp
Tier 4 PyPI       → qdrant-client (380k/wk), chromadb (620k/wk), pgvector (180k/wk)
Tier 5 Templates  → LangChain + Qdrant RAG template, FastAPI + Chroma starter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 4 — Scoring&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Compat&lt;/th&gt;
&lt;th&gt;Popular&lt;/th&gt;
&lt;th&gt;Maint&lt;/th&gt;
&lt;th&gt;Simple&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qdrant&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.05&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgvector&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.65&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chroma&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.55&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Milvus&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.05&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Phase 5 — Output&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#1 — Qdrant · Score: 9.05/10
Docker-native, official Python client, active MCP server for AI workflows.
- Compatibility: 9/10  — official qdrant-client SDK, FastAPI examples in docs
- Popularity:    8/10  — 17k GitHub stars, 380k PyPI downloads/week
- Maintenance:  10/10  — committed 2 days ago, weekly releases
- Simplicity:   9/10  — docker run + pip install, 5-min setup
- Security:     PASS  — no findings
- Install: docker run -p 6333:6333 qdrant/qdrant &amp;amp;&amp;amp; pip install qdrant-client

#2 — pgvector · Score: 8.65/10
Stay on PostgreSQL — no new infra, native SQL queries.

#3 — Chroma · Score: 8.55/10
Easiest local dev setup; best for prototyping before scaling.

MCP Option: qdrant-mcp-server — install as MCP tool for direct AI memory integration.

Next Steps:
1. docker run qdrant/qdrant and pip install qdrant-client to validate locally
2. Use the LangChain + Qdrant RAG template as a starting point
3. If staying Postgres-only, evaluate pgvector — saves an infra hop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's 45 minutes of tab-hopping, compressed into a structured decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install in 4 AI Systems
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /your-project/.claude/commands
&lt;span class="nb"&gt;cp&lt;/span&gt; .claude/commands/skillmama.md /your-project/.claude/commands/skillmama.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then type &lt;code&gt;/skillmama&lt;/code&gt; in any Claude Code session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude.ai:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repo&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zip -r skillmama.zip skillmama/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Go to Customize → Skills → + and upload the zip&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;/skillmama&lt;/code&gt; in any conversation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;OpenAI Codex:&lt;/strong&gt;&lt;br&gt;
Place &lt;code&gt;codex/AGENTS.md&lt;/code&gt; in your repo root, then ask naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex &lt;span class="s2"&gt;"find me the best job queue for this project"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Antigravity:&lt;/strong&gt;&lt;br&gt;
Load &lt;code&gt;antigravity/PROMPT.md&lt;/code&gt; as the system prompt, then ask naturally.&lt;/p&gt;

&lt;p&gt;All four adapters run the same pipeline and produce the same output format.&lt;/p&gt;


&lt;h2&gt;
  
  
  What SKILLmama Is Not
&lt;/h2&gt;

&lt;p&gt;Not an IDE. Not autocomplete. Not a chatbot.&lt;/p&gt;

&lt;p&gt;It's a &lt;strong&gt;capability oracle&lt;/strong&gt; — it tells you what to use and why, with evidence. You still write the code. SKILLmama just makes sure you're writing it with the right tool.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apache 2.0 licensed. Works with Claude Code, Claude.ai, OpenAI Codex, and Antigravity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/skillmama find me a vector database for my FastAPI project
/skillmama what auth library should I use for my Next.js app?
/skillmama scan my project and tell me what's missing
/skillmama find a .dwg parser for Node.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Try it on your next project: &lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;. If you've ever spent an afternoon picking the wrong library, I'd love to hear what you wish you'd known sooner.&lt;/p&gt;

&lt;p&gt;If SKILLmama saves you time, a ⭐ on the repo goes a long way — it helps other developers find it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>antigravity</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Game Identity Registry Pattern — Bridging Mirror netId and Socket.IO playerId</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Sun, 03 May 2026 00:05:04 +0000</pubDate>
      <link>https://dev.to/magithar/the-gameidentityregistry-pattern-bridging-mirror-netid-and-socketio-playerid-45lg</link>
      <guid>https://dev.to/magithar/the-gameidentityregistry-pattern-bridging-mirror-netid-and-socketio-playerid-45lg</guid>
      <description>&lt;p&gt;When you run Mirror and Socket.IO in the same Unity project, you immediately hit a translation problem.&lt;/p&gt;

&lt;p&gt;Mirror identifies players by &lt;code&gt;netId&lt;/code&gt; — a &lt;code&gt;uint&lt;/code&gt; assigned at spawn time by the Mirror host. Socket.IO identifies players by &lt;code&gt;playerId&lt;/code&gt; — a &lt;code&gt;string&lt;/code&gt; assigned by your Node.js backend when they connect.&lt;/p&gt;

&lt;p&gt;These two IDs have nothing to do with each other. They're generated by different systems at different times. But when a &lt;code&gt;score_update&lt;/code&gt; arrives from your Socket.IO backend with a &lt;code&gt;playerId&lt;/code&gt;, you need to find the corresponding Mirror &lt;code&gt;NetworkIdentity&lt;/code&gt; to apply the effect. And when a Mirror player spawns, you need to register which &lt;code&gt;playerId&lt;/code&gt; they belong to so future backend events can reach them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GameIdentityRegistry&lt;/code&gt; is the pattern that solves this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;A static lookup table. Two dictionaries. One clear API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameIdentityRegistry&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_netIdToPlayerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_playerIdToNetId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_netIdToPlayerId&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_playerIdToNetId&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;NetworkIdentity&lt;/span&gt; &lt;span class="nf"&gt;GetNetworkObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;_playerIdToNetId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Check server first, then client&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NetworkServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spawned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NetworkClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spawned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetPlayerId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_netIdToPlayerId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_netIdToPlayerId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;_playerIdToNetId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetNetworkObject&lt;/code&gt; checks &lt;code&gt;NetworkServer.spawned&lt;/code&gt; before &lt;code&gt;NetworkClient.spawned&lt;/code&gt; — this ensures it works correctly in all Mirror roles: dedicated server, host, and client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registration — When and Where
&lt;/h2&gt;

&lt;p&gt;Registration happens in &lt;code&gt;PlayerIdentityBridge&lt;/code&gt;, a &lt;code&gt;NetworkBehaviour&lt;/code&gt; attached to the Mirror player prefab.&lt;/p&gt;

&lt;p&gt;The registration must happen on all instances — server/host and all clients — because &lt;code&gt;GetNetworkObject&lt;/code&gt; might be called on any of them when a backend event arrives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerIdentityBridge&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NetworkBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnStartLocalPlayer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FindObjectOfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LobbyStateStore&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;CmdRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalPlayerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CmdRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Register on server/host&lt;/span&gt;
        &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netIdentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Propagate to all clients&lt;/span&gt;
        &lt;span class="nf"&gt;RpcRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netIdentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ClientRpc&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RpcRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow: local player spawns → &lt;code&gt;OnStartLocalPlayer&lt;/code&gt; fires → &lt;code&gt;CmdRegisterIdentity&lt;/code&gt; runs on the server → &lt;code&gt;RpcRegisterIdentity&lt;/code&gt; propagates to all clients. Every instance now has the mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage — Routing Backend Events
&lt;/h2&gt;

&lt;p&gt;When a Socket.IO event arrives with a &lt;code&gt;playerId&lt;/code&gt;, resolve it to a Mirror object and apply the effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In GameEventBridge.Subscribe()&lt;/span&gt;
&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"score_update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"playerId"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNetworkObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// player may have left&lt;/span&gt;

    &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerScore&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;SetScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"player_killed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;victimId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"victimId"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNetworkObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;victimId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerHealth&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;Die&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The null check on &lt;code&gt;GetNetworkObject&lt;/code&gt; is important — a player may have disconnected between when the server sent the event and when it arrived. Always guard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup — When and Where
&lt;/h2&gt;

&lt;p&gt;Clear the registry in exactly two places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. On ReturnToLobby — match ended normally&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ReturnToLobby&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... Mirror shutdown ...&lt;/span&gt;
    &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ... LeaveRoom ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 2. On Socket.IO disconnect — unexpected disconnection&lt;/span&gt;
&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnDisconnected&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing either case leaves stale mappings. The next match starts with entries from the previous one, &lt;code&gt;GetNetworkObject&lt;/code&gt; returns wrong objects, and events apply to destroyed players. Subtle, intermittent, hard to reproduce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Static?
&lt;/h2&gt;

&lt;p&gt;A static class means no singleton MonoBehaviour, no inspector wiring, no &lt;code&gt;FindObjectOfType&lt;/code&gt;. Any component anywhere — &lt;code&gt;GameEventBridge&lt;/code&gt;, &lt;code&gt;PlayerIdentityBridge&lt;/code&gt;, a HUD script — can call &lt;code&gt;GameIdentityRegistry.Register()&lt;/code&gt; or &lt;code&gt;GameIdentityRegistry.GetNetworkObject()&lt;/code&gt; without a reference.&lt;/p&gt;

&lt;p&gt;This is safe because the registry's lifecycle is explicitly managed: &lt;code&gt;Register()&lt;/code&gt; populates it, &lt;code&gt;Clear()&lt;/code&gt; resets it. There's no implicit state — you always know exactly what's in it based on what's been registered since the last &lt;code&gt;Clear()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader Pattern
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GameIdentityRegistry&lt;/code&gt; is a specific instance of a general pattern: &lt;strong&gt;an identity bridge between two systems that use incompatible ID schemes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The same pattern applies anywhere two systems need to reference the same logical entity by different identifiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mirror &lt;code&gt;netId&lt;/code&gt; ↔ Steam &lt;code&gt;CSteamID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mirror &lt;code&gt;netId&lt;/code&gt; ↔ Photon &lt;code&gt;ActorNumber&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mirror &lt;code&gt;netId&lt;/code&gt; ↔ any backend player ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation is always the same: two dictionaries, a &lt;code&gt;Register()&lt;/code&gt; call on spawn, a lookup on event receipt, a &lt;code&gt;Clear()&lt;/code&gt; on session end.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Context
&lt;/h2&gt;

&lt;p&gt;This pattern is part of the Mirror Integration sample in &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt; — an open-source Socket.IO v4 client for Unity with full WebGL support.&lt;/p&gt;

&lt;p&gt;Install via Package Manager → Add package from git URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/Magithar/socketio-unity.git?path=/package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then import via Package Manager → Samples → "Mirror Integration" to see the full working implementation.&lt;/p&gt;

&lt;p&gt;MIT licensed. Zero paid dependencies.&lt;/p&gt;




&lt;p&gt;Have you needed to bridge IDs between two networking systems before? What was your approach?&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>mirror</category>
    </item>
    <item>
      <title>10 Common Unity Rendering Issues (And How to Fix Them Like a Pro)</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Fri, 24 Apr 2026 18:07:02 +0000</pubDate>
      <link>https://dev.to/magithar/10-common-unity-rendering-issues-and-how-to-fix-them-like-a-pro-hcl</link>
      <guid>https://dev.to/magithar/10-common-unity-rendering-issues-and-how-to-fix-them-like-a-pro-hcl</guid>
      <description>&lt;p&gt;If you spend enough time building in Unity, you start to see the same visual bugs over and over again. Weird shadows, flickering textures, scenes that look washed out for no apparent reason — we've all been there.&lt;/p&gt;

&lt;p&gt;When you're trying to ship a game, spending hours debugging a shadow artifact is the last thing you want to do.&lt;/p&gt;

&lt;p&gt;Over time I've built up a mental cheat sheet for diagnosing these issues quickly. Here are the 10 most common ones, what causes them, and the fastest way to fix each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Shadow Artifacts (Stripes or "Acne")
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Shadow bias or precision issues. The object is casting a shadow on itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Increase the &lt;strong&gt;Shadow Bias&lt;/strong&gt; and &lt;strong&gt;Normal Bias&lt;/strong&gt; on your light source.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you push the bias too high, the shadow will detach from the object (known as "Peter Panning"). Dial it back just enough until the acne disappears.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7vixezerr1opve261dq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7vixezerr1opve261dq.png" alt="Shadow artifacts showing striped shadow acne on the sloped face of a white geometric object caused by rendering issues" width="400" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Jagged or Pixelated Shadows
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Low shadow resolution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Go to Quality Settings and increase &lt;strong&gt;Shadow Resolution&lt;/strong&gt;. If you're using a Directional Light, tweaking &lt;strong&gt;Shadow Cascades&lt;/strong&gt; can dramatically improve quality near the camera.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe697qvul69l8svt6jh29.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe697qvul69l8svt6jh29.jpg" alt="Jagged pixelated shadows with visible stair-step edges cast beside a circular tower structure in a 3D scene" width="406" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Light Bleeding Through Walls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Light leaking — usually caused by thin meshes or bias settings that are too high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Check shadow bias and normal bias first. If those are fine, ensure your meshes have actual thickness. Single-sided planes are notorious for light leaks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmy2i7zm1yzkm3quhi1zs.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmy2i7zm1yzkm3quhi1zs.jpeg" alt="Light bleeding through walls with bright sunlight leaking into the corners and floor of an indoor room in a 3D scene" width="690" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Objects Look Too Dark or Flat
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Missing Global Illumination or an incomplete lighting setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Ensure you have ambient light or skybox lighting active. If you're using static geometry, baking your lighting usually solves this and adds much-needed depth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp0n0cd3wdc5i4wqzv3y.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp0n0cd3wdc5i4wqzv3y.jpeg" alt="3D forest game scene where the player character and objects appear overly dark and flat due to poor lighting or missing shadows" width="690" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Overbright or Washed Out Scene
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Exposure or Post-Processing Tonemapping issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Check your Post-Processing Volume — specifically &lt;strong&gt;Auto Exposure&lt;/strong&gt; and &lt;strong&gt;Color Grading/Tonemapping&lt;/strong&gt;. Default auto-exposure can go wild in high-contrast scenes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvb6zq8cew9qi09k79wd7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvb6zq8cew9qi09k79wd7.jpeg" alt="Unity editor scene with extreme overexposure where the indoor environment appears overbright and washed out, causing loss of detail" width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Flickering Textures (Z-Fighting)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Depth precision issues where two faces occupy the exact same space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Make sure your meshes aren't perfectly overlapping. If they aren't, adjust the &lt;strong&gt;Near&lt;/strong&gt; and &lt;strong&gt;Far Clip Planes&lt;/strong&gt; on your Camera. Moving the Near Clip Plane further away increases depth precision.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlbbfkyjvvxqhc465lht.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlbbfkyjvvxqhc465lht.jpeg" alt="3D object showing flickering texture artifacts caused by z-fighting where overlapping surfaces compete to render in the same space" width="227" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Blurry Textures at an Angle
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Texture import settings — specifically filtering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Select the texture in your Project window and increase the &lt;strong&gt;Anisotropic Filtering&lt;/strong&gt; level. This keeps textures sharp when viewed at oblique angles like roads or floors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkm7jtdr007ltvtbve7k.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkm7jtdr007ltvtbve7k.jpeg" alt="Road texture becoming blurry at an angle in a 3D scene, highlighted to show loss of detail in the distance" width="318" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Sudden Performance Drops in Complex Scenes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; High overdraw (drawing transparent pixels on top of each other) or too many objects pushing the GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Implement LODs (Level of Detail), use GPU Instancing for repeated objects, and check your overdraw in the Scene window's draw mode selector.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Too Many Draw Calls (CPU Bottleneck)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Unbatched objects. The CPU is telling the GPU to draw too many individual things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Enable &lt;strong&gt;Static Batching&lt;/strong&gt; for non-moving objects. For dynamic objects sharing the same material, ensure &lt;strong&gt;GPU Instancing&lt;/strong&gt; or &lt;strong&gt;Dynamic Batching&lt;/strong&gt; is enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Weird Lighting or Dark Patches on Meshes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Broken or inverted normals on the 3D model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Select the model in your Project window, go to Import Settings, and set Normals to &lt;strong&gt;Calculate&lt;/strong&gt; instead of Import. If that doesn't work, recalculate normals in Blender or Maya.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fordysgi86nm6av7yjnf3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fordysgi86nm6av7yjnf3.jpg" alt="3D mesh surface showing weird lighting artifacts with uneven bright areas and dark patches caused by incorrect normals or lighting issues" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;First thing to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shadow stripes / acne&lt;/td&gt;
&lt;td&gt;Self-shadowing&lt;/td&gt;
&lt;td&gt;Shadow Bias on light&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jagged shadows&lt;/td&gt;
&lt;td&gt;Low resolution&lt;/td&gt;
&lt;td&gt;Quality Settings → Shadow Resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light through walls&lt;/td&gt;
&lt;td&gt;Thin mesh / high bias&lt;/td&gt;
&lt;td&gt;Mesh thickness + bias values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat / dark objects&lt;/td&gt;
&lt;td&gt;Missing GI&lt;/td&gt;
&lt;td&gt;Ambient light or baked lighting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Washed out scene&lt;/td&gt;
&lt;td&gt;Post-processing&lt;/td&gt;
&lt;td&gt;Auto Exposure / Tonemapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flickering textures&lt;/td&gt;
&lt;td&gt;Z-fighting&lt;/td&gt;
&lt;td&gt;Near/Far Clip Planes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blurry angled textures&lt;/td&gt;
&lt;td&gt;Filtering&lt;/td&gt;
&lt;td&gt;Anisotropic Filtering level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance drops&lt;/td&gt;
&lt;td&gt;Overdraw / GPU load&lt;/td&gt;
&lt;td&gt;LOD + overdraw view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Too many draw calls&lt;/td&gt;
&lt;td&gt;Unbatched objects&lt;/td&gt;
&lt;td&gt;Static/Dynamic Batching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dark patches on mesh&lt;/td&gt;
&lt;td&gt;Bad normals&lt;/td&gt;
&lt;td&gt;Import Settings → Normals: Calculate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;I ran into almost all of these while building out my recent multiplayer projects. Once you learn to recognize the symptoms, fixing them becomes second nature.&lt;/p&gt;

&lt;p&gt;What rendering issue trips you up most? Got a bug that's driving you crazy right now? Drop a screenshot in the comments and I'll do my best to help diagnose it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Currently building a Unity WebGL multiplayer library in public — &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt;. Follow along to see more of the technical hurdles I run into.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>tutorial</category>
      <category>graphics</category>
    </item>
    <item>
      <title>How to Integrate Mirror and SocketIO in Unity — A Hybrid Multiplayer Architecture</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Thu, 23 Apr 2026 00:34:57 +0000</pubDate>
      <link>https://dev.to/magithar/how-to-integrate-mirror-and-socketio-in-unity-a-hybrid-multiplayer-architecture-3e51</link>
      <guid>https://dev.to/magithar/how-to-integrate-mirror-and-socketio-in-unity-a-hybrid-multiplayer-architecture-3e51</guid>
      <description>&lt;p&gt;If you've built multiplayer in Unity, you've probably hit this question at some point:&lt;/p&gt;

&lt;p&gt;"Do I use Mirror, or do I use a backend with Socket.IO?"&lt;/p&gt;

&lt;p&gt;The answer most tutorials give you is pick one. But they're solving different problems — and using both is not only possible, it's the right architecture for most production multiplayer games.&lt;/p&gt;

&lt;p&gt;This guide covers exactly how to integrate &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt; with Mirror — what each system owns, where the boundary is, and how to implement the integration without the two systems bleeding into each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Principle: Two Systems, Two Jobs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Socket.IO&lt;/strong&gt; is a WebSocket client. It connects your game to a Node.js backend and handles everything that requires a server to broker: matchmaking, lobbies, session identity, authoritative scores, reconnection recovery. The backend is the source of truth. Socket.IO is the pipe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mirror&lt;/strong&gt; is an in-scene networking stack. It synchronises transforms, physics, and animation state between players at frame rate. It has no concept of a backend server and cannot validate gameplay events.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;Mirror never validates — it only synchronises. Socket.IO never touches transforms — it only brokers.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Socket.IO (Node.js Backend)          Mirror (In-Scene)
─────────────────────────────        ──────────────────
Matchmaking, lobbies, session ──►    StartClient() → server
Scores, kills, round state           NetworkTransform, Rigidbody
Reconnect recovery, host ID          NetworkBehaviour lifecycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Decision Table: What Goes Where
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Socket.IO&lt;/th&gt;
&lt;th&gt;Mirror&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Matchmaking / lobby rooms&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session identity across reconnects&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Player transform / position&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rigidbody / physics sync&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animation state&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scores, kill feed, round state&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host migration&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconnect recovery&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anti-cheat / server validation&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebGL browser support&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ (via SimpleWebTransport)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you're unsure which system owns something, ask: &lt;em&gt;does this need a server to broker it, or does it need low-latency peer sync?&lt;/em&gt; The answer tells you where it goes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;socketio-unity&lt;/strong&gt; (v1.4.0+):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;https://github.com/Magithar/socketio-unity.git?path=/package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NativeWebSocket&lt;/strong&gt; (required dependency):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;https://github.com/endel/NativeWebSocket.git#upm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mirror&lt;/strong&gt; — install via Package Manager or &lt;code&gt;.unitypackage&lt;/code&gt; from the Mirror repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The session has four distinct phases. Understanding the phase boundaries is more important than any individual line of code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session Timeline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Lobby (Socket.IO only)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Players connect to the &lt;code&gt;/lobby&lt;/code&gt; namespace, create or join rooms, exchange session credentials. Mirror is not active at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Match Start (handoff)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The host emits &lt;code&gt;start_match&lt;/code&gt;. The backend broadcasts &lt;code&gt;match_started&lt;/code&gt; to all room members. This single event is the handoff point — it triggers &lt;code&gt;MirrorGameOrchestrator&lt;/code&gt;, which starts Mirror.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 — In-Game (both layers active)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mirror syncs positions via &lt;code&gt;NetworkTransform&lt;/code&gt; every frame. Socket.IO delivers authoritative backend events (&lt;code&gt;score_update&lt;/code&gt;, &lt;code&gt;player_killed&lt;/code&gt;) via the &lt;code&gt;/game&lt;/code&gt; namespace. The two systems run in parallel, never touching each other's concerns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4 — Teardown (mandatory order)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Step 1 — Mirror first&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NetworkServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopHost&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2 — Clean /game namespace handlers&lt;/span&gt;
&lt;span class="n"&gt;gameEventBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Step 3 — Clear netId ↔ playerId mappings&lt;/span&gt;
&lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Step 4 — Signal intentional leave&lt;/span&gt;
&lt;span class="n"&gt;lobbyNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LeaveRoom&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reversing steps 1 and 4 is the most common mistake. If you call &lt;code&gt;LeaveRoom()&lt;/code&gt; (which closes the socket) before &lt;code&gt;StopHost()&lt;/code&gt;, Mirror tries to send disconnect packets over a closed transport. Silent failure, maddening to debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bridge: GameIdentityRegistry
&lt;/h2&gt;

&lt;p&gt;Mirror speaks in &lt;code&gt;netId (uint)&lt;/code&gt;. Socket.IO speaks in &lt;code&gt;playerId (string)&lt;/code&gt;. They need a translation layer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GameIdentityRegistry&lt;/code&gt; is a static lookup table that maps between the two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Register on Mirror player spawn&lt;/span&gt;
&lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Resolve when a Socket.IO event arrives&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNetworkObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// apply event to Mirror object&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Clear on ReturnToLobby&lt;/span&gt;
&lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetNetworkObject&lt;/code&gt; checks &lt;code&gt;NetworkServer.spawned&lt;/code&gt; first, then &lt;code&gt;NetworkClient.spawned&lt;/code&gt;, so it works correctly in all Mirror roles — host, server, or client.&lt;/p&gt;

&lt;p&gt;Call &lt;code&gt;Clear()&lt;/code&gt; in two places: &lt;code&gt;ReturnToLobby()&lt;/code&gt; and &lt;code&gt;store.OnDisconnected&lt;/code&gt;. Stale mappings after a disconnect cause events to fire against destroyed objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  GameEventBridge
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GameEventBridge&lt;/code&gt; subscribes to &lt;code&gt;/game&lt;/code&gt; namespace events and routes them to Mirror objects via &lt;code&gt;GameIdentityRegistry&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameEventBridge&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_scoreHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_killHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lobbyNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/game"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;_scoreHandler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"playerId"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNetworkObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerScore&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;SetScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="n"&gt;_killHandler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;victimId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"victimId"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNetworkObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;victimId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerHealth&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;Die&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"score_update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_scoreHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"player_killed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_killHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Cleanup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lobbyNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/game"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"score_update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_scoreHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"player_killed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_killHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; never call &lt;code&gt;Subscribe()&lt;/code&gt; in &lt;code&gt;Start()&lt;/code&gt;. The socket may not be initialized yet. Call it from &lt;code&gt;MirrorGameOrchestrator.HandleMatchStarted()&lt;/code&gt;, which is guaranteed to run after the socket is fully connected.&lt;/p&gt;

&lt;p&gt;Always cache handler references and call &lt;code&gt;Off()&lt;/code&gt; in &lt;code&gt;Cleanup()&lt;/code&gt;. The event registry holds delegate references — failing to unsubscribe causes callbacks to fire against destroyed MonoBehaviours.&lt;/p&gt;

&lt;h2&gt;
  
  
  MirrorGameOrchestrator
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MirrorGameOrchestrator&lt;/code&gt; listens for &lt;code&gt;match_started&lt;/code&gt; and coordinates the startup sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MirrorGameOrchestrator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;LobbyStateStore&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;LobbyNetworkManager&lt;/span&gt; &lt;span class="n"&gt;lobbyNetworkManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;NetworkManager&lt;/span&gt; &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;GameEventBridge&lt;/span&gt; &lt;span class="n"&gt;gameEventBridge&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;ServerMode&lt;/span&gt; &lt;span class="n"&gt;serverMode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;lobbyLayer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;gameLayer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_inGame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnMatchStarted&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;HandleMatchStarted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandleMatchStarted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;sceneName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;hostAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                     &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;kcpPort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;wsPort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_inGame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// dual guard against duplicate events&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NetworkClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;NetworkServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_inGame&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;gameEventBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// must happen before StartHost/Client&lt;/span&gt;

        &lt;span class="n"&gt;lobbyLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;gameLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;ServerMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PeerToPeer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;// Host starts Mirror, others connect to host's LAN IP&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsHost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartHost&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt;
                    &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;networkAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hostAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;ServerMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DedicatedKCP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;// All clients connect to dedicated server&lt;/span&gt;
                &lt;span class="nf"&gt;SetKcpPort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kcpPort&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;networkAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hostAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;ServerMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DedicatedWebSocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;SetWsPort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wsPort&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;networkAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hostAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ReturnToLobby&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NetworkServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopHost&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;mirrorNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;gameEventBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;lobbyNetworkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LeaveRoom&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;gameLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;lobbyLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_inGame&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;_inGame&lt;/code&gt; flag plus the Mirror state check is a dual guard against duplicate &lt;code&gt;match_started&lt;/code&gt; events — the server can broadcast twice if a reconnect happens mid-handshake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GameLayer must be inactive at scene start.&lt;/strong&gt; If it's active when Play begins, &lt;code&gt;NetworkManager.Awake()&lt;/code&gt; runs before the orchestrator can deactivate it, initialising Mirror prematurely.&lt;/p&gt;

&lt;h2&gt;
  
  
  ServerMode
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MirrorGameOrchestrator&lt;/code&gt; exposes a &lt;code&gt;ServerMode&lt;/code&gt; enum as an inspector dropdown. Switch between modes without changing code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Who hosts Mirror&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PeerToPeer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Room creator runs &lt;code&gt;StartHost()&lt;/code&gt;, others connect to LAN IP&lt;/td&gt;
&lt;td&gt;Local / LAN testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DedicatedKCP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All clients connect to &lt;code&gt;hostAddress:kcpPort&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Dedicated server, native builds (UDP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DedicatedWebSocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All clients connect to &lt;code&gt;hostAddress:wsPort&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Dedicated server, WebGL builds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For dedicated server mode, set &lt;code&gt;MIRROR_SERVER_ADDRESS&lt;/code&gt;, &lt;code&gt;MIRROR_KCP_PORT&lt;/code&gt;, and &lt;code&gt;MIRROR_WS_PORT&lt;/code&gt; as environment variables on your lobby server. The server injects them into every &lt;code&gt;match_started&lt;/code&gt; broadcast automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  PlayerIdentityBridge
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PlayerIdentityBridge&lt;/code&gt; runs on the Mirror player prefab and registers the &lt;code&gt;netId ↔ playerId&lt;/code&gt; mapping when the player spawns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerIdentityBridge&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NetworkBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;SyncVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OnDisplayNameChanged&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;_displayName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;TMP_Text&lt;/span&gt; &lt;span class="n"&gt;nameLabel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnStartLocalPlayer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FindObjectOfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LobbyStateStore&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;CmdRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalPlayerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentRoom&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalPlayerId&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
            &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalPlayerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;CmdSetDisplayName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CmdRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netIdentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Sync to all clients&lt;/span&gt;
        &lt;span class="nf"&gt;RpcRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netIdentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ClientRpc&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RpcRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CmdSetDisplayName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_displayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;OnDisplayNameChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// SyncVar hook doesn't fire on host&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDisplayNameChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nameLabel&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;nameLabel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the manual hook call in &lt;code&gt;CmdSetDisplayName&lt;/code&gt; — Mirror SyncVar hooks do not fire on the host when the value is set on the server. This is a common gotcha.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Starting Mirror before &lt;code&gt;match_started&lt;/code&gt;&lt;/strong&gt; — always start Mirror inside &lt;code&gt;HandleMatchStarted&lt;/code&gt;. Calling &lt;code&gt;StartHost()&lt;/code&gt; from a button before the backend confirms creates an orphaned Mirror session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using Mirror &lt;code&gt;[Command]&lt;/code&gt; for game validation&lt;/strong&gt; — &lt;code&gt;[Command]&lt;/code&gt; goes to the Mirror host, which is a client and can be spoofed. Route all validation through Socket.IO. The backend emits the result; Mirror executes the visual effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double-spawning when migrating from PlayerSync&lt;/strong&gt; — if your project previously handled &lt;code&gt;player_join&lt;/code&gt; via Socket.IO, disable those handlers during the Mirror game phase. Mirror owns all in-scene player lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;StartClient()&lt;/code&gt; failure leaving the player stranded&lt;/strong&gt; — wire &lt;code&gt;OnClientDisconnect&lt;/code&gt; to call &lt;code&gt;ReturnToLobby()&lt;/code&gt; so players return to the lobby instead of seeing a blank screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Working Sample
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt; repo ships a full Mirror Integration sample — lobby → match transition, WASD movement synced via &lt;code&gt;NetworkTransform&lt;/code&gt;, lobby display name above each player, graceful shutdown, and a Node.js test server with HTTP endpoints to fire game events from a browser while Unity runs.&lt;/p&gt;

&lt;p&gt;Import via Package Manager → Samples → "Mirror Integration".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;path/to/socketio-unity-mirror-server
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run start:mirror
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT licensed. Zero paid dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live WebGL demo:&lt;/strong&gt; &lt;a href="https://magithar.github.io/socketio-unity/" rel="noopener noreferrer"&gt;magithar.github.io/socketio-unity/&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Have you built a hybrid architecture like this before? What was the hardest part — the boundary between systems, the teardown order, or something else entirely?&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>mirror</category>
    </item>
    <item>
      <title>Why Your Unity WebGL Multiplayer Breaks on Some Networks</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Fri, 10 Apr 2026 23:05:44 +0000</pubDate>
      <link>https://dev.to/magithar/why-your-unity-webgl-multiplayer-breaks-on-some-networks-1462</link>
      <guid>https://dev.to/magithar/why-your-unity-webgl-multiplayer-breaks-on-some-networks-1462</guid>
      <description>&lt;p&gt;You test your Unity WebGL multiplayer game. It works perfectly. You share it with a friend — it works. You share it with someone on a corporate network, or a phone hotspot, or after pushing a new build — and it silently fails to connect.&lt;/p&gt;

&lt;p&gt;No useful error. No crash. Just... nothing.&lt;/p&gt;

&lt;p&gt;This happens because Unity WebGL multiplayer has three distinct failure modes that don't exist in standalone builds: CORS blocks the connection before it opens, WebSocket gets intercepted by a network proxy, and browser cache serves your old build to players days after you deployed a fix.&lt;/p&gt;

&lt;p&gt;Here's how to diagnose and fix all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. CORS — the most common first failure
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What it is
&lt;/h3&gt;

&lt;p&gt;CORS (Cross-Origin Resource Sharing) is a browser security rule. When your WebGL game at &lt;code&gt;https://yoursite.com&lt;/code&gt; tries to connect to &lt;code&gt;https://api.example.com&lt;/code&gt;, the browser first sends a preflight &lt;code&gt;OPTIONS&lt;/code&gt; request asking "are you allowed to talk to me?" Your Socket.IO server must answer yes.&lt;/p&gt;

&lt;p&gt;If it does not, the browser blocks the WebSocket upgrade before it even begins.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you see
&lt;/h3&gt;

&lt;p&gt;In the browser console (F12 → Console):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Access to fetch at 'https://api.example.com/socket.io/?EIO=4...'
from origin 'https://yoursite.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in the Network tab: the &lt;code&gt;/socket.io/&lt;/code&gt; request returns status &lt;code&gt;0&lt;/code&gt; or the WebSocket connection is refused before opening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why WebGL is different from desktop
&lt;/h3&gt;

&lt;p&gt;A Unity desktop or mobile build connects as a native TCP socket — no browser, no CORS check. WebGL runs inside the browser, so every connection goes through the browser's security model. The same server that works for your standalone build may refuse WebGL completely.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;On your Node.js Socket.IO server, pass a &lt;code&gt;cors&lt;/code&gt; option when creating the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;socket.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;httpServer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;httpServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://yoursite.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// exact origin, or "*" for open access&lt;/span&gt;
        &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For local development you often need to allow &lt;code&gt;localhost&lt;/code&gt; and your production domain together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://yoursite.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;origin: "*"&lt;/code&gt; works but removes all cross-origin protection. Fine for a public demo, not for a game with auth tokens.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Verifying it worked
&lt;/h3&gt;

&lt;p&gt;After fixing the server, check the Network tab in DevTools. The &lt;code&gt;/socket.io/&lt;/code&gt; request should return &lt;code&gt;200&lt;/code&gt; with these response headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Origin: https://yoursite.com
Access-Control-Allow-Methods: GET,POST
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. WebSocket blocking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What it is
&lt;/h3&gt;

&lt;p&gt;Some networks actively block WebSocket connections. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Corporate firewalls&lt;/strong&gt; — WebSocket upgrade requests look unusual; many enterprise proxies strip the &lt;code&gt;Upgrade: websocket&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carrier-grade NAT and mobile networks&lt;/strong&gt; — Some mobile carriers terminate WebSocket connections after 30–60 seconds of inactivity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent HTTP proxies&lt;/strong&gt; — Common in schools, hotels, and public Wi-Fi. These intercept HTTP traffic and cannot forward a WebSocket upgrade&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict CSP headers&lt;/strong&gt; — If your hosting adds a &lt;code&gt;Content-Security-Policy&lt;/code&gt; header that does not include &lt;code&gt;connect-src wss://your-server.com&lt;/code&gt;, the browser blocks the connection&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you see
&lt;/h3&gt;

&lt;p&gt;The connection opens briefly, then closes, often with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[SocketIO:Transport] Transport error: WebSocket is closed before connection is established
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the socket connects on the first try but drops every 30–60 seconds on mobile.&lt;/p&gt;

&lt;p&gt;The browser console may show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;WebSocket connection to 'wss://...' failed: Error during WebSocket handshake:
Unexpected response code: 200
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;200&lt;/code&gt; instead of &lt;code&gt;101&lt;/code&gt; means a proxy intercepted and replied instead of letting the WebSocket upgrade through.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mixed content: the silent variant
&lt;/h3&gt;

&lt;p&gt;If your game page is served over &lt;code&gt;https://&lt;/code&gt; but your Socket.IO server URL starts with &lt;code&gt;ws://&lt;/code&gt; (not &lt;code&gt;wss://&lt;/code&gt;), the browser blocks it as &lt;strong&gt;mixed content&lt;/strong&gt; — no error dialog, just a silent drop.&lt;/p&gt;

&lt;p&gt;In Unity, your server URL must match the page protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Page is https:// — use wss://&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;serverUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"wss://your-server.com"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Page is http:// (local dev) — ws:// is fine&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;serverUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ws://localhost:3000"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most hosting platforms (Render, Railway, Fly.io) terminate TLS for you, so your Node.js server can listen on plain HTTP while clients connect over &lt;code&gt;wss://&lt;/code&gt;. The TLS layer is handled at the edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Polling fallback
&lt;/h3&gt;

&lt;p&gt;Socket.IO supports two transports: WebSocket and HTTP long-polling. Long-polling works through most proxies because it looks like normal HTTPS traffic. Socket.IO will automatically fall back to polling if WebSocket fails during the handshake.&lt;/p&gt;

&lt;p&gt;To confirm which transport is active, check the browser Network tab. A persistent WS entry means WebSocket succeeded. Repeated XHR requests to &lt;code&gt;/socket.io/?transport=polling&lt;/code&gt; mean it fell back.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSP fix
&lt;/h3&gt;

&lt;p&gt;If your host adds a Content-Security-Policy header, explicitly allow your Socket.IO server origin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy: connect-src 'self' wss://your-server.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For GitHub Pages there's no way to set response headers directly — if this is a blocker, move to a host that gives you header control (Netlify, Vercel, and Cloudflare Pages all support &lt;code&gt;_headers&lt;/code&gt; files).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Browser cache
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What it is
&lt;/h3&gt;

&lt;p&gt;When you push a new WebGL build, players on repeat visits may run your old code — the browser served it from disk cache before even asking the server. This is particularly disruptive for multiplayer because the old client sends packets the new server no longer understands.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you see
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A bug you fixed reappears after deployment&lt;/li&gt;
&lt;li&gt;Players report "connection looping" while you cannot reproduce it&lt;/li&gt;
&lt;li&gt;The Socket.IO version mismatch shows in the handshake: the &lt;code&gt;EIO=&lt;/code&gt; query parameter differs between clients&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Unity WebGL is especially vulnerable
&lt;/h3&gt;

&lt;p&gt;Unity WebGL produces large binary files (&lt;code&gt;.wasm&lt;/code&gt;, &lt;code&gt;.data&lt;/code&gt;, &lt;code&gt;.framework.js&lt;/code&gt;). Browsers aggressively cache large static assets. Unless filenames include a content hash, users will not get the new version until their cache expires — which could be days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Development (testing your own build):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hard-refresh: &lt;code&gt;Cmd+Shift+R&lt;/code&gt; (Mac) or &lt;code&gt;Ctrl+Shift+R&lt;/code&gt; (Windows/Linux). Use a Private/Incognito window for a fully clean test environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production (your players' browsers):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The correct fix is to version &lt;strong&gt;all four asset URLs&lt;/strong&gt; — the loader, data, wasm, and framework files. The loader is a separate &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, but the data/wasm/framework URLs come from the &lt;code&gt;config&lt;/code&gt; object passed to &lt;code&gt;createUnityInstance&lt;/code&gt; in &lt;code&gt;index.html&lt;/code&gt;. If those filenames are unchanged, the browser will cache them even if the loader itself is fresh.&lt;/p&gt;

&lt;p&gt;Version all four in &lt;code&gt;index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Loader script tag --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"Build/LiveDemo.loader.js?v=1.3.2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// createUnityInstance config — version all asset URLs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;dataUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Build/LiveDemo.data.unityweb?v=1.3.2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;frameworkUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Build/LiveDemo.framework.js.unityweb?v=1.3.2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;codeUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Build/LiveDemo.wasm.unityweb?v=1.3.2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A CI/CD step can rewrite all four at once using the git commit SHA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Linux&lt;/span&gt;
&lt;span class="nv"&gt;GIT_SHA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"s|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;unityweb|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;unityweb?v=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GIT_SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|g"&lt;/span&gt; docs/index.html
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"s|LiveDemo&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;loader&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;js|LiveDemo.loader.js?v=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GIT_SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|"&lt;/span&gt; docs/index.html

&lt;span class="c"&gt;# macOS requires an empty backup suffix&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="s2"&gt;"s|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;unityweb|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;unityweb?v=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GIT_SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|g"&lt;/span&gt; docs/index.html
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="s2"&gt;"s|LiveDemo&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;loader&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;js|LiveDemo.loader.js?v=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GIT_SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|"&lt;/span&gt; docs/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also set appropriate cache headers on your static host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Aggressive caching for content-hashed assets
&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt;-&lt;span class="n"&gt;Control&lt;/span&gt;: &lt;span class="n"&gt;public&lt;/span&gt;, &lt;span class="n"&gt;max&lt;/span&gt;-&lt;span class="n"&gt;age&lt;/span&gt;=&lt;span class="m"&gt;31536000&lt;/span&gt;, &lt;span class="n"&gt;immutable&lt;/span&gt;

&lt;span class="c"&gt;# Short cache or no-cache for the loader and index
&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt;-&lt;span class="n"&gt;Control&lt;/span&gt;: &lt;span class="n"&gt;no&lt;/span&gt;-&lt;span class="n"&gt;cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Diagnostic checklist
&lt;/h2&gt;

&lt;p&gt;When WebGL multiplayer breaks on a specific network or after a deploy, work through this list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CORS
[ ] Server has cors.origin set to match the page origin
[ ] OPTIONS preflight returns 200 with Access-Control-Allow-Origin

WEBSOCKET BLOCKING
[ ] Page is https:// — server URL is wss://, not ws://
[ ] Network tab shows 101 Switching Protocols, not 200
[ ] No CSP connect-src violation in browser console
[ ] Socket.IO falls back to polling on restrictive networks (check for XHR traffic)

CACHE
[ ] Hard-refresh confirms the bug persists (rules out cache)
[ ] Loader URL has version query string updated after deploy
[ ] No players stuck on old EIO version (check server handshake logs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Most likely cause&lt;/th&gt;
&lt;th&gt;First thing to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Works on your network, fails elsewhere&lt;/td&gt;
&lt;td&gt;WebSocket blocked by proxy&lt;/td&gt;
&lt;td&gt;Network tab: 101 or 200?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works standalone, fails in browser&lt;/td&gt;
&lt;td&gt;CORS&lt;/td&gt;
&lt;td&gt;Console: &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on http://, fails on https://&lt;/td&gt;
&lt;td&gt;Mixed content&lt;/td&gt;
&lt;td&gt;Server URL: &lt;code&gt;ws://&lt;/code&gt; vs &lt;code&gt;wss://&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug reappears after you fixed it&lt;/td&gt;
&lt;td&gt;Browser cache&lt;/td&gt;
&lt;td&gt;Hard-refresh, confirm build version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connects then drops every ~60s&lt;/td&gt;
&lt;td&gt;Mobile NAT timeout&lt;/td&gt;
&lt;td&gt;Enable ping keepalive; confirm polling fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Connection refused" after new deploy&lt;/td&gt;
&lt;td&gt;Old client, new server protocol&lt;/td&gt;
&lt;td&gt;Check EIO version in query string&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The repo
&lt;/h2&gt;

&lt;p&gt;All of this applies to &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt; — the open-source Socket.IO v4 client for Unity this series is built around. MIT licensed, WebGL verified, zero paid dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live WebGL demo:&lt;/strong&gt; &lt;a href="https://magithar.github.io/socketio-unity/" rel="noopener noreferrer"&gt;magithar.github.io/socketio-unity/&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Have you hit a WebGL multiplayer issue that isn't on this list? Drop it in the comments — especially anything weird you've seen on specific mobile carriers or corporate networks.&lt;/p&gt;




&lt;h2&gt;
  
  
  See also
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Magithar/socketio-unity/blob/main/Documentation~/WEBGL_NOTES.md" rel="noopener noreferrer"&gt;WEBGL_NOTES.md&lt;/a&gt; — Architecture, jslib setup, IL2CPP stripping&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Magithar/socketio-unity/blob/main/Documentation~/DEBUGGING_GUIDE.md" rel="noopener noreferrer"&gt;DEBUGGING_GUIDE.md&lt;/a&gt; — Trace system, diagnostics overlay, common errors&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Magithar/socketio-unity/blob/main/Documentation~/RECONNECT_BEHAVIOR.md" rel="noopener noreferrer"&gt;RECONNECT_BEHAVIOR.md&lt;/a&gt; — Reconnect strategy and configuration&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>webgl</category>
      <category>multiplayer</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>How to Build a Multiplayer Lobby in Unity with Socket.IO</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 18:56:08 +0000</pubDate>
      <link>https://dev.to/magithar/how-to-build-a-multiplayer-lobby-in-unity-with-socketio-ko9</link>
      <guid>https://dev.to/magithar/how-to-build-a-multiplayer-lobby-in-unity-with-socketio-ko9</guid>
      <description>&lt;p&gt;Building a multiplayer lobby is where most Unity real-time projects fall apart.&lt;/p&gt;

&lt;p&gt;Not the networking part — that's the easy bit. The hard part is everything around it: players joining and leaving mid-session, the host disconnecting and taking the room with them, a player's phone screen locking for 30 seconds and losing their slot forever. Most tutorials stop at "connected". This one doesn't.&lt;/p&gt;

&lt;p&gt;This is a complete walkthrough of building a production-ready lobby in Unity using &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt; — room creation, join by code, ready states, host migration, and reconnect with a grace window. Every line of code is taken from the real &lt;code&gt;Samples~/Lobby/&lt;/code&gt; sample in the repo, so you can run it locally and pull it apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Unity 2020.1+&lt;/li&gt;
&lt;li&gt;Node.js (for the test server)&lt;/li&gt;
&lt;li&gt;socketio-unity installed via &lt;code&gt;Window → Package Manager → Add package from git URL&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/Magithar/socketio-unity.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Before writing a line of code, the architecture matters. A lobby has three distinct concerns — networking, state, and UI — and mixing them is how you end up with spaghetti that breaks every time the server sends an unexpected event.&lt;/p&gt;

&lt;p&gt;The sample uses a clean three-layer split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LobbyNetworkManager   ← Transport: connects, emits, receives
        ↓ writes
LobbyStateStore       ← State: single source of truth, fires C# events
        ↓ events
LobbyUIController     ← View: subscribes to events, drives UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI never touches the socket directly. The network layer never knows about GameObjects. When something breaks — and in multiplayer, something always breaks — you know exactly which layer to look at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Connect to the /lobby Namespace
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;LobbyNetworkManager&lt;/code&gt; MonoBehaviour. Use a namespace socket (&lt;code&gt;_root.Of("/lobby")&lt;/code&gt;) rather than the root socket — this lets the server scope events cleanly per feature without event name collisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;SocketIOClient&lt;/span&gt; &lt;span class="n"&gt;_root&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;NamespaceSocket&lt;/span&gt; &lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_root&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SocketIOClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TransportFactoryHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDefault&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;_root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReconnectConfig&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ReconnectConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;autoReconnect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;_lobby&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/lobby"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:3001"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;autoReconnect = false&lt;/code&gt; so you control reconnect logic — this is essential for restoring sessions with saved credentials rather than connecting as a fresh player.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Define Your Data Models
&lt;/h2&gt;

&lt;p&gt;The server sends JSON snapshots of the room state. Two things are critical here:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[Preserve]&lt;/code&gt; prevents IL2CPP from stripping fields that are only referenced via JSON deserialization — without this, WebGL builds will silently lose data. &lt;code&gt;[JsonProperty]&lt;/code&gt; ensures the fields survive minification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RoomState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"roomId"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hostId"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;hostId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"players"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LobbyPlayer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LobbyPlayer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;     &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;   &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ready"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Preserve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "connected" | "disconnected"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 — Build the State Store
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LobbyStateStore&lt;/code&gt; holds authoritative state and exposes C# events. The UI subscribes here — never to the socket directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LobbyStateStore&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;RoomState&lt;/span&gt; &lt;span class="n"&gt;CurrentRoom&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LocalPlayerId&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;SessionToken&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsHost&lt;/span&gt;             &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CurrentRoom&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;CurrentRoom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;LocalPlayerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;OnConnected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;OnDisconnected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RoomState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;             &lt;span class="n"&gt;OnRoomStateChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LobbyPlayer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;           &lt;span class="n"&gt;OnPlayerJoined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;                &lt;span class="n"&gt;OnPlayerLeft&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// playerId&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnPlayerRemoved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// id, name, reason&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SocketError&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;           &lt;span class="n"&gt;OnError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;                &lt;span class="n"&gt;OnMatchStarted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// sceneName&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_lastRoomVersion&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ApplyRoomState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RoomState&lt;/span&gt; &lt;span class="n"&gt;newState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newState&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;newState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;_lastRoomVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// dedupe&lt;/span&gt;
        &lt;span class="n"&gt;_lastRoomVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;DiffAndFirePlayerEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CurrentRoom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;CurrentRoom&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;OnRoomStateChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CurrentRoom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;DiffAndFirePlayerEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RoomState&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RoomState&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;oldIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;oldIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;OnPlayerJoined&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;newIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;OnPlayerLeft&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CurrentRoom&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;LocalPlayerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;SessionToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;_lastRoomVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;version&lt;/code&gt; check is not optional. During reconnect, the server broadcasts the current room snapshot to the rejoining client — which means the same state can arrive twice. Without the version guard, your UI fires double join/leave events and gets out of sync.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DiffAndFirePlayerEvents&lt;/code&gt; lives on &lt;code&gt;LobbyStateStore&lt;/code&gt;, called by &lt;code&gt;ApplyRoomState&lt;/code&gt; — not on the UI controller. Keep all player diffing logic inside the store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Wire Up Incoming Events
&lt;/h2&gt;

&lt;p&gt;Subscribe to the four server events in &lt;code&gt;LobbyNetworkManager&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server identifies you before room_state arrives&lt;/span&gt;
&lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"player_identity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetLocalPlayerId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"playerId"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetSessionToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"sessionToken"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Authoritative full snapshot&lt;/span&gt;
&lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"room_state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeserializeObject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RoomState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyRoomState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Host started the match&lt;/span&gt;
&lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"match_started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s"&gt;"sceneName"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FireMatchStarted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Player permanently removed (left or reconnect window expired)&lt;/span&gt;
&lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"player_removed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirePlayerRemoved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"playerId"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identity ordering matters: the server sends &lt;code&gt;player_identity&lt;/code&gt; before the ACK and before &lt;code&gt;room_state&lt;/code&gt;. By the time &lt;code&gt;ApplyRoomState&lt;/code&gt; fires, &lt;code&gt;IsHost&lt;/code&gt; evaluates correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Emit Lobby Actions
&lt;/h2&gt;

&lt;p&gt;Use ACK callbacks for &lt;code&gt;create_room&lt;/code&gt; and &lt;code&gt;join_room&lt;/code&gt; — they confirm success and return credentials:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on &lt;code&gt;ParseAck&lt;/code&gt;:&lt;/strong&gt; The real &lt;code&gt;LobbyNetworkManager.cs&lt;/code&gt; handles three response shapes — raw string, JSON array, and JSON object. The snippets below use a simplified version for readability. For production use, copy the full &lt;code&gt;ParseAck&lt;/code&gt; implementation directly from the sample to avoid silent failures on the array case.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CreateRoom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"create_room"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playerName&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ack&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParseAck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetLocalPlayerId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"playerId"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetSessionToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"sessionToken"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;JoinRoom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"join_room"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;roomId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUpper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playerName&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ack&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParseAck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FireError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SocketError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"player_ready"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;StartMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"start_match"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sceneName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LeaveRoom&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"leave_room"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6 — Build the Player List UI
&lt;/h2&gt;

&lt;p&gt;Instantiate one prefab row per player. Update without full rebuilds by diffing — recreating all rows on every state update causes flicker and loses UI state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_playerRows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandlePlayerJoined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LobbyPlayer&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerRowPrefab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerListContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_playerRows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;UpdateRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandlePlayerLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_playerRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_playerRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Called on every room_state to reconcile the authoritative list&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RefreshPlayerRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RoomState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stateIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_playerRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Except&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateIds&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_playerRows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="n"&gt;_playerRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;_playerRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_playerRows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerRowPrefab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerListContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;UpdateRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;UpdateRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LobbyPlayer&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;disconnected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"disconnected"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nameText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NameText"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TextMeshProUGUI&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;nameText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentRoom&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;hostId&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; [Host]&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;disconnected&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;" (Reconnecting...)"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}{(&lt;/span&gt;&lt;span class="n"&gt;disconnected&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;" (Reconnecting...)"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;nameText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;disconnected&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gray&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReadyIcon"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;disconnected&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yellow&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7 — Handle Disconnects and Session Restore
&lt;/h2&gt;

&lt;p&gt;This is where most lobby implementations give up. The server holds a player's slot for 10 seconds after disconnect — their row stays visible with &lt;code&gt;status = "disconnected"&lt;/code&gt;. On reconnect, send saved credentials to reclaim the slot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;PREF_ROOM_ID&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Lobby_LastRoomId"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;PREF_PLAYER_ID&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Lobby_PlayerId"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;PREF_SESSION_TOKEN&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Lobby_SessionToken"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Save on every room_state&lt;/span&gt;
&lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PREF_ROOM_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PREF_PLAYER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalPlayerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PREF_SESSION_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// On reconnect — try to restore&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandleConnected&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PREF_PLAYER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PREF_ROOM_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PREF_SESSION_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;networkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReconnectSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;StartCoroutine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;RejoinTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// give up after 5 sec&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ReconnectSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;sessionToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_lobby&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"reconnect_player"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionToken&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ack&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParseAck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Room expired — clear credentials and return to lobby selection&lt;/span&gt;
            &lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DeleteKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PREF_ROOM_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FireError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SocketError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 5-second rejoin timeout is important. If the room expired while the player was disconnected, you don't want to hang on a reconnect attempt forever — clear the stale credentials and return them to the lobby selection screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8 — Start the Match
&lt;/h2&gt;

&lt;p&gt;Only the host sees the Start button. On &lt;code&gt;match_started&lt;/code&gt;, all clients load the scene:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In Update() — simpler than event-driven for a toggle&lt;/span&gt;
&lt;span class="n"&gt;startMatchButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsHost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandleMatchStarted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;sceneName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sceneName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;SceneManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadScene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sceneName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scene Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Canvas
├── LobbySelectionPanel
│   ├── PlayerNameInput (TMP_InputField)
│   ├── CreateRoomButton
│   ├── JoinRoomCodeInput (TMP_InputField)
│   └── JoinRoomButton
├── RoomPanel
│   ├── RoomCodeText (TextMeshProUGUI)
│   ├── CopyRoomCodeButton
│   ├── LeaveRoomButton
│   ├── ReadyButton
│   ├── StartMatchButton        ← toggled by IsHost
│   ├── PlayerList / Content    ← Vertical Layout Group
│   └── ReconnectPanel (overlay)
└── ConnectionStatusText
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assign &lt;code&gt;LobbyNetworkManager&lt;/code&gt; and &lt;code&gt;LobbyStateStore&lt;/code&gt; to &lt;code&gt;LobbyUIController&lt;/code&gt; in the Inspector. Both should be on a &lt;code&gt;DontDestroyOnLoad&lt;/code&gt; GameObject if you carry them across scenes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Patterns
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;room_state.version&lt;/code&gt; check&lt;/td&gt;
&lt;td&gt;Ignores duplicate snapshots during reconnect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;player_identity&lt;/code&gt; before ACK&lt;/td&gt;
&lt;td&gt;Ensures &lt;code&gt;IsHost&lt;/code&gt; is correct by the first &lt;code&gt;room_state&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;_joinInFlight&lt;/code&gt; guard&lt;/td&gt;
&lt;td&gt;Prevents double-emit during reconnect sequence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PlayerPrefs&lt;/code&gt; session storage&lt;/td&gt;
&lt;td&gt;Survives app backgrounding, not just network blips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-second rejoin timeout&lt;/td&gt;
&lt;td&gt;Clears stale credentials if the room already expired&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Running the Demo
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Samples~/Lobby/&lt;/code&gt; sample includes a full working server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;TestServer~
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run start:lobby   &lt;span class="c"&gt;# or: npm run dev:lobby (auto-restart via nodemon)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server lives at &lt;code&gt;TestServer~/lobby-server.js&lt;/code&gt;. The &lt;code&gt;package.json&lt;/code&gt; at &lt;code&gt;TestServer~/package.json&lt;/code&gt; already declares &lt;code&gt;express&lt;/code&gt; and &lt;code&gt;socket.io&lt;/code&gt; as dependencies — no manual installs needed.&lt;/p&gt;

&lt;p&gt;Then open the Unity scene and press Play. The sample is the fastest way to see all of this in action before adapting it to your own project.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;github.com/Magithar/socketio-unity&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. Socket.IO v4 only. WebGL verified. Zero paid dependencies.&lt;/p&gt;




&lt;p&gt;What's the hardest part of multiplayer lobbies you've had to solve? Host migration, latency compensation, or something else entirely? Drop it in the comments.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>socketio</category>
    </item>
    <item>
      <title>How Unity WebGL Talks to a WebSocket Server (The Hard Way)</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Tue, 07 Apr 2026 16:15:29 +0000</pubDate>
      <link>https://dev.to/magithar/how-unity-webgl-talks-to-a-websocket-server-the-hard-way-21fi</link>
      <guid>https://dev.to/magithar/how-unity-webgl-talks-to-a-websocket-server-the-hard-way-21fi</guid>
      <description>&lt;p&gt;When I built &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt;, the hardest part wasn't the Socket.IO protocol. It wasn't binary packet assembly or reconnect logic.&lt;/p&gt;

&lt;p&gt;It was making WebGL work.&lt;/p&gt;

&lt;p&gt;Not "works on my machine" works. Production-verified, binary-safe, multi-socket, reconnect-safe works.&lt;/p&gt;

&lt;p&gt;This is the full technical writeup of how the WebGL bridge is built — every design decision, every edge case, and every gotcha that will silently destroy you in production if you miss it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WebGL Needs a Bridge at All
&lt;/h2&gt;

&lt;p&gt;Desktop Unity runs on the .NET runtime — you call &lt;code&gt;System.Net.WebSockets&lt;/code&gt; directly and it just works. WebGL is different. Unity compiles your C# to WebAssembly via IL2CPP, which means your code runs inside a browser sandbox with no access to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No raw sockets from C#&lt;/li&gt;
&lt;li&gt;No background threads&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;System.Net&lt;/code&gt; anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every WebSocket operation has to go through the browser's native &lt;code&gt;WebSocket&lt;/code&gt; API. And the only way to reach browser APIs from C# in a WebGL build is through a &lt;code&gt;.jslib&lt;/code&gt; — a JavaScript file Unity compiles into the final build and exposes to your C# code via &lt;code&gt;DllImport("__Internal")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So instead of calling &lt;code&gt;System.Net.WebSockets&lt;/code&gt; directly, you call JavaScript. JavaScript manages the actual socket and fires callbacks back into C# via Unity's &lt;code&gt;SendMessage&lt;/code&gt;. That's the bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dual-Transport Architecture
&lt;/h2&gt;

&lt;p&gt;The library uses one interface for both platforms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ITransport&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendBinary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At compile time, a preprocessor guard selects the right implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR
&lt;/span&gt;    &lt;span class="n"&gt;transport&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WebGLWebSocketTransport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="cp"&gt;#else
&lt;/span&gt;    &lt;span class="n"&gt;transport&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NativeWebSocketTransport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;!UNITY_EDITOR&lt;/code&gt; is not optional. Without it, the Editor tries to compile &lt;code&gt;DllImport("__Internal")&lt;/code&gt; which has no backing implementation there — and fails silently in ways that take a long time to diagnose.&lt;/p&gt;

&lt;p&gt;Everything above the transport layer — EngineIO, SocketIO, namespaces, ACKs, reconnect — is completely platform-agnostic. It never knows which transport it's talking to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Files Involved
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WebGLWebSocketTransport.cs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;C# side — calls JS functions via &lt;code&gt;DllImport&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WebGLSocketBridge.cs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MonoBehaviour singleton — routes JS callbacks to the right transport&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SocketIOWebGL.jslib&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JavaScript — manages browser &lt;code&gt;WebSocket&lt;/code&gt; objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ITransport.cs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Platform-agnostic interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;link.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IL2CPP stripping protection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Socket ID Routing
&lt;/h2&gt;

&lt;p&gt;This is the first non-obvious problem.&lt;/p&gt;

&lt;p&gt;The bridge is a singleton — one &lt;code&gt;WebGLSocketBridge&lt;/code&gt; MonoBehaviour for the entire application. But a single app can have multiple transports open simultaneously (the root namespace plus several custom namespaces, for example).&lt;/p&gt;

&lt;p&gt;When JavaScript fires a callback, how do you know which C# transport it belongs to?&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;WebGLWebSocketTransport&lt;/code&gt; generates a GUID on construction. This ID is passed to JavaScript when creating the socket, and prefixed to every callback payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JS → C#:  "a1b2c3d4-...:2[\"chat\",\"hello\"]"
            ^^^^^^^^^^^^^^ socketId   ^^^^^^^^^ packet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WebGLSocketBridge&lt;/code&gt; maintains five &lt;code&gt;Dictionary&amp;lt;string, Action&amp;gt;&lt;/code&gt; maps — open, close, text, binary, error — keyed by socket ID. When a transport connects, it calls &lt;code&gt;Register()&lt;/code&gt;. When it closes, it calls &lt;code&gt;Unregister()&lt;/code&gt;. The bridge parses the prefix and dispatches to the right handler. No GUID, no routing, no callback collision.&lt;/p&gt;

&lt;p&gt;There's also a fallback: both text and binary parsers fall back to &lt;code&gt;_lastActiveSocketId&lt;/code&gt; if the payload doesn't contain a socket ID prefix. This handles legacy jslib versions and transitional deployments without breaking anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Message Flow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Outbound (emit)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;socket.Emit()
  → EngineIOClient.SendRaw()
  → WebGLWebSocketTransport.SendText() / SendBinary()
  → DllImport → SocketIO_WebSocket_SendText / SendBinary
  → browser WebSocket.send()
  → server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For binary sends, the managed byte array has to be pinned before its pointer is handed to JavaScript — otherwise the GC can move it mid-call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GCHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GCHandleType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pinned&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;SocketIO_WebSocket_SendBinary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socketId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddrOfPinnedObject&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Free&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block is not optional. A leaked &lt;code&gt;GCHandle&lt;/code&gt; is a memory leak that accumulates silently over a session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inbound (receive)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server → browser WebSocket.onmessage
  → JS allocates WASM heap (_malloc), copies bytes, calls SendMessage
  → WebGLSocketBridge.JSOnText / JSOnBinary
  → parses "socketId:payload" or "socketId,ptr,len"
  → dictionary lookup → fires handler on correct transport
  → EngineIOClient parses Socket.IO packet
  → game On() handler invoked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Binary inbound is the most delicate part of the entire bridge. The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;JavaScript receives an &lt;code&gt;ArrayBuffer&lt;/code&gt; from the server&lt;/li&gt;
&lt;li&gt;JS calls &lt;code&gt;_malloc()&lt;/code&gt; to allocate memory on the WASM heap&lt;/li&gt;
&lt;li&gt;JS copies the &lt;code&gt;ArrayBuffer&lt;/code&gt; bytes into that allocation&lt;/li&gt;
&lt;li&gt;JS calls &lt;code&gt;SendMessage&lt;/code&gt; with the pointer and length&lt;/li&gt;
&lt;li&gt;C# calls &lt;code&gt;Marshal.Copy((IntPtr)ptr, data, 0, len)&lt;/code&gt; to pull it into managed memory&lt;/li&gt;
&lt;li&gt;JS immediately calls &lt;code&gt;_free()&lt;/code&gt; on the allocation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 6 has to happen after step 5. If you free before C# has copied, you're reading deallocated memory. If you forget to free at all, you leak WASM heap on every binary message received.&lt;/p&gt;

&lt;p&gt;The jslib handles this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;HEAPU8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;SendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebGLSocketBridge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JSOnBinary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;socketId&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;_free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more thing: &lt;code&gt;binaryType = "arraybuffer"&lt;/code&gt; must be set on the socket at creation time. Without it, binary messages arrive as &lt;code&gt;Blob&lt;/code&gt; — which cannot be synchronously read inside the callback. This is silent. The message arrives, your handler fires, and the data is just... not there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binaryType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arraybuffer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// without this, binary is broken&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Dispatch() is a No-Op
&lt;/h2&gt;

&lt;p&gt;On desktop, &lt;code&gt;Dispatch()&lt;/code&gt; is called every frame to pump the WebSocket message queue. The native transport needs it — async operations need to be ticked.&lt;/p&gt;

&lt;p&gt;On WebGL, &lt;code&gt;Dispatch()&lt;/code&gt; does nothing. The browser's event loop drives message delivery. When a message arrives, the browser fires &lt;code&gt;onmessage&lt;/code&gt;, which calls &lt;code&gt;SendMessage&lt;/code&gt;, which routes directly to the C# handler. There's no queue to pump.&lt;/p&gt;

&lt;p&gt;This is one of those things that looks wrong when you first read it but is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconnection Safety
&lt;/h2&gt;

&lt;p&gt;Each reconnect cycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Closes the old transport — which fires &lt;code&gt;Unregister()&lt;/code&gt;, removing its GUID from all five dictionaries&lt;/li&gt;
&lt;li&gt;Creates a new transport — which generates a new GUID on construction&lt;/li&gt;
&lt;li&gt;Connects, which calls &lt;code&gt;Register()&lt;/code&gt; with the new GUID&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No stale callback references. No GUID collisions. No events firing on a transport that no longer exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  IL2CPP Stripping
&lt;/h2&gt;

&lt;p&gt;WebGL builds use IL2CPP with aggressive dead-code elimination. Any type that's only referenced at runtime — via JSON deserialization, reflection, or &lt;code&gt;SendMessage&lt;/code&gt; — gets stripped and produces silent failures.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;link.xml&lt;/code&gt; in the package preserves the types the bridge depends on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;linker&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;assembly&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"socketio-unity"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"UnityMainThreadDispatcher"&lt;/span&gt; &lt;span class="na"&gt;preserve=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"UnityTickDriver"&lt;/span&gt; &lt;span class="na"&gt;preserve=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/assembly&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/linker&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're deserializing your own data models from Socket.IO events and they're disappearing in WebGL builds, they need entries here too.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebGL vs Native at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;WebGL&lt;/th&gt;
&lt;th&gt;Native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket API&lt;/td&gt;
&lt;td&gt;Browser &lt;code&gt;WebSocket&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;System.Net.WebSockets&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thread model&lt;/td&gt;
&lt;td&gt;Single (browser event loop)&lt;/td&gt;
&lt;td&gt;async/await&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Dispatch()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No-op&lt;/td&gt;
&lt;td&gt;Must call each frame&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary memory&lt;/td&gt;
&lt;td&gt;Manual &lt;code&gt;_malloc&lt;/code&gt; / &lt;code&gt;_free&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Automatic GC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DllImport("__Internal")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Direct .NET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Socket ID&lt;/td&gt;
&lt;td&gt;GUID per transport&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Production Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CORS.&lt;/strong&gt; The Socket.IO server must explicitly allow the page origin. A CORS rejection in WebGL produces no useful error — the socket just fails to connect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser cache.&lt;/strong&gt; Stale &lt;code&gt;.unityweb&lt;/code&gt; assets serve old jslib code after you've updated the bridge. Force-refresh (&lt;code&gt;Cmd+Shift+R&lt;/code&gt;) or use Incognito during development. This has caused more confusion than any other single issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IL2CPP stripping.&lt;/strong&gt; Any type only referenced via JSON deserialization needs &lt;code&gt;link.xml&lt;/code&gt; preservation. The symptoms are random NullReferenceExceptions in WebGL builds that don't reproduce in the Editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;binaryType = "arraybuffer"&lt;/code&gt;.&lt;/strong&gt; Set it on socket creation in the jslib, always. Without it, binary messages arrive as &lt;code&gt;Blob&lt;/code&gt; and are silently unreadable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Picture
&lt;/h2&gt;

&lt;p&gt;The WebGL bridge in socketio-unity isn't a hack or a workaround. It's a proper interop layer — dual transport with a clean interface, GUID-based routing, pinned memory for outbound binary, malloc/free for inbound binary, and IL2CPP stripping protection.&lt;/p&gt;

&lt;p&gt;Most of this complexity is invisible when you're just calling &lt;code&gt;socket.Emit()&lt;/code&gt;. That's the point.&lt;/p&gt;

&lt;p&gt;If you're building Unity multiplayer with a Socket.IO backend — WebGL included:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;github.com/Magithar/socketio-unity&lt;/a&gt; · &lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://magithar.github.io/socketio-unity/" rel="noopener noreferrer"&gt;magithar.github.io/socketio-unity/&lt;/a&gt; · MIT licensed. Zero paid dependencies.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this saved you a few hours of pain, a ⭐ on the repo goes a long way.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>webgl</category>
      <category>websocket</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
