ワンクリックで
lobby-ws
build apps/objects/scripts in the lobby-ws gamedev sdk
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
build apps/objects/scripts in the lobby-ws gamedev sdk
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | lobby-ws |
| description | build apps/objects/scripts in the lobby-ws gamedev sdk |
Project docs are copied into docs/ on init. Start with docs/scripting/README.md.
Apps live in apps/ and each app folder contains blueprint JSON plus a script entry and optional modules (no bundling). There are two ways to create them:
Local-first (project files):
vp run apps:new <AppName> (creates apps/<AppName>/ with index.js + blueprint)world.json entities: vp run world:entities -- add --template-id <id> --transforms <file> / vp run world:entities -- delete --blueprint <name>Tree), generate many transforms and run vp run world:entities -- add --template-id <TreeEntityId> --transforms tmp/forest.json --yes, then delete tmp/forest.jsondocs/world-entities-cli.md for full flags (--replace, --ids, --yes, --world)--transforms (JSON array):
[
{
"position": [0, 0, 0],
"quaternion": [0, 0, 0, 1],
"scale": [1, 1, 1],
"pinned": false,
"props": {},
"state": {}
}
]
assets/ and reference them from blueprint JSON for app.config propsvp run dev for hot reloadExample local layout:
apps/
MyApp/
MyApp.json
index.js
helpers.js
shared/
math.js
assets/
Entry files default to index.js. New apps should export default (world, app, fetch, props, setTimeout) => { ... } to ensure access to all "globals"
If your model includes a placeholder mesh named Block (for example from the built-in Model.glb), disable it:
const block = app.get("Block");
if (block) block.active = false;
Apps are individual objects in a 3D virtual world and each app has its own transform (position, rotation and scale) in the world. All apps have a script attached to them, and the script executes in its own isolated JavaScript compartment. Scripts are able to instantiate shapes and other things to form specific objects like a couch, building or plant. Players are free to grab apps, move them around or duplicate them. The origin of an app should always be treated as the 'ground' position, as players generally move apps across surfaces of other apps. Players are around 1.7m tall, are able to jump around 1.5m high and around 5m in distance when running and jumping.
Scripts execute in isolated compartments with a curated runtime API. See docs/scripting/README.md for the current globals and methods. The fetch, setTimeout, app, world and props helpers are passed into the entry function (they are not globals; fetch is the same as app.fetch). Common ones include:
Not all browser APIs are available; rely on the docs/types as the source of truth.
For all intents and purposes these virtual worlds use the same coordinate system as three.js (X = Right, Y = Up, Z = forward).
The unit of measurement for distance or size is in meters.
Rotations are in radians but you can use degrees by multiplying by the global constant DEG2RAD.
Shapes are the primary way to create visuals, we call these prims. Each type of prim has its own size format. This is how you create them:
const box = app.create("prim", {
type: "box",
size: [1, 2, 3], // width, height, depth
color: "#ff0000", // red
});
const sphere = app.create("prim", {
type: "sphere",
size: [0.5], // radius
color: "#00ff00", // green
});
const cylinder = app.create("prim", {
type: "cylinder",
size: [0.5, 0.5, 1], // topRadius, bottomRadius, height
color: "#0000ff", // blue
});
Once created you can also edit their properties if needed:
const box = app.create("prim", {
type: "box",
size: [1, 2, 3],
color: "#ff0000",
});
// change the box color
box.color = "green";
Some shapes might need to be semi-transparent, and the opacity property controls this:
const window = app.create("prim", {
type: "box",
size: [2, 2, 0.1],
color: "blue",
opacity: 0.5,
});
Creating a node (eg a prim) does not automatically make it visible. Only nodes added to the app global become visible in the world.
app.add(boxA);
It is beneficial to group different prims together to form each part of an overall object. For example when making a wheel for a car, you can construct one wheel and then easily clone it and move it around.
To do this, there is also a special group node that doesn't have a visual and is purely for grouping other nodes.
const wheel = app.create("group");
const tire = app.create("prim", {
type: "cylinder",
size: [0.5, 0.5, 0.2],
color: "black",
physics: "static",
});
const hub = app.create("prim", {
type: "cylinder",
size: [0.3, 0.3, 0.25],
color: "grey",
physics: "static",
});
wheel.add(tire);
wheel.add(hub);
const wheelFL = wheel.clone(true); // clone all children
const wheelFR = wheel.clone(true);
const wheelBL = wheel.clone(true);
const wheelBR = wheel.clone(true);
// ...position the wheels (not shown)
app.add(wheelFL);
app.add(wheelFR);
app.add(wheelBL);
app.add(wheelBR);
It is also very useful to 'change' the pivot point of something and make it easier to work with:
const bar = app.create("group");
const beam = app.create("prim", {
type: "box",
size: [1, 0.2, 10],
position: [0, 0, -5], // shift back
});
bar.add(beam);
// bar.rotation.y now spins the beam at one end of it instead of the center
bar.rotation.y += 45 * DEG2RAD;
Most of the time, players will place apps on top of other surfaces, so app origins should be treated as the 'ground'. This means that most of the time you will need to lift things up:
const box = app.create("prim", {
type: "box",
size: [1, 1, 1],
position: [0, 0.5, 0], // lift up so it sits on the ground surface
});
app.add(box);
When creating prims you can also specify position, rotation (or quaternion) and scale:
const box = app.create("prim", {
type: "box",
size: [1, 1, 1],
position: [0, 2, 0], // xyz in meters
rotation: [0, 45 * DEG2RAD, 0], // xyz in radians
scale: [1, 1, 1], // xyz
});
Eulers for rotation are in radians. Multiply with the globals DEG2RAD and RAD2DEG to convert degrees to radians and vice versa.
Once created they become actual transform class instances (Vector3, Euler, Quaternion) and you can edit them like this:
box.position.set(0, 4, 0);
box.rotation.y += 10 * DEG2RAD;
box.quaternion.slerp(target, 0.2);
box.scale.multiply(otherBox.scale);
These constructs are also available as globals if you need to use them independently:
const pos = new Vector3(0, 2, 0);
const rot = new Euler(0, 0, 0, "YXZ");
const qua = new Quaternion(0, 0, 0, 1);
const sca = new Vector3(1, 1, 1);
Vector3, Euler and Quaternion are identical to three.js
By default prims have no collision but it's likely you'll want to make them have collision so players dont walk or fall through them.
Objects that should have collision should use static collision, but if they move programmatically they should have kinematic collision.
const box = app.create("prim", {
type: "box",
size: [1, 2, 3],
physics: "static", // null, 'static' or 'kinematic'
});
Only when requested, you can make things move or change over time by hooking into the animation cycle:
app.on("animate", (delta) => {
box.rotation.y += 45 * DEG2RAD * delta; // rotate around Y axis each frame 45 degrees per second
});
The animate rate is dynamic based on how far away the app is from the camera, so be sure to use delta time to normalize speeds.
If animations start in response to triggers or actions and have an end time, subscribe and unsubscribe for performance:
const animate = (delta) => {
// do things
};
// subscribe when something needs to happen
app.on("animate", animate);
// unsubscribe when finsihed to save resources
app.off("animate", animate);
In addition to setting the color of a prim you can also push its color into HDR range causing it glow:
const box = app.create("prim", {
type: "box",
size: [1, 1, 1],
color: "red",
emissive: "red", // usually the same as `color`
emissiveIntensity: 5, // 0 is no bloom, ~5 is a nice bloom, 10 is mega bloom (MUST be >= 0)
});
Scripts execute on every client, so if you use any kind of procedural randomisation, it's best to use prng so that each client sees the same thing:
const num = prng(1); // create a prng generate with a seed (1)
const result = num(0, 100, 2); // min, max, decimalPlaces (defaults to 0)
// result is a number between 0 and 100 with 2 dp.
If requested you can add simple response to interaction with an action node.
An action node displays a label when players come near it and if they click it the script is notified:
const action = app.create("action", {
label: "Open",
position: [0, 0.5, 0],
onTrigger: () => {
door.rotation.y = 90 * DEG2RAD;
},
});
app.add(action);
Prims can become trigger zones and notify you when a player enters or leaves the prim volume:
const zone = app.create("prim", {
type: "box",
size: [4, 4, 4],
opacity: 0,
physics: "static", // physics must be enabled
trigger: true, // treat physics as a trigger not a collider
onTriggerEnter: (e) => {
if (!e.isLocalPlayer) return;
// do things...
},
onTriggerLeave: (e) => {
if (!e.isLocalPlayer) return;
// do things...
},
});
Note that you can make invisible trigger areas by setting opacity to 0.
The same script executes on the server and all connected clients. If requested you can network objects to create multiplayer experiences that stay in sync across clients.
if (world.isClient) {
// run client only code
}
if (world.isServer) {
// run server only code
}
// send an event from a client to the server
app.send("someEvent", { some: "data" });
// send an event from the server to all clients
app.send("anotherEvent", { some: "data" });
// subscribe to an event sent from the server or a client
app.on("someEvent", (data) => {
console.log(data); // { some: 'data' }
});
On the server, you have access to a state object to store current state as the app changes. It is just a plain old javascript object.
When clients connect, the current state on the server is sent along to the client so that the client launches with that state, like a snapshot.
When a client reads this state, it is only a single one-time snapshot and does not update anymore, but feel free to use this object to track state as you receive new events from the server.
if (world.isClient) {
if (app.state.ready) {
init(app.state);
} else {
app.on("init", init);
}
function init(state) {
// at this point it is guaranteed that the server has initialised the app and its state.
// sometimes an app runs on the client before the server, eg when clients edit scripts.
// this is where we initialise objects based on state and subscribe to server events...
}
}
if (world.isServer) {
app.state.open = false; // a door variable for example
app.state.ready = true;
app.send("init", app.state);
}
Assets live in top-level assets/ and are referenced from blueprint JSON or file props. For file props:
app.configure([{ key: "image", type: "file", kind: "image", label: "Image" }]);
const image = app.create("image");
image.src = props.image?.url;
app.add(image);
For fixed assets, point to assets/... in the blueprint JSON (eg model, image, or other file props).
You can split code into multiple files using ES module imports:
import { doSomething } from "./helper.js";
import { lerp } from "./utils/math.js";
Shared modules live in shared/ and are imported via @shared/... or shared/.... Bare imports (react, lodash), node builtins, and cross-app imports are not supported.