Physics3D
3D physics world manager (singleton).
| Type | Name | Interface Description |
|---|---|---|
| Variables | gravity: Vector3f | • Function: 3D world gravity acceleration in internal engine units. Default is (0, -980, 0). The value 980 = 9.8 m/s^2 * (100), making gravity consistent with forces applied via (which are also scaled internally). Changing this affects all bodies with = true. Not reset on record start. |
| Variables | gravityFactor: number | • Function: Internal engine unit scaling factor. Defaults to 100 in normal mode, 10 in framerate-independent mode (set automatically during init). All forces applied via and the gravity value read from scene settings are multiplied by this factor before being passed to the engine. This keeps forces and gravity in the same internal unit system. |
| Variables | timeScale: number | • Function: 3D physics simulation time multiplier. Default is 1.0. 1.0 = real-time, 0.5 = half-speed, 0.0 = paused. Values > 1.0 enable a slow-motion mode internally. Not reset on record start. |
| Static Functions | raycast(ray: Ray, maxDistance: number, nearest: boolean, layerMask?: LayerSet): RaycastHit3D[] | • Function: Casts a ray in 3D space and returns hit information. Parameters • • • • Returns Array of . Empty array if nothing was hit. |
Examples
raycast(ray: Ray, maxDistance: number, nearest: boolean, layerMask?: LayerSet): RaycastHit3D[]
// Cast downward and get the first hit
const origin = new APJS.Vector3f(0, 10, 0);
const dir = new APJS.Vector3f(0, -1, 0);
const hits = APJS.Physics3D.raycast(
new APJS.Ray(origin, dir), 50, true);
if (hits.length > 0) {
console.log("Hit:", hits[0].colliderObject?.name);
}
// Only hit objects on layer 1
const layerMask = new APJS.LayerSet();
layerMask.set(1, true);
const hits = APJS.Physics3D.raycast(ray, 50, false, layerMask);
Use Case
Example 1 — 3D physics: tap anywhere to apply an upward impulse to a RigidBody object.
@component()
export class TapImpulse extends APJS.BasicScriptComponent {
@serializeProperty
impulseStrength: number = 50;
private rb: APJS.RigidBody;
private inited = false;
private startPos!: APJS.Vector3f;
private startRot!: APJS.Quaternionf;
private startGravityOn = true;
private onTouch = (event: APJS.IEvent) => {
const touch = event.args[0] as APJS.TouchData;
if (touch.phase === APJS.TouchPhase.Began && this.rb) {
// Impulse: one-shot force, object falls back under gravity
this.rb.addForce(
new APJS.Vector3f(0, this.impulseStrength, 0),
APJS.ForceMode3D.Impulse
);
}
};
// RecordStart: reset per Physics3D §"RecordStart Reset for 3D Physics" — full 6-step
// single-body block (velocity → angularVelocity → position → rotation → useGravity →
// accumulators). See GameState §"RecordStart / RecordEnd Lifecycle".
private onRecordStart = (_event: APJS.IEvent) => {
if (!this.inited) return;
this.rb.velocity = new APJS.Vector3f(0, 0, 0);
this.rb.angularVelocity = new APJS.Vector3f(0, 0, 0);
this.rb.position = new APJS.Vector3f(this.startPos.x, this.startPos.y, this.startPos.z);
this.rb.rotation = this.startRot;
this.rb.useGravity = this.startGravityOn;
};
onUpdate(dt: number): void {
if (!this.inited) {
const obj = this.getSceneObject();
if (!obj) return;
this.rb = obj.getComponent("RigidBody") as APJS.RigidBody;
if (!this.rb) return;
this.startPos = new APJS.Vector3f(this.rb.position.x, this.rb.position.y, this.rb.position.z);
this.startRot = this.rb.rotation;
this.startGravityOn = this.rb.useGravity;
this.inited = true;
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.Touch, this.onTouch);
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.onRecordStart);
}
}
onDestroy(): void {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.Touch, this.onTouch);
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.onRecordStart);
}
}
Example 2 — 3D physics collision event handler. Listens on BoxCollider/SphereCollider for Enter/Stay/Exit events.
@component()
export class CollisionHandler3D extends APJS.BasicScriptComponent {
private collider: APJS.Collider;
private inited = false;
private hitCount = 0;
private onEnter = (event: APJS.IEvent) => {
this.hitCount++;
const infos = event.args[0] as APJS.CollisionInfo[];
for (const info of infos) {
if (info.otherObject) {
console.log("[Collision] Enter #" + this.hitCount + ": " + info.otherObject.name);
console.log(" point: " + info.point.x.toFixed(2) + ", " + info.point.y.toFixed(2) + ", " + info.point.z.toFixed(2));
console.log(" normal: " + info.normal.x.toFixed(2) + ", " + info.normal.y.toFixed(2) + ", " + info.normal.z.toFixed(2));
}
}
};
private onExit = (event: APJS.IEvent) => {
console.log("[Collision] Exit");
};
// RecordStart: only the script-owned hitCount accumulator needs reset; this example
// does not move any RigidBody, so no Physics3D reset block is needed. See GameState SKILL.
private onRecordStart = (_event: APJS.IEvent) => {
this.hitCount = 0;
};
onStart(): void {
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.onRecordStart);
}
onUpdate(dt: number): void {
if (!this.inited) {
const obj = this.getSceneObject();
if (!obj) return;
// Try BoxCollider first, then SphereCollider
this.collider = obj.getComponent("BoxCollider") as APJS.BoxCollider;
if (!this.collider) {
this.collider = obj.getComponent("SphereCollider") as APJS.SphereCollider;
}
if (!this.collider) return;
this.inited = true;
this.collider.emitCollisionEvent = true;
// CRITICAL: pass collider, NOT sceneObject
const emitter = APJS.EventManager.getObjectEmitter(this.collider);
emitter.on(APJS.CollisionEvent.Enter, this.onEnter, this);
emitter.on(APJS.CollisionEvent.Exit, this.onExit, this);
}
}
onDestroy(): void {
if (this.collider) {
const emitter = APJS.EventManager.getObjectEmitter(this.collider);
emitter.off(APJS.CollisionEvent.Enter, this.onEnter, this);
emitter.off(APJS.CollisionEvent.Exit, this.onExit, this);
}
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.onRecordStart);
}
}