Buttons to go back and forth through Dialog

Started by Salzi, Mon 25/02/2019 23:00:40

Previous topic - Next topic

Salzi

Hey there!

I got a dialog going on that displays answers sentence by sentence:

Code: ags

@1
  cAlce.SayAt(400, 530, 700, "&2 Lord Buho, amable como es, prestó tres de sus obras más valiosos al museo.");
  cAlce.SayAt(400, 530, 700, "&3 Hace tres días, en concreto el domingo por la noche, fueron robadas.");
  cAlce.SayAt(400, 530, 700, "&4 Desgraciadamente aquel día no estuve aquí, sino en Alemania...");
  cAlce.SayAt(400, 530, 700, "&5 porque me habían invitado a participar en la convención de repollo.");


I would like to add a button to my GUI displaying the text that lets the player skip to the next sentence and another to enable the player to go back to past sentences and read it again.
Another button should play the speech again.

Do you have any ideas how I would go about that?

Cheers in advance!

morganw

Would this be a one-time thing in a particular room or would you want the whole game to have dialogue that can skip and rewind with GUI buttons?

Salzi

Quote from: morganw on Mon 25/02/2019 23:21:13
Would this be a one-time thing in a particular room or would you want the whole game to have dialogue that can skip and rewind with GUI buttons?

It should be this way in the entire game, but since I am using individual rooms for each dialog, it doesn't really matter.  :)

morganw

The main issue is that Character.Say or Character.SayAt are blocking functions which stop interactions with GUI controls, so the main options for skipping are based around defining a 'Cutscene' and choosing a 'SkipStyle':
https://www.adventuregamestudio.co.uk/manual/ags54.htm#StartCutscene
https://www.adventuregamestudio.co.uk/manual/ags75.htm#Speech.SkipStyle

Someone else was recently doing something similar, where there were showing some lines of speech which functioned as a tutorial, and they wanted to have GUI button to skip the tutorial. They could work around the problem be using Character.SayBackground instead, but only because they didn't need to use a talking view on the character:
https://www.adventuregamestudio.co.uk/manual/ags47.htm#Character.SayBackground
...basically this is a fake say command which is non-blocking. You'd have to manually re-implement code for talking animations (if you needed them) and have some code working through a list of the dialogue that you wanted to show. How hard this is probably depends on how many branches the dialog has.

Would you need to use the talking view on the character and does the dialog in each room ever branch?

Snarky

Yeah, you'll need to basically reimplement the whole speech system. Two main tasks:

-Write a version of Character.Say() that allows you to interact with multiple buttons while the line is displayed. (To make this work even while voice-over is playing requires additional hacks.)
-Write some structure to hold the rewindable dialogs, so that you can use one common bit of code to control the progression. How best to do this depends on exactly how you want the dialog logic to go. If there's any branching whatsoever, it becomes much more complicated.

Years ago, I wrote a version of something similar to implement Phoenix Wright-style testimony (where you have one looping dialog you can navigate forward or backward, and where you can press or object to each statement, introducing branching). For that I used the StateMachine module, wrote another module special-casing that, and then had to code a script for each dialog.

It's by no means the most efficient way to do it (I had barely any idea of what I was doing; if I were to do it now I'd probably start from scratch with something better suited to my purpose), so I'm not really recommending this approach or standing by the code quality, but here's the module:

Code: ags
// TESTIMONY MODULE HEADER

// Convenience class to set up StateMachines for testimonies more easily
struct Testimony extends StateMachine
{
  import static void Objection();

  import void Init(String name,  int lines);
  import void AddObjection(String fromState);
  import void AddPress(String fromState);
  import void AddExit(String fromState);
  
  import void SetObjection(String fromState);
  import void SetPress(String fromState);
  protected import void SetNewState(String fromState, String newState);
  
  import bool HasTransition(String fromState, String transition);
};

import Testimony Testimony_Current;

import function Testify(this Character*, String toSay, bool cross = true, int loop = 0, TextSpeed = Normal, int timeout = 0, bool append = false);


// TESTIMONY MODULE SCRIPT
Testimony Testimony_Current;

void Testimony::Init(String name, int lines)
{
  this.Load(name, "line1");

  int i=1;
  while(i<=lines)
  {
    String thisState = String.Format("line%d",i);
    String lastState = String.Format("line%d",i-1);
    String pressState = thisState.Append("_press");
    String objectState = thisState.Append("_object");

    this.AddState(pressState);
    this.AddState(objectState);
    
    if(i>1)
    {
      this.AddState(thisState);
      this.AddTransition(lastState, "next", thisState);
      this.AddTransition(thisState, "back", lastState);
    }
    this.AddTransition(thisState, "press", pressState);
    this.AddTransition(pressState, "back", thisState);
    this.AddTransition(thisState, "object", objectState);
    this.AddTransition(objectState, "back", thisState);
    i++;
  }

  this.AddState("loop");
  this.AddTransition(String.Format("line%d",lines), "next", "loop");
  this.AddTransition("loop", "return", "line1");
}

void Testimony::AddObjection(String fromState)
{
  this.AddState(fromState.Append("_objected"));
  this.AddState(fromState.Append("_objected_object"));
  this.AddState(fromState.Append("_objected_press"));
  this.AddTransition(fromState.Append("_object"), "next", fromState.Append("_objected"));
  
  this.AddTransition(fromState.Append("_objected"), "object", fromState.Append("_objected_object"));
  this.AddTransition(fromState.Append("_objected"), "press", fromState.Append("_objected_press"));

  this.AddTransition(fromState.Append("_objected_object"), "back", fromState.Append("_objected"));
  this.AddTransition(fromState.Append("_objected_press"), "back", fromState.Append("_objected"));
}

void Testimony::AddPress(String fromState)
{
  this.AddState(fromState.Append("_pressed"));
  this.AddState(fromState.Append("_pressed_object"));
  this.AddState(fromState.Append("_pressed_press"));
  this.AddTransition(fromState.Append("_press"), "next", fromState.Append("_pressed"));
  
  this.AddTransition(fromState.Append("_pressed"), "object", fromState.Append("_pressed_object"));
  this.AddTransition(fromState.Append("_pressed"), "press", fromState.Append("_pressed_press"));

  this.AddTransition(fromState.Append("_pressed_object"), "back", fromState.Append("_pressed"));
  this.AddTransition(fromState.Append("_pressed_press"), "back", fromState.Append("_pressed"));
}

void Testimony::AddExit(String fromState)
{
  this.AddState("exit");
  this.AddTransition(fromState, "return", "exit");
}

protected void Testimony::SetNewState(String fromState,  String newState)
{
  if(this.GetStateID(newState) != -1) // If the objected state exists...
  {
    // Rewire "back"
    int t_back = this.GetTransitionID(fromState, "back");
    if(t_back != -1)
    {
      String lastState = this.GetTransitionToStateByID(t_back);
      this.AddTransition(lastState, "next", newState);  // Overwrite
      this.AddTransition(newState, "back", lastState);
    }
    // Rewire "next"
    int t_next = this.GetTransitionID(fromState, "next");
    if(t_next != -1)
    {
      String nextState = this.GetTransitionToStateByID(t_next);
      this.AddTransition(newState, "next", nextState);
      this.AddTransition(nextState, "back", newState);
    }
  }
}

void Testimony::SetObjection(String fromState)
{
  String newState = fromState.Append("_objected");
  this.SetNewState(fromState, newState);
}

void Testimony::SetPress(String fromState)
{
  String newState = fromState.Append("_pressed");
  this.SetNewState(fromState, newState);
}

bool Testimony::HasTransition(String fromState, String transition)
{
  return (this.GetTransitionID(fromState, transition) != -1);
}

static void Testimony::Objection()
{
    gObjection.Visible = true;
    WaitMouseKey(80);
    gObjection.Visible = false;
}

function Testify(this Character*, String say, bool cross, int loop, TextSpeed speed,  int timeout, bool append)
{
  if(cross)
    SetDialogBlocked();
  this.Speak(say, loop, speed, timeout, append);  // This is my custom Say() function that allows you to interact with GUI controls
  if(cross)
    gCrossExamine.Visible = true;  // You can't cross-examine on the first loop
}

export Testimony_Current;


And here's how you'd script a particular dialog:

Code: ags
// EVELYN TESTIMONY - ROUND 1 

// Set up this the state machine for this testimony
void Testimony_Evelyn1_Init()
{
  Testimony_Current.Init("Evelyn1", 3);  // The main loop has three lines
  Testimony_Current.AddPress("line2");   // Pressing on line2 changes it to another line
  Testimony_Current.AddObjection("line2_pressed");  // Objecting to this changed line leads to another branch
  Testimony_Current.AddExit("line2_pressed_object"); // This leads to the end of the dialog
}

// This is the main interaction loop
void Testify_Evelyn1(bool cross)
{
  if(Testimony_Current.CurrentState == "line1")
  {
    cEvelyn.Testify("REDACTED", cross);
  }
  else if(Testimony_Current.CurrentState == "line2")
  {
    cEvelyn.Testify("REDACTED", cross);
  }
  else if(Testimony_Current.CurrentState == "line2_press")
  {
    Testimony.Objection();
    cDennis.Speak("REDACTED");
    cDennis.Speak("REDACTED");
    cEvelyn.Speak("...");
    cEvelyn.Speak("REDACTED");
    cDennis.Speak("REDACTED");
    cEvelyn.Speak("REDACTED");
    cEvelyn.Speak("REDACTED");
  }
  else if(Testimony_Current.CurrentState == "line2_pressed")
  {
    cEvelyn.Testify("REDACTED");
  }
  else if(Testimony_Current.CurrentState == "line2_pressed_object")
  {
    Testimony.Objection();
    cDennis.Speak("REDACTED");
    cEvelyn.Speak("'REDACTED");
    cDennis.Speak("REDACTED");
    cEvelyn.Speak("REDACTED");
    cDennis.Speak("REDACTED");
    cDennis.Speak("REDACTED");
    cEvelyn.Speak("REDACTED");
  }
  else if(Testimony_Current.CurrentState == "line3")
  {
    cEvelyn.Testify("REDACTED", cross);
  }
  else if(Testimony_Current.CurrentState == "loop")
  {
    cDennis.Speak("REDACTED");
    cJessica.Speak("REDACTED");
  }
}


I'm mainly posting this to give you some sense that with branching dialog, this could be a fairly complicated task.

Salzi

#5
Dammit, that sounds very complicated. Since I am still fairly new to AGS, I think it may be a step too far for me as of right now  ??? Especially because I have audio playing aswell.

Is there a way to simulate it maybe? I could imagine a button that sends you to a dialog option that displays the past text or something, just to show what I want to do for now. (Context: Making the game for a University project so making it for a presentation)
The challenge then is just to implement the button. Maybe it could be a second GUI?

Thanks for your answers, let me know what you think  8-)

SMF spam blocked by CleanTalk