examples/README.md
A selection of simple examples to get you up and running
See them <a href="https://playcanvas.github.io/">running live</a>
This section covers how to locally develop the examples browser application. For information on how to develop individual examples please see the following section.
Ensure you have Node.js installed. Then, install all of the required Node.js dependencies:
npm install
Now run the following command:
npm run dev
Visit the url mentioned in your terminal to view the examples browser.
To run without automatic browser and example reloads:
npm run develop
You can also run the examples browser with a specific version of the engine by running the following command:
ENGINE_PATH=../build/playcanvas.mjs npm run dev
Where ../build/playcanvas.mjs is the path to the ESM version of the engine.
Or directly from the source:
ENGINE_PATH=../src/index.js npm run dev
The dev server binds to 0.0.0.0:5555 by default. Use EXAMPLES_HOST or
EXAMPLES_PORT to override those values.
npm run dev serves the examples browser over plain HTTP on localhost,
which is enough for everyday work — browsers treat localhost as a secure
context, so WebGPU and WebXR features that require one still work.
For testing on a phone, tablet, Quest, or Apple Vision Pro you need to reach
the dev server over the LAN, and the device will require HTTPS for WebXR (and
exposes WebGPU only in a secure context). This is what npm run dev:https is
for. Use npm run develop:https when automatic reloads should be disabled.
Certs are generated locally with mkcert;
none are committed to the repo.
brew install mkcert nss (nss only needed if Firefox testing)choco install mkcert (or scoop install mkcert)mkcert -install
.local hostname
changes, or if you delete the .cert/ folder):
npm run cert
examples/.cert/cert.pem and key.pem. The script prints
the LAN URL to open.npm run dev:https
Opens on:
https://localhost:5555 — this Mac.https://<hostname>.local:5555 — other devices on the same LAN. Find your
hostname with scutil --get LocalHostName (macOS) or hostname (Windows /
Linux).If the cert files are missing, the dev server fails immediately with a hint
to run npm run cert.
The Mac already trusts the mkcert root from mkcert -install. Other devices
need the root CA installed once. The root file lives at
$(mkcert -CAROOT)/rootCA.pem.
iPhone / iPad / Apple Vision Pro (Safari):
rootCA.pem to the device.https://<hostname>.local:5555.Android (Chrome):
rootCA.pem to the device (USB, email, Drive — no AirDrop).https://<hostname>.local:5555. If .local doesn't resolve, fall
back to the Mac's LAN IP and regenerate certs that include it
(npm run cert -- <ip>, see "Adding extra hostnames or LAN IPs" below).Quest 3: Quest Browser does not honor user-installed CAs reliably. The recommended workflow is the Chrome insecure-origin flag, set via ADB:
adb shell am start -a android.intent.action.VIEW \
-d "chrome://flags/#unsafely-treat-insecure-origin-as-secure"
Add https://<hostname>.local:5555 (or http://<lan-ip>:5555) to the
allowed list and restart the browser. This bypasses cert validation and is
fine for local dev only.
If <hostname>.local doesn't resolve on a device (common on Android / Quest),
use the Mac's LAN IP and regenerate certs to include it. Pass extra SANs as
positional args (the -- is required so npm forwards them to the script):
npm run cert -- 10.0.0.42 192.168.1.50
These are added on top of the defaults (localhost, 127.0.0.1, ::1,
<hostname>.local). Already-trusted devices stay trusted — the leaf cert is
re-signed by the same root CA, no re-install needed.
$(mkcert -CAROOT)) — expensive. Re-creating it means
re-trusting on every device. Back this up if you care.examples/.cert/ — cheap. Regenerate with npm run cert;
devices keep trusting them because they're signed by the same root CA.ping <hostname>.local resolves from the
device. On corp WiFi, AP isolation often blocks client-to-client traffic;
a personal hotspot or portable router (e.g. Netgear M6) sidesteps that.allowedHosts in
vite.config.mjs or stick to <hostname>.local.npm run cert -- <ip>).The available examples are written as classes in JavaScript under the paths ./src/examples/<category>/<exampleName>.example.mjs.
To create a new example you can copy any of the existing examples as a template.
Each example consists of two modules to define its behavior:
<exampleName>.example.mjsimport * as pc from 'playcanvas';
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
const app = new pc.Application(canvas, {});
This is the only file that's required to run an example. The code defined in this function is executed each time the example play button is pressed. It takes the example's canvas element from the DOM and usually begins by creating a new PlayCanvas Application or AppBase using that canvas.
The examples loader finds and destroys applications registered to canvases it owns, so the app does not need to be exported. Export a destroy function only when the example creates non-app resources such as timers, DOM overlays, or animation frames that need cleanup.
Examples can also contain comments which allow you to define the default configuration for your examples as well as overrides to particular settings such as deviceType. Check the possible values to set in ExampleConfig in utils/example-source.mjs file for the full list.
// @config
//
// @keybinds
// WASD: Move
// Space: Jump
// Mouse: Look
//
// @credit
// title: Example Asset
// author: Artist Name
// source: https://example.com/asset
// license: CC BY 4.0
//
// @flag HIDDEN
// @flag ENGINE=performance
// @flag NO_DEVICE_SELECTOR
// @flag NO_MINISTATS
// @flag WEBGPU_DISABLED
// @flag WEBGL_DISABLED
import * as pc from 'playcanvas';
...
External ESM packages can be imported directly:
import confetti from 'https://esm.sh/[email protected]';
However, depending on external URLs is maybe not what you want as it breaks your examples once your internet connection is gone. Where possible, add shared files to the examples tree and import them directly.
Legacy non-module scripts should define any loading helper they need inside the example.
<exampleName>.controls.mjsThis file allows you to define a set of PCUI based interface which can be used to display stats from your example or provide users with a way of controlling the example.
/**
* @param {import('../../../app/Example.mjs').ControlOptions} options - The options.
* @returns {JSX.Element} The returned JSX Element.
*/
export function controls({ observer, ReactPCUI, React, jsx, fragment }) {
const { Button } = ReactPCUI;
return fragment(
jsx(Button, {
text: 'Flash',
onClick: () => {
observer.set('flash', !observer.get('flash'));
}
})
);
}
The controls function takes a pcui observer as its parameter and returns a set of PCUI components. Check this link for an example of how to create and use PCUI.
The data observer used in the controls function will be made available as an import from examples/context to use in the example file:
import { data } from 'examples/context';
console.log(data.get('flash'));
Any other file you wish to include in your example can be added to the same folder with the example name prepended (e.g. <exampleName>.shader.vert and <exampleName>.shader.frag). These files can be imported from the example using their relative file name:
import shaderVert from './shader.vert';
import shaderFrag from './shader.frag';
import data from './data.json';
const assets = {
statue: new pc.Asset('statue', 'container', { url: './assets/models/statue.glb' }),
orbit: new pc.Asset('orbit', 'script', { url: './scripts/camera/orbit-camera.js' })
};
Sidecar text files with .frag, .vert, .wgsl, .glsl, .html, .css, and .txt extensions are imported as strings. JSON files are imported as parsed values. Shared example assets use plain runtime URLs starting with ./assets/..., shared engine script asset URLs use ./scripts/..., and local .mjs files are imported as standard JavaScript modules. When using these modules outside the examples browser, configure your bundler to load text extensions as strings.
Run npm run dev from the Local examples browser development section to serve the examples browser with Vite. Use npm run develop when automatic reloads should be disabled.
You can view an individual iframe directly, for example http://localhost:5555/iframe/misc_hello-world.html.
By default, npm run dev and npm run develop serve the engine from ../src/index.js. To test against a built ESM engine instead, pass ENGINE_PATH, for example ENGINE_PATH=../build/playcanvas.mjs npm run dev.
The example script allows you to import examples-only modules that interact with the environment such as the device selector and controls. These are listed below:
examples/context - The observer object data and selected graphics deviceType.examples/assets/* - Shared example modules. Use ./assets/... when a runtime asset URL string is needed.playcanvas/scripts/* - Shared engine script modules. Use ./scripts/... when a runtime script URL string is needed./engine directory:npm install
/engine/examples directory:npm install
npm run build
npm run serve
npm run build:thumbnails
This command spawns its own serve instance on port 12321, so you don't need to care about that.
Copy the contents of the ./dist directory to the root of the playcanvas.github.io repository. Be sure not to wipe the contents of the pcui subdirectory in that repository.
Run git commit -m "Update to Engine 1.XX.X" in the playcanvas.github.io repo
Create a PR for this new commit