Moving objects on mouseover getting stuck when cursor moves too fast

Started by Laura Hunt, Sun 03/11/2024 07:28:08

Previous topic - Next topic

Laura Hunt

I'm implementing a card minigame in my game and I want the cards to "pop" up and move to the front when the mouse moves over them. When the player moves the cursor at a "reasonable" speed, things go well:




However, if the cursor moves too fast, some cards will get "stuck" in their top position and won't go back down:



Also, if I leave the cursor at just the right spot, the card will start jumping up and down, since it will constantly move in and out of the cursor's hotspot:



This last case doesn't worry me too much since it's kind of an edge case, but the cards getting stuck is a real problem that makes the whole thing look kind of bad. Is there any way I could avoid that?

My code:

Code: ags
Object* previouscard;

void descendCard(Object* card)
{
  int ycoord;
  switch(card)
  {
    case oObject1:
    case oObject4:
      ycoord = 220;
      break;
    case oObject2:
    case oObject3:
      ycoord = 205;
      break;
    case oObject0:
      ycoord = 190;
      break;
  }
  card.TweenY(0.25, ycoord, eEaseOutBackTween, eNoBlockTween);
}

void ascendCard (Object* card)
{
  int ycoord;
  switch(card)
  {
    case oObject1:
    case oObject4:
      ycoord = 210;
      break;
    case oObject2:
    case oObject3:
      ycoord = 195;
      break;
    case oObject0:
      ycoord = 180;
      break;
  }
  card.TweenY(0.25, ycoord, eEaseOutBackTween, eNoBlockTween);
}

function room_Load()
{
  
  oObject1.Baseline = 189;
  oObject2.Baseline = 190;
  oObject0.Baseline = 191;
  oObject3.Baseline = 190;
  oObject4.Baseline = 189;
  // there's a series of dynamic sprite rotations here which I'm omitting because they're not relevant

}

function room_RepExec()
{
  Object* o = Object.GetAtScreenXY(mouse.x, mouse.y);
  
  if (o == null) { // to empty space
    if (previouscard != null) {
      previouscard.Baseline = previouscard.Baseline - 3;
      descendCard(previouscard);
      previouscard = null;
    }
  }
  
  else if (o != null && previouscard != null && o != previouscard) { // from one card to another
    o.Baseline = o.Baseline + 3;
    ascendCard(o);
    previouscard.Baseline = previouscard.Baseline - 3;
    descendCard(previouscard);
    previouscard = o;
  }
  
  else if (o != previouscard) { // from empty space to card
    o.Baseline = o.Baseline + 3;
    ascendCard(o);
    previouscard = o;
  }
}


eri0o

Uhm,

Without playing with the code but guessing a bit...

It looks like you have memory of at most 1 action, so if you can overwhelm your memory of 1 it could cause the behavior of the card getting stuck.

The issue of the edge case that causes the card to pop up and down and the previous issue also seems to be an issue of having the behavior and the presentation being mapped on top of the same thing - the object both is how you show things on screen and how you detect things are clicked. When things are moving, this may not be a good idea, it may make more sense to look at the same region of the screen and then execute the behavior - you could use like a hotspot (or 99 transparent stationary object) to detect clicks and mouse over and then animate the other thing.

Laura Hunt

Quote from: eri0o on Sun 03/11/2024 09:02:08It looks like you have memory of at most 1 action, so if you can overwhelm your memory of 1 it could cause the behavior of the card getting stuck.

The issue of the edge case that causes the card to pop up and down and the previous issue also seems to be an issue of having the behavior and the presentation being mapped on top of the same thing - the object both is how you show things on screen and how you detect things are clicked. When things are moving, this may not be a good idea, it may make more sense to look at the same region of the screen and then execute the behavior - you could use like a hotspot (or 99 transparent stationary object) to detect clicks and mouse over and then animate the other thing.

Hey eri0o,

The problem with using hotspots or zones defined by angles is that the interactable area will no longer corresponde to its visual representation. So basically, if a card (say, the Queen) is up and you move the cursor out of its hotspot, the card will go down even though the cursor is visually still on top of that card.

I have managed to solve the issue of cards staying stuck by modifying my code like this:

Code: ags
function room_RepExec()
{
  Object* o = Object.GetAtScreenXY(mouse.x, mouse.y);
  
  if (o == null) {
    if (previouscard != null) {
      previouscard.Baseline = previouscard.Baseline - 3;
      for (int i = 0; i < 5; i++) {
        object[i].StopAllTweens();
        descendCard(object[i]);
      }
      previouscard = null;
    }
  }
  
  else if (o != null && previouscard != null && o != previouscard) {
    o.Baseline = o.Baseline + 3;
    ascendCard(o);
    previouscard.Baseline = previouscard.Baseline - 3;
    for (int i = 0; i < 5; i++) {
        if (object[i] != o) {
          object[i].StopAllTweens();
          descendCard(object[i]);
        }
      }
    previouscard = o;
  }
  
  else if (o != previouscard) {
    o.Baseline = o.Baseline + 3;
    ascendCard(o);
    previouscard = o;
  }
}

With this, cards no longer get stuck since they're always forced downwards. However, this makes the flickering of the edge cards even more noticeable now:



I have also tried using simple Move commands instead of tweens (I prefer tweens because it allows me to use easings, but it's not a dealbreaker), but the issue remains the same.

Crimson Wizard

The way I see this, the logic should be following:

On each game tick:
- check which object is under the cursor and remember it, in local variable
- for each object, in a loop:
  - if this object is the one under the cursor:
    - if it's already up or moving up
         then keep it that way
    - else
         start moving it up
  - if this object is not the one under the cursor:
    - if it's already down or moving down
         then keep it that way
    - else
         start moving down

Note though:
1. You dont need to have to remember "previous object".
2. OTOH You will have to remember each object's state, because, although you can test the static position, AGS does not provide the way to check where object is moving to unfortunately (Characters do have DestinationX/Y properties, but not room objects. This reminds me, this should be added to Object type too).
EDIT: I dont know if Tween allows to do that easily enough. If it does, then you might as well use that instead.

Laura Hunt

Quote from: Crimson Wizard on Sun 03/11/2024 10:56:12The way I see this, the logic should be following:

On each game tick:
- check which object is under the cursor and remember it, in local variable
- for each object, in a loop:
  - if this object is the one under the cursor:
    - if it's already up or moving up
         then keep it that way
    - else
         start moving it up
  - if this object is not the one under the cursor:
    - if it's already down or moving down
         then keep it that way
    - else
         start moving down

Just looking at the logic without implementing it, this would not avoid the flickering, right?

If we have the cursor barely touching an object, our logic tells us to move that object up. That object is no longer under the cursor, so it needs to move down. Upon moving down, it touches the cursor again, and so up and down ad infinitum.

Crimson Wizard

Quote from: Laura Hunt on Sun 03/11/2024 11:06:04If we have the cursor barely touching an object, our logic tells us to move that object up. That object is no longer under the cursor, so it needs to move down. Upon moving down, it touches the cursor again, and so up and down ad infinitum.

Then this is not the problem with overall logic, but the problem with detecting. You would need to detect not only object, but certain area in which it could be located.

I suppose that you may use hotspots for this.

Laura Hunt

Quote from: Crimson Wizard on Sun 03/11/2024 11:07:33I suppose that you may use hotspots for this.

The problem with this approach is the one I mentioned earlier to eri0o: "trigger" areas would stop corresponding to their visual representations. (Not to mention that because the cards are rotated at runtime, drawing the hotspots in the editor would be a huge hassle. Doable, though.)

For example, if I used these hotspots:



Moving the mouse over the purple area would bring the queen up. But then things would look like this:



Now the hotspots don't match the cards. Moving the mouse into the green area would bring the queen card down again, even though the player still has the cursor over it.

Crimson Wizard

Maybe you should define this logically first. Which area exactly would mean that the upped card should stay up, and which area would refer to another card.

Note that you may check both: card object and area, with card object having a priority.

If it turns out that you need "overlaying" areas, then this may be solved by adding a second row of transparent objects, duplicating these cards. These transparent objects do not move and stay in down position, but have their baselines adjusted along with their visible counterparts.

Laura Hunt

Quote from: Crimson Wizard on Sun 03/11/2024 11:34:06If it turns out that you need "overlaying" areas, then this may be solved by adding a second row of transparent objects, duplicating these cards. These transparent objects do not move and stay in down position, but have their baselines adjusted along with their visible counterparts.

Ah-ha, this might be the solution! I'll give it a shot today and let you know if it worked.

Laura Hunt

@Crimson Wizard, this works perfectly! Of course, there is a tiny area in which the position of the cards when they're up doesn't match exactly the "trigger" area, but it's barely noticeable and this solves the flickering issue completely.

I now just need to clean my code a bit because I'm still using my "previousobject" logic, but that'll be the easy part. Thank you so much for the help!

Crimson Wizard

Quote from: Laura Hunt on Sun 03/11/2024 12:30:39I now just need to clean my code a bit because I'm still using my "previousobject" logic, but that'll be the easy part.

Actually, I did not think too much about this earlier, but now I suppose it may be done both ways, using "previousobject" or checking all cards each time.

But the point in any case is to remember the states of objects (moving, and which direction), not only which were the previous one.

Laura Hunt

The code I posted in a previous reply doesn't really use explicit states, though:

- If object under cursor is null:
-- lower all cards.
-- make previous object null

- Else, if the object is different from the previous object and the previous object is not null:
-- raise only this card
-- lower all the others
-- make this card the "previousobject"

- Else, if the object is different from the previous object (implied: previous object is null):
-- raise only this card
-- make this card the "previousobject"

No need to keep track of states this way: if the object under the cursor is the same as the "previous object", no movement gets triggered. And if an object was already moving down and I trigger another Move command with the same destination, the movement does not get re-triggered from the start; it just keeps going from wherever it was.

Snarky

One way to fix the bouncing problem would be to say that if the mouse hasn't moved, don't update the card selection. If combined with the invisible cards in the stationary positions, I think this will solve the problem completely.

Laura Hunt

Quote from: Snarky on Sun 03/11/2024 20:19:36One way to fix the bouncing problem would be to say that if the mouse hasn't moved, don't update the card selection. If combined with the invisible cards in the stationary positions, I think this will solve the problem completely.

That does look like it might have been a simpler solution! Using transparent objects has solved the problem perfectly though, so now that I've implemented the whole shebang, I'll probably just keep it. Thanks though, something to keep in mind for the future. Sometimes solving problems like these is not a matter of code but of re-thinking the problem differently, and this thread has given me a few examples of stuff that I never would have thought of on my own.

Crimson Wizard

Quote from: Snarky on Sun 03/11/2024 20:19:36One way to fix the bouncing problem would be to say that if the mouse hasn't moved, don't update the card selection.

I don't quite understand this suggestion. What if mouse moved but is still over same card? It looks like it does not matter whether mouse moved or not, but whether it is over same card or not?

Snarky

@Crimson Wizard, the point is that the highlighted card should only change because the cursor has moved, not because the card has moved. This will stop the cards from "bouncing" when they move away from the cursor.

In general, the solution is to introduce hysteresis (dependence on history to determine current state) for stability. IOW, once the state changes, that state should be a bit "sticky," not flip back as soon as the trigger condition no longer applies. Both the invisible cards and the mouse movement test are examples of this.

A typical solution is to make the threshold/trigger region for selecting and unselecting different, e.g. for a GUI that pops up at the top of the screen, it might appear when you move the mouse to y<100, but once it has appeared it will only go away if you move the mouse to y>120. That creates a buffer zone where the state doesn't change but depends on history, providing stability.

Crimson Wizard

Quote from: Snarky on Mon 04/11/2024 14:24:03@Crimson Wizard, the point is that the highlighted card should only change because the cursor has moved, not because the card has moved. This will stop the cards from "bouncing" when they move away from the cursor.

But if cursor moved, then it not necessarily is moved outside of this card's trigger zone.
So why checking whether cursor moved specifically when in the end you still will have to check whether cursor is staying within the trigger zone?

Snarky

Quote from: Crimson Wizard on Mon 04/11/2024 14:30:32But if cursor moved, then it not necessarily is moved outside of this card's trigger zone.
So why checking whether cursor moved specifically when in the end you still will have to check whether cursor is staying within the trigger zone?

It's for the case where the cursor hasn't moved but it is now outside of the card's trigger zone because the card has moved, as seen in the example:



Without this check, it will behave as in the recording, unselecting the card as soon as it leaves the cursor, causing it to start to move back, which then selects it again, and making it "bounce" back and forth between the states. With this check, it does not unselect the card as long as you don't move the cursor, even though it's no longer over the card, so you don't get any bouncing.

Crimson Wizard

Quote from: Snarky on Mon 04/11/2024 15:22:33It's for the case where the cursor hasn't moved but it is now outside of the card's trigger zone because the card has moved:

Okay I see.
But that's why I was suggesting using other hotspot / transparent object to define that area. Because frankly relying on mouse not moving alone does not seem to be reliable. A tiny player hand's twitch may cause it to drop down again.

Snarky

Quote from: Crimson Wizard on Mon 04/11/2024 15:35:16Okay I see.
But that's why I was suggesting using other hotspot / transparent object to define that area. Because frankly relying on mouse not moving alone does not seem to be reliable. A tiny player hand's twitch may cause it to drop down again.

You could set an anchor when the selection state changes and then use a higher threshold of movement away from that anchor, but it depends on what you're actually trying to fix. This is solely meant to deal with the case of bouncing (the selection state flipping back and forth rapidly), and handling it in the case of a stationary cursor is sufficient for that.

It does not deal with the issue of wanting the selected state to persist even when the card is no longer under the cursor in general. That's why I suggested combining it with the invisible cards.

Because it seems to me that the invisible cards is not a full solution to the problem, as demonstrated in this mockup:

First we select the leftmost card:



Then we move the cursor off to a location that is not covered by either the current card position or the original one (i.e. the invisible card):



Now we leave the cursor in this position. Since the card is no longer selected, it will begin to drop back to its original position, but this makes it pass over the cursor and get selected again, and so we're back to the bouncing. By adding the mouse movement check, we avoid this: in order to produce bouncing you have to keep jiggling the mouse, and in that case having the card rapidly selected and deselected is expected behavior.

Another way to handle this case would be to include the whole region the card passes over as it moves as part of the "buffer zone" (where it maintains a selection once the selection has been triggered), but that's more complicated.

SMF spam blocked by CleanTalk