<?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: Het Dave</title>
    <description>The latest articles on DEV Community by Het Dave (@thephoenix229).</description>
    <link>https://dev.to/thephoenix229</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3892334%2Ff84c203d-f141-4673-b14f-7e00fbdef31a.png</url>
      <title>DEV Community: Het Dave</title>
      <link>https://dev.to/thephoenix229</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thephoenix229"/>
    <language>en</language>
    <item>
      <title>Introducing CarverJS</title>
      <dc:creator>Het Dave</dc:creator>
      <pubDate>Mon, 27 Apr 2026 06:53:18 +0000</pubDate>
      <link>https://dev.to/thephoenix229/introducing-carverjs-13dn</link>
      <guid>https://dev.to/thephoenix229/introducing-carverjs-13dn</guid>
      <description>&lt;p&gt;Here is a complete 2D game written in React. Move with WASD. Renders in a browser. The whole thing is around 40 lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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;react&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;Game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;World&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Actor&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;@carverjs/core/components&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;useGameLoop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useInput&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;@carverjs/core/hooks&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;useGameStore&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;@carverjs/core/store&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Group&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;@carverjs/core/types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Player&lt;/span&gt;&lt;span class="p"&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Group&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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;setPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGameStore&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAxis&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useInput&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playing&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;span class="nx"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nf"&gt;useGameLoop&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;getAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;KeyA&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;KeyD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;getAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;KeyS&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;KeyW&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Actor&lt;/span&gt;
      &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primitive"&lt;/span&gt;
      &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"circle"&lt;/span&gt;
      &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"royalblue"&lt;/span&gt;
      &lt;span class="na"&gt;geometryArgs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"2d"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;World&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Player&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;World&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;&amp;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;That is CarverJS. It is a React game engine I just published to npm in early beta. Free, MIT, version &lt;code&gt;0.0.1&lt;/code&gt; at the time of this post, with two packages: &lt;code&gt;@carverjs/core&lt;/code&gt; and &lt;code&gt;@carverjs/multiplayer&lt;/code&gt;. The API will move before it hits 1.0.&lt;/p&gt;

&lt;p&gt;This post is the long version of why I built it, what it actually does, and what is still half-finished. If you have ever closed a game-dev tutorial because step one was "install Unity," this one is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;The honest answer: I needed it for my own product.&lt;/p&gt;

&lt;p&gt;I run a small edtech company. We are building game-based learning for K-12 students. Two products: a B2B platform for schools (admin dashboards, teacher tooling, student experience) and a B2C marketplace. Every product surface eventually renders a game. The games are short, social, browser-based, often multiplayer.&lt;/p&gt;

&lt;p&gt;I am a React developer. The rest of my team are React developers. Our admin portal, teacher app, marketing site, internal tools — every product surface is React, all the way down. The natural question was: can the games also be React?&lt;/p&gt;

&lt;p&gt;The non-React paths I evaluated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unity WebGL.&lt;/strong&gt; Exports run 10-50 MB on average, multi-second cold loads, doesn't compose with React UI. Great engine for the wrong runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phaser.&lt;/strong&gt; Mature, deep 2D feature set, but you are back to imperative game loops. State lives outside React.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Godot HTML5.&lt;/strong&gt; Smaller than Unity, same composition problem. GDScript or C#, neither of which is the language we ship in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw Three.js / R3F.&lt;/strong&gt; Excellent for 3D scenes. No game abstractions. You build your own input system, collision system, audio manager, scene manager, asset loader, and multiplayer layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each option meant either learning a separate engine or rebuilding a stack of plumbing. I started writing the plumbing once, then twice, then realized I was writing a game engine. So I made it deliberate, pulled it out of the company monorepo, and open-sourced it.&lt;/p&gt;

&lt;p&gt;That engine is CarverJS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;CarverJS is built on top of React Three Fiber and Three.js, but in normal use you do not import from either. Every component, hook, type, and store you need ships from &lt;code&gt;@carverjs/core&lt;/code&gt; and &lt;code&gt;@carverjs/multiplayer&lt;/code&gt;. If you already know R3F, the API shape will feel familiar. If you do not, you can ship a game without ever learning what an &lt;code&gt;&amp;lt;Object3D&amp;gt;&lt;/code&gt; is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Components describe the world:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"3d"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;World&lt;/span&gt; &lt;span class="na"&gt;physics&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gravity&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;9.81&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="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Actor&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primitive"&lt;/span&gt; &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"box"&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&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;0&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="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Actor&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"model"&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/hero.glb"&lt;/span&gt; &lt;span class="na"&gt;animationName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"run"&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Actor&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sprite"&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/coin.png"&lt;/span&gt; &lt;span class="na"&gt;billboard&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&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;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;World&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hooks drive behavior:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useRef&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;react&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;Actor&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;@carverjs/core/components&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;useGameLoop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useAudio&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;@carverjs/core/hooks&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Group&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;@carverjs/core/types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Player&lt;/span&gt;&lt;span class="p"&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Group&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isPressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getAxis&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useInput&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;jump&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;Space&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;shoot&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;KeyJ&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;play&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAudio&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;jump&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/jump.wav&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sfx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/hit.wav&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sfx&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;span class="nf"&gt;useGameLoop&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;getAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;KeyA&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;KeyD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&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="nf"&gt;isPressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Space&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jump&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Actor&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primitive"&lt;/span&gt; &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"capsule"&lt;/span&gt; &lt;span class="p"&gt;/&amp;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 components are not thin wrappers. &lt;code&gt;&amp;lt;Game&amp;gt;&lt;/code&gt; mounts the entire game stack on the way in: WebGL renderer, lighting presets, audio system, input manager, collision manager, and a game loop with phase tracking (&lt;code&gt;loading&lt;/code&gt;, &lt;code&gt;playing&lt;/code&gt;, &lt;code&gt;paused&lt;/code&gt;, &lt;code&gt;gameover&lt;/code&gt;). &lt;code&gt;&amp;lt;World&amp;gt;&lt;/code&gt; groups actors, sets up a default camera per mode, and optionally enables Rapier physics as a lazy-loaded peer dependency. &lt;code&gt;&amp;lt;Actor&amp;gt;&lt;/code&gt; is a discriminated union over three rendering modes — 3D models (&lt;code&gt;.glb&lt;/code&gt;/&lt;code&gt;.gltf&lt;/code&gt;), sprites (with optional grid- or atlas-based animation), and code-generated primitives — sharing a common base of transform, event, and physics props.&lt;/p&gt;

&lt;p&gt;Hooks then read from the singleton systems &lt;code&gt;&amp;lt;Game&amp;gt;&lt;/code&gt; set up. &lt;code&gt;useGameLoop&lt;/code&gt; is &lt;code&gt;useFrame&lt;/code&gt; with phase awareness, fixed-timestep mode, four staged update slots (&lt;code&gt;earlyUpdate&lt;/code&gt;, &lt;code&gt;fixedUpdate&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;lateUpdate&lt;/code&gt;), and a max-delta cap so a backgrounded tab doesn't spiral the simulation. &lt;code&gt;useInput&lt;/code&gt; reads from a centralized InputManager with action mapping. &lt;code&gt;useCollision&lt;/code&gt; registers AABB / sphere / circle colliders without dragging in a physics engine. &lt;code&gt;useAudio&lt;/code&gt; registers sounds, supports spatial audio, music crossfades, six volume channels, and audio sprites.&lt;/p&gt;

&lt;p&gt;The thing I was hunting for, that no lower-level library gave me cleanly, was this: a place to write game logic that looks and feels like React component logic. No imperative boot file. No animation loop wired by hand. No scene-graph state living outside React. Mount a component, the game starts. Unmount it, it cleans itself up.&lt;/p&gt;

&lt;h2&gt;
  
  
  2D and 3D in the same engine
&lt;/h2&gt;

&lt;p&gt;You change one prop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"2d"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;  // OrthographicCamera, no shadows, no environment, brighter ambient
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"3d"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;  // PerspectiveCamera, shadows, sky, environment, OrbitControls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same &lt;code&gt;&amp;lt;Actor&amp;gt;&lt;/code&gt; components work in both. A sprite in 3D mode billboards at the camera by default; in 2D mode it sits flat. Primitives become &lt;code&gt;&amp;lt;mesh&amp;gt;&lt;/code&gt;es with the appropriate material. Models work in either mode, though most 2D games will not need them. Physics is mode-aware: in 2D, Z translation and X/Y rotation are auto-locked per actor so things stay on the plane.&lt;/p&gt;

&lt;p&gt;This matters because most game projects do not get to pick "2D or 3D" cleanly. You build a 2D game and then realize the menu wants a parallax background with depth. You build a 3D game and then realize the HUD needs 2D widgets in world space. CarverJS gives you both in the same scene, with a &lt;code&gt;&amp;lt;Camera&amp;gt;&lt;/code&gt; component that can override the defaults if you want full control of FOV, near/far, follow targets, or controls (&lt;code&gt;orbit&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;pointerlock&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiplayer without a game server
&lt;/h2&gt;

&lt;p&gt;This was the part I expected to be the hardest, and it turned out to be the part I am most glad to have built.&lt;/p&gt;

&lt;p&gt;Multiplayer in CarverJS is peer-to-peer. One player is the host, all others are clients, and game traffic flows over WebRTC data channels. There is no game server in the architectural sense. The host is the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;MultiplayerProvider&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;@carverjs/multiplayer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MultiplayerProvider&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"my-game"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"2d"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;World&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Actor&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primitive"&lt;/span&gt; &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"circle"&lt;/span&gt; &lt;span class="na"&gt;networked&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;World&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;MultiplayerProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;networked&lt;/code&gt; prop is the entire opt-in. The actor's transform now syncs to every connected peer at 20 Hz, with delta compression. Only changed properties go on the wire.&lt;/p&gt;

&lt;p&gt;Peer discovery is serverless too. You bring your own signaling network:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MQTT&lt;/strong&gt; — default, free, zero-config. Uses public MQTT brokers. Good enough for prototyping and small games.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase RTDB&lt;/strong&gt; — bring your own Firebase project for production. More reliable, more isolated, easy rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare TURN&lt;/strong&gt; (or any TURN provider) — optional, for NAT traversal when peers behind restrictive networks cannot connect directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After signaling, all data flows directly between peers. No room state lives on a server you have to operate.&lt;/p&gt;

&lt;p&gt;Sync modes are layered. You pick the one that fits the actor:&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;Latency Feel&lt;/th&gt;
&lt;th&gt;Bandwidth&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Events&lt;/td&gt;
&lt;td&gt;discrete&lt;/td&gt;
&lt;td&gt;very low&lt;/td&gt;
&lt;td&gt;turn-based games, chat, infrequent triggers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot (default)&lt;/td&gt;
&lt;td&gt;smoothed&lt;/td&gt;
&lt;td&gt;moderate&lt;/td&gt;
&lt;td&gt;RPGs, casual co-op, strategy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prediction&lt;/td&gt;
&lt;td&gt;instant&lt;/td&gt;
&lt;td&gt;higher&lt;/td&gt;
&lt;td&gt;FPS, racing, fighting, fast platformers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Different actors in the same game can use different modes. The player character can use prediction; the chest they are walking towards can use events; the moving platform under their feet can use snapshots. You configure it inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Actor&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"model"&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/player.glb"&lt;/span&gt;
  &lt;span class="na"&gt;networked&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prediction&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;myPeerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Host migration is handled. If the host disconnects, peers sort by ID and the lowest ID becomes the new host. State is preserved through the migration — clients hit a brief &lt;code&gt;migrating&lt;/code&gt; connection state and resume.&lt;/p&gt;

&lt;p&gt;There is a working multiplayer demo in the repo — &lt;code&gt;examples/multiplayer-2d&lt;/code&gt;, a small game called Coin Chase. Two players on different machines, collecting coins in real time, using Firebase RTDB for signaling and Cloudflare TURN for NAT traversal. Clone the repo and run it locally with two browser windows pointed at it.&lt;/p&gt;

&lt;p&gt;I want to be clear about scope: this is not trying to be Photon or Colyseus. It is a peer-to-peer layer that fits inside React, designed for small to medium multiplayer games — roughly 2 to 16 players, browser-based. The trade-off is that the host has to stay online for the game to continue smoothly. Migration helps but is not a replacement for an authoritative server when you actually need one.&lt;/p&gt;

&lt;p&gt;For MoneyTales — small classroom games, friends playing quick rounds — this is exactly the right shape. For your shooter MMO, it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is in the box right now
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@carverjs/core&lt;/code&gt; ships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;Game&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;World&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;Actor&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;Camera&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;AudioListener&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;SceneManager&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;Scene&amp;gt;&lt;/code&gt; for stack-based scene navigation with fade and custom-shader transitions, lazy-loaded scenes via Suspense, and per-scene error boundaries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;AssetLoader&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;LoadingScreen&amp;gt;&lt;/code&gt; for declarative asset preloading with retry, priority, progress, and groups&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;ParticleEmitter&amp;gt;&lt;/code&gt; with presets (&lt;code&gt;fire&lt;/code&gt;, &lt;code&gt;smoke&lt;/code&gt;, &lt;code&gt;explosion&lt;/code&gt;, &lt;code&gt;sparks&lt;/code&gt;, &lt;code&gt;rain&lt;/code&gt;, &lt;code&gt;snow&lt;/code&gt;, &lt;code&gt;magic&lt;/code&gt;, &lt;code&gt;confetti&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useGameLoop&lt;/code&gt;, &lt;code&gt;useInput&lt;/code&gt;, &lt;code&gt;useCamera&lt;/code&gt;, &lt;code&gt;useAnimation&lt;/code&gt;, &lt;code&gt;useScene&lt;/code&gt;, &lt;code&gt;useAssets&lt;/code&gt;, &lt;code&gt;useTween&lt;/code&gt;, &lt;code&gt;useParticles&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useCollision&lt;/code&gt; (AABB / sphere / circle, layer-based filtering, sensors, no physics engine required)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useGridCollision&lt;/code&gt; (tile-based, O(1) lookups — perfect for Snake, tile RPGs, puzzles)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;usePhysics&lt;/code&gt; (Rapier, lazy-loaded peer dependency)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useAudio&lt;/code&gt; (Web Audio with HTML5 fallback, spatial 3D audio, six volume channels, music crossfade, audio sprites, automatic AudioContext unlock)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;@carverjs/multiplayer&lt;/code&gt; ships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;MultiplayerProvider&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;MultiplayerBridge&amp;gt;&lt;/code&gt; (the R3F context bridge, since &lt;code&gt;&amp;lt;Canvas&amp;gt;&lt;/code&gt; runs its own reconciler)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useMultiplayer&lt;/code&gt;, &lt;code&gt;useLobby&lt;/code&gt;, &lt;code&gt;useRoom&lt;/code&gt;, &lt;code&gt;usePlayers&lt;/code&gt;, &lt;code&gt;useHost&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;WebRTC transport with reconnection, host migration, interest management, and network simulation tools (latency, jitter, drop) for testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is TypeScript-first. Every hook and component has full type definitions. The core bundle is under 200KB before your assets, which is the whole point — a Unity WebGL export of an equivalently small game starts in the tens of megabytes.&lt;/p&gt;

&lt;p&gt;Three demo games are open-source in the repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;basic-2d&lt;/code&gt; — a minimal 2D scene with input&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;basic-3d&lt;/code&gt; — a minimal 3D scene with a model and orbit controls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;multiplayer-2d&lt;/code&gt; — Coin Chase, the multiplayer demo above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can run any of them with &lt;code&gt;pnpm install &amp;amp;&amp;amp; pnpm --filter @carverjs/example-basic-2d dev&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CarverJS is not
&lt;/h2&gt;

&lt;p&gt;This is a beta. Things will move before 1.0, and I want to be specific about scope.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is not Unity.&lt;/strong&gt; Do not bring AAA pipelines, baked global illumination, complex animation graphs, or 50-MB asset budgets here. The engine is designed for browser-shaped games.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not Phaser.&lt;/strong&gt; Phaser is older, more battle-tested, and has a deeper 2D feature set, particularly for tile-based platformers. If you want a pure 2D engine that does not care about React, use Phaser. CarverJS is for the case where the game lives in a React app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a managed multiplayer backend.&lt;/strong&gt; No matchmaking service, no analytics, no auth, no "studio plan." Bring your own Firebase or any signaling network. Bring your own TURN if you need NAT traversal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The API will break.&lt;/strong&gt; Minor versions will break things until 1.0. Pin your version. Read the changelog.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What CarverJS actually competes with is the version of "I will write my own game stack on top of R3F" that lives in your head when you start a side project. If that was the alternative, CarverJS will save you a lot of glue code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next
&lt;/h2&gt;

&lt;p&gt;The 1.0 milestone is roughly six months out. Between now and then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stabilize the core hooks and prop shapes (no big API churn after 1.0)&lt;/li&gt;
&lt;li&gt;More multiplayer transports and a proper interest-management profile&lt;/li&gt;
&lt;li&gt;A managed signaling service for teams who do not want to operate their own — paid, optional, the engine itself stays free and MIT&lt;/li&gt;
&lt;li&gt;An asset marketplace for sprites, models, and audio packs, with most of the revenue going to creators&lt;/li&gt;
&lt;li&gt;More examples — a multiplayer platformer, a tile-based puzzle, a 3D space shooter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The signaling service and the marketplace are how the project funds itself. The engine is free and stays free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @carverjs/core
&lt;span class="c"&gt;# or&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @carverjs/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clone the repo and play with the demo games:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/moneytales/carverjs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;carverjs
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm build
pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; @carverjs/example-basic-2d dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.carverjs.dev" rel="noopener noreferrer"&gt;docs.carverjs.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/moneytales/carverjs" rel="noopener noreferrer"&gt;github.com/moneytales/carverjs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Discord: &lt;a href="https://discord.gg/5ymwfD4hYE" rel="noopener noreferrer"&gt;discord.gg/5ymwfD4hYE&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@carverjs/core" rel="noopener noreferrer"&gt;&lt;code&gt;@carverjs/core&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@carverjs/multiplayer" rel="noopener noreferrer"&gt;&lt;code&gt;@carverjs/multiplayer&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with it, send it to me — I want to see what you make. If something is broken, &lt;a href="https://github.com/moneytales/carverjs/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt; is the right place. If something is rough, Discord is where the rough things get tracked.&lt;/p&gt;

&lt;p&gt;Most game engines were built by C++ programmers for C++ programmers, with a web export bolted on later. CarverJS was built backwards: web first, React first, game logic written the way component logic is written. It is an opinion as much as a tool. If the opinion fits, you will like the tool.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>gamedev</category>
      <category>react</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
