Why JGengine
The boilerplate belongs in the engine, not your game.
Most game code is the same plumbing rebuilt per project — a world, a loop, an authoritative host, a scene you place by hand. JGengine makes that the SDK's job so your game is the part that's actually yours. Here's what that buys you, and where the line is.
Hand-rolled vs. authored
Same meadow. One of these you maintain forever.
Building a world by hand means owning the noise, the instancing, the culling, and the collision — then rebuilding it for the next game. On the SDK the world is a thin place declaration, the meadow is content you author in the 3D editor, and every consumer reads the same document.
// The whole thing, by hand, in three.js — nothing elided.import * as THREE from "three";// --- renderer + camera + resize ---const renderer = new THREE.WebGLRenderer({ antialias: true });renderer.setPixelRatio(Math.min(devicePixelRatio, 2));renderer.setSize(innerWidth, innerHeight);renderer.shadowMap.enabled = true;document.body.appendChild(renderer.domElement);const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.5, 2000);camera.position.set(0, 40, 90);addEventListener("resize", () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight);});const scene = new THREE.Scene();scene.fog = new THREE.Fog(0x9ec7ff, 120, 900);// --- value noise: three ships none, so write it ---function hash(x, z) { const n = Math.sin(x * 127.1 + z * 311.7) * 43758.5453; return n - Math.floor(n);}function noise2D(x, z) { const xi = Math.floor(x), zi = Math.floor(z); const xf = x - xi, zf = z - zi; const u = xf * xf * (3 - 2 * xf), v = zf * zf * (3 - 2 * zf); const a = hash(xi, zi), b = hash(xi + 1, zi); const c = hash(xi, zi + 1), d = hash(xi + 1, zi + 1); return (a * (1 - u) + b * u) * (1 - v) + (c * (1 - u) + d * u) * v;}function heightAt(x, z) { // fbm — and this same fn must feed collision AND grass grounding let h = 0, amp = 8, freq = 0.01; for (let o = 0; o < 4; o++) { h += noise2D(x * freq, z * freq) * amp; amp *= 0.5; freq *= 2; } return h;}// --- terrain mesh: displace, recompute normals ---const SIZE = 256, SEG = 200;const geo = new THREE.PlaneGeometry(SIZE, SIZE, SEG, SEG);geo.rotateX(-Math.PI / 2);const pos = geo.attributes.position;for (let i = 0; i < pos.count; i++) pos.setY(i, heightAt(pos.getX(i), pos.getZ(i)));geo.computeVertexNormals();const ground = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x3f7d2d, roughness: 1 }));ground.receiveShadow = true;scene.add(ground);// --- lighting + shadow frustum ---scene.add(new THREE.HemisphereLight(0xbfe3ff, 0x2a3d1a, 0.9));const sun = new THREE.DirectionalLight(0xffffff, 2.2);sun.position.set(80, 120, 40);sun.castShadow = true;sun.shadow.camera.left = sun.shadow.camera.bottom = -SIZE / 2;sun.shadow.camera.right = sun.shadow.camera.top = SIZE / 2;sun.shadow.mapSize.set(2048, 2048);scene.add(sun);// --- grass: one InstancedMesh, grounded to the heightfield ---const COUNT = 200 * 200 * 2; // density 2 /m² over 200×200const blade = new THREE.PlaneGeometry(0.06, 0.6).translate(0, 0.3, 0);const grass = new THREE.InstancedMesh( blade, new THREE.MeshStandardMaterial({ color: 0x6bbf4a, side: THREE.DoubleSide }), COUNT,);const m = new THREE.Matrix4(), q = new THREE.Quaternion(), up = new THREE.Vector3(0, 1, 0);let rng = 1337;const rand = () => ((rng = (rng * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff);for (let i = 0; i < COUNT; i++) { const x = (rand() - 0.5) * 200, z = (rand() - 0.5) * 200; q.setFromAxisAngle(up, rand() * Math.PI * 2); const s = 0.7 + rand() * 0.6; m.compose(new THREE.Vector3(x, heightAt(x, z), z), q, new THREE.Vector3(s, s, s)); grass.setMatrixAt(i, m);}grass.instanceMatrix.needsUpdate = true;grass.castShadow = true;scene.add(grass);// --- per-chunk frustum culling so 80k blades don't tank the GPU ---grass.computeBoundingSphere();const frustum = new THREE.Frustum(), pv = new THREE.Matrix4();// --- render loop: wind, collision clamp, cull, draw ---const clock = new THREE.Clock();function frame() { const t = clock.getElapsedTime(); grass.material.userData.wind = Math.sin(t); // and rewire the shader for it… camera.position.y = heightAt(camera.position.x, camera.position.z) + 6; // "collision" pv.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); frustum.setFromProjectionMatrix(pv); grass.visible = frustum.intersectsSphere(grass.boundingSphere); renderer.render(scene, camera); requestAnimationFrame(frame);}frame();// …and you rewrite all of it for the next game.import { world } from "@jgengine/core/world/place";// The world is the place you play in: substrate + laws.export const overworld = world({ id: "overworld", ground: { mode: "flat", size: { x: Infinity, z: Infinity } }, physics: { gravity: -24 },});// The meadow itself — sky look, terrain sculpt, scattered grass — is// authored in the 3D editor and saved to editor.scene.json. Rendering,// GPU-instanced grass, fog, and collision all read that one document.The honest cut
Strengths, and the trade-offs we won't hide
A tool that claims no downsides is selling something. Here's the real shape of it.
Where it shines
One language, from sim to shader
Pure TypeScript. @jgengine/core imports nothing — no React, no renderer, no backend — and every layer above earns its dependencies through a one-directional chain.
Multiplayer & persistence from day one
An authoritative host, WebSocket / WebRTC / loopback transports over one protocol, and Postgres or Convex persistence are seams in the SDK — not a rewrite you bolt on at the end.
Author scenes in a real 3D editor
Place spawns, sculpt terrain, scatter foliage, lay paths — save editor.scene.json and render it generically. Runs embedded in a game or fully standalone on any folder.
Genre-agnostic primitives
Combat, loot, quests, economy, crafting, turns, inventory — opt in per game. Built for the next ten games, so game N+1 is data plus glue.
Agent-native by design
Focused skills let a coding agent build the whole game from a prompt — intake, the right API domains, and a browserless verification gate, no docs to paste.
Where it won't
Apache-2.0 — keep the NOTICE
The published packages are Apache-2.0: commercial and closed-source use is fine. The one string attached — keep the NOTICE crediting jgengine.com in what you ship.
Opinionated render shell
The renderer is React Three Fiber + three.js. If you need a native AAA pipeline, a non-web target, or your own engine, this isn't that.
Young and moving
The API surface is still evolving. Primitives are extracted as real games need them, which means occasional churn between versions.
An SDK, not a no-code studio
You (or your agent) write TypeScript. There's a 3D editor for scenes, but gameplay is code — this is a toolkit, not a drag-and-drop game maker.
Who it's for
Reach for JGengine when…
You want a multiplayer-capable web game in TypeScript without hand-building a netcode stack, a scene editor, and ten gameplay systems first — and you're happy to drive it with an AI coding agent.
Look elsewhere if you need a closed-source commercial build today, a native/console target, a no-code editor, or a battle-hardened engine with a decade of stability. We'd rather you know now.
Try it in one prompt.
Paste this into any coding agent. If it doesn't fit your project, you'll know within one build — not one month.
Make a game that ... with jgengine# or open the editor on any folder, no game requirednpx jgengine editor