Willy and Mathilda's Houseboat Adventure: The Game

  • Today I thought I was being clever by leveraging FlxTween chaining, which I thought was pretty nice syntactic sugar, in cases where I wanted a delay in between .then() calls. How? Simply by using an empty .num() tween: FlxTween.num(0,1,delay).then(...).

    Of course, that's how I found a bug in the library. :P


  • Today's hard-to-track-down bug I thought I should share with you.

    HaxeFlixel can be deceptively straightforward to work with sometimes. Things that seem to just take care of themselves may actually be hiding something from your understanding that it would be better if you knew. I wish I remembered where I read, but one article I saw had a HaxeFlixel first-timer diving into the internals to understand how the basic engine worked, rather than diving into "coding fun stuff." At the time, I thought, "Wow, that's really hardcore! But I just want to code fun stuff, that's why I picked up a ready-made library to begin with." Now I see the wisdom of it!

    The basic engine knowledge that I needed today was that FlxGroup.update() iterates through its members array, calling each member's update() in turn. However, it does so using a simple loop counter. So, if you happen to update the members array in any of the members' update() functions, it may, sometimes, randomly, depending on how much your code messes with .members and how randomly so, cause any of the following things to happen during the current outer update():

    • a member's update() may not be called
    • a member's update() may be called more than once

    Obviously, this is bad, and can be tough to track down!

    In my case, my HUD appeared to be mysteriously updated twice in a row. For a long time my debugging was fruitless (partly because I'm normally on cpp without stepping tools set up yet :( ...I only get things working in Flash when I'm as stuck as I was today) because I looked at that and said, "No, HaxeFlixel doesn't do that. It must be something else," like, I added two HUDs or something. Nope. Here's what happened within one outer update() call:

    Members before and after an inner update(), during an outer update()

    As you can see, my HUD.update()'s own chain reaction of code caused, somewhere, the HUD's index within .members to move down a spot. Then FlxGroup.update() simply incremented its counter, and ran .update() on the next member...which was my HUD again. So, yes, if you mess with .members, HaxeFlixel does do that.

    Somewhere in the docs I remember reading, "Don't mess with .members unless you know what you're doing." So, for those of you, like me, who, after a while, thought they knew what they were doing, but didn't know enough to know that they didn't know what they were doing...now you hopefully know what you're doing. :)

    The solution? Well, more of the problem, first: I actually need to modify .members because I have some z-order changes that depend on the user's actions, i.e. are not known in advance. So I members.remove() and members.insert() these items as needed (and the insertions are not on top of the stack, BTW.) So I think the only HaxeFlixely way to do it without the above breakage is to put everything into additional FlxGroups that wasn't already...at least everything that might need a z-order change. That way, the outer members array shouldn't have any changes to it. So hopefully by following that design pattern, we all can avoid such bugs sneaking up on us.

    I haven't thought about, and don't care to think about at the moment, what I would need to do if I needed to z-order change something that also had an overridden update(), and that this z-order change had to occur in the overridden update() of something in the same group. (E.g. what if I needed an object to change its own z-order during its own update()?)

  • Another word of caution, if you're add()ing rather than insert()ing with the thought of putting something at the top of the z-order, the above strategy of putting it in a FlxGroup is insufficient. Check FlxGroup.add()'s doc--it will just put your object back in the same spot--or even lower--because it reuses the first null spot it comes across.

  • Furthermore, if you're using the workaround I am, putting everything into a group, you may run into problems if anything you previously had directly added to the state was itself a group, because recursion is currently broken.

  • administrators

    Note that that bug should only affect forEachOfType(), other forEach functions should be fine.

  • @Gama11 Ah, good to know. In my case that's the one I use the most :)

  • In today's news, I went to add a feature that I thought would be easy. The minigame "Acrobatics as the Catfish Fairy" has a series of hoops for you to jump through, stacked vertically. I wanted the higher ones (worth more points) to also be gradually smaller (vertical stretch of the existing graphic.)

    So, I stretched them:

    collidable.setGraphicSize(Std.int(collidable.width), Std.int(collidable.height * Proportion));

    They look correct. Good! However, pixel perfect collision checks, which are pretty crucial here, seemed to be misbehaving. So I modified the game in-place to see exactly how they were misbehaving (a lot easier than jumping on the trampoline over and over)--whenever a collision is happening, the catfish fairy goes red:

    catfish fairy at various hoop points

    What should be happening is, the catfish fairy is red when touching the bottom or top (solid) parts of the hoop, and not at all during the middle part. As you can see, the top/bottom isn't colliding, then a spot on either side of the centre is colliding, but the very centre is again not colliding.

    This particular hoop is actually stretched to be bigger than the original graphic by about 1.5x. Visually, is seems as though the PPC check is happening against the original graphic size, centred.


    Seems that the pre-resized values are being used, as I thought, in FlxCollision.pixelPerfectCheck() (currently I'm only looking at the unrotated case):

    boundsB.width = Target.frameWidth;
    boundsB.height = Target.frameHeight;

    These are the pre-resized values, because if you look at setGraphicSize(), you see that frameWidth/Height are not set therein. They're actually kept around as a reference to the original sizes on purpose.

    More importantly, there's reference to framePixels for the actual pixel data being checked.

    OK, what's the next course of action?

    Either I could prerender all the hoops at different sizes (I don't like this approach, since it's more difficult to tweak things), or change the PPC check function. I'll take a crack at the latter and see what we come up with, but it might not be an acceptable change, since this is a function that gets run a lot and it needs to stay quick.

    Meanwhile on closer examination of setGraphicSize, it seems I'm using it quite inefficiently, since it's meant to help in the cases where scale stays the same. Much better would be simply collidable.scale.y = Proportion;.

  • And...today we went with The Middle Way. I did not have to prerender, thank goodness. I also did not have to change HaxeFlixel, which, if nobody else was needing this feature, is probably the best anyway.

    The solution was simply to use FlxSprite.stamp() to render the resized image onto a new sprite, which is then at scale 1 according to flixel:

    var collidableToScale = new FlxSprite(0, 0, AssetPaths.acrobaticsBigRingCollidable__png);
    collidableToScale.scale.y = Proportion;
    collidable.makeGraphic(Std.int(collidableToScale.width), Std.int(collidableToScale.height * Proportion), FlxColor.TRANSPARENT, true);
    collidableToScale.origin.set(0, 0); // important, otherwise the stamp will be vertically off

    After that we throw away the temporary collidableToScale and are done with it. The result:

    catfish ppc checking correctly with various hoop sizes


  • Today's adventure was with one of my favourite addons, FlxMouseEventManager. It's a favourite because it tidily takes care of mouse events for any FlxObject for you--you just pass it the handlers, and it does the rest.

    I had used FMEM extensively with success. However, today I went to add some events to an object, and found that some of the events weren't working. I figured it was maybe because I was using FlxSpriteGroups, and I've sometimes encountered oddities with them. (Sometimes it's not the fault of FlxSpriteGroup, but a misbehaving member. E.g. if you have a FlxText that's misreporting its height--a bug I've encountered but yet to figure out how to fix--then it can make the whole group misreport its height.)

    After some debugging, I realized that the events I added with FMEM were working, but the events that the object had already registered with FMEM itself--those were the ones that broke. Turns out that if you call FlxMouseEventManager.add() twice on the same object, the latter call somehow overrides the former call. All I needed to do was modify my object so that I could pass it some events to pass on to FMEM all in one call, and everything worked as expected.

    Again, a lesson in "know what thine libraries are doing for thee." It may not always be possible, but it is with flixel, at least, because you have the source. :)

  • @IBwWG Just a note on stamping, this of course doesn't apply to animated sprites. In that case it's easier to get them before you initialize them, resizing the whole graphic before it gets parsed as frames:

    		if (Scale == 1.0) {
    			loadGraphic(GraphicAsset, true, OriginalWidth, OriginalHeight);
    		} else {
    			var originalBitmap:BitmapData = FlxAssets.getBitmapData(GraphicAsset);
    			var matrix = new Matrix();
    			matrix.scale(Scale, Scale);
    			var newWidth = Std.int(OriginalWidth * Scale);
    			var newHeight = Std.int(OriginalHeight * Scale);
    			var scaledBitmap = new BitmapData(newWidth, newHeight, true, 0); // fill with transparent
    			scaledBitmap.draw(originalBitmap, matrix);
    			loadGraphic(scaledBitmap, true, newWidth, newHeight);

Log in to reply

Looks like your connection to HaxeFlixel was lost, please wait while we try to reconnect.