diff --git a/flixel/tweens/FlxTween.hx b/flixel/tweens/FlxTween.hx index 6abc12cadc..647f5b84ca 100644 --- a/flixel/tweens/FlxTween.hx +++ b/flixel/tweens/FlxTween.hx @@ -307,6 +307,49 @@ class FlxTween implements IFlxDestroyable return globalManager.quadPath(Object, Points, DurationOrSpeed, UseDuration, Options); } + /** + * Cancels all related tweens on the specified object. + * + * Note: Any tweens with the specified fields are cancelled, if the tween has other properties they + * will also be cancelled. + * + * @param Object The object with tweens to cancel. + * @param FieldPaths Optional list of the tween field paths to search for. If null or empty, all tweens on the specified + * object are canceled. Allows dot paths to check child properties. + * + * @since 4.9.0 + */ + public static function cancelTweensOf(Object:Dynamic, ?FieldPaths:Array):Void + { + globalManager.cancelTweensOf(Object, FieldPaths); + } + + /** + * Immediately updates all tweens on the specified object with the specified fields that + * are not looping (type `FlxTween.LOOPING` or `FlxTween.PINGPONG`) and `active` through + * their endings, triggering their `onComplete` callbacks. + * + * Note: if they haven't yet begun, this will first trigger their `onStart` callback. + * + * Note: their `onComplete` callbacks are triggered in the next frame. + * To trigger them immediately, call `FlxTween.globalManager.update(0);` after this function. + * + * In no case should it trigger an `onUpdate` callback. + * + * Note: Any tweens with the specified fields are completed, if the tween has other properties they + * will also be completed. + * + * @param Object The object with tweens to complete. + * @param FieldPaths Optional list of the tween field paths to search for. If null or empty, all tweens on + * the specified object are completed. Allows dot paths to check child properties. + * + * @since 4.9.0 + */ + public static function completeTweensOf(Object:Dynamic, ?FieldPaths:Array):Void + { + globalManager.completeTweensOf(Object, FieldPaths); + } + /** * The manager to which this tween belongs * @since 4.2.0 @@ -622,6 +665,19 @@ class FlxTween implements IFlxDestroyable } } + /** + * Returns true if this is tweening the specified field on the specified object. + * + * @param Object The object. + * @param Field Optional tween field. Ignored if null. + * + * @since 4.9.0 + */ + function isTweenOf(Object:Dynamic, ?Field:String):Bool + { + return false; + } + /** * Set both type of delays for this tween. * @@ -1120,6 +1176,118 @@ class FlxTweenManager extends FlxBasic _tweens.splice(0, _tweens.length); } + /** + * Cancels all related tweens on the specified object. + * + * Note: Any tweens with the specified fields are cancelled, if the tween has other properties they + * will also be cancelled. + * + * @param Object The object with tweens to cancel. + * @param FieldPaths Optional list of the tween field paths to search for. If null or empty, all tweens on the specified + * object are canceled. Allows dot paths to check child properties. + * + * @since 4.9.0 + */ + public function cancelTweensOf(Object:Dynamic, ?FieldPaths:Array):Void + { + forEachTweensOf(Object, FieldPaths, function (tween) tween.cancel()); + } + + /** + * Immediately updates all tweens on the specified object with the specified fields that + * are not looping (type `FlxTween.LOOPING` or `FlxTween.PINGPONG`) and `active` through + * their endings, triggering their `onComplete` callbacks. + * + * Note: if they haven't yet begun, this will first trigger their `onStart` callback. + * + * Note: their `onComplete` callbacks are triggered in the next frame. + * To trigger them immediately, call `FlxTween.globalManager.update(0);` after this function. + * + * In no case should it trigger an `onUpdate` callback. + * + * Note: Any tweens with the specified fields are completed, if the tween has other properties they + * will also be completed. + * + * @param Object The object with tweens to complete. + * @param FieldPaths Optional list of the tween field paths to search for. If null or empty, all tweens on + * the specified object are completed. Allows dot paths to check child properties. + * + * @since 4.9.0 + */ + public function completeTweensOf(Object:Dynamic, ?FieldPaths:Array):Void + { + forEachTweensOf(Object, FieldPaths, + function (tween) + { + if ((tween.type & FlxTweenType.LOOPING) == 0 && (tween.type & FlxTweenType.PINGPONG) == 0 && tween.active) + tween.update(FlxMath.MAX_VALUE_FLOAT); + } + ); + } + + /** + * Internal helper for iterating tweens with specific parameters. + * + * Note: loops backwards to allow removals. + * + * @param Object The object with tweens you are searching for. + * @param FieldPaths Optional list of the tween field paths to check. If null or empty, any tween of the specified + * object will match. Allows dot paths to check child properties. + * @param Function The function to call on each matching tween. + * + * @since 4.9.0 + */ + function forEachTweensOf(Object:Dynamic, ?FieldPaths:Array, Function:FlxTween->Void) + { + if (Object == null) + throw "Cannot cancel tween variables of an object that is null."; + + if (FieldPaths == null || FieldPaths.length == 0) + { + var i = _tweens.length; + while (i-- > 0) + { + var tween = _tweens[i]; + if (tween.isTweenOf(Object)) + Function(tween); + } + } + else + { + // check for dot paths and convert to object/field pairs + var propertyInfos = new Array(); + for (fieldPath in FieldPaths) + { + var target = Object; + var path = fieldPath.split("."); + var field = path.pop(); + for (component in path) + { + target = Reflect.getProperty(target, component); + if (!Reflect.isObject(target)) + break; + } + + if (Reflect.isObject(target)) + propertyInfos.push({ object:target, field:field }); + } + + var i = _tweens.length; + while (i-- > 0) + { + var tween = _tweens[i]; + for (info in propertyInfos) + { + if (tween.isTweenOf(info.object, info.field)) + { + Function(tween); + break; + } + } + } + } + } + /** * Immediately updates all tweens that are not looping (type `FlxTween.LOOPING` or `FlxTween.PINGPONG`) * and `active` through their endings, triggering their `onComplete` callbacks. @@ -1127,7 +1295,7 @@ class FlxTweenManager extends FlxBasic * Note: if they haven't yet begun, this will first trigger their `onStart` callback. * * Note: their `onComplete` callbacks are triggered in the next frame. - * To trigger them immediately, call `FlxTween.manager.update(0);` after this function. + * To trigger them immediately, call `FlxTween.globalManager.update(0);` after this function. * * In no case should it trigger an `onUpdate` callback. * @@ -1152,3 +1320,9 @@ class FlxTweenManager extends FlxBasic Function(tween); } } + +private typedef TweenProperty = +{ + object:Dynamic, + field:String +} diff --git a/flixel/tweens/misc/AngleTween.hx b/flixel/tweens/misc/AngleTween.hx index 9b7369fcd4..ddd41f1718 100644 --- a/flixel/tweens/misc/AngleTween.hx +++ b/flixel/tweens/misc/AngleTween.hx @@ -59,4 +59,9 @@ class AngleTween extends FlxTween sprite.angle = spriteAngle; } } + + override function isTweenOf(object:Dynamic, ?field:String):Bool + { + return sprite == object && (field == null || field == "angle"); + } } diff --git a/flixel/tweens/misc/ColorTween.hx b/flixel/tweens/misc/ColorTween.hx index 5b282d445f..d87149e138 100644 --- a/flixel/tweens/misc/ColorTween.hx +++ b/flixel/tweens/misc/ColorTween.hx @@ -59,4 +59,9 @@ class ColorTween extends FlxTween sprite.alpha = color.alphaFloat; } } + + override function isTweenOf(object:Dynamic, ?field:String):Bool + { + return sprite == object && (field == null || field == "color"); + } } diff --git a/flixel/tweens/misc/VarTween.hx b/flixel/tweens/misc/VarTween.hx index 6a9eded786..3be9161466 100644 --- a/flixel/tweens/misc/VarTween.hx +++ b/flixel/tweens/misc/VarTween.hx @@ -37,6 +37,7 @@ class VarTween extends FlxTween _propertyInfos = []; this.duration = duration; start(); + initializeVars(); return this; } @@ -49,10 +50,9 @@ class VarTween extends FlxTween super.update(elapsed); else { - // We don't initialize() in tween() because otherwise the start values - // will be inaccurate with delays - if (_propertyInfos.length == 0) - initializeVars(); + // Wait until the delay is done to set the starting values of tweens + if (Math.isNaN(_propertyInfos[0].startValue)) + setStartValues(); super.update(elapsed); @@ -81,23 +81,31 @@ class VarTween extends FlxTween throw 'The object does not have the property "$component" in "$fieldPath"'; } - if (Reflect.getProperty(target, field) == null) - throw 'The object does not have the property "$field"'; - - var value:Dynamic = Reflect.getProperty(target, field); - if (Math.isNaN(value)) - throw 'The property "$field" is not numeric.'; - - var targetValue:Dynamic = Reflect.getProperty(_properties, fieldPath); _propertyInfos.push({ object: target, field: field, - startValue: value, - range: targetValue - value + startValue: Math.NaN, // gets set after delay + range: Reflect.getProperty(_properties, fieldPath) }); } } + function setStartValues() + { + for (info in _propertyInfos) + { + if (Reflect.getProperty(info.object, info.field) == null) + throw 'The object does not have the property "${info.field}"'; + + var value:Dynamic = Reflect.getProperty(info.object, info.field); + if (Math.isNaN(value)) + throw 'The property "${info.field}" is not numeric.'; + + info.startValue = value; + info.range = info.range - value; + } + } + override public function destroy():Void { super.destroy(); @@ -105,6 +113,17 @@ class VarTween extends FlxTween _properties = null; _propertyInfos = null; } + + override function isTweenOf(object:Dynamic, ?field:String):Bool + { + for (property in _propertyInfos) + { + if (object == property.object && (field == property.field || field == null)) + return true; + } + + return false; + } } private typedef VarTweenProperty = diff --git a/flixel/tweens/motion/Motion.hx b/flixel/tweens/motion/Motion.hx index 9ee19a7ee7..76f95cd8b6 100644 --- a/flixel/tweens/motion/Motion.hx +++ b/flixel/tweens/motion/Motion.hx @@ -54,4 +54,9 @@ class Motion extends FlxTween _object.setPosition(x, y); } } + + override function isTweenOf(object:Dynamic, ?field:String):Bool { + return _object == object + && (field == null || field == "x" || field == "y"); + } } diff --git a/tests/unit/src/flixel/tweens/FlxTweenTest.hx b/tests/unit/src/flixel/tweens/FlxTweenTest.hx index 0ccad22e16..11c1133ae5 100644 --- a/tests/unit/src/flixel/tweens/FlxTweenTest.hx +++ b/tests/unit/src/flixel/tweens/FlxTweenTest.hx @@ -231,6 +231,78 @@ class FlxTweenTest extends FlxTest Assert.isFalse(tween.finished); } + @Test // #2273 + function testCancelTweensOf() + { + var foo1 = {a: 0, b: 0}; + var tween1a = FlxTween.tween(foo1, {a: 1}, 0.1); + step(); + var tween1b = FlxTween.tween(foo1, {b: 1}, 0.1); + FlxTween.cancelTweensOf(foo1); + Assert.isTrue(tween1a.finished); + Assert.isTrue(tween1b.finished); + + var foo2 = {a: 0, b: 0}; + var tween2a = FlxTween.tween(foo2, {a: 1}, 0.1); + var tween2b = FlxTween.tween(foo2, {b: 1}, 0.1); + FlxTween.cancelTweensOf(foo2, ["a"]); + Assert.isTrue(tween2a.finished); + Assert.isFalse(tween2b.finished); + + var foo3 = {a: {f:0}, b: {f:0}, c: {f:0}}; + var tween3a = FlxTween.tween(foo3, {"a.f": 1}, 0.1); + var tween3b = FlxTween.tween(foo3, {"b.f": 1}, 0.1); + var tween3c = FlxTween.tween(foo3, {"c.f": 1}, 0.1); + FlxTween.cancelTweensOf(foo3, ["a.f"]); + FlxTween.cancelTweensOf(foo3.b, ["f"]); + Assert.isTrue(tween3a.finished); + Assert.isTrue(tween3b.finished); + Assert.isFalse(tween3c.finished); + } + + @Test // #2273 + function testCompleteTweensOf() + { + var foo1 = {a: 0, b: 0}; + var complete1a = false; + var complete1b = false; + var tween1a = FlxTween.tween(foo1, {a: 1}, 0.1, {onComplete: function(_) complete1a = true}); + step(); + var tween1b = FlxTween.tween(foo1, {b: 1}, 0.1, {onComplete: function(_) complete1b = true}); + FlxTween.completeTweensOf(foo1); + FlxTween.globalManager.update(0); + Assert.isTrue(tween1a.finished); + Assert.isTrue(complete1a, "tween1a.onComplete was expected to fire but did not"); + Assert.isTrue(tween1b.finished); + Assert.isTrue(complete1b, "tween1b.onComplete was expected to fire but did not"); + + var foo2 = {a: 0, b: 0}; + var complete2a = false; + var tween2a = FlxTween.tween(foo2, {a: 1}, 0.1, {onComplete: function(_) complete2a = true}); + var tween2b = FlxTween.tween(foo2, {b: 1}, 0.1); + FlxTween.completeTweensOf(foo2, ["a"]); + FlxTween.globalManager.update(0); + Assert.isTrue(tween2a.finished); + Assert.isTrue(complete2a, "tween2a.onComplete was expected to fire but did not"); + Assert.isFalse(tween2b.finished); + + // dot paths + var foo3 = {a: {f:0}, b: {f:0}, c: {f:0}}; + var complete3a = false; + var complete3b = false; + var tween3a = FlxTween.tween(foo3, {"a.f": 1}, 0.1, {onComplete: function(_) complete3a = true}); + var tween3b = FlxTween.tween(foo3, {"b.f": 1}, 0.1, {onComplete: function(_) complete3b = true}); + var tween3c = FlxTween.tween(foo3, {"c.f": 1}, 0.1); + FlxTween.completeTweensOf(foo3, ["a.f"]); + FlxTween.completeTweensOf(foo3.b, ["f"]); + FlxTween.globalManager.update(0); + Assert.isTrue(tween3a.finished); + Assert.isTrue(complete3a, "tween3a.onComplete was expected to fire but did not"); + Assert.isTrue(tween3b.finished); + Assert.isTrue(complete3b, "tween3b.onComplete was expected to fire but did not"); + Assert.isFalse(tween3c.finished); + } + @Test // #665 @:access(flixel.tweens.FlxTweenManager.remove) function testManipulateListInCallback()