Typewriter Text Without Blocking

Started by KodiakBehr, Wed 13/01/2016 16:43:30

Previous topic - Next topic

KodiakBehr

I'm using Phemar and Dual's Typewriter Text module, and it's great, but I need to make some adjustments for it to suit my needs and I'd like a second opinion as to whether I'm attempting the impossible.

I'm looking to produce typewriter text without blocking or pausing.  The typewriter text must be occurring while the game code is running.  Because of the way the module uses Wait() commands, I didn't think it would be practical.  But then I started thinking that anything with a wait command in it should also be doable with cleverly used timers.

For context, I'll zoom into the critical slice of code, but I can provide more if needed:

Code: =ags

int i = 0;

  while (i < text.Length) {

          displayedLine = String.Format ("%s%c", displayedLine, text.Chars[i]);
          if (display==eTypewriterDisplay_Overlay)
          {
            olTypedText.SetText (System.ViewportWidth - x, font, color, displayedLine);
          }
          else if (display==eTypewriterDisplay_UIElement)
          {
            if (element.AsButton!=null)   element.AsButton.Text=displayedLine;
            else if (element.AsLabel!=null)     element.AsLabel.Text=displayedLine;    
          }

      if (text.Chars[i] == ' ')
        Wait(delay);
      else if (text.Chars[i] == '[')
        Wait(delay*4);
      else {
        if (sound)
          sound.Play ();
          Wait(delay);
       }
      i++;
  }


I've been experimenting with putting SetTimers in various places and IsTimerExpired checks in various places within the while command, but I can't figure out a way to make the code run correctly without these waits.  Does anyone have any insight into how I could pull this off -- or is it a lost cause?

Snarky

Yes, redoing it to be non-blocking should be very doable. I'd personally go with a hook in repeatedly_execute() rather than a Timer, but that's just a minor tweak.

I don't have time to rewrite it right now, but basically I would refactor the loop into a function and add a "waiting" counter. Then call the function each cycle (whenever there is typewriter text to display), and if the counter is >0 decrement it, otherwise increment i and run the loop body.

Monsieur OUXX

Something like this

Code: ags

////// VARIABLES /////////////////////////////////////////

//all critical variables are now global. Watch out for names conflicts!
int i ;
String text;
int delay;
 
///// SOME LOUSY, NON-BLOCKING WAIT //////////////////////
void NonBlockingWait(int d)
{
	delay = d;
}

//this means that we WERE waiting but just reached zero
bool IsWaitOver()
{	
	return delay==0;
}

//this means that we're not waiting (a.k.a our timer is null or doesn't exist, whatever)
bool IsWaitActive()
{
	return delay>=0;
}
void UpdateWait()
{
	if (delay>=0) delay--;
}

///// MAIN FUNCTION - now made parallel //////////////////

void Update_Rendering()
{
  if (i < text.Length) {
	if (!IsWaitActive())
	{
          displayedLine = String.Format ("%s%c", displayedLine, text.Chars[i]);
          if (display==eTypewriterDisplay_Overlay)
          {
            olTypedText.SetText (System.ViewportWidth - x, font, color, displayedLine);
          }
          else if (display==eTypewriterDisplay_UIElement)
          {
            if (element.AsButton!=null)   element.AsButton.Text=displayedLine;
            else if (element.AsLabel!=null)     element.AsLabel.Text=displayedLine;    
          }
		  
		  if (text.Chars[i] == ' ')
			NonBlockingWait(delay);
		  else if (text.Chars[i] == '[')
			NonBlockingWait(delay*4);
		  else {
			if (sound)
			  sound.Play ();
			  NonBlockingWait(delay);
		   }
	}
	else
	{
		if (!IsWaitOver()) UpdateWait();
		else
			i++;
	}
      
  } else {
	over=true;
  }
}

void Reset_Rendering(String t)
{
	over = false;
	i = 0;
	text = t;
}

bool IsRendering()
{
	if (text==null) return false;
	if (text=="") return false;
	if (i >= text.Length) return false;
	return true;
}

//// REPEATEDLY-EXECUTE //////////////////////////////

void repeatedly_execute()
{
	if (!IsRendering())
	{
		Reset_Rendering("this is a nice text to render")
	} else {
		Update_Rendering();
	}
}
 

KodiakBehr

Thank you so much Monsieur for taking a crack at it.  I'm afraid my inability to understand how your code works is making implementation very difficult.  I'm hoping by giving you more information, it'll be easier to see what I'm doing wrong.

I've taken a sledgehammer to the original typewriter module code to get rid of extraneous features and make things easier to read.  What is currently happening is a series of variables is being passed from GlobalScript to a script called Typewriter.

The GlobalScript code presently looks like this:

Code: =ags
Typewriter.Type(0, 0, 1, 2016, eFontfntAnnouncements, "All I wanted was love and affection", **a given sound file**, lblAnnouncement);


The Typewriter.asc code presently looks like this:

Code: =ags
Overlay *olTypedText;
 
static void Typewriter::Type (int x, int y, int delay, int color, int font,
                              String text, AudioClip *sound, GUIControl *element) {
  
  String displayedLine = "";
  element.OwningGUI.Visible=true;
    if (element.AsButton!=null)
    {
      element.AsButton.Text=displayedLine;
      element.AsButton.TextColor=color;
      element.AsButton.SetPosition(x, y);
      element.AsButton.SetSize(System.ViewportWidth - x, element.AsButton.Height);
    }
    else if (element.AsLabel!=null)
    {
      element.AsLabel.Text=displayedLine;
      element.AsLabel.TextColor=color;
      element.AsLabel.SetPosition(x, y);
      element.AsLabel.SetSize(System.ViewportWidth - x, element.AsLabel.Height);
    }
  
  
  int i = 0;

  while (i < text.Length) {
    
    
    
          displayedLine = String.Format ("%s%c", displayedLine, text.Chars[i]);
            if (element.AsButton!=null)   element.AsButton.Text=displayedLine;
            else if (element.AsLabel!=null)     element.AsLabel.Text=displayedLine;    
          

      if (text.Chars[i] == ' ')
        Wait(delay);
      else if (text.Chars[i] == '[')
        Wait(delay*4);
      else {
        if (sound)
          sound.Play ();
          Wait(delay);
       }
 
      i++;    
   
  }
}



You've taken what is in Typewriter.asc and put it in a new void called Update_Rendering, which presumably wouldn't work if everything is being called from Typewriter.Type?  Is Update_Rendering supposed to go in GlobalScript?  Does Reset_Rendering need to apply new text, when Typewriter.Type should be handling it?  Should IsWaitActive be returning if delay>=1, rather than delay>=0?  Is there a reason why this is more effective than using timers?

I would be grateful for some hand-holding to get where I need to be.

Monsieur OUXX

I'm sorry I cannot spend more time on that, I wrote the code "as is" but I didn't test it and I don't have a test environment. :(
 

Phemar

Take a look at my background speech module. It's very possible to combine the two modules and just add a function which calls the typewriter, and uses the non blocking wait command.

My background speech basically just uses well places timers, with an integer array that stores the order commands are programmed in and an integrr array which tracks which command is currently being played.

Crimson Wizard

I have a module that types text letter by letter in the designated box, non-blocking, and can stop/skip text any time:
AnimatedTextBox.ash
AnimatedTextBox.asc

Perhaps it could be useful as a reference?

KodiakBehr

This is phenomenal.  I wish I noticed this thread was still alive sooner.  Thank you so much!

SMF spam blocked by CleanTalk