constructing a catalog of variables, need help

Started by hedgefield, Fri 24/06/2011 15:04:17

Previous topic - Next topic

hedgefield

Hey guys, I wondered if you could help me hash out part of a custom dialog system I'm trying to build.

In the simplest terms, I'm trying to create a sort of dialog options catalog in my code. I have a dialog GUI with a limited number of buttons on it, of which the button text (and matching response) changes depending on different variables. To give you a better idea of the structure, this is how the GUI operates:



There are four main options -or categories-, that each expand into a sub-menu, where each sub-option again expands into a second sub-menu. So you can see the set of options nescessary for each NPC quickly becomes quite complex.

So say I choose to talk to NPC 1. The set of questions I associated with NPC 1 is loaded onto my dialog GUI. If I then move to NPC 2, the options are exchanged for the set of questions I defined for NPC 2.

The problem I have with it now is in the way I have defined those questions. Whenever I load the dialog GUI, it calls ActivateColumn, which in turn calls the function SetTxt, which uses a series of if-statements to see which set of questions to pass to ActivateColumn. For example:

Code: ags
    //SetTxt excerpt:
    if (column1word == "ASK") {
      if (column2word == cGirl.Name) {
        btn[9].txt = "FOR HER NAME.";
        btn[10].txt = "WHAT SHE IS DOING HERE.";
        btn[11].txt = "WHERE SHE IS FROM.";
        btn[12].txt = "FOR DIRECTIONS";
      }


The btn[] variables come from a struct array I created to store the option texts in. The function ActivateColumn then uses those variables after they have been filled by SetTxt to put the appropriate options onto the appropriate button.

But right now the options are more or less set in stone because they are defined within the SetTxt function. So if I disable an option during the conversation, it will be re-enabled the next time I load that particular options set.

So what I'm looking for is a way to create some sort of master list of options. Basically this would be similar to C++ case switching (as far as I understand it), where SetTxt or ActivateColumn would select the appropriate case for a certain situation and retrieve the set of questions from it to feed to the dialog GUI. But since AGS does not support that kind of structure, I'm drawing a blank on how to construct something similar - short of creating a list of Strings for every single option. :-\

Anyone have any suggestions?

Khris

#1
You need to store the options in a struct.

Code: ags
//header

struct dialog_option_struct {

  String txt = "";
  bool show = true, asked;
};

import dialog_option_struct do[840];

// main
dialog_option_struct do[840];  // 10 NPCs
export do;


Now you need a formula to get to a particular main, sub or sub-sub option.

options per npc:
4 main, 16 sub, 64 sub-sub = 84 total
structure: m-s-ss-ss-ss-ss-s-ss-ss-ss-ss-s-ss-ss-ss-ss-s-ss-ss-ss-ss times four

Code: ags
// header 
import GetIndex(Character*npc, int main, int sub = 0, int subsub = 0)

// main
int GetIndex(Character*npc, int main, int sub, int subsub) {

  int ret = (npc.ID-1)*84 + (main-1)*21;
  if (sub > 0) ret += sub*5 - 4;
  ret += subsub;
  return  ret;
}


Usage:
  do[GetIndex(cGirl, 1)].txt = "main option 1";
  do[GetIndex(cGirl, 1, 1)].txt = "sub option 1";
  do[GetIndex(cGirl, 1, 1, 1)].txt = "sub-sub option 1";
  ...
  do[GetIndex(cGirl, 1, 2)].txt = "sub option 2";
  ...


Edit: is the main option going to be "ask" and the sub option "girl's name"?
Because this sounds more like a type of parser than a conversation structure.

monkey0506

#2
Why can't the SetTxt function read the state of the options directly? Even if you used a switch statement..that's basically just a shortcut to if-else if-else..so the capabilities it would offer are technically just as feasible.

Something you might possibly consider doing would be to use dynamic arrays with accessor functions in place of the classic struct model, which would let you dynamically create new options if that's something you're interested in. I did this in my Verbcoin module with preallocation for the default memory sizes (to allow individual characters/hotspots/inventory items/objects to override the interaction text for the buttons on a case-by-case basis), and it worked quite nicely.

Edit: @Khris: I figured since he's apparently not using the built-in dialog structure at all that he was already storing that information. It didn't seem to be part of the question, but I might be wrong there.

Regarding what you asked about a parser, that might be something he could make use of, but it seems to me that the primary issue he'd then be asking about now would be how to let the user build up the parser string from a predetermined list of options, and display that graphically to the user at each step along the way. Essentially to get the same graphical effect he would need this same type of system ultimately anyway, at least so far as I understand what he's trying to achieve.

Khris

As far as I understood his question about a master structure, it's about storing the state of all the options.
That's why I suggested a way of storing all the information in a format that allows to divulge it step by step.

This of course entails the rewriting of the existing SetTxt function.

hedgefield

#4
Thanks for the suggestions so far guys. I'll have a go at it again tomorrow but I'll just offer some clarification here on how the system is supposed to work.

Basically there are four categories you can dive into, like ASK SOMETHING or GIVE ITEM, which then open a submenu to determine who you want to ask/give to - my system is not structured like the default dialogs where you initiate a conversation with a specific person, you can just access these categories at any time and then select who you want to adress, like GIRL, EVERYONE or even YOURSELF. So that is what is in the second column, which is built up by checking which characters are currently in the room with you. Then once you select a person, the 3rd submenu opens where the actual questions are, and these ofcourse differ slightly depending on which person you ask. And in that list I would also like to be able to add or remove options depending on the gamestate, and that's where the master catalog would come in.

So how it works right now is that I have a seperate script called Dialog.asc, which has the SetTxt function in it (aswell as some functions that control the GUI and whatnot), and in there are all the definitions for every possible set of options. So when the player opens the GUI it gets populated from there. Then there is another script called Conversations.asc, which has a rep_ex function in it that first checks if the player selected an option in all 3 columns, and then checks that combination against all the possible combinations to trigger the appropriate response.

So no, I don't use any part of the built-in dialog system at all :) I hope that clears things up a bit more. It's already a pretty complicated system even to me so let me know if you need any further explanation of a particular part.

hedgefield

Since the previous post I've been trying a few things, with relative success. I ran with the idea of storing the options in a struct, and came up with this:

Code: ags
//Dialog.ash:

struct dialogue {
  String Text;
  bool Enabled;
  String Response;
};

import dialogue dlg[444];


which works pretty well.
444 is not actually the number of maximum options, but rather a matrix of button combinations. The number represents the last option for each column (so 4 in column A, 4 in B and 4 in C). So dlg[130].Text would give me the text for the 3rd button in the submenu for option 1 from the main column. During the game I store this array number in the int index, so at any time I can use that to find out exactly where in the dialogue I am. Still with me?

I wasn't sure how to handle the buttons that are dynamically filled with the character names, since for instance in one room character GIRL could be on button 3 and in another she could be on button 4. So I thought I would make seperate structs that handle only the options relating to specific characters, but when defining multiple structs I got the error "Variable "1" already defined."

Code: ags
import dialogue dlg[444];  //main dialogue
import dialogue GIRL[444];
import dialogue JOHN[444];


Another way I thought could solve this issue would be to somehow make .Response pass the index number to a function that can decide for me which response should be played. But I don't know if it's possible to call a function from a struct, probably not. Right now I just check the index in the rep_ex and define the response there.

Code: ags
    if (index == 100) {  //option 1 column 1
      player.Say("Something.");
      dlg[index].Enabled = false;
    }


Khris

Without getting into specifics, if the second column is the character and you store actions and responses on a per character basis, you'd only need two indices.

So for instance john[23] is ask(2) john about topic(3).

I'd still use my method though because if you use the character's ID to get to the struct index you avoid using ifs that assign the struct variables to the characters.

Just rewrite it a bit:

Code: ags
int GetIndex(Character*npc, int action, int topic) {

  return npc.ID-1*100 + action*10 + topic;
}


Action could also be an enum (eGive, eAsk, ...).

Not sure what's happening with the error you're getting, which line throws it?

hedgefield

Hmm that is an interesting spin on it, I'll look into that. Not everything is purely character-based, but three of the four column A options do have a submenu that involves selecting characters, yes. The other one will eventually have responses/emotes that you can use in reference to something a character said (using the index number to identify the context).

A quick usecase might be:

ASK > GIRL > NAME : you're too shy to talk to her
TELL> YOURSELF > YOU CAN DO THIS : confidence++
ASK > GIRL > NAME : her name is anna, she asks for yours
TELL > ANNA > YOUR NAME : etc etc


I get the error on the line with import dialogue GIRL[444];
Any reference to a second struct throws it. And if I remove this definition, but not the checks in the rep_ex for example, it throws PE04 parse error at '1' or something similar, like symbol '1' undefined. Remove any mention of those additional structs and I get no more errors.

Khris

I was stumped by the '1' but then I realized what's happening; due to backwards compatibility, if you name a character cGirl (or anything else starting with a 'c'), AGS automatically defines an all-caps constant that holds the ID, in this case 'GIRL' (the scriptname minus 'c').

So to AGS, the line looks like this:
import dialogue 1[444];

The solution of course is to not use GIRL but girl or even better, something like dlg_girl.

hedgefield

Ahh excellent, thanks!
At first I wanted to see if I could name the structs the same as the NPC's since I already store the adressed NPC in the variable charID, and if I could use that like charID.Name[123].Text all the code would remain independant of the name, in case that would change later in development. But that's probably a whole other unnescessary can of worms, so dlg_girl would indeed seem like the best way to go.

Khris

In order to use something at least similar to charID.Name[123].Text, which itself unfortunately isn't usable, you could use extender functions.

Code: ags
String GetText(this Character*, int index) {

  if (this == cGirl) return dlg_girl[index].Text;
  ...
}

void SetText(this Character*, int index, String text) {

  if (this == cGirl) dlg_girl[index].Text = text;
  ...
}


Assuming that cha is a pointer pointing to the current NPC, you can use those like this:
  button1.Text = cha.GetText(12);
  cGirl.SetText(13, "topic xyz");

Again, if you use the NPC's ID inside the index, you can avoid all the ifs (one per NPC), and there's really no reason not to.

Also, is charID an int? Because it is much more straightforward to use a Character pointer.

hedgefield

The name might be a little deceiving, charID is actually a Character*.

Thanks for the help so far Khris, I really feel like we're getting somewhere I could never have gotten by myself. I'm not the biggest programming whiz, really abstract concepts with a lot of numbers take a while before I can grasp them. That being said, I like where you were going with the rewritten GetIndex, but I'm not sure I fully follow how it would operate exactly. So far I adapted it to:

Code: ags
int GetIndex(charID, int action, int topic) {
  return charID.ID-1*100 + action*10 + topic;
}


My charID already stores the character pointer for whichever character I selected in Column B, so that takes care of that. I made an enum for the actions, that should be no problem. Where I start to get a little foggy is the topic variable. I know how to manually specify it, but how do I dynamically extract it from the button I just clicked? There are 4 buttons in Column C but there might be 23 options total in the struct for this NPC (of which 19 will currently be disabled obviously). Would you mind explaining a bit more how you would format the different conversations in the structs? And why is that -1*100 and *10 in the formula for GetIndex, what does that do? Also, am I correct in assuming this would actually eliminate the need for seperate dlg_npcname structs since the first digit now already specifies the NPC?

Khris

To answer the last question first: yes, that's the point.

You need to put charID.ID-1 in parentheses; the idea is that multiplying it by 100 and the action by 10 gives you a unique index number for every combination.

If there are more then four (or rather: ten) possible topics, you have to increase the factors.

Example: charID.ID is 5, action is 2 and topic is 18.
The revised formula would be: (charID.ID-1)*1000 + action*100 + topic, so the result is 4218.

Every topic button must "know" which topic is on them. So what you need is int arrays storing action & topic for each button. The button's stored value is then inserted into the formula as action/topic after the click.

hedgefield

I actually realized why the multiplication was there right before you posted :) it makes a lot of sense once you get it.

But good news! I've started slowly hooking all this code up to the old system, and the first test indicates it works.
I realized I hadn't accounted for there being more topics than buttons when I defined the array at [444] earlier, which admittedly was pretty dumb. Then I started thinking the same thing about the characters, there might be more than 9 so I should make that one double digits too. But as an array cannot start with a 0, I reversed the action and character properties. So now it looks like this:

Code: ags
dialogue dlg[49999];  //4 categories, 99 characters and 99 possible topic slots


And GetIndex looks like this:

Code: ags
int GetIndex(int action, Character *cha, int topic) {
  return action*10000 + (cha.ID-1)*100 + topic;  
}


I just haven't figured out how to read the right topic from the button yet. I thought of looping through all the topics in the struct to check them against the button text, but with 99 options that would probably be kinda slow.

I think that's the only remaining issue to be worked out, because like I said, I gave it a testrun by defining the topics for ASK > GIRL in game_start, plus I tried disabling the option I clicked on, and it all seems to work, so the logic behind the system is sound.

Khris

#14
What you've done there is really bad and unnecessary I'm afraid.

I put the number of characters at the 'start' of the index precisely because I had no idea how many there are going to be.
The point is, say there were 500 NPCs: for NPC #487, the formula will calculate e.g. 486,ATT which again is a unique index number (A being the action digit and TT the two topic digits).

Also, leading zeros are a non-issue here; in the above model, NPC #4 will have e.g. indices 3101-3419. Again, those are unique numbers, and also perfectly valid array indices.

The main point of using a good formula is not wasting memory by having lots of unused array members.
With the characters at the beginning it's straightforward: 16 characters -> dlg[16000]

The ideal way of course is having limits for the number of actions and topics that are set in stone, then choose a formula that'll generate as less "data holes" as possible.

Quote from: hedgefield on Thu 30/06/2011 22:34:16I just haven't figured out how to read the right topic from the button yet. I thought of looping through all the topics in the struct to check them against the button text, but with 99 options that would probably be kinda slow.

This is the worst way of doing it. With what you want to do, it's very unlikely if not impossible that two buttons ever contain the same string, but this method relies on that and should never be used in general. It'll break as soon as two of the (arbitrary) messages are the same. Granted, a dialog system is an exception where this is more or less feasible. It's still wrong.

Like I said, store the topic number for each button in an array.
Code: ags
int button_topic[4];

// while putting text on the buttons:

  // i: 0-3
  g.Controls[i+10].Text = ....;   // buttons have IDs 10-13
  button_topic[i] = first_topic + i;


A click on a button will then read the topic # from the appropriate button_topic[X].

hedgefield

Ah it won't expect a second digit at the 'start' of the array even if you told it there might be one? That's good to know.
Well, that's what you get when you let designers mess with advanced programming features I suppose :) I cranked the array back down to [4440], and I guess people are just gonna have to adjust that number to suit their needs. I'm still wondering if there is a smarter way to manage it since it only encompasses character-related interactions now so I would still have to define item-based interactions and other miscellanious interactions seperately. But I'll get back to you on that once I play around with this some more.

Khris

Quote from: hedgefield on Thu 30/06/2011 23:23:32Ah it won't expect a second digit at the 'start' of the array even if you told it there might be one? That's good to know.

I never tell it there might be one; all I do is multiply the character ID by 1000 and add stuff smaller than 1000.
The result is the ID followed by three digits, not more, not less. No leading zeros at all.

Imagine you didn't have any topics or actions; then you wouldn't need a formula, the index would simply be the character's ID. Why would you even pay attention to the fact that as soon as you have ten characters, the ID and thus the index ends up being 10, a two digit number? You don't have to "tell" this to AGS, nor do you treat this as a special case.
We are doing the exact same thing, just times 1000. 1000-5 isn't 0995, it's 995.

The array index is an int, and in terms of ints, 032 is the same as 32 or even 0032.
Leading zeros are usually only important in a visual context like a telephone number or safe combination or stuff like that.

As for object interactions, the principle is the same. I'm beginning to wonder though what the point of all this is. I get that you want context sensitive interactions, but does it really have to stick to this three column form all the way?
Say I wanted to interact in some way with a door, what's going to end up on all the buttons?

hedgefield

Ah yes I understand now. All the null pointer errors I've gotten in the past have made me a little paranoid of using zeroes. ;) I actually used 0 just now for the dialogue with the player character itself (which is character ID 0) and it works just fine.

I'm not actually putting every game interaction on this GUI, perhaps that wording was poorly chosen. I meant the act of giving/showing/exchanging items WITH the characters you're talking to, and the emotes you perform during the conversation. The whole system is just for dialogues, nothing else. It's an experiment, trying to find a new way to present conversations. It might end up not being the greatest thing ever, but I'll have to see it through to find out.

SMF spam blocked by CleanTalk