PlayCanvas Tooling Evaluation
PlayCanvas Tooling Evaluation (#2312)
Section titled “PlayCanvas Tooling Evaluation (#2312)”Evaluated 2026-10-01 on farm-01 (Node 24.13.1). The React and web-components
layers were scaffolded with create-playcanvas@0.9.0, installed, typechecked,
built and rendered in headless Chromium in a throwaway directory outside the
repo; playcanvas-sync@2.1.1 was read from its published package. No
PlayCanvas account exists, so nothing touched the hosted Editor.
Verdict: archive (reference only). The tooling is good and the declarative
layers work as advertised, but no BoB-managed project renders 3D (no
playcanvas, three, @babylonjs/core or @react-three/fiber dependency in
any project on this machine), and create-playcanvas is already a better
scaffolder for a PlayCanvas app than a BoB template would be. When a project
needs 3D, it runs the upstream scaffolder and then bob init, as below.
Nothing goes into templates/ or the registry, and no follow-up is filed.
What was looked at
Section titled “What was looked at”| Package | Version | Licence | Last published | Role |
|---|---|---|---|---|
playcanvas |
2.23.0 | MIT | 2026-10-01 | The engine: WebGL2/WebGPU, WebXR, glTF |
@playcanvas/react |
0.11.7 | MIT | 2026-09-25 | React components over the engine (React 18.3 or 19) |
@playcanvas/web-components |
0.27.0 | MIT | 2026-09-30 | <pc-app>, <pc-entity>… custom elements |
@playcanvas/pcui |
6.1.4 | MIT | 2026-05-14 | UI components for editor-style tools (not spiked; no AC) |
create-playcanvas |
0.9.0 | MIT | 2026-09-30 | npm create playcanvas scaffolder |
playcanvas-sync |
2.1.1 | MIT | 2026-06-22 | pcsync / pcwatch: local files ↔ PlayCanvas Editor |
All are actively maintained, as the issue says.
The spike: React and web components
Section titled “The spike: React and web components”Each format was generated with the default starter (a lit, spinning cube):
npx create-playcanvas@0.9.0 app-react -f react -ynpx create-playcanvas@0.9.0 app-web-components -f web-components -ynpx create-playcanvas@0.9.0 app-engine -f engine -y --no-skills # baseline| React | Web components | Engine API (baseline) | |
|---|---|---|---|
| Scene code | 36-line App.tsx |
29-line index.html + 21-line main.ts |
65-line main.ts |
npm install |
25 s, 375 packages, 287 MB | 12 s, 367 packages, 281 MB | — |
tsc --noEmit |
clean | clean | — |
vite build |
1.3 s | 1.2 s | — |
| JS bundle | 2,337 kB, 614 kB gzip | 2,300 kB, 589 kB gzip | 1,140 kB, 302 kB gzip |
| Headless render | cube drawn, no console errors | cube drawn, no console errors | — |
The renders were taken with Playwright’s Chromium on SwiftShader (no GPU). Both screenshots show the same lit cube on the clear colour, about two seconds from navigation to first frame.
Developer experience. Both declarative forms read well. React gives a
scene graph of <Entity> with <Camera>, <Light>, <Render> components and
hooks such as useAppEvent('update', …) for the frame loop. Web components put
the whole scene in HTML (<pc-entity position="0 0 3"><pc-camera>…), with one
wrinkle: a script bundled by Vite cannot be named in markup
(<pc-script-instance> only resolves scripts fetched at runtime), so the
starter attaches its Rotate script from main.ts after whenReady().
TypeScript, ESLint and Prettier come configured; the generated engines field
needs Node ≥ 22.23.2.
Bundle footprint is the cost. Either declarative layer roughly doubles the
shipped engine: about 290–310 kB gzip on top of the 302 kB engine-only build
for the same cube. Both wrappers import a large slice of the engine up front —
@playcanvas/web-components statically imports about 150 engine symbols and
declares sideEffects: true, because registering its custom elements is the
side effect; @playcanvas/react reaches gaussian splats, gizmos, animation and
Ammo physics from its component set — so the bundler cannot drop features the
scene never uses. For a product viewer on a marketing page that is a real
cost; for a tool or a game it is small next to the assets. A project that
cares writes against the engine API, or lazy-loads the 3D view with a dynamic
import().
create-playcanvas against BoB’s provisioning
Section titled “create-playcanvas against BoB’s provisioning”They do different jobs and do not overlap.
create-playcanvas |
BoB | |
|---|---|---|
| Scope | One-shot app scaffold: Vite + TypeScript + lint/format + one of 12 starters in 3 formats | Ongoing tooling for any repo: .claude/ links, manifest-driven registry items, labels, ports, dev.json |
| Re-run | Never; --overwrite deletes the target’s files |
Idempotent, converges (bob ready, bob provision refresh) |
| Agent skills | Copies 15 @playcanvas/skills skills as real files into .claude/skills/ and .agents/skills/ (--no-skills omits them) |
Symlinks provisioned registry items into .claude/ and gitignores them |
The one point of contact is .claude/skills/. The copied skills are real
files, so under BoB’s authored vs. derived
rule they are authored project tooling: committed, never in the managed
ignore block (which lists symlinks only), and untouched by bob provision.
They name no collision with BoB’s registry skills. So the order for a new 3D
project is:
npm create playcanvas@latest <name> -- -f react -y # or web-components / enginecd <name> && git initbob ready "$(pwd)"Keep the skills unless the duplicate .agents/skills/ copy is unwanted, in
which case delete that directory — Claude Code reads only .claude/.
A BoB template would reproduce what upstream already maintains, with a version that drifts. Reuse is the right answer: point at the scaffolder.
playcanvas-sync against BoB’s workflows
Section titled “playcanvas-sync against BoB’s workflows”pcsync keeps a local directory and a branch of a project in PlayCanvas’s
hosted Editor in step: diff, pull, push, rename, rm, and watch
(push local changes as they happen), configured by pcconfig.json or
PLAYCANVAS_* variables with an API key. It only applies to projects built in
the hosted Editor, and BoB has none, so the tool itself has no use here.
Its design choices, read for patterns:
- Local is authoritative. Only
pullandinitwrite local files; every other command changes the remote. BoB already holds the same rule: source repo →deploy.sh→BOB_HOME, never the reverse. - One file names the hand-managed set.
pcignore.txtlists files that a PlayCanvas merge leaves alone, and the same list is whatpcsyncmanages, so the two can’t disagree. BoB’s managed ignore block plays this role for provisioned links. --dry-runon every command and a multi-instance check beforewatchstarts. BoB has both (report-by-default CLIs; the live-session guard ondeploy.sh).- A documented unsupported case. Two people, one in
watchand one in the browser editor on the same branch, overwrite each other; the README says so instead of trying to merge. A fair model for stating a limit.
Nothing new to take: each pattern is already in BoB.
Recommendation
Section titled “Recommendation”Archive. Do not adopt any package into BoB, and do not add a template.
- When a BoB project needs 3D, scaffold it with
create-playcanvasand thenbob ready, as above. Choose web components for a 3D element inside an otherwise static or server-rendered page, React when the app is already React, the engine API when bundle size matters. - Budget about 300 kB gzip for the engine and as much again for a declarative layer, or load the view lazily.
@playcanvas/skillsstays upstream in the generated project. It would only become a registry candidate if several BoB projects used PlayCanvas.
Not verified
Section titled “Not verified”@playcanvas/pcui(no acceptance criterion asked for it).- The other eleven starters, WebGPU, and WebXR; only the default cube was built.
- Rendering on a real GPU; the screenshots used SwiftShader.
playcanvas-syncagainst a live Editor project (no account or API key).