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:
(https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExdGVqOXkzd2w3cG9teG12NGc3ZXF0MGw0cmNvNzdqdHUwOHB5N3ZhbiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7mE29J5BG340O5xP0f/giphy.gif)
However, if the cursor moves too fast, some cards will get "stuck" in their top position and won't go back down:
(https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3B3NHNjOTlrc2FrZjRrNmI3M3VnbmJrbXJ3M3A1NGpuMzVlZnU2OCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/Abs1g0jWhefGh5VUVu/giphy.gif)
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:
(https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExN3NtaXMxcm5xMzQ1b2NvYXNsN3MxM2k0dGQ2NWMxazU1aXoyam1yaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/0eYkFIssCrqNclV7bA/giphy.gif)
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:
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;
}
}
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.
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:
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:
(https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExMjI4em1kdWVnZXh2YTN3N3hraDMwNG91M25hajAyYjQ2OWs5MWd4YyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/auxU6aevGauS5igePy/giphy.gif)
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.
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.
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.
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.
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:
(https://i.imgur.com/PgcHXYg.png)
Moving the mouse over the purple area would bring the queen up. But then things would look like this:
(https://i.imgur.com/4Adm4up.png)
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.
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.
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.
@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!
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.
The code I posted in a previous reply (https://www.adventuregamestudio.co.uk/forums/index.php?msg=636666625) 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.
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.
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.
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?
@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.
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?
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:
(https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExN3NtaXMxcm5xMzQ1b2NvYXNsN3MxM2k0dGQ2NWMxazU1aXoyam1yaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/0eYkFIssCrqNclV7bA/giphy.gif)
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.
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.
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:
(https://i.imgur.com/de0Qeg2.png)
(https://i.imgur.com/XHtZdIb.png)
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):
(https://i.imgur.com/kNdA39P.png)
(https://i.imgur.com/CsRc9WJ.png)
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.
Quote from: Snarky on Mon 04/11/2024 17:13:27Now 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.
I was kind of expecting that the "second way" is used, meaning that the trigger zone is always the same, regardless of where the card is located in the current moment.
Quote from: Snarky on Mon 04/11/2024 17:13:27Now 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
This never happens because the trigger is the invisible card, not the "real" one. In fact, the "real" cards are not even clickable, so they don't get detected by mouseovers or object detections at all. Therefore "this makes it pass over the cursor and get selected again" is not a situation that can happen with this method because the trigger card (the invisible one) never moves.
Quote from: Crimson Wizard on Mon 04/11/2024 17:40:21I was kind of expecting that the "second way" is used, meaning that the trigger zone is always the same, regardless of where the card is located in the current moment.
That's exactly the way I'm doing it, yes.
A-ha. I was going by your earlier statement:
Quote from: Laura Hunt on Sun 03/11/2024 11:07:36The problem with this approach is the one I mentioned earlier to eri0o: "trigger" areas would stop corresponding to their visual representations.
Therefore I assumed you would use both the visible and the invisible cards as trigger areas. But if you think having part of the selected cards be outside of the trigger area and cause them to be unselected is not a problem, then sure. (You could even do that with hotspots, just having the overlapping card regions as a separate hotspot that matches whichever adjacent hotspot was last active.)
Quote from: Crimson Wizard on Mon 04/11/2024 17:40:21I was kind of expecting that the "second way" is used, meaning that the trigger zone is always the same, regardless of where the card is located in the current moment.
Quote from: Laura Hunt on Mon 04/11/2024 17:43:12That's exactly the way I'm doing it, yes.
If by "second way" CW means the second alternative I described, it's not quite the same solution.
Quote from: Snarky on Mon 04/11/2024 18:10:25A-ha. I was going by your earlier statement:
Quote from: Laura Hunt on Sun 03/11/2024 11:07:36The problem with this approach is the one I mentioned earlier to eri0o: "trigger" areas would stop corresponding to their visual representations.
Therefore I assumed you would use both the visible and the invisible cards as trigger areas. But if you think having part of the selected cards be outside of the trigger area and cause them to be unselected is not a problem, then sure.
The key "eureka" moment was CW's suggestion that the invisible cards should not move up and down, BUT their baselines should move to the front or to the back together with the "real" card that's being activated, making overlaps possible and making the trigger areas almost identical to their visible counterparts.
So sure, in the end there's a
tiny area in the trigger card that doesn't match the "real" card exactly when the card is lifted (10 vertical pixels to be precise), but in my tests it's barely noticeable and never ruins the effect.
Quote from: Snarky on Mon 04/11/2024 18:10:25If by "second way" CW means the second alternative I described, it's not quite the same solution.
Honestly I'm kind of lost at this point with all this back and forth :-D But long story short, what matters is that the dummy cards method works great, and it's solved my issue perfectly :)