diff --git a/cocos/tween/actions/action-instant.ts b/cocos/tween/actions/action-instant.ts index 69f4fe81a18..f0cbaf2bb60 100644 --- a/cocos/tween/actions/action-instant.ts +++ b/cocos/tween/actions/action-instant.ts @@ -72,7 +72,7 @@ export abstract class ActionInstant extends FiniteTimeAction { */ export class Show extends ActionInstant { update (_dt: number): void { - const target = (this.workerTarget ?? this.target) as T; + const target = this._getWorkerTarget(); if (!target) return; const _renderComps = target.getComponentsInChildren(Renderer); for (let i = 0; i < _renderComps.length; ++i) { @@ -112,7 +112,7 @@ export function show (): Show { */ export class Hide extends ActionInstant { update (_dt: number): void { - const target = (this.workerTarget ?? this.target) as T; + const target = this._getWorkerTarget(); if (!target) return; const _renderComps = target.getComponentsInChildren(Renderer); for (let i = 0; i < _renderComps.length; ++i) { @@ -152,7 +152,7 @@ export function hide (): Hide { */ export class ToggleVisibility extends ActionInstant { update (_dt: number): void { - const target = (this.workerTarget ?? this.target) as T; + const target = this._getWorkerTarget(); if (!target) return; const _renderComps = target.getComponentsInChildren(Renderer); for (let i = 0; i < _renderComps.length; ++i) { @@ -204,7 +204,7 @@ export class RemoveSelf extends ActionInstant { } update (_dt: number): void { - const target = (this.workerTarget ?? this.target) as T; + const target = this._getWorkerTarget(); if (!target) return; target.removeFromParent(); if (this._isNeedCleanUp) { @@ -302,7 +302,7 @@ export class CallFunc extends ActionInstant { */ execute (): void { if (this._callback) { - const target = (this.workerTarget ?? this.target) as Target; + const target = this._getWorkerTarget() as Target; this._callback.call(this._callbackThis, target, this._data); } } diff --git a/cocos/tween/actions/action-interval.ts b/cocos/tween/actions/action-interval.ts index ca63c9f3bd7..d3c4bbed8f9 100644 --- a/cocos/tween/actions/action-interval.ts +++ b/cocos/tween/actions/action-interval.ts @@ -28,7 +28,7 @@ import { FiniteTimeAction } from './action'; import { macro, logID, errorID } from '../../core'; import { ActionInstant } from './action-instant'; -import type { TweenUpdateCallback } from '../tween'; +import type { Tween, TweenUpdateCallback } from '../tween'; // Extra action for making a Sequence or Spawn when only adding one action to it. class DummyAction extends FiniteTimeAction { @@ -351,16 +351,20 @@ export class Sequence extends ActionInterval { return action; } - updateWorkerTarget (workerTarget: T): void { + updateOwner (owner: Tween): void { if (this._actions.length < 2) { return; } - this._actions[1].workerTarget = workerTarget; const actionOne = this._actions[0]; + const actionTwo = this._actions[1]; + + if (!actionTwo._owner) { + actionTwo._owner = owner; + } if (actionOne instanceof Sequence || actionOne instanceof Spawn) { - actionOne.updateWorkerTarget(workerTarget); - } else { - actionOne.workerTarget = workerTarget; + actionOne.updateOwner(owner); + } else if (!actionOne._owner) { // action's owner should never be changed, so only set owner when it's not set yet. + actionOne._owner = owner; } } @@ -833,16 +837,18 @@ export class Spawn extends ActionInterval { return this; } - updateWorkerTarget (workerTarget: T): void { + updateOwner (owner: Tween): void { if (!this._one || !this._two) { return; } - this._two.workerTarget = workerTarget; + if (!this._two._owner) { + this._two._owner = owner; + } const one = this._one; if (one instanceof Spawn || one instanceof Sequence) { - one.updateWorkerTarget(workerTarget); - } else { - one.workerTarget = workerTarget; + one.updateOwner(owner); + } else if (!one._owner) { // action's owner should never be changed, so only set owner when it's not set yet. + one._owner = owner; } } diff --git a/cocos/tween/actions/action.ts b/cocos/tween/actions/action.ts index b7dbcbcbc87..1695bf6ed34 100644 --- a/cocos/tween/actions/action.ts +++ b/cocos/tween/actions/action.ts @@ -25,6 +25,8 @@ THE SOFTWARE. */ +import type { Tween } from '../tween'; + export enum ActionEnum { /** * @en Default Action tag. @@ -54,44 +56,9 @@ export abstract class Action { protected target: unknown = null; /** - * The `workerTarget` was added from Cocos Creator 3.8.4 and it's used for nest `Tween` functionality. - * It stores the target of sub-tween and its value may be different from `target`. - * - * Example 1: - * ```ts - * tween(node).to(1, { scale: new Vec3(2, 2, 2) }).start(); - * // target and original target are both `node`, workerTarget is `null`. - * ``` - * - * Example 2: - * ```ts - * tween(node).parallel( // ----- Root tween - * tween(node).to(1, { scale: new Vec3(2, 2, 2) }), // ----- Sub tween 1 - * tween(node).to(1, { position: new Vec3(10, 10, 10) }) // ----- Sub Tween 2 - * ).start(); - * // Note that only root tween is started here. We call tweens in `parallel`/`sequence` sub tweens. - * // The `target` and `originalTarget` of all internal actions are `node`. - * // Actions in root tween: workerTarget = null - * // Actions in sub tween 1: workerTarget = node - * // Actions in sub tween 2: workerTarget = node - * ``` - * - * Example 3: - * ```ts - * tween(node).parallel( // ----- Root tween - * tween(node).to(1, { scale: new Vec3(2, 2, 2) }), // ----- Sub tween 1 - * tween(node.getComponent(UITransform)).to(1, { // ----- Sub Tween 2 - * contentSize: new Size(10, 10) - * }) - * ).start(); - * // Note that only root tween is started here. We call tweens in `parallel`/`sequence` sub tweens. - * // The `target` and `originalTarget` of all internal actions are `node`. - * // Actions in root tween: workerTarget = null - * // Actions in sub tween 1: workerTarget = node - * // Actions in sub tween 2: workerTarget = node's UITransform component - * ``` + * The tween who owns this action. */ - public workerTarget: unknown = null; + public _owner: Tween | null = null; protected tag = ActionEnum.TAG_INVALID; @@ -177,6 +144,48 @@ export abstract class Action { this.originalTarget = originalTarget; } + /** + * Return the worker target of the current action applys on. + * + * Example 1: + * ```ts + * tween(node).to(1, { scale: new Vec3(2, 2, 2) }).start(); + * // target and original target are both `node`, _getWorkerTarget returns `null`. + * ``` + * + * Example 2: + * ```ts + * tween(node).parallel( // ----- Root tween + * tween(node).to(1, { scale: new Vec3(2, 2, 2) }), // ----- Sub tween 1 + * tween(node).to(1, { position: new Vec3(10, 10, 10) }) // ----- Sub Tween 2 + * ).start(); + * // Note that only root tween is started here. We call tweens in `parallel`/`sequence` sub tweens. + * // The `target` and `originalTarget` of all internal actions are `node`. + * // Actions in root tween: _getWorkerTarget returns `node`, + * // Actions in sub tween 1: _getWorkerTarget returns `node`, + * // Actions in sub tween 2: _getWorkerTarget returns `node`. + * ``` + * + * Example 3: + * ```ts + * tween(node).parallel( // ----- Root tween + * tween(node).to(1, { scale: new Vec3(2, 2, 2) }), // ----- Sub tween 1 + * tween(node.getComponent(UITransform)).to(1, { // ----- Sub Tween 2 + * contentSize: new Size(10, 10) + * }) + * ).start(); + * // Note that only root tween is started here. We call tweens in `parallel`/`sequence` sub tweens. + * // The `target` and `originalTarget` of all internal actions are `node`. + * // Actions in root tween: workerTarget = `node`, + * // Actions in sub tween 1: workerTarget = `node`, + * // Actions in sub tween 2: workerTarget = `node`'s UITransform component. + * ``` + */ + protected _getWorkerTarget (): T | null { + const workerTarget: T | null = this._owner?.getTarget(); + return (workerTarget ?? this.target) as T; + } + /** * @en get tag number. * @zh 获取用于识别动作的标签。 diff --git a/cocos/tween/tween-action.ts b/cocos/tween/tween-action.ts index 4d38913f865..f549ffd58c0 100644 --- a/cocos/tween/tween-action.ts +++ b/cocos/tween/tween-action.ts @@ -205,7 +205,7 @@ export class TweenAction extends ActionInterval { clone (): TweenAction { const action = new TweenAction(this._duration, this._originProps, this._opts); action._reversed = this._reversed; - action.workerTarget = this.workerTarget; + action._owner = this._owner; action._id = this._id; this._cloneDecoration(action); return action; @@ -220,7 +220,7 @@ export class TweenAction extends ActionInterval { const action = new TweenAction(this._duration, this._originProps, this._opts); this._cloneDecoration(action); action._reversed = !this._reversed; - action.workerTarget = this.workerTarget; + action._owner = this._owner; return action; } @@ -229,7 +229,7 @@ export class TweenAction extends ActionInterval { if (!isEqual) return; super.startWithTarget(target); - const workerTarget = (this.workerTarget ?? this.target) as T; + const workerTarget = this._getWorkerTarget(); if (!workerTarget) return; const relative = !!this._opts.relative; const props = this._props; @@ -355,7 +355,7 @@ export class TweenAction extends ActionInterval { } update (t: number): void { - const workerTarget = (this.workerTarget ?? this.target) as T; + const workerTarget = this._getWorkerTarget(); if (!workerTarget) return; if (!this._opts) return; diff --git a/cocos/tween/tween.ts b/cocos/tween/tween.ts index a97101cce5f..3bd5f5c9328 100644 --- a/cocos/tween/tween.ts +++ b/cocos/tween/tween.ts @@ -233,7 +233,7 @@ export class Tween { if (action) { reversedAction = action.reverse(); - reversedAction.workerTarget = t._target; + reversedAction._owner = t; } else { warnID(16391, `${actionId}`); } @@ -259,17 +259,17 @@ export class Tween { */ private insertAction (other: FiniteTimeAction): Tween { const action = other.clone(); - this.updateWorkerTargetForAction(action); + this.updateOwnerForAction(action); this._actions.push(action); return this; } - private updateWorkerTargetForAction (action: Action | null): void { + private updateOwnerForAction (action: Action | null): void { if (!action) return; if (action instanceof Sequence || action instanceof Spawn) { - action.updateWorkerTarget(this._target); - } else { - action.workerTarget = this._target; + action.updateOwner(this); + } else if (!action._owner) { // action's owner should never be changed, so only set owner when it's not set yet. + action._owner = this; } } @@ -284,12 +284,6 @@ export class Tween { */ target (target: U): Tween { (this as unknown as Tween)._target = target; - - for (let i = 0, len = this._actions.length; i < len; ++i) { - const action = this._actions[i]; - this.updateWorkerTargetForAction(action); - } - return this as unknown as Tween; } @@ -834,12 +828,12 @@ export class Tween { TweenSystem.instance.ActionManager.resumeTarget(target); } - private _union (updateWorkerTarget: boolean): Sequence | null { + private _union (needUpdateOwner: boolean): Sequence | null { const actions = this._actions; if (actions.length === 0) return null; const action = sequence(actions); - if (updateWorkerTarget) { - this.updateWorkerTargetForAction(action); + if (needUpdateOwner) { + this.updateOwnerForAction(action); } return action; } @@ -857,29 +851,33 @@ export class Tween { return action; } - private static readonly _tmp_args: FiniteTimeAction[] = []; + private static readonly _tmpArgs: FiniteTimeAction[] = []; private static _tweenToActions (args: Tween[]): void { - const tmp_args = Tween._tmp_args; - tmp_args.length = 0; + const tmpArgs = Tween._tmpArgs; + tmpArgs.length = 0; for (let l = args.length, i = 0; i < l; i++) { const t = args[i]; const action = t._union(true); if (action) { action.setSpeed(t._timeScale); - tmp_args.push(action); + tmpArgs.push(action); } } } private static _wrappedSequence (args: Tween[]): Sequence | null { Tween._tweenToActions(args); - return sequence(Tween._tmp_args); + const ret = sequence(Tween._tmpArgs); + this._tmpArgs.length = 0; + return ret; } private static _wrappedParallel (args: Tween[]): Spawn | null { Tween._tweenToActions(args); - return spawn(Tween._tmp_args); + const ret = spawn(Tween._tmpArgs); + this._tmpArgs.length = 0; + return ret; } } legacyCC.Tween = Tween; diff --git a/tests/tween/tween.test.ts b/tests/tween/tween.test.ts index 323b5a06953..8459964d3a1 100644 --- a/tests/tween/tween.test.ts +++ b/tests/tween/tween.test.ts @@ -6,8 +6,7 @@ import { game, director } from "../../cocos/game"; import { UITransform } from "../../cocos/2d/framework/ui-transform"; import { Canvas } from "../../cocos/2d/framework/canvas"; import { Batcher2D } from "../../cocos/2d/renderer/batcher-2d"; -import { Label, UIOpacity } from "../../cocos/2d"; -import { Sprite } from "../../cocos/2d"; +import { UIOpacity, Sprite } from "../../cocos/2d"; function isSizeEqualTo(a: Size, b: Size) { return approx(a.width, b.width) && approx(a.height, b.height); @@ -239,6 +238,136 @@ test('Test different target in sequence', function() { director.unregisterSystem(sys); }); +test('Test different target in sequence nesting parallel', function() { + const sys = new TweenSystem(); + (TweenSystem.instance as any) = sys; + director.registerSystem(TweenSystem.ID, sys, System.Priority.MEDIUM); + + const scene = new Scene('test'); + director.runSceneImmediate(scene); + + const node = new Node('TestNode'); + const uiOpacity = node.addComponent(UIOpacity) as UIOpacity; + node.parent = scene; + + tween(node) + .sequence( + tween(node) + .to(1, {scale: v3(2, 2, 2)}), + tween(node) + .parallel( + tween(uiOpacity) + .to(1, { opacity: 0 }), + tween(node) + .to(1, { scale: v3(1, 1, 1) }) + ) + ) + .start(); + + runFrames(1); // Kick off + + runFrames(30); + expect(node.scale.equals(new Vec3(1.5, 1.5, 1.5))).toBeTruthy(); + expect(uiOpacity.opacity).toBeCloseTo(255); + runFrames(30); + expect(node.scale.equals(new Vec3(2, 2, 2))).toBeTruthy(); + runFrames(30); + expect(node.scale.equals(new Vec3(1.5, 1.5, 1.5))).toBeTruthy(); + expect(uiOpacity.opacity).toBeCloseTo(255/2); + runFrames(30); + expect(node.scale.equals(new Vec3(1, 1, 1))).toBeTruthy(); + expect(uiOpacity.opacity).toBeCloseTo(0); + + // test end + director.unregisterSystem(sys); +}); + +test('Test different target in parallel nesting sequence', function() { + const sys = new TweenSystem(); + (TweenSystem.instance as any) = sys; + director.registerSystem(TweenSystem.ID, sys, System.Priority.MEDIUM); + + const scene = new Scene('test'); + director.runSceneImmediate(scene); + + const node = new Node('TestNode'); + const uiOpacity = node.addComponent(UIOpacity) as UIOpacity; + node.parent = scene; + + tween(node) + .parallel( + tween(node).sequence( + tween(node) + .to(1, { scale: v3(2, 2, 2) }), + tween(node) + .to(1, { position: v3(100, 100, 0) }) + ), + tween(uiOpacity) + .to(2, { opacity: 0 }), + ) + .start(); + + runFrames(1); // Kick off + + runFrames(30); + expect(node.scale.equals(new Vec3(1.5, 1.5, 1.5))).toBeTruthy(); + expect(node.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + runFrames(30); + expect(node.scale.equals(new Vec3(2, 2, 2))).toBeTruthy(); + expect(node.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + + expect(uiOpacity.opacity).toBeCloseTo(255/2); + + runFrames(60); + expect(node.scale.equals(new Vec3(2, 2, 2))).toBeTruthy(); + expect(node.position.equals(new Vec3(100, 100, 0))).toBeTruthy(); + expect(uiOpacity.opacity).toBeCloseTo(0); + + // test end + director.unregisterSystem(sys); +}); + +test('Test different target in sequence nesting parallel and re-target', function() { + const sys = new TweenSystem(); + (TweenSystem.instance as any) = sys; + director.registerSystem(TweenSystem.ID, sys, System.Priority.MEDIUM); + + const scene = new Scene('test'); + director.runSceneImmediate(scene); + + const node1 = new Node('TestNode1'); + node1.parent = scene; + + const node2 = new Node('TestNode2'); + node2.parent = scene; + + const node3 = new Node('TestNode3'); + node3.parent = scene; + + tween(node1) + .sequence( + tween(node1) + .to(1, { position: v3(100, 100, 0) }).target(node3), + ) + .target(node2) + .to(1, { position: v3(200, 200, 200) }) + .start(); + + runFrames(1); // Kick off + runFrames(60); + expect(node1.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + expect(node2.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + expect(node3.position.equals(new Vec3(100, 100, 0))).toBeTruthy(); + + runFrames(60); + expect(node1.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + expect(node2.position.equals(new Vec3(200, 200, 200))).toBeTruthy(); + expect(node3.position.equals(new Vec3(100, 100, 0))).toBeTruthy(); + + // test end + director.unregisterSystem(sys); +}); + test('Test different target in then', function() { // @ts-expect-error director.root!._batcher = new Batcher2D(director.root!); @@ -4811,4 +4940,4 @@ test('parallel with two call tween', function () { expect(cb2).toBeCalledTimes(1); director.unregisterSystem(sys); -}); \ No newline at end of file +});