Ray
APJS Script API reference for the Ray class.
| Type | Name | Interface Description |
|---|---|---|
| Variables | direction: Vector3f | • Function: Gets the normalized vector representing the direction of the ray. Returns The direction vector of the ray. |
| Variables | origin: Vector3f | • Function: Gets the origin point of the ray. Returns The origin point of the ray. |
| Functions | constructor() | |
| Functions | constructor(min: Vector3f, max?: Vector3f) | Parameters • • |
| Functions | clone(): Ray | • Function: Creates and returns a deep copy of the current ray. Returns A new instance of Ray with the same origin and direction as the original. |
| Functions | equals(other: Ray): boolean | • Function: Compares this ray with another ray for equality. Parameters • Returns A boolean indicating whether the two rays are equal. |
| Functions | toString(): string | • Function: Returns a string representation of the Ray object. Returns A string describing the Ray object. |
Examples
constructor()
let obj = new APJS.Ray();
constructor(min: Vector3f, max?: Vector3f)
let obj = new APJS.Ray();
Use Case
Example 1 — Platformer jump with Physics2D raycast ground detection. Uses screenTransform.localPosition for world-unit coordinates (NOT rb.position which is always (0,0) fo…
@component()
export class PlatformerJump extends APJS.BasicScriptComponent {
@serializeProperty
jumpForce: number = 12;
private rb!: APJS.RigidBody2D;
private st!: APJS.ScreenTransform;
private myObj!: APJS.SceneObject;
private isGrounded = false;
private initialized = false;
private halfHeight = 1.5; // half collider height in world units
private startLocalPos!: APJS.Vector3f;
private onTouchEvent = (event: APJS.IEvent) => {
const touchInfo = event.args[0] as APJS.TouchData;
if (touchInfo.phase === APJS.TouchPhase.Began && this.isGrounded) {
this.rb.addForce(
new APJS.Vector2f(0, this.jumpForce),
APJS.ForceMode2D.Impulse
);
this.isGrounded = false;
}
};
// RecordStart: reset platformer player per Physics2D §"RecordStart Reset for 2D Physics".
// velocity → ScreenTransform.localPosition (PlatformerJump uses world-unit localPosition,
// not anchoredPosition) → isGrounded reset (raycast will re-detect). See GameState SKILL.
private onRecordStart = (_event: APJS.IEvent) => {
if (!this.initialized) return;
this.rb.velocity = new APJS.Vector2f(0, 0);
if (this.startLocalPos) {
this.st.localPosition = new APJS.Vector3f(
this.startLocalPos.x, this.startLocalPos.y, this.startLocalPos.z
);
}
this.isGrounded = false;
};
onUpdate(dt: number): void {
if (!this.initialized) {
this.myObj = this.getSceneObject();
const rbComp = this.myObj.getComponent("RigidBody2D");
if (rbComp) this.rb = rbComp as APJS.RigidBody2D;
const stComp = this.myObj.getComponent("ScreenTransform");
if (stComp) this.st = stComp as APJS.ScreenTransform;
if (this.rb && this.st) {
const lp = this.st.localPosition;
this.startLocalPos = new APJS.Vector3f(lp.x, lp.y, lp.z);
this.initialized = true;
APJS.EventManager.getGlobalEmitter().on(
APJS.EventType.Touch,
this.onTouchEvent
);
APJS.EventManager.getGlobalEmitter().on(
APJS.EventType.RecordStart,
this.onRecordStart
);
}
return;
}
// Use localPosition for world-unit coords (inherited from Transform)
// Do NOT use rb.position (always 0,0 for 2D objects)
const lp = this.st.localPosition;
// Raycast downward from player bottom edge
const origin = new APJS.Vector3f(lp.x, lp.y - this.halfHeight, 0);
const dir = new APJS.Vector3f(0, -1, 0);
const ray = new APJS.Ray(origin, dir);
const hits = APJS.Physics2D.raycast2D(ray, 0.5, false);
// Filter self-hits (raycast can hit player's own collider)
this.isGrounded = false;
for (let i = 0; i < hits.length; i++) {
const co = hits[i].colliderObject;
if (co) {
const hitName = co.name;
if (hitName !== this.myObj.name) {
this.isGrounded = true;
break;
}
}
}
}
onDestroy(): void {
APJS.EventManager.getGlobalEmitter().off(
APJS.EventType.Touch,
this.onTouchEvent
);
APJS.EventManager.getGlobalEmitter().off(
APJS.EventType.RecordStart,
this.onRecordStart
);
}
}
Example 2 — AR plane-world tap-to-place pattern. The placement object lives under AR Plane (so it renders inside the AR Tracking render group, on top of the camera feed).
// AR plane-world tap-to-place + InteractableObject co-existence.
// Tap projection uses the DeviceTracker-backed AR Camera and intersects the Y=0 XOZ ground plane.
// The cube center is placed at the hit XZ plus PLACEMENT_Y, so the cube rests on the AR Plane.
// Tap is recognized on Touch.Ended only when the finger stayed within TAP_MOVE_THRESHOLD.
const GROUND_Y = 0;
const PLACEMENT_Y = 16;
const RAY_EPSILON = 0.0001;
const TAP_MOVE_THRESHOLD = 0.02;
@component()
export class TapPlaceController extends APJS.BasicScriptComponent {
@serializeProperty
placementCube!: APJS.SceneObject;
@serializeProperty
scoreText!: APJS.SceneObject;
private touchCallback!: (event: APJS.IEvent) => void;
private cubeTransform!: APJS.Transform;
private scoreTextComp!: APJS.Text;
private placementCamera!: APJS.Camera;
private hasPlacementCamera = false;
private placedCount = 0;
private touchActive = false;
private touchMoved = false;
private touchStartX = 0;
private touchStartY = 0;
private startCubePos: APJS.Vector3f | null = null;
// RecordStart: reset placedCount + return cube to its cached start position + reset score
// text + clear transient touch state. AR plane tracking is managed by DeviceTracker /
// ARCamera and resets automatically. See GameState §"RecordStart / RecordEnd Lifecycle".
private onRecordStart = (_event: APJS.IEvent) => {
this.placedCount = 0;
if (this.cubeTransform && this.startCubePos) {
this.cubeTransform.setWorldPosition(this.startCubePos);
}
if (this.scoreTextComp) {
this.scoreTextComp.text = "Score: 0";
}
this.touchActive = false;
this.touchMoved = false;
};
onStart(): void {
if (this.placementCube) {
this.cubeTransform = this.placementCube.getComponent("Transform") as APJS.Transform;
if (this.cubeTransform) {
const p = this.cubeTransform.getWorldPosition();
this.startCubePos = new APJS.Vector3f(p.x, p.y, p.z);
}
}
if (this.scoreText) {
this.scoreTextComp = this.scoreText.getComponent("Text") as APJS.Text;
}
this.resolvePlacementCamera();
this.touchCallback = (event: APJS.IEvent) => {
const t = event.args[0] as APJS.TouchData;
if (t.phase === APJS.TouchPhase.Began) {
this.touchActive = true;
this.touchMoved = false;
this.touchStartX = t.position.x;
this.touchStartY = t.position.y;
return;
}
if (!this.touchActive) return;
if (t.phase === APJS.TouchPhase.Moved) {
this.touchMoved = this.touchMoved || this.touchDistanceSq(t.position.x, t.position.y) > TAP_MOVE_THRESHOLD * TAP_MOVE_THRESHOLD;
return;
}
if (t.phase === APJS.TouchPhase.Ended) {
const isTap = !this.touchMoved && this.touchDistanceSq(t.position.x, t.position.y) <= TAP_MOVE_THRESHOLD * TAP_MOVE_THRESHOLD;
this.touchActive = false;
this.touchMoved = false;
if (isTap) {
this.handleTap(t.position.x, t.position.y);
}
return;
}
if (t.phase === APJS.TouchPhase.Canceled) {
this.touchActive = false;
this.touchMoved = false;
}
};
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.Touch, this.touchCallback, this);
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.onRecordStart);
}
private touchDistanceSq(nx: number, ny: number): number {
const dx = nx - this.touchStartX;
const dy = ny - this.touchStartY;
return dx * dx + dy * dy;
}
private resolvePlacementCamera(): boolean {
if (this.hasPlacementCamera) {
return true;
}
const sceneObjects = this.getSceneObject().scene.getAllSceneObjects();
let fallbackCamera!: APJS.Camera;
for (let i = 0; i < sceneObjects.length; i += 1) {
const camera = sceneObjects[i].getComponent("Camera") as APJS.Camera;
if (!camera || camera.cameraType !== APJS.CameraType.Perspective) {
continue;
}
if (!fallbackCamera) {
fallbackCamera = camera;
}
if (sceneObjects[i].name === "AR Camera" || sceneObjects[i].getComponent("DeviceTracker")) {
this.placementCamera = camera;
this.hasPlacementCamera = true;
return true;
}
}
if (fallbackCamera) {
this.placementCamera = fallbackCamera;
this.hasPlacementCamera = true;
return true;
}
return false;
}
private handleTap(nx: number, ny: number): void {
if (!this.cubeTransform || !this.resolvePlacementCamera()) {
return;
}
const ray = this.placementCamera.viewportPointToRay(new APJS.Vector2f(nx, 1 - ny));
if (ray.direction.y > -RAY_EPSILON && ray.direction.y < RAY_EPSILON) {
return;
}
const t = (GROUND_Y - ray.origin.y) / ray.direction.y;
if (t <= RAY_EPSILON) {
return;
}
const hitX = ray.origin.x + ray.direction.x * t;
const hitZ = ray.origin.z + ray.direction.z * t;
const worldPoint = new APJS.Vector3f(hitX, PLACEMENT_Y, hitZ);
this.cubeTransform.setWorldPosition(worldPoint);
this.placedCount += 1;
if (this.scoreTextComp) {
this.scoreTextComp.text = "Score: " + this.placedCount;
}
}
onDestroy(): void {
if (this.touchCallback) {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.Touch, this.touchCallback, this);
}
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.onRecordStart);
}
}