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