MODULE: SpeechBubble v0.8.0

Started by Snarky, Sun 03/12/2017 11:28:04

Previous topic - Next topic

Snarky

Yup, merged.

Quote from: bx83 on Tue 24/03/2020 10:06:17
What I want to happen:
1. Buddhist speaks, speech bubble appears
2. The sound sample 1 is over; delete this speech bubble, and create the second one; start the second bit of speech
3. When that's over, delete speechbubble, speech sample 2 has obviously stopped because it's non-blocking
4. Fade out screen

I'm not quite sure I understand this description. What do you mean that "speech sample 2 has obviously stopped because it's non-blocking"? Is the behavior you're after different in any way from normal speech behavior? (Show speech text and play speech clip, then show next speech text and next speech clip.)

Quote from: bx83 on Tue 24/03/2020 10:06:17but what is blocking/non-blocking?
Should I just write 'Wait(the length of time for the 2 speech samples)', and included it at the beginning/end of the block of code?

No, definitely not. But again, what do you mean "what is blocking/non-blocking"?

Quote from: bx83 on Tue 24/03/2020 10:06:17The speech bubbles are obviously ending early because they estimate a shorter amount of time to read the bubble than the speech sample has to end;

Well, if you're providing a voice clip it should be using the length of the clip automatically. In fact, it's not calculating anything, just displaying the speech bubble, then calling the AGS Character.Say() function with the speech clip ID as the line and waiting until it's finished.

I think I recall from testing your game before that these lines are extremely long, though. Like, around a minute or so? And you're using the LipSync module as well, right? I seem to remember that there is some limitation somewhere that they overflow (either somewhere in the modules or in AGS). I would first try cutting them down to a more reasonable length and see if that solves things.

Another thing you could test is to replace these SayBubble() calls with Say(). If you still have the problem then it's not an issue with the module.

bx83

Solved: made sound files shorter.

bx83

#82
I am having a compile error on this line:

Code: ags
void SB_sayImpl(this Character*, String message)
{
	//BLR ADDED
  this.SaySync(message);
}


I'm getting the error:
Code: ags
SpeechBubble_0.8.0.asc(19): Error (line 19): '.SaySync' is not a public member of 'Character'. Are you sure you spelt it correctly (remember, capital letters are important)?


...but it is. SaySync is most definitely a member of characters.

However I think I've made a typo around this code, but I can't find it.
It's just shot this error now, after months of not updating it.

here's the scripts:
https://bluekeystudios.com/filez/speechbubble.zip

Snarky

Have you reordered the modules so that TotalLipSync is no longer above SpeechBubble? That's the only explanation I can think of.

bx83

SOLVED!

I didn't even know the order in the right-column for Scripts had a meaning - you learn something every day :D

bx83

I'm using SayAtBubble to position the bubble as I want. I've put two defines into the global.ash to  set x and y coordinates:

in global.ash:
Code: ags

#define hare_x					590
#define hare_y					12


in a conversation with the Hare character:
Code: ags

@6
  cJulius.SayAtBubble(hare_x,hare_y,"&615 How did you get invited to this animal party village green thing?");
  cHare.SayBubble("&16 How'd *you* get invited? You an animal?");
...


...BUT the X coordinate does nothing. It always positions the bubble in the centre of Julius head:



Here's the function code:

Code: ags

void realSayAtBubble(this Character*, int x, int y, String message, GUI* bubbleGui, DynamicSprite* bubbleSprite)
{
  // Render and display the speech bubble
  if(bubbleSprite == null)
    bubbleSprite = this.renderBubble32(message, true);
  Overlay* bubbleOverlay;
  if(bubbleGui == null && _defaultGui == null)
    bubbleOverlay = Overlay.CreateGraphical(x, y, bubbleSprite.Graphic, true);
  else
  {
    if(bubbleGui == null)
      bubbleGui = _defaultGui;
      
    bubbleGui.Clickable = false;
    bubbleGui.X = _clampInt(x, 0, System.ViewportWidth - bubbleSprite.Width);
    bubbleGui.Y = _clampInt(y, 0, System.ViewportHeight - bubbleSprite.Height);
    bubbleGui.Width = bubbleSprite.Width;
    bubbleGui.Height = bubbleSprite.Height;
    bubbleGui.BackgroundGraphic = bubbleSprite.Graphic;
    bubbleGui.Transparency = 0;
    bubbleGui.Visible = true;
  }
  SpeechBubble* bubble = SpeechBubble.Create(this, message, bubbleSprite, bubbleGui, bubbleOverlay);
  
  bubble.setX(x);
  bubble.setY(y);
  bubble.setBackgroundSpeech(false);
  bubble.setThinking(false);
  _addBubbleChar(this);
  
  // Play speech (this chunk blocks until speech is complete)

  String lineNumber = getLineNumber(message);
  // If we have set an invisible font, just call Say() - or whatever custom Say() implementation we have
  if(SpeechBubble.get_InvisibleFont() != -1)
  {
    FontType speechFont = Game.SpeechFont;
    Game.SpeechFont = _invisibleFont;
    this.SB_sayImpl(message);
    Game.SpeechFont = speechFont;
  }
  // Else if we're going to play a voice clip, call Say() with the clip number and a blank line of text
  // (takes care of animation and doesn't display any text). This doesn't work with text-based lip-sync,
  // so if you're using text-based lip-sync, you MUST set an invisible font to get lip-sync to work
  else if(lineNumber != null && Speech.VoiceMode != eSpeechTextOnly) // && !GetGameOption(OPT_LIPSYNCTEXT))
  {
    String s = lineNumber;
    while(s.Length < message.Length)
      s = s.AppendChar(' ');
    this.SB_sayImpl(s);
  }
  // Otherwise we have to do it manually...
  else
  {
    bubble.setAnimating(true);
    this.animateSpeech(message);
  }
  
  
  // Remove the bubble
  bubble.Remove();
}

...

void SayAtBubble(this Character*, int x, int y, String message, GUI* bubbleGui)
{
  if(message == null) return;
  if(!game.bgspeech_stay_on_display)
    _stopAllBackgroundBubbles();
  if((Speech.VoiceMode == eSpeechVoiceOnly && hasVoiceClip(message)) || message == "...") {
    this.SB_sayImpl(message);
  } else {
    DynamicSprite* bubbleSprite = this.renderBubble32(message, true);
    x = this.x - GetViewportX() - bubbleSprite.Width/2;
    x = _clampInt(x, 0, System.ViewportWidth - bubbleSprite.Width);
    
    this.realSayAtBubble(x, y, message, bubbleGui, null);
  }
}

...


// Draw a speech bubble in 32-bit (using transparency)
DynamicSprite* renderBubble32(this Character*, String message, bool talkTail)
{
  // Calculate text dimensions
  int textWidth = _maxTextWidth;
  if(textWidth <= 0)
    textWidth = calculateDefaultTextWidth(this);
  textWidth = _minInt(textWidth, System.ViewportWidth - _paddingLeft - _paddingRight);
  int textHeight = GetTextHeight(message, Game.SpeechFont, textWidth);
  
  textWidth = calculateExactTextWidth(message, Game.SpeechFont, textWidth, textHeight);
  
  // Calculate bubble dimensions
  int totalWidth = textWidth + _paddingLeft + _paddingRight;
  int bubbleHeight = textHeight + _paddingTop + _paddingBottom;
  int totalHeight;
  if(talkTail)
    totalHeight = bubbleHeight + _talkTailHeight;
  else
    totalHeight = bubbleHeight + _thinkTailHeight;
  
  SpeechBubbleHeight_blr=totalHeight;//BLR
  
  // Set up the canvases
  DynamicSprite* bubbleSprite = DynamicSprite.Create(totalWidth, totalHeight, true);
  DrawingSurface* bubbleSurface = bubbleSprite.GetDrawingSurface();
  //bubbleSurface.Clear();
  
  DynamicSprite* bgSprite; DrawingSurface* bgSurface;
  DynamicSprite* borderSprite; DrawingSurface* borderSurface;
  if(_backgroundTransparency == 0)
  {
    bgSprite = bubbleSprite;
    bgSurface = bubbleSurface;
  }
  else
  {
    bgSprite = DynamicSprite.Create(totalWidth, totalHeight, true);
    bgSurface = bgSprite.GetDrawingSurface();
  }
  if(_borderTransparency == 0)
  {
    borderSprite = bubbleSprite;
    borderSurface = bubbleSurface;
  }
  else
  {
    borderSprite = DynamicSprite.Create(totalWidth, totalHeight, true);
    borderSurface = borderSprite.GetDrawingSurface();
  }
  
  int bgColor = mixColors(this.SpeechColor, _backgroundColor, _backgroundSpeechTint);
  int borderColor = mixColors(this.SpeechColor, _borderColor, _borderSpeechTint);
  
  // Draw!
  bgSurface.DrawingColor = bgColor;
  bgSurface.DrawRectangle(1, 1, totalWidth-2, bubbleHeight-1);
  drawRoundedCorners32(bgSurface, borderSurface, borderColor, 0, bubbleHeight);
  String tail[]; int tailWidth; int tailHeight;
  if(talkTail)
  {
    tail = _talkTail; tailWidth = _talkTailWidth; tailHeight = _talkTailHeight;
  }
  else
  {
    tail = _thinkTail; tailWidth = _thinkTailWidth; tailHeight = _thinkTailHeight;
  }
  bgSurface.DrawingColor = bgColor;
  bgSurface.drawPixelArray(tail, totalWidth/2-tailWidth, bubbleHeight, tailWidth, tailHeight, 'O', false, false);
  borderSurface.DrawingColor = borderColor;
  borderSurface.drawPixelArray(tail, totalWidth/2-tailWidth, bubbleHeight, tailWidth, tailHeight, 'X', false, false);
  borderSurface.DrawLine(_cornerRoundingRadius, 0, totalWidth - _cornerRoundingRadius, 0);
  // Left Line
  borderSurface.DrawLine(0, _cornerRoundingRadius, 0, bubbleHeight - _cornerRoundingRadius);
  // Right Line
  borderSurface.DrawLine(totalWidth-1, _cornerRoundingRadius, totalWidth-1, bubbleHeight - _cornerRoundingRadius);
  // Bottom Lines
  borderSurface.DrawLine(_cornerRoundingRadius, bubbleHeight, totalWidth/2 - tailWidth, bubbleHeight);
  borderSurface.DrawLine(totalWidth/2, bubbleHeight, totalWidth - _cornerRoundingRadius, bubbleHeight);
  
  if(_backgroundTransparency != 0)
  {
    bgSurface.Release();
    bubbleSurface.DrawImage(0, 0, bgSprite.Graphic, _backgroundTransparency);
    bgSprite.Delete();
  }
  if(_borderTransparency != 0)
  {
    borderSurface.Release();
    bubbleSurface.DrawImage(0, 0, borderSprite.Graphic, _borderTransparency);
    borderSprite.Delete();
  }
  
  bubbleSurface.DrawingColor = this.SpeechColor;
  int outlineColor = mixColors(this.SpeechColor, _textOutlineColor, _textOutlineSpeechTint);
  if(_textOutlineWidth > 0)
    bubbleSurface.drawStringWrappedOutline(_paddingLeft, _paddingTop, textWidth, _textOutlineStyle, Game.SpeechFont, _textAlign, message, _textTransparency, outlineColor, _textOutlineWidth);
  else
    bubbleSurface.drawStringWrappedAA(_paddingLeft, _paddingTop, textWidth, Game.SpeechFont, _textAlign, message, _textTransparency);
  
  bubbleSurface.Release();
  return bubbleSprite;
}


I can include more functions; I've altered like 10 lines of code in this module, but the above *looks* okay...???

bx83

#86

Snarky

It's because of one of the lines you've added to SayAtBubble():

Code: ags
    x = this.x - GetViewportX() - bubbleSprite.Width/2;


Why have you changed the module code?

bx83

I don't know... :/
I'll test it.

bx83


Dave Gilbert

#90
Hello! Experimenting with this module and really digging it so far!

I naturally have a question about something. In my (1080 x 1920 resolution) game, the player character is communicating with someone by radio, and I wanted to give the radio guy his own bubble. I created a function that sets the radio guy's bubble position 300 pixels behind where the player is standing. Here are the results.

at player.x-300:


at player.x+300:


As you can see, the second speech bubble is significantly further away than the first. Any idea what is causing that?

Also, while I'm here, is there any way to temporarily get rid of the bubble's tail?

Thanks much!

-Dave

Dave Gilbert

#91
Woo. I managed to find a work around. I created an invisible character for the radio guy that changes it's position just before it speaks, and used the regular "SayBubble" command instead of "SayAtBubble." The positioning is much more consistent now.

Also I discovered the Screen2Gif software:



That said, I still would like to remove the bubble tail whenever the radio guy speaks!  Any clues on how to do that?

Snarky

Hmm. The way it's intended to work is that you supply a new TalkTail array:

Code: ags
  String tail[] = new String[5];
  tail[0] = " OOOO";
  tail[1] = "  OOO";
  tail[2] = "   OO";
  tail[3] = "    O";
  tail[4] = null;
 
  SpeechBubble.TalkTail = tail;


If you set it to an array with just one entry, null, that would disable the tail completely. However, this feature is broken, because AGS doesn't actually allow you to set arrays via attributes. :~(

To address this and other limitations, I did a complete rewrite of the module a couple of years ago, to make it easier to customize the appearance (it would allow you to set the tail as an array or as a sprite), but never published it because I didn't get around to reimplementing 100% of the old functionality. There's also a longstanding issue with the module causing momentary freezes unless you supply an invisible font.

There are currently various branches and hacked versions floating around the forums. I really should clean it all up and post a new release. Which AGS engine version are you targeting?

Dave Gilbert

#93
I'm using the latest version of AGS. I'm still at the early stages of my project so I still update it when ever a new version comes out.

edit: Wait, so you can change the shape of the tail using those arrays? I was wondering what those were for. I'm curious how that works. Is there any documentation on how that is done?

Dave Gilbert

Aha! Figured it out and found a workaround! I just set the tail to this shape:

Code: AGS

function setNozzoTail()
{
  _nozzoTalkTail = new String[10];
  _nozzoTalkTail[0] = "XXXXXXXX";
  _nozzoTalkTail[1] = "        ";
  _nozzoTalkTail[2] = "        ";
  _nozzoTalkTail[3] = "        ";
  _nozzoTalkTail[4] = "        ";
  _nozzoTalkTail[5] = "        ";
  _nozzoTalkTail[6] = "        ";
  _nozzoTalkTail[7] = "        ";
  _nozzoTalkTail[8] = "        ";
  _nozzoTalkTail[9] = null;
  SpeechBubble.set_TalkTail(_nozzoTalkTail);   
}


And that did the job!

Dave Gilbert

Hello! I have encountered another issue, although I am not sure what causes it. I notice that when I click through dialog, occasionally the game registers an extra click and either clicks through the next piece of dialog or (if it's the end of the dialog) activates a click where-ever the mouse cursor happens to be. This happens only very sporadically, but consistently (at least once every five minutes or so) and there's no common cause that I can see.

When I de-activate the bubbles and go back to regular speech, the problem goes away. So I am pretty sure it involves the bubbles in some way. Do you have any idea what might cause this to happen?

Snarky

 :-\

My guess is that it's to do with the Wait() logic that emulates speech blocking. There have been a number of reported bugs linked back to this part of the code (I think I've even heard something like this reported before). Crimson Wizard and the other AGS devs are currently working on extending the Wait() API so it will support the same behavior as speech blocking (e.g. block indefinitely until there's a mouse or keyboard press) without complicated workarounds.

Meanwhile, if you haven't already done so, I would strongly suggest setting and activating an invisible font, which will bypass all this logic and instead just use the built-in AGS speech blocking. There's a link to download one on the GitHub page for the module: http://www.angelfire.com/pr/pgpf/if.html

eri0o

AGS has a time for which things are ignored after the end of a speech, the unforgettable Game.IgnoreUserInputAfterTextTimeoutMs. This prevents spurious clicks after ending the speech, by blocking input for some milliseconds every time text is skipped or ends by timeout. Maybe there's a way to implement it in SpeechBubble?


Snarky

#98
Yup, I'm aware ;) (I believe I was actually the one who suggested that feature originally â€" Edit: indeed), but it would not have any effect in this case, since it's not happening after a timeout, but when the user actively clicks away the dialog.

It's something that would probably be worth adding (though it couldn't be 100% the same, since I can't control what happens once the dialog is over), but only if and when this and related bugs are fixed.

eri0o

Here's how the thing you asked got implemented: https://github.com/adventuregamestudio/ags/blob/760635024c547face5ee423d62ccbf9abc7976f8/Engine/ac/display.cpp#L311

Maybe it helps to figure out how to add this in the module. :/

SMF spam blocked by CleanTalk