scratchfoundation / scratch-gui

Graphical User Interface for creating and running Scratch 3.0 projects.
https://scratchfoundation.github.io/scratch-gui/develop/
BSD 3-Clause "New" or "Revised" License
4.42k stars 3.5k forks source link

point towards + movement will oscillate sprite's direction #4890

Open gunchleoc opened 5 years ago

gunchleoc commented 5 years ago

Expected Behavior

Sprite keeps its direction when the mouse is not moved.

Actual Behavior

When point towards mouse-pointer is followed by move 10 steps, the sprite direction will oscillate even if the mouse position is not changed.

Steps to Reproduce

oscillating_direction

Use this project to try it out.

Operating System and Browser

So, looks like an engine rather than a UI issue.

TheLogFather commented 5 years ago

Your Scratch cat costume is way off-centre, so it's not surprising the first case oscillates so much. (I mean, even when it is well centred, I don't see how you expect to avoid some oscillation -but it's the off-centre problem you have that's causing it to flip so badly in your case.)

gunchleoc commented 5 years ago

I put it off-center on purpose so that the problem would be obvious for the purpose of this bug report.

I came across a bug while looking at some racing games created by schoolchildren. We did the same project last year with Scratch 2 and I don't remember it being that bad - maybe I am remembering it wrong though.

My wild guess is that it's a floating point issue where it keeps flipping a pixel.

joker314 commented 5 years ago

Actually, this is what should happen!

Suppose the mouse pointer is positioned at the origin (0, 0). Then, imagine the cat is positioned at (5, 0).

The cat is 5 units to the right of the mouse pointer

Next, the cat will travel 10 units in the direction of the mouse (to the left). However, this will cause it to overshoot and be in the position of (-5, 0)

The cat is 5 units to the left of the mouse pointer

The flickering is because the "move 10 steps" causes it to consistently overshoot unless the mouse is exactly 10 units away.

"Go to mouse pointer" avoids this, it can't overshoot, it goes exactly to the position of the mouse.

I agree that decreasing the "move 10 steps" to "move 0.1 steps" doesn't fix the issue, though, so you might be rights about the floating point/precision issues when step size is small. But I think for step size = 10, this is how it should be.

towerofnix commented 5 years ago

I agree that decreasing the "move 10 steps" to "move 0.1 steps" doesn't fix the issue, though, so you might be rights about the floating point/precision issues when step size is small. But I think for step size = 10, this is how it should be.

It's still always going to oscillate, and with good reason. Suppose the sprite were exactly 10 units away in distance, to the right (and that the mouse is not moving for our demonstration). It will point towards the mouse (leftwards), then move towards it 10 units. Now the sprite is at the exact same position as the mouse (at least for our demo). Now, the next frame, it is going to point "towards" the mouse. Of course, since it is already at the mouse position, there is no correct value for "towards" here, but Scratch defines the result to be the sprite pointing in direction 90 (rightwards). Thus, exactly at the script defines, the sprite will move 10 steps -- to the right. Upon the next frame, we are exactly where we started: 10 units away from the mouse, to the right. It points left and repeats the process all over again: oscillating.

Accordingly, this pattern arises from any distance traveled, no matter what, provided the script "forever: point towards mouse, move N steps" - even a fractional value less than 1. Even if the position does not visibly change (a change so small might not), the direction will still change, because internally the sprite is still oscillating between the exact position of the mouse and slightly to the right.

Of course, since "move N steps" moves it forwards in the direction the sprite is pointing, it would still overshoot exactly as @joker314 described above most of the time, because the sine/cosine values generally wouldn't add up to integer values (and be exactly equal to the mouse position). This results in the oscillation not being exactly left-right (overshooting means it'll be ahead of the sprite in the direction it was facing as it moved). But even if the sprite's position weren't fractional (e.g. you used "go to x: (round (x position)) y: (round (y position))" after moving), you would still end up with the oscillation, left-right as I described above.

There is a way to avoid this behavior, although it involves changing the script; naturally, with that script, the behavior is inherent. Here's one example solution:

"forever: if distance to mouse > 10, point towards mouse and move 10 steps; else, go to mouse"

Because the sprite only changes direction and moves forwards once it is more than 10 units away, it does not oscillate when it is closer, instead going directly to the mouse position and staying in the same direction (no "point towards mouse" in the else branch).

The other option is to use a "smooth" movement script, like so:

"forever: point towards mouse, move 0.50 * distance to mouse"

You'll notice, if you use this script, the sprite will seemingly pause when it becomes very close to the mouse (within a pixel distance), and will stay in the same direction. This is because the sprite is still getting closer to the mouse, but the distance it moves is so small doesn't change what's apparent on the screen. If the mouse stays still for long enough, eventually JavaScript's decimal precision will fail and the sprite's distance will become equal to zero. (It's worth noting that this precision is more accurate than Scratch's monitors display, if you show the monitors for x/y position: the position of the sprite becomes equal to the position of the mouse slightly later than the monitor reports them as apparently equal.) At this point, the sprite faces right, as defined by Scratch when the distance is zero, like I mentioned earlier. If you prefer the "smooth" movement of this script, you can work around that by combining with the first example script; if the distance is less than 1, go directly to sprite, and do not change direction.

(edit: typo!)

gunchleoc commented 5 years ago

Thanks for the analysis!

I'm not a teacher, so I am wondering which would be better - keeping it like this (hard for beginners), or adding a tolerance if the values get smaller than 1 (students learn more about floating points).

TheLogFather commented 5 years ago

TBH, this kind of thing is actually a great opportunity for teaching kids one of the most important aspects of coding...

Learning to carefully work through your code under various scenarios, to figure out what it is you've actually told the computer to do (rather than how you expect it to behave), can be really critical in some situations.

The code may work the vast majority of the time in exactly the way you expect. But simply missing, or failing to test for, some (even very rare) edge/corner-case (such as not dealing with some not-as-expected sensor-reading situation) is the kind of thing that can lead to loss of your spacecraft (e.g. Mars Polar Lander), or leaving your super-advanced 'smart' warship crippled in the water for several hours (e.g. USS Yorktown), or your radiation therapy machine overdosing & killing patients (e.g. Therac-25), or your autonomous vehicle not noticing a cyclist, or your 737 crashing...

Makes you think, though – I mean, as software complexity is increasing so rapidly these days, and as there's more & more reliance on machine-learning (where we don't really know 'how it does it'), you do start to wonder how it can be possible to thoroughly test it all, and how smart it is to deploy it so widely in potentially life-critical situations.