SphereCollider
Represents a sphere-shaped collider component used for physics collision detection.
| Type | Name | Interface Description |
|---|---|---|
| Variables | radius: number | • Function: Gets or sets the authored radius of the sphere collider. Larger values produce a larger spherical collision volume around the object. This is a radius value, not a diameter. Defaults to 5, the same as the default sphere size. |
| Functions | constructor() |
Examples
constructor()
let obj = new APJS.SphereCollider();
Use Case
Example 1 — 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);
}
}
Example 2 — Hoop / basketball-ring trigger scoring — detects a projectile passing through a horizontal hoop using two stacked thin BoxCollider triggers (TopTrigger above, B…
@component()
export class HoopScorer extends APJS.BasicScriptComponent {
@serializeProperty topTrigger!: APJS.SceneObject;
@serializeProperty bottomTrigger!: APJS.SceneObject;
@serializeProperty ball!: APJS.SceneObject;
@serializeProperty scoreText!: APJS.SceneObject;
@serializeProperty mode: number = 0;
@serializeProperty dualWindowSeconds: number = 0.6;
@serializeProperty gravityY: number = -9.81;
// Scene authoring (DSL):
// - TopTrigger: Empty Object @ (0, 1.7, -4) + BoxCollider size (1.5, 0.2, 1.5), isTangible=false.
// - BottomTrigger: Empty Object @ (0, 1.3, -4) + BoxCollider size (1.5, 0.2, 1.5), isTangible=false.
// - RimMarker: Cube scaled (0.2, 0.02, 0.2) at (0, 1.5, -4) -- visual only, no collider.
// - Ball: Sphere with RigidBody + SphereCollider; either drop-tested or fired by ChargeMeterThrow.
// - ScoreText: 2D Text in safe zone, sibling under 2D Camera.
//
// CollisionEvent footgun: pass the COLLIDER component, NOT the SceneObject, into
// APJS.EventManager.getObjectEmitter(...). Passing the SceneObject silently fails.
// Also: collider.emitCollisionEvent must be set to true on init -- default is false.
private inited = false;
private topCol!: APJS.BoxCollider;
private botCol!: APJS.BoxCollider;
private ballName = "";
private scoreTextComp!: APJS.Text;
private score = 0;
private topArmed = false;
private topArmTime = 0;
private now = 0;
private topHandler!: (e: APJS.IEvent) => void;
private bottomHandler!: (e: APJS.IEvent) => void;
private recordStartCb!: () => void;
private initOnce(): void {
this.topCol = this.topTrigger.getComponent("BoxCollider") as APJS.BoxCollider;
this.botCol = this.bottomTrigger.getComponent("BoxCollider") as APJS.BoxCollider;
if (!this.topCol || !this.botCol) return;
this.scoreTextComp = this.scoreText.getComponent("Text") as APJS.Text;
if (this.ball) this.ballName = this.ball.name;
this.topCol.emitCollisionEvent = true;
this.botCol.emitCollisionEvent = true;
this.topHandler = (e: APJS.IEvent) => this.onTopEnter(e);
this.bottomHandler = (e: APJS.IEvent) => this.onBottomEnter(e);
APJS.EventManager.getObjectEmitter(this.topCol).on(APJS.CollisionEvent.Enter, this.topHandler, this);
APJS.EventManager.getObjectEmitter(this.botCol).on(APJS.CollisionEvent.Enter, this.bottomHandler, this);
if (typeof this.gravityY === "number") {
const g = APJS.Physics3D.gravity;
APJS.Physics3D.gravity = new APJS.Vector3f(g.x, this.gravityY, g.z);
console.log("[HoopScorer] gravity set to y=" + this.gravityY);
}
this.recordStartCb = () => this.resetScore();
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.recordStartCb, this);
this.refreshScoreText();
console.log("[HoopScorer] init mode=" + this.mode + " ball=" + this.ballName);
this.inited = true;
}
private isBall(other: APJS.SceneObject | null): boolean {
if (!other) return false;
if (this.ballName && other.name === this.ballName) return true;
return !this.ballName;
}
private onTopEnter(event: APJS.IEvent): void {
const infos = event.args[0] as APJS.CollisionInfo[];
for (const info of infos) {
if (!this.isBall(info.otherObject)) continue;
console.log("[HoopScorer] TOP enter @t=" + this.now.toFixed(3));
if (this.mode === 0) {
this.score++;
this.refreshScoreText();
} else {
this.topArmed = true;
this.topArmTime = this.now;
}
return;
}
}
private onBottomEnter(event: APJS.IEvent): void {
const infos = event.args[0] as APJS.CollisionInfo[];
for (const info of infos) {
if (!this.isBall(info.otherObject)) continue;
console.log("[HoopScorer] BOTTOM enter @t=" + this.now.toFixed(3) + " armed=" + this.topArmed);
if (this.mode === 1 && this.topArmed) {
const dt = this.now - this.topArmTime;
if (dt <= this.dualWindowSeconds) {
this.score++;
this.refreshScoreText();
console.log("[HoopScorer] DUAL score! window=" + dt.toFixed(3) + "s");
} else {
console.log("[HoopScorer] DUAL miss \u2014 window expired (" + dt.toFixed(3) + "s)");
}
this.topArmed = false;
}
return;
}
}
private refreshScoreText(): void {
if (this.scoreTextComp) this.scoreTextComp.text = "Hoops: " + this.score;
}
private resetScore(): void {
this.score = 0;
this.topArmed = false;
this.topArmTime = 0;
this.refreshScoreText();
}
onUpdate(dt: number): void {
if (!this.inited) {
if (this.topTrigger && this.bottomTrigger && this.scoreText) {
this.initOnce();
}
return;
}
this.now += dt;
if (this.topArmed && this.now - this.topArmTime > this.dualWindowSeconds) {
this.topArmed = false;
}
}
onDestroy(): void {
if (this.topHandler && this.topCol) {
APJS.EventManager.getObjectEmitter(this.topCol).off(APJS.CollisionEvent.Enter, this.topHandler, this);
}
if (this.bottomHandler && this.botCol) {
APJS.EventManager.getObjectEmitter(this.botCol).off(APJS.CollisionEvent.Enter, this.bottomHandler, this);
}
if (this.recordStartCb) {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.recordStartCb, this);
}
}
}