What's the least inelegant way to have two characters speak at once

Started by FortressCaulfield, Wed 04/09/2024 12:50:29

Previous topic - Next topic

FortressCaulfield

Like that part in Monkey island where the pirate leaders all speak together occasionally.

One character using say background and another using say seems like the best option, but say_background doesn't trigger animation, I'd also be concerned that both talk messages and both speech cycles are cleared at the same time if the player presses the skip button.
"I can hear you! My ears do more than excrete toxic mucus, you know!"

-Hall of Heroes Docent, Accrual Twist of Fate

Snarky

Pick one of them as the "primary" speaker. For the others, use SayBackground and animate them manually. Then remove the background speech and end the animation when the primary speech ends:

(Rough sketch without all the function arguments, for three characters)

Code: ags
o1 = c1.SayBackground();
c1.LockView();
c1.Animate();
o2 = c2.SayBackground();
c2.LockView();
c2.Animate();
game.bgspeech_stay_on_display = 1;

c3.Say();

o1.Remove();
o2.Remove();
c1.UnlockView();
c2.UnlockView();
game.bgspeech_stay_on_display = 0;

You can wrap up all of this into a convenient function if you're going to do it multiple times.

Danvzare

Wait... isn't "least inelegant" just a roundabout way of saying "most elegant"?  (laugh)

Khris

(Got interrupted in between writing and posting this)

If this is just for a single occasion:

Code: ags
  cChar1.LockView(cChar1.SpeechView);
  cChar1.Animate(cchar1.Loop, 5, eRepeat, eNoBlock);
  Overlay* o1 = cChar1.SayBackground("HA HA HA");
  
  cChar2.Say("HA HA HA");

  // clean up
  if (o1 != null) o1.Remove();
  cChar1.UnlockView();
Not tested.

In general you'll probably want to write a Character extender function and implement a way to clean up all background speech.

Crimson Wizard

I suppose this would require to script a "system" of background talks. Store pointers to talking characters and their background speech overlay in a struct, and have array of such structs.
Make a function for adding background talks, which creates text, starts animation and stores character and overlay references in a free slot of this array.
Make a function for interrupting all the active background talks, which iterates over array, removes overlays, stops animations, and clears slots.
Finally, make a "Update" kind of function run from repeatedly_execute_always, which iterates this array, and tests if a background speech has timed out automatically, in which case stop animation and clear the particular slot.

I would not be surprised if there's already a script module that does this.


Of course it's a very interesting question why AGS does not provide "background speech with animation". In theory, this must be easily doable within the engine itself. I think one of the reasons to not do this unconditionally is that background speech is meant to be used also while character is walking, in which case you cannot automatically play speech animation. But there could have been a separate function, or an argument, that runs speech animation at background, at least until it is overridden by something else.


Rik_Vargard

Maybe you can use timers? So when different timers expire at the same time the characters all will "SayBackground"?

eri0o

The thing about script modules for this is there isn't a clean way to pass your custom say function forward. So you would need to have a custom function to process the events that is meant to run in a loop under rep exec always to consume this and give out any say events so someone can overload them - perhaps passing one string for the background say plus some extra string for data in case the person's background say needs additional parameters.

I imagine it would be like a stack of things you push on top of the other...

Code: ags
// TO-DO: figure a better name
struct BgThings {
  Command* _stack[];
  void Say(Character* c, string message, String extra = 0);
  // ... other commands
};

void BgThings::Say(Character* c, string message, String extra)
{
  Command* cmd = new Command;
  cmd.Type = eCmdSay;
  cmd.Character = c;
  cmd.Text = message;
  cmd.Extra = extra;
  Push(_stack, cmd);
}

// ... Draw the rest of the owl

The problem is designing the API as I do this in a completely different way in real life so it would be easier if people could dream how this would work and imagine a few designs for this.

Snarky

It seems to me that if there were an animated + optionally queued + optionally voiced background speech function, along with an API to interrupt a particular character's background speech or all ongoing background speeches, that's 99% of the problem solved. (The remaining one percent is to integrate it with other custom speech functions. Oh, and that you have to set the game.bgspeech_stay_on_display flag.)

I don't remember exactly what the different modules do, but I seem to recall there's one that does most of this (probably not voice, since there didn't use to be a way to play voice clips as audio clips).

Not sure I understand your point about repeatedly_execute, @eri0o. Just put a repexec in the module, with other APIs that control how it runs via flags, surely?

eri0o

Imagine someone wants to use MySayBackground or BubbleSayBackground, this is why I think I would give out the say specifically and then let people do whatever - since it's the only way to do a callback I know.

Also using my command scheme you could even do If(string expression) Else() and End() to be completely do whatever in background, plus other fun commands - I wrote this in JS once in a custom engine.

Crimson Wizard

If we speak about a generic scheduling module, I once worked on a project that had a "Sequencer" system for running background actions, and I did its restructuring into 2 or more components, where the base component only ran action state logic, updating in rep exec, and other components read the state of their respective groups of actions and performed necessary work.
The trick here was that each component had its own array of action parameters and states. This let to define each custom action's data in a clear way instead of breaking your head about how to pack it in a generic "actiondata" variable(s).

It was a part of someone else game, so I cannot give the exact code out (otoh... I am not sure if I still have it).

But I used similar idea with modules like TypedText, or DragDrop, where a base component does not know what is going on exactly and only sets "state" flags, and then other components do something based on that.

In simple terms, you have a Sequencer which stores an array of Commands for instance. These describe:
- string Command's group name (or component name)
- string Command's name
- Command's state (init, running, suspended, completed...)

Then you make more structs, like CharacterSequencer, which stores array of CharacterCommands.
The CharacterSequencer lets you to sequence character actions, it adds its CharacterCommand object into its array, and then adds a command in base Sequencer, linking CharacterCommand with base Command with an index.
For a dirty example:
Code: ags
int CharacterSequencer::AddCharCommand( Character *c, .... )
{
    int this_index = _nextFreeIndex;
    int base_cmd_index = Sequencer.AddCommand("charactergroup", cmd_name);
    CharCommands[this_index]._CmdIndex = use_cmd_index; // <--- bind charactercommand to a basic command
    CharCommands[this_index]._Char = c;
    CharCommands[this_index]._MoreParams = moreparams;
    return this_index;
}

And then someone may add more "sub-sequencer" components like that.


EDIT:
But frankly, no matter how you look at this, this is ugly, and only is because AGS script does not support proper inheritance and virtual methods (or function pointers).

Snarky

I feel like that goes into overengineering territory. What I have done in some modules is to factor out the calls to engine functions that users may want to replace into its own little function. So in this case, every call to SayBackground could be turned into a call to SayBackgroundBubble (or whatever) by changing one line.

(Ideally you would do it as a macro, but the AGS macro language isn't sufficiently powerful.)

Crimson Wizard

Quote from: Snarky on Sat 07/09/2024 13:27:27What I have done in some modules is to factor out the calls to engine functions that users may want to replace into its own little function. So in this case, every call to SayBackground could be turned into a call to SayBackgroundBubble (or whatever) by changing one line.

I suppose for getting most customization options there has to be 3 replaceable functions, which start, update and finish/cleanup background speech. Even if some of the default ones don't do anything.

eri0o

I think only start and the update and finish can be the same as if it was called directly unless there's a situation I am not seeing.

I think I would have a Say command that is SayBackground and a SayBackground that is SayBackgroundBackground or need to figure some fake Join command that marks "we wait for things before this finish" and then this is accomplished by doing a quick preprocess of all commands to put the things in order and throw a proper wait max in there or something - assume we know the duration in loops of all things in background, then the "Join" preprocess would clean the wait time of things before and just throw a max there with the wait of things before.

Anyway, have a few ideas, but I wanted to work on other things this weekend so I will probably not have time to work on this idea - will let it sit until later...

Crimson Wizard

Quote from: eri0o on Sat 07/09/2024 14:33:30I think only start and the update and finish can be the same as if it was called directly unless there's a situation I am not seeing.

This is a good example of why it is sometimes difficult to understand what you are saying.
Is it "start and the update and finish" - "can be the same as...", or
"only start and the update" (something about them?), and then "and finish can be the same as..."?
Punctuation, or splitting into two separate sentences could've helped. Here I had to spend time figuring this out, trying to fit in the context of the previous posts.

The rest also rather cryptic. I may make guesses, because I understand the concepts of asynchronous programming, and even then there's no guarantee that I understand precisely. But what about other people? This is "Beginners Technical Questions" forum.

eri0o

First the best answer was provided by @Snarky here: https://www.adventuregamestudio.co.uk/forums/beginners-technical-questions/what-s-the-least-inelegant-way-to-have-two-characters-speak-at-once/msg636665461/#msg636665461 .

I don't write on the forums because I talking here, it's just the most constructive way to advance the engine and related things I can contribute when I only have my phone and am not home which is most of the time overall, but unfortunately my keyboard keeps eating what I write (you don't use phones but perhaps anyone else here using a multi language mobile keyboard at least understands that it constantly guesses the language wrong...). Anyway I am just trying to exercise the design of some solution of something since @Khris brought up that all existing modules are old and you already mentioned the proper solution would be co-routines. This exact issue of scheduling background conversation and actions has appeared in the forums over and over so I was just thinking that there has to be some strategy to script some somewhat general solution that could be packaged into a module.

Anyway, I will leave this discussion here and later see if I can throw some sketch in the advance technical later.

SMF spam blocked by CleanTalk