Animation Loop Causing Crash

Started by boosterterrik, Wed 18/01/2023 15:45:36

Previous topic - Next topic

boosterterrik

Hey friends, I've run into a strange brick wall here. Most of my problems have been solvable with some time in the documentation, but I can't find any references to this issue anywhere.

I have a scene set up with multiple characters in a restaurant. Because AGS idle animations are triggered too slowly, I have each character set to roll through a series of animations upon fading into the Room.

So far, this has worked fine with five of the characters on-screen. But now, my 6th character's animation is causing the game to crash at the end of her first animation cycle, despite the fact that I have checked the "Run next loop after this to make long animation" on each loop in the locked view.

When I reduced the number of loops from 9 to 4, this solved the problem-- but I don't want her animation to be so short!

I cannot figure out why the animation loops are tripping up AGS. I have dealt with the issue before, but that was just the simple mistake of having forgotten to check the continue loop check box. I have double checked that I am using the correct view, and the weirdest part for me is that the problem goes away when I reduce the number of loops in the view. All of the other characters have long loop strings, so why is this any different? Any thoughts?

Here's what I have written in the room script:

function room_AfterFadeIn()
{
cCastellanos.LockView(7);
cCastellanos.Animate(0, 8, eRepeat, eNoBlock);

cFisher.LockView(3);
cFisher.Animate(0, 4, eRepeat, eNoBlock);

cOHalloran.LockView(11);
cOHalloran.Animate(0, 4, eRepeat, eNoBlock);

cLeblanc.LockView(14);
cLeblanc.Animate(0, 4, eRepeat, eNoBlock);

cLorne.LockView(17);
cLorne.Animate(0, 4, eRepeat, eNoBlock);

cMarienne.LockView(19);
cMarienne.Animate(0, 4, eRepeat, eNoBlock);

}


Snarky

#1
The first thing I would try is to comment out the first five characters, to check whether there's something wrong with this particluar animation, or if it's something about the total number of (very long) simultaneous animations (which could be an AGS limitation, perhaps something to do with having to load all of them into memory).

Edit: I would also be curious about whether this approach is actually necessary. What do you mean "idle animations are triggered too slowly"? You can set how quickly they trigger. Also, do the animations really need to be so long? Are you for example adding the same frame multiple times to add a delay, or something like that?

boosterterrik

I have checked every instance within the documentation of idle animations, and I have found no places to make changes to how quickly idle animations trigger. There's the frame-delay on idle animation, but that won't make characters not stand completely still for ~15 sec between idle animations (one character, for instance, is holding a lit cigarette).

This approach is likely not strictly necessary, but I am at a loss for other solutions.

Outside of the issue I have presented here, the initial problem I sought to solve was the issue of needing characters to appear natural and occasionally fidget, drink a drink, or smoke a cigarette, without player input. Without the functionality of making the "idle" status trigger *far* more quickly, like say, 3 seconds, I haven't known a better way to handle this.


The other issue is that I am doing this for work, am not a coder, but am the only one willing to learn. Feels like half my time these days is spent reading documentation and trying to wrap my head around concepts that are likely simplistic to those with more experience.


Snarky

Quote from: boosterterrik on Wed 18/01/2023 20:43:32I have checked every instance within the documentation of idle animations, and I have found no places to make changes to how quickly idle animations trigger. There's the frame-delay on idle animation, but that won't make characters not stand completely still for ~15 sec between idle animations (one character, for instance, is holding a lit cigarette).

It's not the most intuitive API, but Character.SetIdleView() has a delay parameter that allows you to set the time of inactivity before idle animations are triggered.

If you want to have a variety of "idle" animations triggering without any discernible patterns, one way is to use a timer with a random delay, and then pick an animation at random from a selection of animations. You can also combine this with a continuous "rest" animation (whether that's breathing, fidgeting, swaying, or what-have-you) that runs at all other times.

But this is a more "programmer-y" way of approaching the issue. Certainly, just creating a very long animation loop should also work. Did you have a chance to do the test I suggested to see whether it's the particular animation loop that causes the crash, or the combination of all the animations?

Snarky

Quote from: boosterterrik on Wed 18/01/2023 20:43:32The other issue is that I am doing this for work, am not a coder, but am the only one willing to learn. Feels like half my time these days is spent reading documentation and trying to wrap my head around concepts that are likely simplistic to those with more experience.

BTW, a job that forces reluctant employees to write AGS code? That sounds cruel and very unusual.

Khris

#5
As far as I understand your posts, you're looking for idle animations that play immediately upon entering the room but then have a 15 second pause between them?

Here's example code:
Code: ags
function room_Load()
{
  cHologram.SetIdleView(VHOLO_IDLE, 1);
  int idleAnimFrames = Game.GetFrameCountForLoop(VHOLO_IDLE, cHologram.Loop) * 5;
  SetTimer(1, GetGameSpeed() + idleAnimFrames + 15);
}

function room_RepExec()
{
  if (IsTimerExpired(1)) {
    cHologram.SetIdleView(VHOLO_IDLE, 10);
  }
}

boosterterrik

Quote from: Snarky on Wed 18/01/2023 21:03:38But this is a more "programmer-y" way of approaching the issue. Certainly, just creating a very long animation loop should also work. Did you have a chance to do the test I suggested to see whether it's the particular animation loop that causes the crash, or the combination of all the animations?


In terms of command logic, this makes sense and is not overwhelming or confusing. Trouble is, this is all still new enough to me that a lot of capabilities are still unknown to me. I've read a good bit of the documentation (which is what helped we better understand room structure, connecting them, and the creation of inventory stuff etc), but theres plenty that, without context, completely escapes me. I have long wanted to take the time to really settle in and learn to code properly, and I suppose thats what I'm doing now, if a bit sloppily.

Importantly, I would prefer to try things that are difficult and right, rather than easy and wrong, you know? I appreciate you taking the time to respond to what is likely a boringly simple thing for those with experience.


As for trying what you said, I caught the strong whiff of having done something wrong at a bedrock level, so I deleted the animation in question to prepare for completely changing how I do animations so that they are right in the future. That being said, I do sort of suspect that it is a problem of too many animations, as reducing the number of loops "solved" the problem.

Lastly, as for working with AGS for work; Its kinda my fault. I told my boss that, for the game we had in mind, we might as well use AGS. I opened it up and monkeyed around with a few things just successfully enough that one thing led to another and now I am the Developer (for now). We received some arts funding from the gov't for our game, so we are putting it towards making a viable demo of our game's opening segment. My boss and I (its just the two of us) are both games writers, and I am also a professional video game and board game designer. As the one with knowledge of systems and theory, it kinda just fell to me to handle this until we have the budget for a proper, dedicated Developer.

Tomorrow I'll get to work trying out Character.SetIdleView() instead of persistent animations. Thanks for the help

boosterterrik

Quote from: Khris on Wed 18/01/2023 21:13:57As far as I understand your posts, you're looking for idle animations that play immediately upon entering the room but then have a 15 second pause between them?


Sort of, but the timers will all vary. Its a restaurant scene with most of the game's cast present, and we want it to seem lively. I'm going to work on learning a little more about the Character.SetIdleView command tommorrow to see what I can sort out, but I appreciate your taking the time to create the example code.

On a side note though, as I am brand new to this, how are you getting blocks of code into the forum to look like that?

Snarky

Quote from: boosterterrik on Thu 19/01/2023 01:35:27On a side note though, as I am brand new to this, how are you getting blocks of code into the forum to look like that?

Tip: to see how to get a certain effect in a forum post, just try quoting a post that does it. In this case, it's:

[code=ags][/code]

boosterterrik

Quote from: Snarky on Wed 18/01/2023 21:03:38If you want to have a variety of "idle" animations triggering without any discernible patterns, one way is to use a timer with a random delay, and then pick an animation at random from a selection of animations. You can also combine this with a continuous "rest" animation (whether that's breathing, fidgeting, swaying, or what-have-you) that runs at all other times.


I am missing something, as I am trying to use timers to trigger animations between rests, but I can't get the resting animation to return after the timer animation. Specifically, when I include the "cCharacter.UnlockView()" command, the timer animation no longer triggers at all! Here's what I've been trying,

Code: ags
function room_AfterFadeIn()
{
  
SetTimer(1, 500);

cCastellanos.LockView(7);
cCastellanos.Animate(0, 8, eRepeat, eNoBlock);

cFreddie.SetIdleView(20,5);

cFisher.SetIdleView(3, 15);

cOHalloran.SetIdleView(11, 15);

cLeblanc.SetIdleView(13, 3);

cLorne.SetIdleView(15, 1);

cMarienne.SetIdleView(19, 10);

 if (IsTimerExpired(1)){
  cLeblanc.LockView(12);
  cLeblanc.Animate(0, 4, eOnce, eNoBlock);
  cLeblanc.UnlockView();
  cLeblanc.SetIdleView(13, 3);
  }
}

Previously, I placed the "if (IsTimerExpired(1)" segment in a separate function for repeatedly executing within the room, but I was starting to believe that the reason "setidleview" wasn't working is because it was repeatedly being 'set'.

My core question is: should I be using timers set in the global script instead of the Room script for this sort of thing?

If this is too rudimentary, feel free to ignore it. I've just been experimenting most of the morning and struggling to find the answers.

p.s: I originally was trying

Code: ags

function Room_RepExec()
{
If (IsTimerExpired(1)){
  cLeblanc.LockView(12);
  cLeblanc.Animate(0, 4, eOnce, eNoBlock);
  cLeblanc.UnlockView();
}
else {
cLeblanc.setidleview(13,3)
}
}

and all that did was make her no longer animate at all. I'm feeling as if there is something core to the logic here that I am missing. Again, I don't want to waste anyone's time, I'm just feeling genuinely at a loss.


Snarky

#10
In the top version, this code never runs at all:

Code: ags
 if (IsTimerExpired(1)){
  cLeblanc.LockView(12);
  cLeblanc.Animate(0, 4, eOnce, eNoBlock);
  cLeblanc.UnlockView();
  cLeblanc.SetIdleView(13, 3);
  }

Because at the time the function runs, the timer is not expired, so the test is false. Therefore the view 12 animation is not played, and the idle view is not set, which is why that never plays.

I think your original code was close to correct; I'd just lose the else clause and simply set the idle view once, in room_AfterFadeIn(). I don't believe you need to set it again after the animation ends, and you certainly don't need to set it every game loop; that is presumably why it wouldn't play, because the timeout got reset each time you set it. (If you want the View 12 animation to play repeatedly, though, with the idle animation in between, you need to set the timer again once it has expired.)

Quote from: boosterterrik on Mon 23/01/2023 17:49:09My core question is: should I be using timers set in the global script instead of the Room script for this sort of thing?

No, there's no reason to do that. You can do it just as well in the Room script, and AFAICT from the snippets posted, that is where it belongs.

Cassiebsg

Code: ags
else {
cLeblanc.setidleview(13,3)
}

Quoteand you certainly don't need to set it every game loop; that is presumably why it wouldn't play,

I'll translate this into "regular human" (as in, not a coder) language: The else here is in fact running. The reason why you can't see it animate is because it's being played "40 frames a second" (the default for a loop)... meaning that in a 1/40 of a second it resets (and thus you never get to see more than the 1st sprite).
So next time you put something in rep_exec_always, ask yourself "do I really really need this to run 40 times in a second?" If the answer is no, then there's probably another solution. ;)
There are those who believe that life here began out there...

Khris

I haven't read everything, just want to point out that this:
Code: ags
  cLeblanc.Animate(0, 4, eOnce, eNoBlock);
  cLeblanc.UnlockView();
won't work as expected; a non-blocking animation will not pause the execution while it runs, so UnlockView is called immediately after the Animate command, before the character starts animating.

boosterterrik

Quote from: Khris on Mon 23/01/2023 23:50:46won't work as expected; a non-blocking animation will not pause the execution while it runs, so UnlockView is called immediately after the Animate command, before the character starts animating.


Thank you for this, I had no idea that NoBlock would lead the next commands to immediately begin executing. It makes sense though.

boosterterrik

Quote from: Cassiebsg on Mon 23/01/2023 20:51:44So next time you put something in rep_exec_always, ask yourself "do I really really need this to run 40 times in a second?" If the answer is no, then there's probably another solution. ;)


That makes complete sense, and is the reason I removed the commands from the RepExec function, as I had a feeling it was just locking itself up by never carrying out the command, only triggering it.

My trouble now is that I am struggling to find places to put these animation commands, as setting timers is strangely difficult.

I had hoped it would be, "Set Timer [X] for [Y] frames; when [Y] reaches 0, trigger [Z] animation." But it appears more difficult than that.

boosterterrik

Quote from: Snarky on Wed 18/01/2023 21:03:38You can also combine this with a continuous "rest" animation (whether that's breathing, fidgeting, swaying, or what-have-you) that runs at all other times.


This last part, concerning "runs at all other times," seems to be my sticking point. I can't get "all other times". I'll set up an idle animation for the characters, then, if a different animation is triggered, I can't get them to go back!

The documentation for AGS is both very extensive and well-written in places, while being aggravatingly sparse in others.

For instance, I thought I had a moment of realisation that "BlinkingView" could be used as a "rest" animation, thus solving my issue through the character properties. Then, after taking *way* too long online, I discovered that BlinkingView only triggers during that character's speech. There is *one* mention of that in the documentation, but it is strangely NOT in the place where you are told how to use Character.BlinkingView.

I feel like I need a long weekend with the manual

Snarky

#16
In cases like this, I would first check that it works in the simpler case where you use a blocking animation to interrupt the idle animation. That way we can be more confident that there isn't some other part of the code messing things up.

So I start by confirming that it works with this code (where the animations are called V_IDLE and V_INTERRUPT, respectively—it's better to name the views than to just refer to them by number):

Code: ags
function room_AfterFadeIn()
{
  cLeBlanc.SetIdleView(V_IDLE, 1);
  SetTimer(1, GetGameSpeed()*12);
}

function room_RepExec()
{
  if(IsTimerExpired(1))
  {
    cLeBlanc.LockView(V_INTERRUPT);
    cLeBlanc.Animate(0, 4, eOnce, eBlock);
    cLeBlanc.UnlockView();
    SetTimer(1, GetGameSpeed()*12);
  }
}

And yup, that works.

Given that it works in principle, we now need to make it work with a non-blocking animation. In that case, we have to hold off on doing the UnlockView() and SetTimer() until the animation has finished. How do we do that? Well, let's try to keep checking in room_RepExec() to see if the animation has stopped:

Code: ags
function room_RepExec()
{
  if(IsTimerExpired(1))
  {
    cLeBlanc.LockView(V_INTERRUPT);
    cLeBlanc.Animate(0, 4, eOnce, eNoBlock);
  }
  else if(!cLeBlanc.Animating)
  {
    cLeBlanc.UnlockView();
    SetTimer(1, GetGameSpeed()*12);
  }
}

What happens when we run this? We see immediately that the idle animation stops playing altogether: something has gone wrong. With a little consideration, we should see that this new "else if(!cLeBlanc.Animating)" test will be true before the idle animation starts playing, so we keep calling UnlockView() over and over each game loop. Apparently this resets the idle counter, so the idle animation never triggers. (Edit: And it also keeps resetting the timer, so that it will never time out and trigger the interrupt animation. So we're left with... nothing happening.)

(And actually, there's an undocumented quirk in AGS here: Character.Animating will be false even when the idle animation is playing; it "doesn't count." I believe it's only animations specifically triggered by Character.Animate() that set Character.Animating true, though I'm not sure about Talking and Walking animations.)

How can we fix that? Simple: by making sure we only run this chunk of code when the view is set to V_INTERRUPT, because that must mean it has just finished playing the V_INTERRUPT animation specifically (as long as V_INTERRUPT != any of the other views used by cLeBlanc, such as the NormalView, IdleView, SpeechView if used, etc.):

Code: ags
function room_RepExec()
{
  if(IsTimerExpired(1))
  {
    cLeBlanc.LockView(V_INTERRUPT);
    cLeBlanc.Animate(0, 4, eOnce, eNoBlock);
  }
  else if(cLeBlanc.View == V_INTERRUPT && !cLeBlanc.Animating)
  {
    cLeBlanc.UnlockView();
    SetTimer(1, GetGameSpeed()*12);
  }
}

And now it works.

Snarky

Quote from: boosterterrik on Tue 24/01/2023 14:02:11The documentation for AGS is both very extensive and well-written in places, while being aggravatingly sparse in others.

I think it's mainly that AGS itself is too complex and too inconsistent, with too many features interacting. That's because it's been gradually added to over 25 years, and there probably hasn't been enough streamlining and clearing away of old, unnecessary stuff.

That said, if you report things missing in the manual, it can be updated without much difficulty.

Quote from: boosterterrik on Tue 24/01/2023 14:02:11I feel like I need a long weekend with the manual

Yeah, I think anyone serious about AGS should read through the manual front-to-back at least once. It's not that long. And in older version (up to AGS 3.3) you could browse through all the script APIs in the TOC menu on the left.

Cassiebsg

Let me just add, that you don't need to use the AGS timer, you can create your own or use one of the modules made by others.

Also, like Snarky did, you can always check if the character is animating, and only do something if he's not.
There are those who believe that life here began out there...

Khris

Chaining non-blocking stuff is one of the hardest things to get to work correctly, especially as an AGS beginner.
One way of implementing them is a FSM where the character switches between different (animation/idle) states based on timers or other events.

The good thing is that AGS is pretty flexible; many available functions are convenience functions that can be completely replaced by custom code. You can for instance count frames instead of using timers, or simply set a character's view/loop/frame directly instead of relying on .Animate().

SMF spam blocked by CleanTalk