HaxeFlixel/flixel

View on GitHub
flixel/group/FlxSpriteGroup.hx

Summary

Maintainability
Test Coverage
package flixel.group;

import openfl.display.BitmapData;
import openfl.display.BlendMode;
import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFrame;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.group.FlxGroup;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.FlxAssets;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
import flixel.util.FlxSort;

/**
 * `FlxSpriteGroup` is a special `FlxSprite` that can be treated like a single sprite even if it's
 * made up of several member sprites. It shares the `FlxGroup` API, but it doesn't inherit from it.
 * Note that `FlxSpriteContainer` also exists.
 * 
 * ## When to use a group or container
 * `FlxGroups` are better for organising arbitrary groups for things like iterating or collision.
 * `FlxContainers` are recommended when you are adding them to the current `FlxState`, or a
 * child (or grandchild, and so on) of the state.
 * Since `FlxSpriteGroups` and `FlxSpriteContainers` are usually meant to draw groups of sprites
 * rather than organizing them for collision or iterating, it's recommended to always use
 * `FlxSpriteContainer` instead of `FlxSpriteGroup`.
 */
typedef FlxSpriteGroup = FlxTypedSpriteGroup<FlxSprite>;

/**
 * A `FlxSpriteGroup` that only allows specific members to be a specific type of `FlxSprite`.
 * To use any kind of `FlxSprite` use `FlxSpriteGroup`, which is an alias for
 * `FlxTypedSpriteGroup<FlxSprite>`.
 */
class FlxTypedSpriteGroup<T:FlxSprite> extends FlxSprite
{
    /**
     * The actual group which holds all sprites.
     */
    public var group(default, set):FlxTypedGroup<T>;

    /**
     * The link to a group's `members` array.
     */
    public var members(get, never):Array<T>;

    /**
     * The number of entries in the members array. For performance and safety you should check this
     * variable instead of `members.length` unless you really know what you're doing!
     */
    public var length(get, never):Int;

    /**
     * Whether to attempt to preserve the ratio of alpha values of group members, or set them directly through
     * the alpha property. Defaults to `false` (preservation).
     * @since 4.5.0
     */
    public var directAlpha:Bool = false;

    /**
     * The maximum capacity of this group. Default is `0`, meaning no max capacity, and the group can just grow.
     */
    public var maxSize(get, set):Int;

    /**
     * Optimization to allow setting position of group without transforming children twice.
     */
    var _skipTransformChildren:Bool = false;

    /**
     * Array of all the `FlxSprite`s that exist in this group for
     * optimization purposes / static typing on cpp targets.
     */
    @:deprecated("_sprites is deprecated, use group.members")
    var _sprites(get, never):Array<FlxSprite>;

    /**
     * @param   X         The initial X position of the group.
     * @param   Y         The initial Y position of the group.
     * @param   MaxSize   Maximum amount of members allowed.
     */
    public function new(x = 0.0, y = 0.0, maxSize = 0)
    {
        initGroup(maxSize);
        super(x, y);
    }
    
    function initGroup(maxSize:Int):Void
    {
        group = new FlxTypedGroup<T>(maxSize);
    }
    
    /**
     * This method is used for initialization of variables of complex types.
     * Don't forget to call `super.initVars()` if you'll override this method,
     * or you'll get `null` object error and app will crash.
     */
    override function initVars():Void
    {
        flixelType = SPRITEGROUP;

        offset = new FlxCallbackPoint(offsetCallback);
        origin = new FlxCallbackPoint(originCallback);
        scale = new FlxCallbackPoint(scaleCallback);
        scrollFactor = new FlxCallbackPoint(scrollFactorCallback);

        scale.set(1, 1);
        scrollFactor.set(1, 1);

        initMotionVars();
    }

    /**
     * **WARNING:** A destroyed `FlxBasic` can't be used anymore.
     * It may even cause crashes if it is still part of a group or state.
     * You may want to use `kill()` instead if you want to disable the object temporarily only and `revive()` it later.
     *
     * This function is usually not called manually (Flixel calls it automatically during state switches for all `add()`ed objects).
     *
     * Override this function to `null` out variables manually or call `destroy()` on class members if necessary.
     * Don't forget to call `super.destroy()`!
     */
    override public function destroy():Void
    {
        // normally don't have to destroy FlxPoints, but these are FlxCallbackPoints!
        offset = FlxDestroyUtil.destroy(offset);
        origin = FlxDestroyUtil.destroy(origin);
        scale = FlxDestroyUtil.destroy(scale);
        scrollFactor = FlxDestroyUtil.destroy(scrollFactor);

        @:bypassAccessor
        group = FlxDestroyUtil.destroy(group);

        super.destroy();
    }

    /**
     * Recursive cloning method: it will create a copy of this group which will hold copies of all sprites
     *
     * @return  copy of this sprite group
     */
    override public function clone():FlxTypedSpriteGroup<T>
    {
        var newGroup = new FlxTypedSpriteGroup<T>(x, y, maxSize);
        for (sprite in group.members)
        {
            if (sprite != null)
            {
                newGroup.add(cast sprite.clone());
            }
        }
        return newGroup;
    }

    /**
     * Check and see if any sprite in this group is currently on screen.
     *
     * @param   Camera   Specify which game camera you want. If `null`, it will just grab the first global camera.
     * @return  Whether the object is on screen or not.
     */
    override public function isOnScreen(?Camera:FlxCamera):Bool
    {
        for (sprite in group.members)
        {
            if (sprite != null && sprite.exists && sprite.visible && sprite.isOnScreen(Camera))
                return true;
        }

        return false;
    }

    /**
     * Checks to see if a point in 2D world space overlaps any `FlxSprite` object from this group.
     *
     * @param   Point           The point in world space you want to check.
     * @param   InScreenSpace   Whether to take scroll factors into account when checking for overlap.
     * @param   Camera          Specify which game camera you want. If `null`, it will just grab the first global camera.
     * @return  Whether or not the point overlaps this group.
     */
    override public function overlapsPoint(point:FlxPoint, InScreenSpace:Bool = false, ?Camera:FlxCamera):Bool
    {
        var result:Bool = false;
        for (sprite in group.members)
        {
            if (sprite != null && sprite.exists && sprite.visible)
            {
                result = result || sprite.overlapsPoint(point, InScreenSpace, Camera);
            }
        }

        return result;
    }

    /**
     * Checks to see if a point in 2D world space overlaps any of FlxSprite object's current displayed pixels.
     * This check is ALWAYS made in screen space, and always takes scroll factors into account.
     *
     * @param   Point    The point in world space you want to check.
     * @param   Mask     Used in the pixel hit test to determine what counts as solid.
     * @param   Camera   Specify which game camera you want.  If `null`, it will just grab the first global camera.
     * @return  Whether or not the point overlaps this object.
     */
    override public function pixelsOverlapPoint(point:FlxPoint, Mask:Int = 0xFF, ?Camera:FlxCamera):Bool
    {
        var result:Bool = false;
        for (sprite in group.members)
        {
            if (sprite != null && sprite.exists && sprite.visible)
            {
                result = result || sprite.pixelsOverlapPoint(point, Mask, Camera);
            }
        }

        return result;
    }

    override public function update(elapsed:Float):Void
    {
        group.update(elapsed);

        if (path != null && path.active)
            path.update(elapsed);

        if (moves)
            updateMotion(elapsed);
    }

    override public function draw():Void
    {
        group.draw();

        #if FLX_DEBUG
        if (FlxG.debugger.drawDebug)
            drawDebug();
        #end
    }

    /**
     * Replaces all pixels with specified `Color` with `NewColor` pixels.
     * WARNING: very expensive (especially on big graphics) as it iterates over every single pixel.
     *
     * @param   Color            Color to replace
     * @param   NewColor         New color
     * @param   FetchPositions   Whether we need to store positions of pixels which colors were replaced.
     * @return  `Array` with replaced pixels positions
     */
    override public function replaceColor(Color:Int, NewColor:Int, FetchPositions:Bool = false):Array<FlxPoint>
    {
        var positions:Array<FlxPoint> = null;
        if (FetchPositions)
        {
            positions = new Array<FlxPoint>();
        }

        var spritePositions:Array<FlxPoint>;
        for (sprite in group.members)
        {
            if (sprite != null)
            {
                spritePositions = sprite.replaceColor(Color, NewColor, FetchPositions);
                if (FetchPositions)
                {
                    positions = positions.concat(spritePositions);
                }
            }
        }

        return positions;
    }

    /**
     * Adds a new `FlxSprite` subclass to the group.
     *
     * @param   Sprite   The sprite or sprite group you want to add to the group.
     * @return  The same object that was passed in.
     */
    public function add(Sprite:T):T
    {
        preAdd(Sprite);
        return group.add(Sprite);
    }

    /**
     * Inserts a new `FlxSprite` subclass to the group at the specified position.
     *
     * @param   Position The position that the new sprite or sprite group should be inserted at.
     * @param   Sprite   The sprite or sprite group you want to insert into the group.
     * @return  The same object that was passed in.
     *
     * @since 4.3.0
     */
    public function insert(Position:Int, Sprite:T):T
    {
        preAdd(Sprite);
        return group.insert(Position, Sprite);
    }

    /**
     * Adjusts the position and other properties of the soon-to-be child of this sprite group.
     * Private helper to avoid duplicate code in `add()` and `insert()`.
     *
     * @param    Sprite    The sprite or sprite group that is about to be added or inserted into the group.
     */
    function preAdd(sprite:T):Void
    {
        sprite.x += x;
        sprite.y += y;
        sprite.alpha *= alpha;
        sprite.scrollFactor.copyFrom(scrollFactor);
        sprite.cameras = _cameras; // _cameras instead of cameras because get_cameras() will not return null

        if (clipRect != null)
            clipRectTransform(sprite, clipRect);
    }

    /**
     * Recycling is designed to help you reuse game objects without always re-allocating or "newing" them.
     * It behaves differently depending on whether `maxSize` equals `0` or is bigger than `0`.
     *
     * `maxSize > 0` / "rotating-recycling" (used by `FlxEmitter`):
     *   - at capacity:  returns the next object in line, no matter its properties like `alive`, `exists` etc.
     *   - otherwise:    returns a new object.
     *
     * `maxSize == 0` / "grow-style-recycling"
     *   - tries to find the first object with `exists == false`
     *   - otherwise: adds a new object to the `members` array
     *
     * WARNING: If this function needs to create a new object, and no object class was provided,
     * it will return `null` instead of a valid object!
     *
     * @param   ObjectClass     The class type you want to recycle (e.g. `FlxSprite`, `EvilRobot`, etc).
     * @param   ObjectFactory   Optional factory function to create a new object
     *                          if there aren't any dead members to recycle.
     *                          If `null`, `Type.createInstance()` is used,
     *                          which requires the class to have no constructor parameters.
     * @param   Force           Force the object to be an `ObjectClass` and not a super class of `ObjectClass`.
     * @param   Revive          Whether recycled members should automatically be revived
     *                          (by calling `revive()` on them).
     * @return  A reference to the object that was created.
     */
    public inline function recycle(?ObjectClass:Class<T>, ?ObjectFactory:Void->T, Force:Bool = false, Revive:Bool = true):T
    {
        return group.recycle(ObjectClass, ObjectFactory, Force, Revive);
    }

    /**
     * Removes the specified sprite from the group.
     *
     * @param   sprite  The `FlxSprite` you want to remove.
     * @param   splice  Whether the object should be cut from the array entirely or not.
     * @return  The removed sprite.
     */
    public function remove(sprite:T, splice = false):T
    {
        sprite.x -= x;
        sprite.y -= y;
        // alpha
        sprite.cameras = null;
        return group.remove(sprite, splice);
    }

    /**
     * Replaces an existing `FlxSprite` with a new one.
     *
     * @param   oldObject  The sprite you want to replace.
     * @param   newObject  The new object you want to use instead.
     * @return  The new sprite.
     */
    public inline function replace(oldObject:T, newObject:T):T
    {
        preAdd(newObject);
        return group.replace(oldObject, newObject);
    }

    /**
     * Call this function to sort the group according to a particular value and order.
     * For example, to sort game objects for Zelda-style overlaps you might call
     * `group.sort(FlxSort.byY, FlxSort.ASCENDING)` at the bottom of your `FlxState#update()` override.
     *
     * @param   Function   The sorting function to use - you can use one of the premade ones in
     *                     `FlxSort` or write your own using `FlxSort.byValues()` as a "backend".
     * @param   Order      A constant that defines the sort order.
     *                     Possible values are `FlxSort.ASCENDING` (default) and `FlxSort.DESCENDING`.
     */
    public inline function sort(Function:Int->T->T->Int, Order:Int = FlxSort.ASCENDING):Void
    {
        group.sort(Function, Order);
    }

    /**
     * Call this function to retrieve the first object with `exists == false` in the group.
     * This is handy for recycling in general, e.g. respawning enemies.
     *
     * @param   ObjectClass   An optional parameter that lets you narrow the
     *                        results to instances of this particular class.
     * @param   Force         Force the object to be an `ObjectClass` and not a super class of `ObjectClass`.
     * @return  A `FlxSprite` currently flagged as not existing.
     */
    public inline function getFirstAvailable(?ObjectClass:Class<T>, Force:Bool = false):T
    {
        return group.getFirstAvailable(ObjectClass, Force);
    }

    /**
     * Call this function to retrieve the first index set to `null`.
     * Returns `-1` if no index stores a `null` object.
     *
     * @return  An `Int` indicating the first `null` slot in the group.
     */
    public inline function getFirstNull():Int
    {
        return group.getFirstNull();
    }

    /**
     * Call this function to retrieve the first object with `exists == true` in the group.
     * This is handy for checking if everything's wiped out, or choosing a squad leader, etc.
     *
     * @return  A `FlxSprite` currently flagged as existing.
     */
    public inline function getFirstExisting():T
    {
        return group.getFirstExisting();
    }

    /**
     * Call this function to retrieve the first object with `dead == false` in the group.
     * This is handy for checking if everything's wiped out, or choosing a squad leader, etc.
     *
     * @return  A `FlxSprite` currently flagged as not dead.
     */
    public inline function getFirstAlive():T
    {
        return group.getFirstAlive();
    }

    /**
     * Call this function to retrieve the first object with `dead == true` in the group.
     * This is handy for checking if everything's wiped out, or choosing a squad leader, etc.
     *
     * @return  A `FlxSprite` currently flagged as dead.
     */
    public inline function getFirstDead():T
    {
        return group.getFirstDead();
    }

    /**
     * Call this function to find out how many members of the group are not dead.
     *
     * @return  The number of `FlxSprite`s flagged as not dead. Returns `-1` if group is empty.
     */
    public inline function countLiving():Int
    {
        return group.countLiving();
    }

    /**
     * Call this function to find out how many members of the group are dead.
     *
     * @return  The number of `FlxSprite`s flagged as dead. Returns `-1` if group is empty.
     */
    public inline function countDead():Int
    {
        return group.countDead();
    }

    /**
     * Returns a member at random from the group.
     *
     * @param   StartIndex  Optional offset off the front of the array.
     *                      Default value is `0`, or the beginning of the array.
     * @param   Length      Optional restriction on the number of values you want to randomly select from.
     * @return  A `FlxSprite` from the `members` list.
     */
    public inline function getRandom(StartIndex:Int = 0, Length:Int = 0):T
    {
        return group.getRandom(StartIndex, Length);
    }

    /**
     * Iterate through every member
     *
     * @return An iterator
     */
    public inline function iterator(?filter:T->Bool):FlxTypedGroupIterator<T>
    {
        return new FlxTypedGroupIterator<T>(members, filter);
    }

    /**
     * Applies a function to all members.
     *
     * @param   Function   A function that modifies one element at a time.
     * @param   Recurse    Whether or not to apply the function to members of subgroups as well.
     */
    public inline function forEach(Function:T->Void, Recurse:Bool = false):Void
    {
        group.forEach(Function, Recurse);
    }

    /**
     * Applies a function to all `alive` members.
     *
     * @param   Function   A function that modifies one element at a time.
     * @param   Recurse    Whether or not to apply the function to members of subgroups as well.
     */
    public inline function forEachAlive(Function:T->Void, Recurse:Bool = false):Void
    {
        group.forEachAlive(Function, Recurse);
    }

    /**
     * Applies a function to all dead members.
     *
     * @param   Function   A function that modifies one element at a time.
     * @param   Recurse    Whether or not to apply the function to members of subgroups as well.
     */
    public inline function forEachDead(Function:T->Void, Recurse:Bool = false):Void
    {
        group.forEachDead(Function, Recurse);
    }

    /**
     * Applies a function to all existing members.
     *
     * @param   Function   A function that modifies one element at a time.
     * @param   Recurse    Whether or not to apply the function to members of subgroups as well.
     */
    public inline function forEachExists(Function:T->Void, Recurse:Bool = false):Void
    {
        group.forEachExists(Function, Recurse);
    }

    /**
     * Applies a function to all members of type `Class<K>`.
     *
     * @param   ObjectClass   A class that objects will be checked against before Function is applied, ex: `FlxSprite`.
     * @param   Function      A function that modifies one element at a time.
     * @param   Recurse       Whether or not to apply the function to members of subgroups as well.
     */
    public inline function forEachOfType<K>(ObjectClass:Class<K>, Function:K->Void, Recurse:Bool = false)
    {
        group.forEachOfType(ObjectClass, Function, Recurse);
    }

    /**
     * Remove all instances of `FlxSprite` from the list.
     * WARNING: does not `destroy()` or `kill()` any of these objects!
     */
    public inline function clear():Void
    {
        group.clear();
    }

    /**
     * Calls `kill()` on the group's members and then on the group itself.
     * You can revive this group later via `revive()` after this.
     */
    override public function kill():Void
    {
        _skipTransformChildren = true;
        super.kill();
        _skipTransformChildren = false;
        group.kill();
    }

    /**
     * Revives the group.
     */
    override public function revive():Void
    {
        _skipTransformChildren = true;
        super.revive(); // calls set_exists and set_alive
        _skipTransformChildren = false;
        group.revive();
    }

    override public function reset(X:Float, Y:Float):Void
    {
        for (sprite in group.members)
        {
            if (sprite != null)
                sprite.reset(sprite.x + X - x, sprite.y + Y - y);
        }
        
        // prevent any transformations on children, mainly from setter overrides
        _skipTransformChildren = true;
        
        // recreate super.reset() but call super.revive instead of revive
        touching = NONE;
        wasTouching = NONE;
        x = X;
        y = Y;
        // last.set(x, y); // null on sprite groups
        velocity.set();
        super.revive();
        
        _skipTransformChildren = false;
    }

    /**
     * Helper function to set the coordinates of this object.
     * Handy since it only requires one line of code.
     *
     * @param   X   The new x position
     * @param   Y   The new y position
     */
    override public function setPosition(X:Float = 0, Y:Float = 0):Void
    {
        // Transform children by the movement delta
        var dx:Float = X - x;
        var dy:Float = Y - y;
        multiTransformChildren([xTransform, yTransform], [dx, dy]);

        // don't transform children twice
        _skipTransformChildren = true;
        x = X; // this calls set_x
        y = Y; // this calls set_y
        _skipTransformChildren = false;
    }

    /**
     * Handy function that allows you to quickly transform one property of sprites in this group at a time.
     *
     * @param   Function   Function to transform the sprites. Example:
     *                     `function(sprite, v:Dynamic) { s.acceleration.x = v; s.makeGraphic(10,10,0xFF000000); }`
     * @param   Value      Value which will passed to lambda function.
     */
    @:generic
    public function transformChildren<V>(Function:T->V->Void, Value:V):Void
    {
        if (_skipTransformChildren || group == null)
            return;

        for (sprite in group.members)
        {
            if (sprite != null)
                Function(cast sprite, Value);
        }
    }

    /**
     * Handy function that allows you to quickly transform multiple properties of sprites in this group at a time.
     *
     * @param   FunctionArray   `Array` of functions to transform sprites in this group.
     * @param   ValueArray      `Array` of values which will be passed to lambda functions
     */
    @:generic
    public function multiTransformChildren<V>(FunctionArray:Array<T->V->Void>, ValueArray:Array<V>):Void
    {
        if (_skipTransformChildren || group == null)
            return;

        var numProps:Int = FunctionArray.length;
        if (numProps > ValueArray.length)
            return;

        var lambda:T->V->Void;
        for (sprite in group.members)
        {
            if ((sprite != null) && sprite.exists)
            {
                for (i in 0...numProps)
                {
                    lambda = FunctionArray[i];
                    lambda(cast sprite, ValueArray[i]);
                }
            }
        }
    }

    // PROPERTIES GETTERS/SETTERS

    override function set_camera(Value:FlxCamera):FlxCamera
    {
        if (camera != Value)
            transformChildren(cameraTransform, Value);
        return super.set_camera(Value);
    }

    override function set_cameras(Value:Array<FlxCamera>):Array<FlxCamera>
    {
        if (_cameras != Value)
            transformChildren(camerasTransform, Value);
        return super.set_cameras(Value);
    }

    override function set_exists(Value:Bool):Bool
    {
        if (exists != Value)
            transformChildren(existsTransform, Value);
        return super.set_exists(Value);
    }

    override function set_visible(Value:Bool):Bool
    {
        if (exists && visible != Value)
            transformChildren(visibleTransform, Value);
        return super.set_visible(Value);
    }

    override function set_active(Value:Bool):Bool
    {
        if (exists && active != Value)
            transformChildren(activeTransform, Value);
        return super.set_active(Value);
    }

    override function set_alive(Value:Bool):Bool
    {
        if (alive != Value)
            transformChildren(aliveTransform, Value);
        return super.set_alive(Value);
    }

    override function set_x(Value:Float):Float
    {
        if (exists && x != Value)
            transformChildren(xTransform, Value - x);// offset
        return x = Value;
    }

    override function set_y(Value:Float):Float
    {
        if (exists && y != Value)
            transformChildren(yTransform, Value - y);// offset
        return y = Value;
    }

    override function set_angle(Value:Float):Float
    {
        if (exists && angle != Value)
            transformChildren(angleTransform, Value - angle);// offset
        return angle = Value;
    }

    override function set_alpha(Value:Float):Float
    {
        Value = FlxMath.bound(Value, 0, 1);

        if (exists && alpha != Value)
        {
            var factor:Float = (alpha > 0) ? Value / alpha : 0;
            if (!directAlpha && alpha != 0)
                transformChildren(alphaTransform, factor);
            else
                transformChildren(directAlphaTransform, Value);
        }
        return alpha = Value;
    }

    override function set_facing(Value:Int):Int
    {
        if (exists && facing != Value)
            transformChildren(facingTransform, Value);
        return facing = Value;
    }

    override function set_flipX(Value:Bool):Bool
    {
        if (exists && flipX != Value)
            transformChildren(flipXTransform, Value);
        return flipX = Value;
    }

    override function set_flipY(Value:Bool):Bool
    {
        if (exists && flipY != Value)
            transformChildren(flipYTransform, Value);
        return flipY = Value;
    }

    override function set_moves(Value:Bool):Bool
    {
        if (exists && moves != Value)
            transformChildren(movesTransform, Value);
        return moves = Value;
    }

    override function set_immovable(Value:Bool):Bool
    {
        if (exists && immovable != Value)
            transformChildren(immovableTransform, Value);
        return immovable = Value;
    }

    override function set_solid(Value:Bool):Bool
    {
        if (exists && solid != Value)
            transformChildren(solidTransform, Value);
        return super.set_solid(Value);
    }

    override function set_color(Value:Int):Int
    {
        if (exists && color != Value)
            transformChildren(gColorTransform, Value);
        return color = Value;
    }

    override function set_blend(Value:BlendMode):BlendMode
    {
        if (exists && blend != Value)
            transformChildren(blendTransform, Value);
        return blend = Value;
    }

    override function set_clipRect(rect:FlxRect):FlxRect
    {
        if (exists)
            transformChildren(clipRectTransform, rect);
        return super.set_clipRect(rect);
    }

    override function set_pixelPerfectRender(Value:Bool):Bool
    {
        if (exists && pixelPerfectRender != Value)
            transformChildren(pixelPerfectTransform, Value);
        return super.set_pixelPerfectRender(Value);
    }

    /**
     * This functionality isn't supported in SpriteGroup
     */
    override function set_width(Value:Float):Float
    {
        return Value;
    }

    override function get_width():Float
    {
        if (length == 0)
            return 0;
        
        return findMaxXHelper() - findMinXHelper();
    }

    /**
     * Returns the left-most position of the left-most member.
     * If there are no members, x is returned.
     * 
     * @since 5.0.0
     */
    public function findMinX()
    {
        return length == 0 ? x : findMinXHelper();
    }
    
    function findMinXHelper()
    {
        var value = Math.POSITIVE_INFINITY;
        for (member in group.members)
        {
            if (member == null)
                continue;
            
            var minX:Float;
            if (member.flixelType == SPRITEGROUP)
                minX = (cast member:FlxSpriteGroup).findMinX();
            else
                minX = member.x;
            
            if (minX < value)
                value = minX;
        }
        return value;
    }
    
    /**
     * Returns the right-most position of the right-most member.
     * If there are no members, x is returned.
     * 
     * @since 5.0.0
     */
    public function findMaxX()
    {
        return length == 0 ? x : findMaxXHelper();
    }
    
    function findMaxXHelper()
    {
        var value = Math.NEGATIVE_INFINITY;
        for (member in group.members)
        {
            if (member == null)
                continue;
            
            var maxX:Float;
            if (member.flixelType == SPRITEGROUP)
                maxX = (cast member:FlxSpriteGroup).findMaxX();
            else
                maxX = member.x + member.width;
            
            if (maxX > value)
                value = maxX;
        }
        return value;
    }
    
    /**
     * This functionality isn't supported in SpriteGroup
     */
    override function set_height(Value:Float):Float
    {
        return Value;
    }

    override function get_height():Float
    {
        if (length == 0)
            return 0;
        
        return findMaxYHelper() - findMinYHelper();
    }
    
    /**
     * Returns the top-most position of the top-most member.
     * If there are no members, y is returned.
     * 
     * @since 5.0.0
     */
    public function findMinY()
    {
        return length == 0 ? y : findMinYHelper();
    }
    
    function findMinYHelper()
    {
        var value = Math.POSITIVE_INFINITY;
        for (member in group.members)
        {
            if (member == null)
                continue;
            
            var minY:Float;
            if (member.flixelType == SPRITEGROUP)
                minY = (cast member:FlxSpriteGroup).findMinY();
            else
                minY = member.y;
            
            if (minY < value)
                value = minY;
        }
        return value;
    }
    
    /**
     * Returns the top-most position of the top-most member.
     * If there are no members, y is returned.
     * 
     * @since 5.0.0
     */
    public function findMaxY()
    {
        return length == 0 ? y : findMaxYHelper();
    }
    
    function findMaxYHelper()
    {
        var value = Math.NEGATIVE_INFINITY;
        for (member in group.members)
        {
            if (member == null)
                continue;
            
            var maxY:Float;
            if (member.flixelType == SPRITEGROUP)
                maxY = (cast member:FlxSpriteGroup).findMaxY();
            else
                maxY = member.y + member.height;
            
            if (maxY > value)
                value = maxY;
        }
        return value;
    }

    // GROUP FUNCTIONS

    inline function get_length():Int
    {
        return group.length;
    }

    inline function get_maxSize():Int
    {
        return group.maxSize;
    }

    inline function set_maxSize(Size:Int):Int
    {
        return group.maxSize = Size;
    }

    inline function get_members():Array<T>
    {
        return group.members;
    }

    // TRANSFORM FUNCTIONS - STATIC TYPING

    inline function xTransform(Sprite:FlxSprite, X:Float)
        Sprite.x += X; // addition

    inline function yTransform(Sprite:FlxSprite, Y:Float)
        Sprite.y += Y; // addition

    inline function angleTransform(Sprite:FlxSprite, Angle:Float)
        Sprite.angle += Angle; // addition

    inline function alphaTransform(Sprite:FlxSprite, Alpha:Float)
    {
        if (Sprite.alpha != 0 || Alpha == 0)
            Sprite.alpha *= Alpha; // multiplication
        else
            Sprite.alpha = 1 / Alpha; // direct set to avoid stuck sprites
    }

    inline function directAlphaTransform(Sprite:FlxSprite, Alpha:Float)
        Sprite.alpha = Alpha; // direct set

    inline function facingTransform(Sprite:FlxSprite, Facing:Int)
        Sprite.facing = Facing;

    inline function flipXTransform(Sprite:FlxSprite, FlipX:Bool)
        Sprite.flipX = FlipX;

    inline function flipYTransform(Sprite:FlxSprite, FlipY:Bool)
        Sprite.flipY = FlipY;

    inline function movesTransform(Sprite:FlxSprite, Moves:Bool)
        Sprite.moves = Moves;

    inline function pixelPerfectTransform(Sprite:FlxSprite, PixelPerfect:Bool)
        Sprite.pixelPerfectRender = PixelPerfect;

    inline function gColorTransform(Sprite:FlxSprite, Color:Int)
        Sprite.color = Color;

    inline function blendTransform(Sprite:FlxSprite, Blend:BlendMode)
        Sprite.blend = Blend;

    inline function immovableTransform(Sprite:FlxSprite, Immovable:Bool)
        Sprite.immovable = Immovable;

    inline function visibleTransform(Sprite:FlxSprite, Visible:Bool)
        Sprite.visible = Visible;

    inline function activeTransform(Sprite:FlxSprite, Active:Bool)
        Sprite.active = Active;

    inline function solidTransform(Sprite:FlxSprite, Solid:Bool)
        Sprite.solid = Solid;

    inline function aliveTransform(Sprite:FlxSprite, Alive:Bool)
        Sprite.alive = Alive;

    inline function existsTransform(Sprite:FlxSprite, Exists:Bool)
        Sprite.exists = Exists;

    inline function cameraTransform(Sprite:FlxSprite, Camera:FlxCamera)
        Sprite.camera = Camera;

    inline function camerasTransform(Sprite:FlxSprite, Cameras:Array<FlxCamera>)
        Sprite.cameras = Cameras;

    inline function offsetTransform(Sprite:FlxSprite, Offset:FlxPoint)
        Sprite.offset.copyFrom(Offset);

    inline function originTransform(Sprite:FlxSprite, Origin:FlxPoint)
        Sprite.origin.copyFrom(Origin);

    inline function scaleTransform(Sprite:FlxSprite, Scale:FlxPoint)
        Sprite.scale.copyFrom(Scale);

    inline function scrollFactorTransform(Sprite:FlxSprite, ScrollFactor:FlxPoint)
        Sprite.scrollFactor.copyFrom(ScrollFactor);

    inline function clipRectTransform(Sprite:FlxSprite, ClipRect:FlxRect)
    {
        if (ClipRect == null)
            Sprite.clipRect = null;
        else
            Sprite.clipRect = FlxRect.get(ClipRect.x - Sprite.x + x, ClipRect.y - Sprite.y + y, ClipRect.width, ClipRect.height);
    }

    // Functions for the FlxCallbackPoint
    inline function offsetCallback(Offset:FlxPoint)
        transformChildren(offsetTransform, Offset);

    inline function originCallback(Origin:FlxPoint)
        transformChildren(originTransform, Origin);

    inline function scaleCallback(Scale:FlxPoint)
        transformChildren(scaleTransform, Scale);

    inline function scrollFactorCallback(ScrollFactor:FlxPoint)
        transformChildren(scrollFactorTransform, ScrollFactor);

    // NON-SUPPORTED FUNCTIONALITY
    // THESE METHODS ARE OVERRIDDEN FOR SAFETY PURPOSES

    /**
     * This functionality isn't supported in SpriteGroup
     * @return this sprite group
     */
    override public function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite
    {
        #if FLX_DEBUG
        throw "This function is not supported in FlxSpriteGroup";
        #end
        return this;
    }

    /**
     * This functionality isn't supported in SpriteGroup
     * @return this sprite group
     */
    override public function loadGraphic(Graphic:FlxGraphicAsset, Animated:Bool = false, Width:Int = 0, Height:Int = 0, Unique:Bool = false,
            ?Key:String):FlxSprite
    {
        return this;
    }

    /**
     * This functionality isn't supported in SpriteGroup
     * @return this sprite group
     */
    override public function loadRotatedGraphic(Graphic:FlxGraphicAsset, Rotations:Int = 16, Frame:Int = -1, AntiAliasing:Bool = false,
            AutoBuffer:Bool = false, ?Key:String):FlxSprite
    {
        #if FLX_DEBUG
        throw "This function is not supported in FlxSpriteGroup";
        #end
        return this;
    }

    /**
     * This functionality isn't supported in SpriteGroup
     * @return this sprite group
     */
    override public function makeGraphic(Width:Int, Height:Int, Color:Int = FlxColor.WHITE, Unique:Bool = false, ?Key:String):FlxSprite
    {
        #if FLX_DEBUG
        throw "This function is not supported in FlxSpriteGroup";
        #end
        return this;
    }

    override function set_pixels(Value:BitmapData):BitmapData
    {
        return Value;
    }

    override function set_frame(Value:FlxFrame):FlxFrame
    {
        return Value;
    }

    override function get_pixels():BitmapData
    {
        return null;
    }
    
    inline function get__sprites():Array<FlxSprite>
    {
        return cast group.members;
    }
    
    function set_group(value:FlxTypedGroup<T>):FlxTypedGroup<T>
    {
        return this.group = value;
    }
    
    /**
     * Internal function to update the current animation frame.
     *
     * @param    RunOnCpp    Whether the frame should also be recalculated if we're on a non-flash target
     */
    override inline function calcFrame(RunOnCpp:Bool = false):Void
    {
        // Nothing to do here
    }

    /**
     * This functionality isn't supported in SpriteGroup
     */
    override inline function resetHelpers():Void {}

    /**
     * This functionality isn't supported in SpriteGroup
     */
    override public inline function stamp(Brush:FlxSprite, X:Int = 0, Y:Int = 0):Void {}

    override function set_frames(Frames:FlxFramesCollection):FlxFramesCollection
    {
        return Frames;
    }

    /**
     * This functionality isn't supported in SpriteGroup
     */
    override inline function updateColorTransform():Void {}
}