Navigation

How a multi-scene drag bug was fixed and a tutorial on moving and stopping physics objects.

Published on 7th January 2019

Welcome to the first Dev Log of 2019. I had only been back in the office 2 days at the time of writing this, so it's a bit shorter than usual and half of it is taken-up with a mini tutorial.

My priority for the coming weeks is to finish and release Phaser 3.16. There are a few changes to wrap-up in the Spine Plugin, and the Scale Manager to finish, but both of these are progressing well and I'm increasingly happy with the state of them. The plan is to take a deeper look at the new Scale Manager in the next Dev Log.

Multiple Scene Drag

Just before I finished for Christmas there was a really interesting bug posted to GitHub Issues. Raised by @probt, issue #4249 detailed how if you had multiple Scenes running in parallel, with draggable Game Objects in them, that only Game Objects in the top Scene could be properly dragged. Those in any Scene lower down the Scene list would start dragging but never complete, getting stuck in a locked state.

I set about creating a minimal test case. Once I'd done that, I could see what was happening for myself. Although a test had been provided (thank you @probt!) it had a bit too much going on to be useful for debugging. It did, however, demonstrate the bug clearly, so I knew something was going awry. My own test case proved it as well.

It transpired that the issue was caused by a Pointers dragState getting locked. When a Pointer interacts with a draggable Game Object it maintains a 'drag state'. By default, this state is zero, meaning that the Pointer isn't dragging anything. During the life-cycle of the Input Plugin, it will run through a series of checks, changing the drag state as it goes. For example, there is a specific state for when a Pointer was actively dragging a Game Object in the previous frame, but has since been released, allowing the object to dispatch its dragend event.

There are 6 drag states like this in total that a Pointer can go through in its lifecycle. The issue is that this state was being stored on the Pointer instance itself, in its dragState property. Ordinarily, this would be fine, and indeed it worked fine if you only had a single active Scene, or only one Scene with draggable objects within it. But when you have more than one Scene the problem arises, because Pointer instances are global.

When a Scene's Input Plugin runs, it operates on Pointer instances that belong to the global Input Manager. This means that if SceneA sets the drag state of a Pointer, then when it came around to SceneB checking the drag state, the state had already been mutated by SceneA and was now holding an invalid value. What's more, you can't just reset the drag state between the Scenes, because it needs to be remembered across potentially many frames.

The solution was to stop the Pointers being responsible for maintaining the drag state at all.

After some tests I added two new methods to the Input Plugin: getDragState and setDragState, both of which take a Pointer instance as an argument. The state is now stored locally in the Input Plugin, on a per Pointer basis, indexed by the Pointer ID. As the Input Plugin is owned by the Scene, it moves it away from being stored on a global level, to being stored on a Scene level.

I ran a few more tests, tidied up the code and pushed the fix. Now, you can have draggable Game Objects spread across as many parallel Scenes as you like. As with most issues, the fundamental change that was made to the API was quite small. The results, though, are significant. While it may not be the most interesting of fixes, and is likely to be hastily scrolled-past when scanning the 3.16 Change Log, it's still a really important one.

It was a bug that came about because an early design decision didn't factor in a specific use-case. Once the flaw was demonstrated, it was easy to re-evaluate the approach and replace it with a better one. I like to think it's a prime example of how the feedback loop provided by GitHub can sometimes work really well.

image

Tutorial: How to make an Arcade Physics Sprite stop at a specific position

Someone asked me on Twitter how they could make an Arcade Physics Sprite move to a specific location and then stop once reached. They wanted to use normal physics forces (i.e. velocity) to move the Sprite and not a Tween. While Arcade Physics has a built-in method moveToObject, all it does is set the initial velocity required. It doesn't monitor the object and stop it when it reaches the given destination. For that, we'll need some extra logic.

First, let's create a simple test case. Here we load an image, create an Arcade Physics Sprite and position it in the Scene:

image

Next, let's add an input handler to set our 'target' destination. When you click anywhere on the Scene, it will use the pointer position as the destination and then call the moveToObject function. The x and y values are stored in target, which is a Vec2:

image

This is all good and well. If you now click, the source Sprite will start off quite merrily on its way, but it still won't stop yet. To do that, we need to monitor the Sprite in our update loop and figure out how far away it is from the destination target:

image

This uses the built-in distance function to work out how far the source is from the target. If the source is moving, which is determined by checking its speed, and if the distance is less than 4, then we consider it as having 'arrived'. The distance test is used because, due to the way physics and number rounding works in JavaScript, the Sprite is almost certainly never going to exactly reach the target coordinates. So instead, we check to see if the Sprite gets within an acceptable distance from the target, and if it does, bingo, the body is reset.

Calling reset on an Arcade Physics body cancels out all of its current velocity and also, optionally, resets the position of the body to the given x/y coordinates. Because we know our Sprite isn't going to be at exactly our target coordinate we take advantage of the reset arguments to force it there.

You can test it for yourself in the following example, which you can download from the Phaser Labs:

image

Why did we use a distance value of 4? And not a lower value? It's because of the speed the Sprite is moving at. The faster it goes, the more tolerance you need to allow for in your calculations, as the further it can 'overshoot' its target in a given step. The slower it moves, the tighter the distance check can be.

There are also other things which may prevent it from ever reaching the target. For example, if a static body is in the way. It will hit the body and possibly rebound (depending on its bounce setting), forgetting all about its target. Calling moveToObject simply sets a velocity that should, given a clean run, get the body to that location. Once the velocity changes, however that may happen, all bets are off. Even so, this simple distance check is often more than enough to ensure an object reaches its intended position.

That's it for this Dev Log. At the time of writing it's early Monday morning, so there's a whole week of Phaser development ahead of me - let's see what it brings!