A Few More Noob Questions for my Dialogue GUI

Started by Custerly, Tue 12/12/2023 19:44:22

Previous topic - Next topic

Custerly

Hi all,

Firstly, thank you to this community for the help you've offered thus far. You guys are great, and I'm trying not to unduly bother you here, but I am a noob, and I've not been able to find answers to these questions online.

My dialogue GUI has been progressing nicely, but I've hit a couple more stumbling blocks:

Is there a way to enable text wrapping on button text? I'm using buttons for the player dialogue options, but I've found that when the dialogue is too lengthy, it is cut off at the right edge of the button. I even tried inserting a line break manually with
Code: ags
dgOpt1.Text = dgOpt1.Text.Append ("[")
as I've done in the past with label text, but that doesn't seem to work on a button.

Once I crack that, I'll need the dialogue function to automatically adjust the button height to provide space for the 2nd line of text. I've already got the dialogue options setup to adjust its y position dynamically based on the position of other dialogue options and its own height.

Lastly, I am trying to setup hotkeys, so that during dialogue, the player can click the associated number button to select  a dialogue option. I thought this would do the trick
Code: ags
if (keycode == eKey1) dgOpt1_OnClick(GUIControl *control, MouseButton button);
but that brings about this error: "GlobalScript.asc(983): Error (line 983): Parse error in expr near 'MouseButton'
".

Any help is much appreciated.

Rub

#1
What about using a GUI Label, which does wrap text, then you make a button that is transparent (or made to blend in someway) over the GUI Label.
Below the GUI Label you just need something to fit your menu or background.

Snarky

As you've discovered, Button texts do not support linebreaks. Another possible workaround (apart from putting a label over it) is to draw the text to a DynamicSprite using DrawingSurface.DrawStringWrapped, and set that as the button Graphic. (But if you set a graphic, the default gray button isn't drawn, so if you want that to appear normally you would have to draw it yourself.)

As for the second problem, it's probably that you cannot call a function that is declared further down in the script. If you move the OnClick function above the function where you call it, that should fix that—but it might still not work, because I don't think the normal click-handling functions run during dialog selection. However, there is a game setting called "Number dialog options" that should do what you want without any code.

Crimson Wizard

#3
The second problem is because the syntax of a function call is wrong.

Code: ags
dgOpt1_OnClick(GUIControl *control, MouseButton button)

This is a function declaration, it mentions the argument types and argument names.

When you call a function, you need to pass actual values there, for example:

Code: ags
dgOpt1_OnClick(dgOpt1, eMouseLeft);


I'm not sure if this will work at all in your case, because neither gui buttons nor regular "on_key_press" function works during dialog options. Maybe this is not standard dialog options, but some kind of a custom dialog system?

Custerly

#4
@Crimson Wizard to the rescue again!

Your solution worked (this is a custom dialogue system I'm making, so hotkeys aren't disabled during dialogue).

I did indeed copy the function declaration from the global script, hoping that was the info I needed to call the function. As you can see, I have cobbled together my "knowledge" of AGS as a first-time coder through just playing with it, and that has resulted in a very flawed understanding of how some of these basic things work.

If I understand correctly, what your corrected code did was to call on the function, and the info in the brackets are parameters that the function uses when it runs. Is that correct? Assuming so, I understand what the second parameter does (gives the dgOpt1_OnClick function a left click as input), but why does it require the first parameter (dgOpt1) when I've already stated [if (keycode == eKey1) dgOpt1_OnClick...]? That first parameter doesn't seem to actually serve any purpose. For arguments sake, I tried switching that first parameter to dgOpt3 like this
Code: ags
if (keycode == eKey1) dgOpt1_OnClick(dgOpt3, eMouseLeft);
and it still triggered the dgOpt1_OnClick function.

Custerly

#5
@Rub & @Snarky, thanks for your input regarding the button's inability to support text wrapping. I was worried someone would say that, but your suggestion to put a label over the button is very smart and will work well in my situation (my gui already uses buttons with transparent background, as I'm trying to emulate the dialogue gui of classic crpgs).

This still leaves me with 1 problem: How can I have the height of the label adjust dynamically when the text is so long that it wraps to the next line? This is essential so that the buttons/labels can be aligned neatly one above the next for the player. Is there a way to determine the pixel length of the text, and once it passes a certain threshold, I make [label name].Height = [label name].Height + [height of a single text line]?

Khris

#6
Quote from: Custerly on Wed 13/12/2023 02:35:24why does it require the first parameter (dgOpt1) when I've already stated [if (keycode == eKey1) dgOpt1_OnClick...]? That first parameter doesn't seem to actually serve any purpose.

The control/button is passed along because you can use a single function to handle multiple events to avoid duplicate code.
If I were to code this I'd call the function "dgOpt_OnClick", then paste this name into each dialog button's event table. This way, all button clicks cause the same function to run, and I can now use the "control" parameter to find out which button was clicked:

Code: ags
  if (control == dgOpt1) ...
  else if (control == dgOpt2) ...
  ...

Since controls are numbered, I can even skip that step and use the ID directly:
Code: ags
  int choice = control.ID;


Regarding the height of the label:
AGS has a function called GetTextHeight.

Assuming the labels have IDs 0, 2, 4, ... and the respective buttons have IDs 1, 3, 5, ...:

Code: ags
  int y = 10;
  for (int i = 0; i < 5; i++) {
    Label* l = gDialog.Controls[i * 2].AsLabel;
    Button * b = gDialog.Controls[i * 2 + 1].AsButton;
    int h = GetTextHeight(l.Text, l.Font, l.Width);
    // resize and position label and button
    l.Height = h;
    l.Y = y;
    b.Height = h;
    b.Y = y;
    y += h + 5; // next button moves down accordingly, plus 5 pixels 
  }

Custerly

@Khris thank you x2. That all makes sense now. You've helped me in the past too. I really appreciate your time sharing that code and your explanations.

Custerly

@Khris and/or @Crimson Wizard bonus question for you:

I've implemented your suggestions above, and I've got text wrapping on the button labels, and the button sizes adjusting dynamically based on the button label / text height. Above the buttons, I've got a label which displays the NPC responses to the player, and that label's height also adjusts dynamically so that it never stretches past the top visibile button.

The only issue is that once that NPC dialogue label is full, no new text can be displayed. I figure the solution is to make it so that once the text height = the label's height - 30 or so, then the top line of the text is removed and all of the text goes up a line (leaving a space at the bottom of the label for the new line of text). The removed text could go into a storage string, and if the player wants to review text from earlier in the dialogue, they press an up arrow, and it feeds that text back into the label.

I see there is a 'truncate' function, but that applies to the end of the string (not the beginning), it truncates based on specified character length (I'd like to chop off a line of text at a time), and it doesn't seem to store the removed text anywhere (I'd like to keep that text somewhere so I can implement the arrow feature mentioned above).

How would you suggest I proceed? Any help you can provide is much appreciated.

Khris

You can use a string array to store text.
Code: ags
// above functions that need it
String npc_text[3]; // show last three lines of dialogue

  // inside a function where line is said
  storage += "[" + npc_text[0];
  npc_text[0] = npc_text[1];
  npc_text[1] = npc_text[2];
  npc_text[2] = line;

  lblNpc.Text = npc_text[0] + "[" + npc_text[1] + "[" + npc_text[2];

Custerly

#10
Thanks @Khris. I get that I can use "[" to line break, and that I can store each line of text in a string array, but the main challenge I was referring to in the last post was how to automatically break up chunks of text that take up more than one line in the 'lblnpc' label.

Let's say, for example, that a single response from the NPC looks like this in lblnpc:

Character 1: Hello there. Haven't seen
you in a long time. I sure hope you've
been keeping well.

I've sent that text to lblnpc as a single string without manually inputting any line breaks, and lblnpc has wrappped the text automatically into three lines of text. Later in the convo, once lblnpc becomes full of text, it needs to make room for another new line of text by removing just the first line of text in lblnpc. In the above example, that would mean "Character 1: Hello there. Haven't seen" would be removed, so that the first text in the label becomes "you in a long time. I sure hope you've". I can use a string array to hold the text from the first line of lblnpc that we've just removed, but how do I go about separating just the first line of text (considering that until now, it has been part of a string that takes up more than one line of lblnpc)?

EDIT:

Thinking about this some more, I think I have a solution (just need to write it out and implement it).

I'll feed the original string into the label. If the text height of the label > 36 (one line of text), the last character of the label's text is removed and stored separately. then we run again until the text height of the label = 36.

I'm gonna try to hack this together tonight.

Khris

Right, that makes sense.
You can also draw the messages on a dynamic sprite using DrawingSurface.DrawStringWrapped(), then crop the sprite to a certain height and put it on a non-clickable button.

Crimson Wizard

I might post here, there's undocumented behavior when label wraps text, which might affect required height.
Following is a list of known cases:
https://github.com/adventuregamestudio/ags-manual/issues/103

Custerly

@Khris thanks for the suggestion re dynamic sprites, but I've got everything working as desired now via the route I mentioned in my last post, and thanks largely to help from you and Crimson Wizard.

@Crimson Wizard thanks for the tip re the extra pixel on line spacing in labels. Does this mean that since my font height is 36 pixels per line, it actually takes up 37 pixels per line when printed to a label?

Currently, the setup I'm using in the main dialogue label to cycle out old dialogue text and make room for new text seems to be working as intended in the limited testing I've done tonight.

That part of the system is setup like this:
1) the label adjusts its height to fall 50 pixels above the highest visible dialogue option
2) if the label's text height > the label's height, it cuts out the oldest line of text (or more if needed)

I never actually directly mention the text pixel height in that code, just infer it from [GetTextHeight(dgLabel.Text, dgLabel.Font, dgLabel.Width)]. Assuming that the GetTextHeight function does not take into account the extra pixel per line height added by the label, I should assume that there may be scenarios where the text height could be greater than the label height despite [GetTextHeight(dgLabel.Text, dgLabel.Font, dgLabel.Width)] returning a value less than the label height. Is that correct?

That being said, I only know my font height (the stock AGS font) is 36 via [GetTextHeight(dgLabel.Text, dgLabel.Font, dgLabel.Width)] in the first place, so perhaps the font height is actually 35 and I've been working with the extra pixel all along.

Crimson Wizard

#14
Quote from: Custerly on Mon 18/12/2023 06:11:38@Crimson Wizard thanks for the tip re the extra pixel on line spacing in labels. Does this mean that since my font height is 36 pixels per line, it actually takes up 37 pixels per line when printed to a label?

This means that it prints lines of text in "font height + 1" steps. This is different from how GetTextHeight calculates it, as it does not have this +1.
If there's only one line then you won't notice anything.

Latest AGS has GetFontHeight and GetFontLineSpacing functions that may be easier to use if you only need to know these values.

EDIT: I cannot answer other questions now, I will find more spare time later today.

Snarky

#15
To wrap a string to fit on a label, you can use the module I shared here. Then you can simply do:

Code: ags
  lblnpc.SetTextWrap(originalString);
  // Later, to remove the first line
  String wrappedString = lblnpc.Text;
  int firstBreak = wrappedString.IndexOf("\n"); // This is a "proper" linebreak, which is what the module inserts to wrap the String
  if(firstBreak != -1)
  {
    String strippedString = wrappedString.Substring(firstBreak+1,wrappedString.Length-firstBreak-1);
    lblnpc.Text = strippedString;
  }

(It also has configurations for wrapping strings that are meant to be read right-to-left, and for inserting hyphens if it has to wrap in the middle of words, but you won't need any of that.)

Custerly

Thanks @Crimson Wizard as always your time is much appreciated. Don't worry about answering the other minor questions in my last post, I already know everything I need to.

Custerly

Thanks @Snarky for sharing that module. I've already slapped together a solution that is getting 'er done for me.

SMF spam blocked by CleanTalk