[SOLVED] Is there any non-blocking equivalent to Character.Say beside SayBackground?

Started by imagazzell, Sun 17/12/2023 06:53:13

Previous topic - Next topic

imagazzell

Hi all,

Okay, here's the premise of what I'm trying to do: simply put, I want the player to be able to click a button on a custom GUI while another character is speaking (LucasArts-style).

I think I've got most of it handled. The one big sticking point, as far as I can see, is Character.Say being a blocking function, making the mouse/cursor unavailable while the character is talking.

I don't think I want to use SayBackground, because I want the character's speaking animation and speech clip to play like normal. I suppose, with no other option, I could fake it with SayBackground, a non-blocking animation of the character's talking view, and audio clips of the lines of dialogue, but that seems a bit clunky and sacrifices lip-syncing.

Is there any other (better) way to do this?

Thanks in advance for any tips.
Music Composing Services: https://youtu.be/BbT3kfhgA4E

Kastchey

What action should be triggered by the custom GUI button?

You could display the cursor movement during a blocking function like Character.Say by creating a GUI control that looks like a cursor, and adding this in repeatedly_execute_always:

  btnCursor.X = mouse.x;
  btnCursor.Y = mouse.y;

but you cannot call a blocking function from repeatedly_execute_always, so whatever immediately happens when the player hovers over the button area would need to be non-blocking as well.
Not sure if it helps anything though, since mouse clicks are (I think) handled on game loops.

Crimson Wizard

Quote from: imagazzell on Sun 17/12/2023 06:53:13I don't think I want to use SayBackground, because I want the character's speaking animation and speech clip to play like normal. I suppose, with no other option, I could fake it with SayBackground, a non-blocking animation of the character's talking view, and audio clips of the lines of dialogue, but that seems a bit clunky and sacrifices lip-syncing.

Making a custom (blocking or non-blocking) speech is a rather common case in AGS games today, and it's done more or less how you described:
- Display speech using a text overlay or gui (note that SayBackground is practically a equivalent to Overlay.CreateTextual);
- Play voice using Game.PlayVoiceClip (available since AGS 3.5.0);
https://adventuregamestudio.github.io/ags-manual/Game.html#gameplayvoiceclip
- Lipsyncing is a bigger issue. I've heard there's a module for custom lipsyncing, but I do not know a lot of details about it, so that's something to investigate: https://www.adventuregamestudio.co.uk/forums/modules-plugins-tools/module-totallipsync-v0-5/

To make the above easier to use, these actions are coded into a extender function, so to be able to call like "cCharacter.MyBackgroundSay(...)".
https://adventuregamestudio.github.io/ags-manual/ExtenderFunctions.html

All the above makes sense if you plan to have this behavior throughout the game. If you only need this few times in game, then perhaps it's better to design the event differently, to make things easier for you.

imagazzell

Thanks for the replies, guys. I'm still having trouble getting it to work. Perhaps a bit more context would be helpful...

I'm trying to create a little mini-game within my game in which the player must repeatedly click a button on a custom GUI for as long as the NPC is speaking in order to succeed. I suppose you could call it something like a quick time event.

The scripting for it is currently spread across a few modules.

The first one simply handles the animation of a meter on the custom GUI that the player must keep from running out by clicking the button repeatedly. That all works fine, as tested on its own without characters speaking. The function should be called at the start and end of the quick time event with "meter_toggle(int on_off)" (where 1 = on, 0 = off), to start and stop the animation of the meter.

In the script for the room where the quick time event happens, I have some simple commands for what happens if the meter DOES run out, under the repeatedly_execute_always function. Again, no issue here so far.

The bulk of the mini-game is handled in a dedicated script for the QTE, with the relevant portions as follows (originally):

Code: ags
gMeter.Visible = true;
meter_toggle(1);

cNPC.Say("&1 Blah, blah, blah.");

meter_toggle(0);

But, as was the point of my OP, since Character.Say is a blocking function, this doesn't allow the player to click the GUI button.

So I tried implementing Crimson's suggestion...

Code: ags
gMeter.Visible = true;
meter_toggle(1);

Overlay* myOverlay = Overlay.CreateTextual(50, 80, 120, Game.SpeechFont, 15, "Blah, blah, blah.");
Game.PlayVoiceClip(cNPC, 1, true);
    
meter_toggle(0);

...but I must be doing something wrong because, while it displays the text, the voice clip does not play, and the script races onward to the subsequent lines. I'll also need to figure out how to implement myOverlay.Remove after the audio stops playing.

And I'll still need to work in playing the character's speaking animation (sans lip-sync) somewhere, but I'm trying to get one step to work at a time.

Any further advice?


PS: Kastchey, I like the clever workaround for getting the cursor to show, but like you said, clicks are handled on game loops, so I don't think that's going to do the trick here.
Music Composing Services: https://youtu.be/BbT3kfhgA4E

Snarky

Quote from: imagazzell on Sun 17/12/2023 23:03:46...but I must be doing something wrong because, while it displays the text, the voice clip does not play, and the script races onward to the subsequent lines.

The fact that the script "races on" is the whole idea of making it non-blocking. If you don't want to block but also don't want to run the rest of the script right away, you need to move it to some other function, and only call it when it is supposed to run. One common way to do this is by starting a Timer and then running the rest of the script when it finishes, but it depends on how exactly you want it to work.

That the voice clip doesn't appear to play is probably because you do something else later in the script that stops or supersedes it, like making another Say() call.

imagazzell

Thank you, Snarky. Timers are one thing I was definitely missing in my scripting, and using one has gotten me much closer. I've got it set up so that the subsequent scripts only happen once the timer runs out, allowing the voice clip to play as desired. I've even got the pseudo speaking animation working.

The only issue I'm experiencing now is that my overlay disappears as soon as my script cuts away to another function (as I'm sure it's designed to do), but I need it to stay displayed while my script bounces around between other functions. Can this be done?

I can't use Wait commands in conjunction with the timer, because the player needs un-blocked control while it's running.

I tried putting the Overlay.CreateTextual command (with conditional statements) in repeatedly_execute and repeatedly_execute_always, but AGS didn't seem to like that and kept crashing.

How else can I keep the overlay displayed when leaving the function in which it was generated?
Music Composing Services: https://youtu.be/BbT3kfhgA4E

Crimson Wizard

Quote from: imagazzell on Tue 19/12/2023 01:02:44How else can I keep the overlay displayed when leaving the function in which it was generated?

As noted in the manual, for example here:
https://adventuregamestudio.github.io/ags-manual/Overlay.html#overlaycreategraphical

Quoteif the Overlay variable goes out of scope, the overlay will be removed. Hence, if you want the overlay to last on-screen outside of the script function where it was created, the Overlay* variable declaration needs to be at the top of the script and outside any script functions.

To put it simpler, make a global variable and store Overlay* pointer there.
The Overlay is removed either when all variables that reference it go out of scope, assigned null, or when you explicitly call Overlay.Remove.

This issue is the same with every other dynamically created object in AGS: String, Overlay, DynamicSprite, DrawingSurface, File (and anything I forgot).

imagazzell

Thanks, Crimson, but I already did have the Overlay pointer declared in Global Variables (I apologize if my last post was confusing on that; when I said "the function in which it was generated" I meant the function in which the speech text was generated, not the pointer).

Here's my "quick time event" script:

Code: ags
function quick_time_event()   // called in room or dialog scripts
{    
  gMeter.Visible = true;   // makes the custom meter GUI visible
  meter_toggle(1);   // activates meter animation in the meter GUI, handled in a separate script
   
  SetTimer(1, 360);
  cNPC.LockView(NPCSPEACH);
  cNPC.Animate(cNPC.Loop, 4, eRepeat, eNoBlock);
  overlay.CreateTextual(50, 80, 120, Game.SpeechFont, 15, "Blah blah blah.");
  Game.PlayVoiceClip(cNPC, 1, true);
}

function player_response()   // what happens after NPC's speech, called by repeatedly_execute function below
{
  cNPC.LockView(NPCSPEACH);   // to stop NPC speaking animation
  cNPC.UnlockView();
  meter_toggle(0);   // deactivates meter animation and hides meter GUI
  cEgo.Say("&1 Response.");
}

function repeatedly_execute()
{
  if (gMeter.Visible == true && IsTimerExpired(1)) {
    player_response();   // launches the above function once the timer expires, only if the meter GUI is visible, so that the function doesn't run repeatedly
  }
}

The NPC's voice clip plays while the player's response correctly waits, the speaking animation starts and stops as expected, and the custom GUI appears, functions, and disappears as desired. The only thing not working right is the Overlay text, which, as noted, disappears as soon as the quick_time_event function reaches its end (before the voice clip finishes playing), despite the Overlay pointer being defined in Global Variables.

So what does it look like I'm doing wrong here? (Sorry if it's something obvious that just hasn't clicked with me yet.)
Music Composing Services: https://youtu.be/BbT3kfhgA4E

Crimson Wizard

This is wrong:
Code: ags
overlay.CreateTextual(50, 80, 120, Game.SpeechFont, 15, "Blah blah blah.");

CreateTextual is a static function, which means that it has to be called "from type", rather than from a variable, and its result assigned to a variable:
Code: ags
overlay = Overlay.CreateTextual(50, 80, 120, Game.SpeechFont, 15, "Blah blah blah.");

This is assuming that lowercase "overlay" is your variable.

imagazzell

Eureka! That did it, Crimson. Much thanks to you and Snarky. Your encyclopedic knowledge of the program never fails to impress! It may be elementary to you, but to me, even after years of playing with AGS, there are just so many little things that trip me up that you all set me straight on, seemingly with but a snap of your fingers. Thanks again!
Music Composing Services: https://youtu.be/BbT3kfhgA4E

SMF spam blocked by CleanTalk