ignifx0.x · unpublished
GitHub

Recipes·skills/ignifx/references/recipes/spawn-a-prefab-on-click.md

Spawn a prefab on click

<!-- Generated by `pnpm docs:recipes`. Do not edit by hand. -->

A click is a ray. camera.screenToRay turns the pointer's pixel into a world-space ray, app.physics.raycast reports what it hits, and world.instantiate stamps the prefab out at the hit point. Instantiation is synchronous — a SceneAsset arrives with every asset it references already loaded — so the prop exists before update returns and falls from the next fixed step.

Both halves of the click agree on one space: <Pointer>/position reports backing-store pixels, which is exactly what screenToRay reads, so nothing is converted between them. A physics query needs one completed fixed step before it answers, or it throws IGX-0902.

There is no mesh file format, so crate.prefab.json beside this file carries the material, the collider and the body, and CrateMesh builds the box in awake: a primitive lives at a memory: address that no file can name.

typescript
import { Camera, MeshAsset, MeshRenderer, Script, createApp, entityRef, f32 } from "@ignifx/core";import { defineInputActions, input } from "@ignifx/input";import { BoxCollider, physics } from "@ignifx/physics";import type { AssetHandle, Entity, SceneAsset, ScriptCallbacks } from "@ignifx/core";/** Gives the prefab's `MeshRenderer` a box; the render sync picks it up in `PreRender`. */class CrateMesh extends Script.define({ size: f32(0.6) }) implements ScriptCallbacks {  static typeId = "recipes/CrateMesh";  #mesh: AssetHandle<MeshAsset> | null = null;  awake(): void {    this.#mesh = MeshAsset.box(this.app, { size: this.size });    this.entity.requireComponent(MeshRenderer).mesh = this.#mesh;  }  onDestroy(): void {    this.#mesh?.release(); // One release for the one factory call.    this.#mesh = null;  }}/** Spawns {@link Spawner.prefab} wherever the player clicks. */class Spawner extends Script.define({ camera: entityRef<Entity>(), lift: f32(0.4) }) implements ScriptCallbacks {  static typeId = "recipes/Spawner";  /** An asset handle is not a schema field here: `asset()` needs an asset *class* as its token. */  prefab: AssetHandle<SceneAsset> | null = null;  update(): void {    const camera = this.camera?.getComponent(Camera) ?? null;    const prefab = this.prefab;    if (camera === null || prefab === null || !this.app.input.actions.get("spawn").wasPressedThisFrame) {      return;    }    const pointer = this.app.input.actions.get("point").vector;    const ray = camera.screenToRay(pointer.x, pointer.y);    const hit = ray === null ? null : this.app.physics.raycast(ray.origin, ray.direction, 60);    if (hit === null) {      return;    }    // Along the surface normal, so a prop dropped on a wall does not start inside it.    this.world.instantiate(prefab.value, {      position: {        x: hit.point.x + hit.normal.x * this.lift,        y: hit.point.y + hit.normal.y * this.lift,        z: hit.point.z + hit.normal.z * this.lift,      },    });  }}const canvas = document.querySelector("canvas");if (!(canvas instanceof HTMLCanvasElement)) {  throw new Error("ignifx renders into a <canvas> element.");}const app = await createApp({ canvas, extensions: [physics(), input()], settings: { assets: { root: "assets" } } });app.registerComponents([CrateMesh, Spawner]);const spawn = { name: "spawn", bindings: [{ path: "<Pointer>/press" }] };const point = { name: "point", type: "vector2", bindings: [{ path: "<Pointer>/position" }] } as const;app.input.loadActions(defineInputActions({ maps: [{ name: "Player", actions: [spawn, point] }] }));// A floor to catch the ray: a collider with no `Rigidbody` is placed once as a static body.app.world  .createEntity("Floor")  .addComponent(BoxCollider, { size: { x: 40, y: 1, z: 40 }, center: { x: 0, y: -0.5, z: 0 } });const eye = app.world.createEntity("Main Camera");eye.transform.localPosition.set(0, 6, -6);eye.transform.lookAt({ x: 0, y: 0, z: 0 });eye.addComponent(Camera, { fov: 55, far: 200 });// Loaded before `start()`, where a completed load settles at once instead of waiting for a frame.const crate = await app.assets.loadAsync<SceneAsset>("prefabs/crate.prefab.json");app.world.createEntity("Spawner").addComponent(Spawner, { camera: eye }).prefab = crate;await app.start();

Source: examples/recipes/spawn-a-prefab-on-click/main.ts