Skip to content

nappycats/usm

Repository files navigation

USM – Universal State Machine (TypeScript)

USM is a tiny, adapter-first state machine for games, 3D sites, and interactive apps. Keep your state logic pure; plug in adapters for Three.js, GSAP, Input (keyboard/pointer), Time/Loop, Audio, Loader, and Picking.


Install

npm i @nappycat/usm
# optional peers: three, gsap (only if you use those adapters)

Quick start (ESM)

import { USM } from '@nappycat/usm';

type Ctx = { score: number };
const machine = new USM<Ctx>({
  id: 'demo',
  initial: 'menu',
  context: { score: 0 },
  states: {
    menu: { on: { START: 'play' } },
    play: {
      enter(ctx){ ctx.score = 0; },
      tick(dt, ctx){ /* game loop here */ },
      on: { GAME_OVER: 'over' }
    },
    over: { on: { RESTART: 'play' } }
  }
});
machine.start();

Adapters

  • threeAdapter({ THREE, scene, camera, renderer, mode:'container'|'window' })
    Handles DPR clamp + resize + camera.aspect updates.
  • keyboardAdapter({ down, up, preventRepeat, combo })
    Tracks Set<string> of pressed keys (KeyboardEvent.code), dispatches events.
  • pointerAdapter({ target, bind, onDrag, onTap, onWheel })
    Unifies mouse/touch/pen, swipe detection, wheel.
  • uiAdapter()
    Syncs <body data-state="..."> for CSS.
  • creativeAudioAdapter({ autoUnlock, suspendOnStop, volume })
    Web Audio helper: master/bus gains, load/play buffers, fades.
import { adapters } from '@nappycat/usm';
// machine = new USM({ ..., adapters: [ adapters.keyboardAdapter({...}), ... ] })

Creative audio quick start

import { USM, adapters } from '@nappycat/usm';

const m = new USM({
  initial: 'menu',
  states: {
    menu: { on: { START: 'play' } },
    play: {
      async enter(ctx) {
        // prepare audio
        await ctx.audio.loadAll({ click: '/sfx/click.mp3', loop: '/music/loop.mp3' });
        ctx.audio.play('loop', { loop: true, bus: 'music', volume: 0.6 });
      },
      on: { CLICK: 'menu' }
    }
  },
  adapters: [
    adapters.creativeAudioAdapter({ autoUnlock: true, suspendOnStop: false, volume: 0.9 })
  ]
});

CDN / UMD usage

<script src="https://cdn.jsdelivr.net/npm/@nappycat/usm/dist/usm.global.js"></script>
<script>
  const { USM, adapters } = window.USM;
  const m = new USM({ initial:'menu', states:{ menu:{ on:{ START:'play' } }, play:{} } });
  m.start();
</script>

License

MIT © nappycat


Project layout (where to put things)

This repo has two main use-cases:

  1. Developing USM itself (TypeScript library)
  2. Running the examples (plain HTML/JS using the built dist files)

1) Developing USM (library authoring)

usm/
├─ src/                      # TypeScript source for the library
│  ├─ usm-core.ts            # core machine
│  ├─ adapters/              # adapters (three, gsap, time, pointer, keyboard, audio, loader, picking)
│  └─ index.ts               # exports
├─ dist/                     # built outputs (generated by `npm run build`)
│  ├─ usm.esm.js             # ESM build (for bundlers)
│  ├─ usm.cjs                # CommonJS build (Node require)
│  ├─ usm.global.js          # UMD build (single <script> in the browser)
│  └─ adapters/              # per-adapter builds (esm/cjs/d.ts)
├─ examples/                 # demo apps that use the built files from dist
├─ README.md
├─ LICENSE
└─ package.json
  • Make code changes in src/**.
  • Run npm run build to regenerate dist/**.
  • Examples load from dist/**, so always rebuild after changes.

2) Running an example (simple HTML)

Create a folder under examples/ with this shape:

examples/my-demo/
├─ index.html
├─ main.js
└─ assets/
   ├─ pixel.png
   └─ beep.wav

index.html (UMD mode; no bundler required):

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>USM Demo</title>
    <style> html,body,#app{height:100%;margin:0} canvas{display:block} .hint{position:fixed;left:8px;top:8px;color:#fff;font:12px monospace}</style>
  </head>
  <body>
    <div id="app"><div class="hint">Loading…</div></div>

    <!-- libs (optional) -->
    <script src="https://cdn.jsdelivr.net/npm/three/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap/dist/gsap.min.js"></script>
    <!-- USM UMD build from your local dist -->
    <script src="../../dist/usm.global.js"></script>
    <!-- your app code -->
    <script src="./main.js"></script>
  </body>
</html>

main.js (minimal working skeleton):

const { USM: Machine, adapters } = window.USM;
const THREE = window.THREE; // if you use three

const app = document.getElementById('app');
const renderer = new THREE.WebGLRenderer({ antialias: true });
app.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b0f1a);
const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
camera.position.set(0,1.2,3);

function resize(){
  const w = app.clientWidth || window.innerWidth;
  const h = app.clientHeight || window.innerHeight;
  renderer.setSize(w, h); camera.aspect = w/h; camera.updateProjectionMatrix();
}
window.addEventListener('resize', resize); resize();

const cube = new THREE.Mesh(new THREE.BoxGeometry(1,1,1), new THREE.MeshStandardMaterial({color:0x44aaff}));
scene.add(new THREE.AmbientLight(0xffffff,0.6));
const dir = new THREE.DirectionalLight(0xffffff,0.8); dir.position.set(1,2,2); scene.add(dir);

const setHint = (t)=>{ const el=document.querySelector('.hint'); if(el) el.textContent=t; };

const m = new Machine({
  initial: 'menu',
  states: {
    menu: { on: { START: 'play' }, enter(){ setHint('Click or Space to start'); } },
    play: {
      enter(){ scene.add(cube); },
      tick(dt){ cube.rotation.y += dt; renderer.render(scene, camera); },
      on: { QUIT: 'menu' },
      exit(){ scene.remove(cube); }
    }
  },
  adapters: [
    adapters.timeAdapter({ fixedStep: 1/60 }),
    adapters.pointerAdapter({ target: renderer.domElement, capture: renderer.domElement, map: { click: 'START' } }),
    adapters.keyboardAdapter({ bindings: { Space:'START', KeyQ:'QUIT' } }),
    adapters.threeAdapter({ THREE, scene, camera, renderer, mode:'window' })
  ]
});

m.start();

Assets: put images/audio under examples/my-demo/assets/ and reference with ./assets/… from main.js.

Common paths cheat-sheet

  • UMD script in examples: ../../dist/usm.global.js
  • Local asset in an example: ./assets/pixel.png
  • Adapter import (ESM in another project): import { timeAdapter } from '@nappycat/usm/adapters/time'

Using USM in your own app (bundler/ESM)

If you’re not inside this repo, install the package and import only what you need:

import { USM } from '@nappycat/usm';
import { timeAdapter }    from '@nappycat/usm/adapters/time';
import { pointerAdapter } from '@nappycat/usm/adapters/pointer';
// import { threeAdapter }   from '@nappycat/usm/adapters/three';
// import { pickingAdapter } from '@nappycat/usm/adapters/picking';

Use Vite/webpack/Parcel to serve your app; these will tree-shake unused adapters.

About

USM (Universal State Machine) for games, 3D sites, and interactive apps — with adapters for Three.js, GSAP, input, and UI.

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors