Skip to main content

Vector3f

APJS Script API reference for the Vector3f class.

TypeNameInterface Description
Variablesx: number

• Function: Represents the x-coordinate in a 3-dimensional vector.

Variablesy: number

• Function: Represents the y-coordinate in a 3-dimensional vector.

Variablesz: number

• Function: Represents the z-coordinate in a 3-dimensional vector.

Functionsconstructor()

Functionsconstructor(x?: number, y?: number, z?: number)

Parameters

• x: - The X component of the vector (optional).

• y: - The Y component of the vector (optional).

• z: - The Z component of the vector (optional).

Functionsadd(other: Vector3f): this

• Function: Adds the components of other to this vector in place.

Parameters

• other: - The vector whose components will be added to this vector.

Returns This vector with updated components after addition.

FunctionsangleTo(other: Vector3f): number

• Function: Returns the angle in radians between the current vector and another specified vector.

Parameters

• other: - The target vector to calculate the angle against.

Returns The angle in radians between the two vectors.

FunctionsclampLength(length: number): this

• Function: Clamps the length of this vector to the specified length and returns this vector.

Parameters

• length: - The length to which the vector's length should be clamped.

Returns This vector with its length clamped to the specified value.

Functionsclone(): Vector3f

• Function: Creates and returns a new Vector3f with the same component values. Use this before calling mutating math methods when the original value must be preserved.

Returns A new Vector3f that is a clone of this vector.

Functionscross(other: Vector3f): this

• Function: Computes the cross product with other and stores the result in this vector.

Parameters

• other: - The vector to compute the cross product with.

Returns This vector after computing the cross product.

Functionsdistance(other: Vector3f): number

• Function: Calculates and returns the Euclidean distance between the current vector and another vector.

Parameters

• other: - The target vector to calculate the distance to.

Returns The distance between the two vectors.

Functionsdivide(value: number | Vector3f): this

• Function: Divides this vector in place by value.

Parameters

• value: - The divisor, which can be either a Vector3f or a number.

Returns This vector after division.

Functionsdot(other: Vector3f): number

• Function: Returns the dot product of the vector and another vector.

Parameters

• other: - The vector to compute the dot product with.

Returns The dot product of the two vectors.

Functionsequals(other: Vector3f): boolean

• Function: Determines if this vector is equal to the specified vector.

Parameters

• other: - The vector to compare with.

Returns A boolean indicating whether the vectors are equal.

Functionsinverse(): this

• Function: Replaces each component with its reciprocal in place.

Returns This vector with inverted x, y, and z values.

Functionsmagnitude(): number

• Function: Calculates and returns the magnitude (length) of the vector.

Returns The magnitude of the vector.

Functionsmultiply(value: number | Vector3f): this

• Function: Multiplies this vector in place. If value is a Vector3f, multiplication is performed component-wise. If value is a number, all components are scaled by that number.

Parameters

• value: - A Vector3f or a number to multiply with the vector's components.

Returns This vector after multiplication.

FunctionsmultiplyScalar(scalar: number): this

• Function: Multiplies all components of this vector by scalar in place.

Parameters

• scalar: - The number to multiply each component of the vector by.

Returns This vector with updated components.

Functionsnormalize(): this

• Function: Normalizes this vector in place so its magnitude becomes 1. If the magnitude is zero, the vector is left unchanged.

Returns This vector after normalization.

Functionsproject(other: Vector3f): this

• Function: Projects this vector onto other in place. If other has zero length, this vector is set to (0, 0, 0).

Parameters

• other: - The vector onto which this vector is projected.

Returns This vector after projection.

FunctionsprojectOnPlane(normal: Vector3f): this

• Function: Projects the current vector onto the plane defined by the given normal vector.

Parameters

• normal: - The normal vector of the plane to project onto.

Returns This vector after being projected onto the specified plane.

Functionsreflect(normal: Vector3f): this

• Function: Reflects this vector across the plane defined by the normal normal in place.

Parameters

• normal: - The normal vector defining the reflection plane.

Returns This vector after reflection.

Functionsset(x: number, y: number, z: number): this

• Function: Sets the three-dimensional coordinates of this vector in place.

Parameters

• x: - The x-coordinate value to set.

• y: - The y-coordinate value to set.

• z: - The z-coordinate value to set.

Returns This instance with updated coordinates.

FunctionssqrMagnitude(): number

• Function: Returns the squared magnitude (length) of the vector.

Returns The squared magnitude of the vector.

Functionssubtract(other: Vector3f): this

• Function: Subtracts the components of other from this vector in place.

Parameters

• other: - The vector to subtract from this vector.

Returns This vector after subtraction.

FunctionstoString(): string

• Function: Returns a string representation of the vector.

Returns A string in the format "Vector3f(x, y, z)" where x, y, and z are fixed to 5 decimal places.

Static FunctionscompareApproximately(vec1: Vector3f, vec2: Vector3f, dist: number): boolean

• Function: Approximately compares two vectors by the value of each component with a specified tolerance.

Parameters

• vec1: - The first vector to compare.

• vec2: - The second vector to compare.

• dist: - The maximum allowed difference between corresponding components for the vectors to be considered approximately equal.

Returns True if the vectors are approximately equal within the given tolerance, false otherwise.

Static Functionslerp(vecA: Vector3f, vecB: Vector3f, t: number): Vector3f

• Function: Linearly interpolates between the two vectors vecA and vecB by the factor t.

Parameters

• vecA: - The starting vector.

• vecB: - The ending vector.

• t: - The interpolation factor, typically ranging from 0 to 1.

Returns A new Vector3f that represents the interpolated position between vecA and vecB.

Static Functionsmax(vecA: Vector3f, vecB: Vector3f): Vector3f

• Function: Returns a new vector containing the largest value of each component from the two input vectors.

Parameters

• vecA: - The first vector to compare.

• vecB: - The second vector to compare.

Returns A new Vector3f instance with components set to the maximum values from vecA and vecB.

Static Functionsmin(vecA: Vector3f, vecB: Vector3f): Vector3f

• Function: Returns a new vector containing the smallest value of each component from the two input vectors.

Parameters

• vecA: - The first vector to compare.

• vecB: - The second vector to compare.

Returns A new Vector3f instance with components set to the minimum values from vecA and vecB.

Examples​

constructor()​

let obj = new APJS.Vector3f();

constructor(x?: number, y?: number, z?: number)​

let obj = new APJS.Vector3f();

compareApproximately(vec1: Vector3f, vec2: Vector3f, dist: number): boolean​

let a = new Vector3f(0.0000001, 0.0000001, 0.0000001);
let b = new Vector3f(0.0000000, 0.0000000, 0.0);
Vector3f.compareApproximately(a, b, 0.0001); // true

Use Case​

Example 1 — 3D bowling-lane timing game — the ball auto-ping-pongs laterally in the ready state, touch Began locks the current lane line, hold duration maps linearly to lau…​

@component()
export class BowlingGameManager extends APJS.BasicScriptComponent {
@serializeProperty ball: APJS.SceneObject;
@serializeProperty pins: APJS.SceneObject[] = [];
@serializeProperty scoreTextObject: APJS.SceneObject;
@serializeProperty resetButtonObject: APJS.SceneObject;

@serializeProperty minLaunchSpeed: number = 30;
@serializeProperty maxLaunchSpeed: number = 60;
@serializeProperty maxChargeSeconds: number = 1.4;
@serializeProperty settleVelocityThreshold: number = 0.12;
@serializeProperty settleAngularThreshold: number = 0.28;
@serializeProperty pinSettleFrames: number = 14;
@serializeProperty ballSettleFrames: number = 10;
@serializeProperty roundTimeoutSeconds: number = 5.8;
@serializeProperty fallenDotThreshold: number = 0.6;
@serializeProperty ballStartXClamp: number = 3.1;
@serializeProperty pinActivationZ: number = -1.5;
@serializeProperty aimPingPongSpeed: number = 2.2;

private static readonly STATE_READY = 0;
private static readonly STATE_CHARGING = 1;
private static readonly STATE_ROLLING = 2;
private static readonly STATE_SETTLING = 3;
private static readonly STATE_RESULT = 4;
private static readonly BALL_SPIN_FACTOR = 1.35;
private static readonly PIN_GRAVITY_DELAY_FRAMES = 3;
private static readonly PIN_FAST_SLEEP_FRAMES = 8;
private static readonly PIN_SLEEP_CLEARANCE_Z = 0.28;

private initialized = false;
private currentState = BowlingGameManager.STATE_READY;
private chargeSeconds = 0;
private roundElapsed = 0;
private ballStillFrames = 0;
private touchActive = false;
private score = 0;
private pinsActive = false;
private pinResetFramesRemaining = 0;
private pinGravityDelayFramesRemaining = 0;
private aimDirection = 1;
private lockedAimX = 0;
private debugApproachLogged = false;
private debugPastLogged = false;
private debugResultLogged = false;

private ballRigidBody!: APJS.RigidBody;
private ballTransform!: APJS.Transform;
private scoreText!: APJS.Text;
private resetButtonImage!: APJS.Image;
private ballStartPosition!: APJS.Vector3f;
private ballStartRotation!: APJS.Quaternionf;

private pinRigidBodies: APJS.RigidBody[] = [];
private pinTransforms: APJS.Transform[] = [];
private pinStartPositions: APJS.Vector3f[] = [];
private pinStartRotations: APJS.Quaternionf[] = [];
private pinLowMotionFrames: number[] = [];
private pinCounted: boolean[] = [];
private pinAssistApplied: boolean[] = [];

private touchCallback = (event: APJS.IEvent) => {
this.onTouch(event);
};

private onRecordStart = (_event: APJS.IEvent) => {
this.resetRound();
};

private logBallAndHeadPin(label: string): void {
if (this.pinRigidBodies.length === 0) {
console.log(label + " pins=0");
return;
}

const ballPos = this.ballTransform.getWorldPosition();
const headPos = this.pinTransforms[0].getWorldPosition();
const headRb = this.pinRigidBodies[0];
const headVel = headRb.velocity;
console.log(
label +
" pins=" + this.pinRigidBodies.length +
" ball=(" + ballPos.x.toFixed(3) + "," + ballPos.y.toFixed(3) + "," + ballPos.z.toFixed(3) + ")" +
" head=(" + headPos.x.toFixed(3) + "," + headPos.y.toFixed(3) + "," + headPos.z.toFixed(3) + ")" +
" headVel=(" + headVel.x.toFixed(3) + "," + headVel.y.toFixed(3) + "," + headVel.z.toFixed(3) + ")" +
" headStatic=" + headRb.static +
" headGravity=" + headRb.useGravity +
" headCounted=" + this.pinCounted[0]
);
}

private initOnce(): void {
if (!this.ball || this.pins.length === 0 || !this.scoreTextObject || !this.resetButtonObject) return;

this.ballRigidBody = this.ball.getComponent("RigidBody") as APJS.RigidBody;
this.ballTransform = this.ball.getTransform();
this.scoreText = this.scoreTextObject.getComponent("Text") as APJS.Text;
this.resetButtonImage = this.resetButtonObject.getComponent("Image") as APJS.Image;
if (!this.ballRigidBody || !this.ballTransform || !this.scoreText || !this.resetButtonImage) return;

const ballWorld = this.ballTransform.getWorldPosition();
this.ballStartPosition = new APJS.Vector3f(ballWorld.x, ballWorld.y, ballWorld.z);
this.ballStartRotation = this.ballTransform.getWorldRotation().clone();
this.lockedAimX = this.ballStartPosition.x;

this.pinRigidBodies = [];
this.pinTransforms = [];
this.pinStartPositions = [];
this.pinStartRotations = [];
this.pinLowMotionFrames = [];
this.pinCounted = [];
this.pinAssistApplied = [];

for (let i = 0; i < this.pins.length; i++) {
const pin = this.pins[i];
if (!pin) continue;
const rb = pin.getComponent("RigidBody") as APJS.RigidBody;
const tf = pin.getTransform();
if (!rb || !tf) continue;
this.pinRigidBodies.push(rb);
this.pinTransforms.push(tf);
const pos = tf.getWorldPosition();
this.pinStartPositions.push(new APJS.Vector3f(pos.x, pos.y, pos.z));
this.pinStartRotations.push(tf.getWorldRotation().clone());
this.pinLowMotionFrames.push(0);
this.pinCounted.push(false);
this.pinAssistApplied.push(false);
}

console.log("init pins=" + this.pinRigidBodies.length);
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.Touch, this.touchCallback, this);
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.onRecordStart, this);
this.initialized = true;
this.resetRound();
}

private resetRigidBody(rb: APJS.RigidBody, position: APJS.Vector3f, rotation: APJS.Quaternionf, useGravity: boolean): void {
rb.static = false;
rb.velocity = new APJS.Vector3f(0, 0, 0);
rb.angularVelocity = new APJS.Vector3f(0, 0, 0);
rb.position = new APJS.Vector3f(position.x, position.y, position.z);
rb.rotation = rotation.clone();
rb.useGravity = useGravity;
}

private freezePin(index: number): void {
const rb = this.pinRigidBodies[index];
rb.velocity = new APJS.Vector3f(0, 0, 0);
rb.angularVelocity = new APJS.Vector3f(0, 0, 0);
rb.useGravity = false;
rb.static = true;
}

private forcePinsToStartPose(): void {
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
const tf = this.pinTransforms[i];
const startPosition = this.pinStartPositions[i];
const startRotation = this.pinStartRotations[i];
rb.static = false;
rb.velocity = new APJS.Vector3f(0, 0, 0);
rb.angularVelocity = new APJS.Vector3f(0, 0, 0);
rb.useGravity = false;
rb.position = new APJS.Vector3f(startPosition.x, startPosition.y, startPosition.z);
rb.rotation = startRotation.clone();
tf.setWorldPosition(new APJS.Vector3f(startPosition.x, startPosition.y, startPosition.z));
tf.setWorldRotation(startRotation.clone());
rb.static = true;
}
}

private setPinsActive(active: boolean): void {
this.pinsActive = active;
if (active) {
this.pinResetFramesRemaining = 0;
this.pinGravityDelayFramesRemaining = BowlingGameManager.PIN_GRAVITY_DELAY_FRAMES;
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
rb.static = false;
rb.velocity = new APJS.Vector3f(0, 0, 0);
rb.angularVelocity = new APJS.Vector3f(0, 0, 0);
rb.useGravity = false;
}
return;
}

this.pinGravityDelayFramesRemaining = 0;
this.pinResetFramesRemaining = 3;
this.forcePinsToStartPose();
}

private updatePinGravityDelay(): void {
if (!this.pinsActive || this.pinGravityDelayFramesRemaining <= 0) return;
this.pinGravityDelayFramesRemaining -= 1;
if (this.pinGravityDelayFramesRemaining > 0) return;

for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
if (!rb.static) {
rb.useGravity = true;
}
}
}

private resetRound(): void {
if (!this.initialized) return;
this.currentState = BowlingGameManager.STATE_READY;
this.chargeSeconds = 0;
this.roundElapsed = 0;
this.ballStillFrames = 0;
this.touchActive = false;
this.score = 0;
this.aimDirection = 1;
this.lockedAimX = this.ballStartPosition.x;
this.debugApproachLogged = false;
this.debugPastLogged = false;
this.debugResultLogged = false;

this.resetRigidBody(this.ballRigidBody, this.ballStartPosition, this.ballStartRotation, false);
this.setPinsActive(false);

for (let i = 0; i < this.pinRigidBodies.length; i++) {
this.pinLowMotionFrames[i] = 0;
this.pinCounted[i] = false;
this.pinAssistApplied[i] = false;
}

this.updateHud();
}

private updateHud(): void {
if (!this.scoreText) return;
if (this.currentState === BowlingGameManager.STATE_READY) {
this.scoreText.text = "Tap and hold";
return;
}
if (this.currentState === BowlingGameManager.STATE_CHARGING) {
const power = Math.min(100, Math.round((this.chargeSeconds / this.maxChargeSeconds) * 100));
this.scoreText.text = "Power: " + power + "%";
return;
}
if (this.currentState === BowlingGameManager.STATE_RESULT) {
this.scoreText.text = "Final: " + this.score + "/10";
return;
}
if (this.currentState === BowlingGameManager.STATE_ROLLING || this.currentState === BowlingGameManager.STATE_SETTLING) {
this.scoreText.text = "Score: " + this.score + "/10";
return;
}
}

private isBallIdle(): boolean {
const v = this.ballRigidBody.velocity;
const a = this.ballRigidBody.angularVelocity;
const v2 = v.x * v.x + v.y * v.y + v.z * v.z;
const a2 = a.x * a.x + a.y * a.y + a.z * a.z;
return v2 < this.settleVelocityThreshold * this.settleVelocityThreshold && a2 < this.settleAngularThreshold * this.settleAngularThreshold;
}

private startCharging(): void {
this.touchActive = true;
this.chargeSeconds = 0;
this.currentState = BowlingGameManager.STATE_CHARGING;
this.updateHud();
}

private onTouch(event: APJS.IEvent): void {
if (!this.initialized) return;
const touch = event.args[0] as APJS.TouchData;

if (touch.phase === APJS.TouchPhase.Began && APJS.TouchUtils.isScreenPointOnImage(touch.position, this.resetButtonImage)) {
this.resetRound();
return;
}

if (this.currentState === BowlingGameManager.STATE_ROLLING || this.currentState === BowlingGameManager.STATE_SETTLING) {
return;
}

if (touch.phase === APJS.TouchPhase.Began) {
if (this.currentState === BowlingGameManager.STATE_RESULT) {
return;
}
if (this.currentState === BowlingGameManager.STATE_READY) {
this.lockedAimX = this.ballRigidBody.position.x;
this.startCharging();
return;
}
}

if ((touch.phase === APJS.TouchPhase.Ended || touch.phase === APJS.TouchPhase.Canceled) && this.touchActive && this.currentState === BowlingGameManager.STATE_CHARGING) {
this.touchActive = false;
this.launchBall();
}
}

private launchBall(): void {
const ratio = Math.max(0, Math.min(1, this.chargeSeconds / this.maxChargeSeconds));
const speed = this.minLaunchSpeed + (this.maxLaunchSpeed - this.minLaunchSpeed) * ratio;
const spinSpeed = speed * BowlingGameManager.BALL_SPIN_FACTOR;

if (!this.pinsActive) {
this.setPinsActive(true);
}

this.ballRigidBody.velocity = new APJS.Vector3f(0, 0, -speed);
this.ballRigidBody.angularVelocity = new APJS.Vector3f(spinSpeed, 0, 0);
this.ballRigidBody.useGravity = true;

this.logBallAndHeadPin("launch speed=" + speed.toFixed(3) + " aimX=" + this.lockedAimX.toFixed(3));
this.currentState = BowlingGameManager.STATE_ROLLING;
this.roundElapsed = 0;
this.ballStillFrames = 0;
this.updateHud();
}

private isPinTilted(index: number): boolean {
const up = this.pinTransforms[index].getWorldRotation().multiplyVector(new APJS.Vector3f(0, 0, -1));
return up.y < this.fallenDotThreshold;
}

private maybeAssistPins(): void {
for (let i = 0; i < this.pinRigidBodies.length; i++) {
if (this.pinAssistApplied[i] || this.pinCounted[i] || this.isPinTilted(i)) continue;

const rb = this.pinRigidBodies[i];
if (rb.static) continue;

const velocity = rb.velocity;
const horizontalSpeed2 = velocity.x * velocity.x + velocity.z * velocity.z;
if (horizontalSpeed2 < 0.08) continue;

const horizontalSpeed = Math.sqrt(horizontalSpeed2);
const invLen = 1 / horizontalSpeed;
const dirX = velocity.x * invLen;
const dirZ = velocity.z * invLen;
const liftBias = 0.18;
const impulse = Math.max(3.6, Math.min(7.2, horizontalSpeed * 4.8));
const pinPos = this.pinTransforms[i].getWorldPosition();
const pushForce = new APJS.Vector3f(dirX * impulse * 0.35, impulse * liftBias, dirZ * impulse * 0.55);
const pushPoint = new APJS.Vector3f(pinPos.x, pinPos.y + 0.18, pinPos.z);
rb.addForceAt(pushForce, pushPoint, false, APJS.ForceMode3D.Impulse);
rb.addTorque(new APJS.Vector3f(dirZ * impulse * 0.22, 0, -dirX * impulse * 0.22), APJS.ForceMode3D.Impulse);
this.pinAssistApplied[i] = true;
}
}

private updatePinScoreState(): void {
let counted = 0;
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
const isTilted = this.isPinTilted(i);
const v = rb.velocity;
const a = rb.angularVelocity;
const v2 = v.x * v.x + v.y * v.y + v.z * v.z;
const a2 = a.x * a.x + a.y * a.y + a.z * a.z;
const isLowMotion = v2 < this.settleVelocityThreshold * this.settleVelocityThreshold && a2 < this.settleAngularThreshold * this.settleAngularThreshold;

if (isLowMotion) {
this.pinLowMotionFrames[i] += 1;
} else {
this.pinLowMotionFrames[i] = 0;
}

if (!this.pinCounted[i] && isTilted) {
this.pinCounted[i] = true;
}
if (this.pinCounted[i]) counted++;
}

if (counted !== this.score) {
this.score = counted;
this.updateHud();
}
}

private freezeQuietPinsAfterBallPass(ballZ: number): void {
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
if (rb.static) continue;
if (this.pinLowMotionFrames[i] < BowlingGameManager.PIN_FAST_SLEEP_FRAMES) continue;

if (this.pinCounted[i]) {
this.freezePin(i);
continue;
}

const pinZ = this.pinTransforms[i].getWorldPosition().z;
const ballPassedPin = ballZ <= pinZ - BowlingGameManager.PIN_SLEEP_CLEARANCE_Z;
if (ballPassedPin && !this.isPinTilted(i)) {
this.freezePin(i);
}
}
}

private freezeQuietPinsNow(): void {
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
if (rb.static) continue;
if (this.pinLowMotionFrames[i] >= BowlingGameManager.PIN_FAST_SLEEP_FRAMES) {
this.freezePin(i);
}
}
}

private updateAimPingPong(dt: number): void {
const minX = this.ballStartPosition.x - this.ballStartXClamp;
const maxX = this.ballStartPosition.x + this.ballStartXClamp;
let targetX = this.ballRigidBody.position.x + this.aimPingPongSpeed * this.aimDirection * dt;

if (targetX >= maxX) {
targetX = maxX;
this.aimDirection = -1;
} else if (targetX <= minX) {
targetX = minX;
this.aimDirection = 1;
}

this.resetRigidBody(this.ballRigidBody, new APJS.Vector3f(targetX, this.ballStartPosition.y, this.ballStartPosition.z), this.ballStartRotation, false);
}

private updateRollingState(dt: number): void {
this.roundElapsed += dt;

const ballPos = this.ballTransform.getWorldPosition();
if (!this.debugApproachLogged && ballPos.z <= -1.1) {
this.debugApproachLogged = true;
this.logBallAndHeadPin("approach");
}
if (!this.debugPastLogged && ballPos.z <= -1.95) {
this.debugPastLogged = true;
this.logBallAndHeadPin("past");
}

if (!this.pinsActive && ballPos.z <= this.pinActivationZ) {
this.setPinsActive(true);
}

this.updatePinGravityDelay();
//this.maybeAssistPins();

if (this.pinsActive) {
this.updatePinScoreState();
this.freezeQuietPinsAfterBallPass(ballPos.z);
}

if (this.isBallIdle()) {
this.ballStillFrames += 1;
} else {
this.ballStillFrames = 0;
}

if (this.ballStillFrames >= this.ballSettleFrames || ballPos.z < -2.8 || this.roundElapsed >= this.roundTimeoutSeconds) {
if (!this.pinsActive) {
this.setPinsActive(true);
}
this.currentState = BowlingGameManager.STATE_SETTLING;
this.ballStillFrames = 0;
this.updateHud();
}
}

private updateSettlingState(dt: number): void {
this.roundElapsed += dt;
this.updatePinGravityDelay();
//this.maybeAssistPins();
this.updatePinScoreState();
this.freezeQuietPinsNow();

let allPinsQuiet = true;
for (let i = 0; i < this.pinRigidBodies.length; i++) {
if (this.pinLowMotionFrames[i] < this.pinSettleFrames) {
allPinsQuiet = false;
break;
}
}

if (!this.debugResultLogged && (allPinsQuiet || this.roundElapsed >= this.roundTimeoutSeconds + 1.5)) {
this.debugResultLogged = true;
this.logBallAndHeadPin("result");
}

if (allPinsQuiet || this.roundElapsed >= this.roundTimeoutSeconds + 1.5) {
this.currentState = BowlingGameManager.STATE_RESULT;
this.updatePinScoreState();
this.updateHud();
}
}

private updateResultState(): void {
this.updatePinScoreState();
this.freezeQuietPinsNow();
}

onUpdate(dt: number): void {
if (!this.initialized) {
this.initOnce();
return;
}

if (this.pinResetFramesRemaining > 0) {
this.forcePinsToStartPose();
this.pinResetFramesRemaining -= 1;
}

if (this.currentState === BowlingGameManager.STATE_READY) {
this.updateAimPingPong(dt);
if (this.pinsActive) {
this.setPinsActive(false);
}
return;
}

if (this.currentState === BowlingGameManager.STATE_CHARGING) {
this.chargeSeconds += dt;
if (this.chargeSeconds > this.maxChargeSeconds) {
this.chargeSeconds = this.maxChargeSeconds;
}
this.resetRigidBody(this.ballRigidBody, new APJS.Vector3f(this.lockedAimX, this.ballStartPosition.y, this.ballStartPosition.z), this.ballStartRotation, false);
if (this.pinsActive) {
this.setPinsActive(false);
}
this.updateHud();
return;
}

if (this.currentState === BowlingGameManager.STATE_ROLLING) {
this.updateRollingState(dt);
return;
}

if (this.currentState === BowlingGameManager.STATE_SETTLING) {
this.updateSettlingState(dt);
return;
}

if (this.currentState === BowlingGameManager.STATE_RESULT) {
this.updateResultState();
}
}

onDestroy(): void {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.Touch, this.touchCallback, this);
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.onRecordStart, this);
}
}

Example 2 — 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);
}
}
Copyright © 2026 TikTok. All rights reserved.
About TikTokHelp CenterCareersContactLegalTerms of ServicePrivacy PolicyCookies