Skip to content

Commit

Permalink
v1.1.5 performance (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
reececomo authored Apr 26, 2024
1 parent f6d3f0a commit c748555
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 119 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pixijs-actions",
"version": "1.1.4",
"version": "1.1.5",
"author": "Reece Como <[email protected]>",
"authors": [
"Reece Como <[email protected]>",
Expand Down
16 changes: 2 additions & 14 deletions src/DisplayObject.mixin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { _ as Action } from "./Action";
import { ActionTicker } from "./lib/ActionTicker";
import { getSpeed } from "./lib/utils/displayobject";

//
// ----- DisplayObject Mixin: -----
Expand Down Expand Up @@ -28,19 +27,8 @@ export function registerDisplayObjectMixin(displayObject: any): void {
ActionTicker.runAction(key, this, action);
};

prototype.runAsPromise = function (
action: Action,
timeoutBufferMs: number = 100
): Promise<void> {
const node = this;
return new Promise(function (resolve, reject) {
const timeLimitMs = timeoutBufferMs + (getSpeed(node) * action.duration * 1_000);
const timeoutCheck = setTimeout(() => reject('Took too long to complete.'), timeLimitMs);
node.run(action, () => {
clearTimeout(timeoutCheck);
resolve();
});
});
prototype.runAsPromise = function (action: Action): Promise<void> {
return new Promise(resolve => this.run(action, () => resolve()));
};

prototype.action = function (forKey: string): Action | undefined {
Expand Down
73 changes: 73 additions & 0 deletions src/__tests__/Action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,79 @@ describe('DefaultTimingMode static properties', () => {
});
});

describe('keyed actions', () => {
it('allows actions to be checked/retrieved', () => {
const action = Action.sequence([
Action.moveByX(5, 5.0),
Action.moveByX(-5, 5.0),
]);

const node = new Container();
node.runWithKey(action, 'myKey');

simulateTime(5.0);
expect(node.position.x).toBeCloseTo(5.0);

expect(node.action('myKey')).toBe(action);
expect(node.hasActions()).toBe(true);

simulateTime(5.0);
expect(node.position.x).toBeCloseTo(0.0);

expect(node.action('myKey')).toBeUndefined();
expect(node.hasActions()).toBe(false);
});

it('replaces identical keyed actions on run()', () => {
const action = Action.sequence([
Action.moveByX(5, 5.0),
Action.moveByX(-5, 5.0),
]);

const node = new Container();
node.runWithKey(action, 'myKey');

simulateTime(5.0);
expect(node.position.x).toBeCloseTo(5.0);

expect(node.action('myKey')).toBe(action);
expect(node.hasActions()).toBe(true);

// Run again
node.runWithKey(action, 'myKey');

simulateTime(5.0);
expect(node.position.x).toBeCloseTo(10.0);

expect(node.action('myKey')).toBe(action);
expect(node.hasActions()).toBe(true);

simulateTime(5.0);
expect(node.position.x).toBeCloseTo(5.0);

expect(node.action('myKey')).toBeUndefined();
expect(node.hasActions()).toBe(false);
});

it('does not replace non-identical actions on run()', () => {
const action = Action.sequence([
Action.moveByX(5, 5.0),
Action.moveByX(-5, 5.0),
]);

const node = new Container();
node.runWithKey(action, 'myKey');
node.runWithKey(action, 'myKey');
node.run(action);
node.run(action);

simulateTime(5.0);
expect(node.position.x).toBeCloseTo(15.0); // 3/4 should run.

node.removeAllActions();
});
});

describe('Action Chaining', () => {
describe('sequence()', () => {
it('complete all steps in order', () => {
Expand Down
168 changes: 94 additions & 74 deletions src/lib/ActionTicker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Action } from "./Action";
import { TimingModeFn } from "../TimingMode";
import { getIsPaused, getSpeed } from "./utils/displayobject";

const EPSILON = 0.0000000001;
const EPSILON_ONE = 1 - EPSILON;
Expand All @@ -12,112 +11,116 @@ const EPSILON_ONE = 1 - EPSILON;
export class ActionTicker {

/** All currently executing actions. */
protected static _running: ActionTicker[] = [];
protected static _tickers: Map<TargetNode, Map<string | ActionTicker, ActionTicker>> = new Map();

//
// ----- Static Methods: -----
// ----- Global ticker: -----
//

/**
* Tick all actions forward.
*
* @param deltaTimeMs Delta time given in milliseconds.
* @param categoryMask (Optional) Bitmask to filter which categories of actions to update.
* @param onErrorHandler (Optional) Handler errors from each action's tick.
*/
public static tickAll(
deltaTimeMs: number,
categoryMask: number | undefined = undefined,
onErrorHandler?: (error: any) => void
): void {
const deltaTime = deltaTimeMs * 0.001;

for (const [target, tickers] of this._tickers.entries()) {
const [isPaused, speed] = _getTargetState(target);

if (isPaused || speed <= 0) {
continue;
}

for (const actionTicker of tickers.values()) {
if (categoryMask !== undefined && (categoryMask & actionTicker.action.categoryMask) === 0) {
continue;
}

try {
actionTicker.tick(deltaTime * speed);
}
catch (error) {
// Isolate individual action errors.
if (onErrorHandler === undefined) {
console.error('Action failed with error: ', error);
}
else {
onErrorHandler(error);
}

// Remove offending ticker.
this._removeActionTicker(actionTicker);
}
}
}
}

//
// ----- Ticker Management: -----
//

/** Adds an action to the list of actions executed by the node. */
public static runAction(key: string | undefined, target: TargetNode, action: Action): void {
if (key !== undefined) {
// Stop any existing, identical-keyed actions on insert.
ActionTicker.removeTargetActionForKey(target, key);
if (!this._tickers.has(target)) {
this._tickers.set(target, new Map());
}

this._running.push(new ActionTicker(key, target, action));
const actionTicker = new ActionTicker(key, target, action);

// Replaces any existing, identical-keyed actions on insert.
this._tickers.get(target).set(key ?? actionTicker, actionTicker);
}

/** Whether a target has any actions. */
public static hasTargetActions(target: TargetNode): boolean {
return ActionTicker._running.find(at => at.target === target) !== undefined;
return this._tickers.has(target);
}

/** Retrieve an action with a key from a specific target. */
public static getTargetActionForKey(target: TargetNode, key: string): Action | undefined {
return this._getTargetActionTickerForKey(target, key)?.action;
return this._tickers.get(target)?.get(key)?.action;
}

/** Remove an action with a key from a specific target. */
public static removeTargetActionForKey(target: TargetNode, key: string): void {
const actionTicker = this._getTargetActionTickerForKey(target, key);

if (!actionTicker) {
return;
const actionTicker = this._tickers.get(target)?.get(key);
if (actionTicker) {
this._removeActionTicker(actionTicker);
}

ActionTicker._removeActionTicker(actionTicker);
}

/** Remove all actions for a specific target. */
public static removeAllTargetActions(target: TargetNode): void {
for (let i = ActionTicker._running.length - 1; i >= 0; i--) {
const actionTicker = ActionTicker._running[i];

if (actionTicker.target === target) {
ActionTicker._removeActionTicker(actionTicker);
}
}
this._tickers.delete(target);
}

//
// ----- Internal helpers: -----
//

/**
* Tick all actions forward.
* Remove an action ticker for a target.
*
* @param deltaTimeMs Delta time given in milliseconds.
* @param categoryMask (Optional) Bitmask to filter which categories of actions to update.
* @param onErrorHandler (Optional) Handler errors from each action's tick.
* This cleans up any references to target too.
*/
public static tickAll(
deltaTimeMs: number,
categoryMask: number | undefined = undefined,
onErrorHandler?: (error: any) => void
): void {
const deltaTime = deltaTimeMs * 0.001;

for (let i = ActionTicker._running.length - 1; i >= 0; i--) {
const actionTicker = ActionTicker._running[i];

if (categoryMask !== undefined && (categoryMask & actionTicker.action.categoryMask) === 0) {
continue;
}

if (getIsPaused(actionTicker.target)) {
continue;
}

try {
actionTicker.tick(deltaTime * getSpeed(actionTicker.target));
}
catch (error) {
// Isolate individual action errors.
if (onErrorHandler === undefined) {
console.error('Action failed with error: ', error);
}
else {
onErrorHandler(error);
}

// Remove offending ticker.
ActionTicker._removeActionTicker(actionTicker);
}
protected static _removeActionTicker(actionTicker: ActionTicker): void {
const tickers = this._tickers.get(actionTicker.target);
if (tickers === undefined) {
return; // No change.
}
}

/** Retrieve the ticker for an action with a key from a specific target. */
protected static _getTargetActionTickerForKey(
target: TargetNode,
key: string
): ActionTicker | undefined {
return ActionTicker._running.find(a => a.target === target && a.key === key);
}
tickers.delete(actionTicker.key ?? actionTicker);

/** Remove an action ticker for a target. */
protected static _removeActionTicker(actionTicker: ActionTicker): ActionTicker {
const index = ActionTicker._running.indexOf(actionTicker);
if (index >= 0) {
ActionTicker._running.splice(index, 1);
if (tickers.size === 0) {
this._tickers.delete(actionTicker.target);
}
return actionTicker;
}

//
Expand Down Expand Up @@ -250,7 +253,7 @@ export class ActionTicker {
return;
}

const scaledTimeDelta = deltaTime * this.speed /* target speed is applied at the root */;
const scaledTimeDelta = deltaTime * this.speed;

if (this.scaledDuration === 0) {
// Instantaneous action.
Expand Down Expand Up @@ -297,3 +300,20 @@ export class ActionTicker {
(this.action as any).onTickerDidReset(this);
}
}

/**
* Get the global action processing state of a descendent target.
*/
function _getTargetState(target: TargetNode): [isPaused: boolean, speed: number] {
let leaf = target;
let isPaused = leaf.isPaused;
let speed = leaf.speed;

while (!isPaused && leaf.parent != null) {
isPaused = leaf.parent.isPaused;
speed *= leaf.parent.speed;
leaf = leaf.parent;
}

return [isPaused, speed];
}
28 changes: 0 additions & 28 deletions src/lib/utils/displayobject.ts

This file was deleted.

0 comments on commit c748555

Please sign in to comment.