[Solved] Can we recycle a class object as an extended class object?



  • Hi, lately, I'm optimising my game but I've run into a bump...

    Let's say that I have one class (which extends FlxSprite) named Bullet with several usefull functions in it.
    Then, depending on which fighter the player choose, he can shoot several types of projectiles, such as Boomerang, Fireball, LightningBolt... Each of this projectiles extends my Bullet class and have their own special functions and parameters in them.

    Here's a stripped down exemple of how I used to do this:

    public function fireBullet(fighter:Fighter, power:Int, dir:Int = 0, type:String):Void
    {
    	var bullet:Bullet = null;
    	switch(type)
    	{
    		case "gun":
    			sound.play("gun");
    			bullet = new Gun(fighter, type);
    
    		case "kiwave":
    			sound.play("kiwave");
    			bullet = new KiWave(fighter, type, createKiClash); // createKiClash = special callback function for kiwave
    
    		case "lightningball":
    			sound.play("electric");
    			bullet = new LightningBall(fighter, type, lightning); // same here
    	}
    	bullet.power = power;
    	// And so on ...
    }
    

    Now, instead of shooting a new Bullet each and every time, I'm trying to recycle them.
    So I've made two functions to create a pool of Bullet and retrieve the first one available:

    private function createBulletPool(fighter:Fighter, group:FlxTypedGroup<Bullet>):Void
    {
    	for (i in 0 ... 20)
    	{
    		var bullet:Bullet = new Bullet(fighter, "empty");
    		group.add(bullet);
    		bullet.kill();
    	}
    }
    
    private function recycleBullet(group:FlxTypedGroup<Bullet>):Bullet
    {
    	var bullet:Bullet = cast group.getFirstDead();
    	if (bullet == null)
    	{
    		trace ("error");
    		return null;
    	}
    	bullet.revive();
    	return bullet;
    }
    

    And now here's the problem:
    How can I say that the recycled Bullet is, for exemple, a KiWave with its own settings and callback function passed down as if it was a new one ?
    Maybe there's some kind of wizard cast that I'm not aware of? Or is this the limit of object pools?

    If there are no other solutions, I'm going to move everything inside the Bullet class and forget about extending it for other kind of projectiles. But that was very handy in order to easily create new kind of Bullet...

    Thanks for your help !


  • administrators

    You say you're recycling, but you don't actually seem to be using the recycle() method, and do things manually instead, using getFirstDead() and revive()?

    http://api.haxeflixel.com/flixel/group/FlxTypedGroup.html#recycle

    With recycle(), you can control the ObjectClass. Or even better, supply an ObjectFactorymethod - this is both type-safe (ObjectClass will use Reflection) and you have more control. You could even pass a constructor reference to it (for instance KiWave.new, see here) and then bind() some arguments to it (see here).

    This commit I recently made to flixel-demos may be interesting to you:

    https://github.com/HaxeFlixel/flixel-demos/commit/4eb3a79b1a24af3a925f3eb2e9ab030543172d74



  • Thanks @Gama11 , that's perfect !
    Now I remember that I first saw the getFirstDead / revive method in an old tutorial some years ago and that I stucked to it... It's time to update some of my games that are using that old method :)



  • Ok, so after a lot of tests, I still have some question concerning the recycling of extended objects...
    I've made a simple example out of what I'm doing in my game:

    Here's the main thing:

    package;
    import flixel.FlxG;
    import flixel.FlxState;
    import flixel.ui.FlxButton;
    import flixel.group.FlxGroup.FlxTypedGroup;
    
    class Test extends FlxState
    {
    	private var group:FlxTypedGroup<Bullet>;
    
    	override public function create():Void
    	{
    		super.create();
    
    		group = new FlxTypedGroup<Bullet>();
    //		group = new FlxTypedGroup<Bullet>(10); // With ou without limit ?
    		add(group);
    
    		createButton(20, 20, "circle");
    		createButton(20, 40, "square");
    	}
    
    	private function createButton(x:Float, y:Float, title:String):Void
    	{
    		var button:FlxButton = new FlxButton(x, y, title, shoot.bind(title));
    		add(button);
    	}
    
    	private function shoot(type:String):Void
    	{
    		var bullet:Bullet = null;
    		var force:Bool = false; // or true ?
    		switch (type)
    		{
    		case "circle":
    			bullet = group.recycle(Circle.new.bind(200, 200), force);
    		case "square":
    			bullet = group.recycle(Square.new.bind(300, 200), force);
    		}
    		bullet.fire(FlxG.random.int(0, 360));
    		trace("fire " + type + " in group (" + group.members.length + ")");
    	}
    }
    

    Here's the Bullet class:

    package;
    import flixel.FlxG;
    import flixel.FlxSprite;
    import flixel.math.FlxVelocity;
    
    class Bullet extends FlxSprite
    {
    	private var speed:Int = 0;
    
    	public function new(x:Float, y:Float)
    	{
    		super(x, y);
    	}
    
    	override public function update(elapsed:Float):Void
    	{
    		super.update(elapsed);
    		if (x < -width || x > (FlxG.width + width) || y < -height || y > (FlxG.height + height)) kill();
    	}
    
    	public function fire(_angle:Float):Void
    	{
    		angle = _angle;
    		velocity.copyFrom(FlxVelocity.velocityFromAngle(angle, speed));
    	}
    }
    

    And the Circle and Square class that extends the Bullet class:

    package;
    using flixel.util.FlxSpriteUtil;
    
    class Circle extends Bullet
    {
    	public function new(x:Float, y:Float)
    	{
    		super(x, y);
    		makeGraphic(40, 40, 0);
    		FlxSpriteUtil.drawCircle(this, 20, 20, 14, 0xFFEE1111);
    		speed = 800;
    		trace("circle");
    	}
    }
    
    package;
    using flixel.util.FlxSpriteUtil;
    
    class Square extends Bullet
    {
    	public function new(x:Float, y:Float)
    	{
    		super(x, y);
    		makeGraphic(40, 40, 0);
    		FlxSpriteUtil.drawRect(this, 0, 0, 40, 40, 0xFF00CC11);
    		speed = 600;
    		trace("square");
    	}
    }
    

    So here's my results:

    group maxSize == 0 && bullet force == false
    new is called only if recycle return null.

    group maxSize == 0 && bullet force == true
    Nothing is recycled. A new extended Bullet is created each and every time.
    Also, if I shoot a square, and then a circle, we can see the circle above the square, so there must be something that I'm missing while killing or creating the extended Bullet...

    group maxSize > 0 && bullet force == false
    It creates a new extended Bullet untill we reach maxSize, then it recycling without calling new...
    If the group have a maxSize and the bullet is recycled with Force = false, new is called only if recycle return null.

    group maxSize > 0 && bullet force == true
    Same as above.

    So I need some help, because I don't see what is wrong and why the recycling doesn't call new, even if it is called using bullet = group.recycle(Circle.new.bind(200, 200), force);



  • I don't think that's how you call recycle(). Look:

    recycle(?ObjectClass:Class<T>, ?ObjectFactory:Void‑>T, Force:Bool = false, Revive:Bool = true):T
    

    Instead, should it be

    bullet = group.recycle(Circle, Circle.new.bind(200, 200), force);
    

    It's weird that there's no compile error



  • Ok, since I had no errors, I thought that was because the ? tells that they are optional arguments and that the compiler don't care about them.
    Am I wrong? https://haxe.org/manual/types-function-optional-arguments.html

    Also, it does not fix my problem :)


  • administrators

    This post is deleted!

  • administrators

    There's two separate issues here.

    The first one is that you never reset the position of your recycled objects, hence they get killed immediately in Bullet#update(). Adding this to Bullet solves that:

    override public function revive()
    {
        super.revive();
        setPosition(0, 0);
    }
    

    The second issue is that you sometimes get a Square when you request a Circle and vice-versa.

    You're right in that you can just skip optional arguments (if the arguments' types are different, which they are in this case). However, ObjectClass is not completely redundant if anObjectFactory is supplied (I admit that in my previous answer, it sounded like it's an either-or-situation). It's still used as an argument for getFirstAvailable()in the "grow-style-recycling" case (with maxLength == 0).

    If you add an explicit ObjectClass to your revive() calls, the type of the recycled object is enforced:

    switch (type)
    {
    case "circle":
    	bullet = group.recycle(Circle, Circle.new.bind(200, 200), force);
    case "square":
    	bullet = group.recycle(Square, Square.new.bind(300, 200), force);
    }
    


  • Thanks. Now I get it :)
    One last question: the recycle method seems pretty efficient in order to recycle a bullet or create a new one if there is nothing to recycle. So do we really need to create a pool of bullets at the start of the game?


  • administrators

    Well, it depends - what device is the game running on, how many bullets do you need, etc..

    On a low-end mobile device, allocating a lot of objects while the game is already running could lead to some hiccups. Personally, I'd say it's a bit of a premature optimization until you actually start having issues though.


Log in to reply