In parts 1 and 2 in this series, we used only three different dialog item types: buttons, images, and labels. While most dialogs will only ever use these controls, occassionally the need for other control types comes up. Galaxy editor has a number of dialogs controls to choose from, but we'll start with just one and add more as we go on. For now, we're going to focus on the edit box.
Also, the dialogs in the previous tutorials always showed the same thing to all players. This works great for an introductory demonstation, but very few real-world examples behave like this. In reality, you'll often want to show dialogs to different players at different times, and some times the same dialog will even look wildly different. We'll explore one way you can show different information to different players using the same dialog.
To demonstrate this, we'll be building a general-purpose edit box dialog. We'll start with the map from part 2. We won't be using the Unit Selection Dialog directly, but we will be using the same dialog constants for consistancy. We'll also be using the custom styles from part 2, and we'll be adding some more.
The map for part 3 will be built on top of the tutorial map from part 2. If you do not already have the tutorial map for part 2, and you want to follow through this tutorial, I suggest you go back to part 2 and download the map attachment now.
Choosing When a User Sees a Dialog
In parts 1 and 2, we created and displayed the dialog in a single step. Since the dialog was being displayed at the beginning of the game, and only once, this worked great. This would work no matter when we wanted to show the dialog, so long as we only wanted to show it once. If we wanted to show the same dialog more than once, however, this doesn't work so well.
Specifically, using this method can create problems if you accidently display the same dialog twice at the same time. If we create the dialog new each time, and save the dialog into the same variable, we lose the first dialog when we create a second. If this first dialog isn't hidden or destroyed before this happens, it will never go away and will constantly block the players vision. We obviously don't want this. We could possibly get around this by destroying the old dialog before creating a new one, but this doesn't work if we're using the same dialog for multiple players.
The easiest way to do this is to break up creating and showing the dialog into two different steps. We can create the dialog at the very beginning of the game, and then just not show it. Then, you can call the Show Dialog function whenever you need a user to see the dialog. What's more, you can show a dialog for only one player (or multiples), so breaking this apart gives us some additional flexibility.
These days, every dialog I create has at least 3 action definitions, "Create", "Show", and "Hide". Create is always called at the beginning of the game. In fact, I have one action called "Dialog Initialization" that is called from the Melee Initialization trigger, that calls all of the dialog create actions. You could call the "Show/Hide Dialog" directly from your code, but often showing or hiding a dialog requires more than one step. Even if it doesn't, you may add functionality in the future, and using an action to show/hide your dialog will keep you from having to change code in more than one place. I recommend using show/hide action definitions for all dialogs, even if the action definition is only one line long.
Showing Different Things to Different Players
Dialogs in GE are actually incredibly customizeable on a per-player basis. There is a lot you can do without having to create a seperate dialog for each player. With only a few exceptions, almost all of the dialog functions have a player group parameter. This means you can show and hide dialog controls, change the image for an image control, change the value of a edit or list control, even resize or move controls, all for one player only. If this doesn't make a lot of sense yet, don't worry. We'll be showing concrete examples as we move forward.
There are a couple of noteable exceptions to this, and they are a little unfortunate: you can not move or resize a dialog on a per-player basis. You can move or resize dialog items, but you can't move or resize the dialog itself. There a couple of ways around this, the easiest is to just create a seperate dialog for each player. We will not be covering how to do that in this tutorial. This tutorial will focus on using one dialog for all players, but customizing the content of the dialog to fit the player. Actually using a seperate dialog for each player will be the topic of another tutorial; it is the exception, not the rule.
Before we get started, let's lay a little groundwork. Preparation and planning actually compose a very large percentage of development time, but this time is usually paid back several times when it takes less time to develop and maintain a project. This is why I have focused on planning through all of these tutorials.
Before we move too far, I'd like to share a useful utility function with you. In part 1, we did a calculation to center objects that looked something like this: (cButtonSize / 2) - (cIcon Size / 2), or in more general terms: (Total Width / 2) - (Item Width / 2). It's not a very complicated calculation, but it can be a hassle to put this into the GUI every time we need to use it (and we will be using it several times). To avoid having to do this over and over, we'll create a simple function.
This will be the first actual function will have created in these tutorials. I more-or-less accidently use the terms "action definition" and "function" interchangeably because in other programming languages there is no difference between the two, and they are both typically called "functions". In GE, however, there are a few discrete differences between action definitions and functions. The only really noteworthy difference (for now at least), is that a function can return a value. That is, when you call a function you actually get a value back. In our case, this will be the end-result of our calculation.
For now, I put this function at the root. We can always move it later. As you end up creating more functions like this, you might end up with a folder called "Functions". Eventually, they might end up being grouped by type.
Our dialog is going to have standard "OK" and "Cancel" buttons at the bottom of it, so let's create a few constants to describe the size of these buttons, as well as the gap between the other dialog items and these buttons:
We're also going to end up with some constants that are specific to the dialogs that we are creating. We could put these constants with the dialog itself, but I find it's better to keep all your dialog constants in one place. Also, although unlikely, it is possible that you will want to use that constant for something else. For organization purposes, I put them in seperate folders inside the Dialog Constants folder. As an example, create a folder called "Edit Box", and create the following two constants inside of it:
We will use these constants shortly to build a simple edit box dialog. If we decided to create a dialog with another edit box later, we might use these constants to establish consistancy, or we might not depending on the circumstances and size of the dialog.
As one final step, to avoid confusion, I've decided to rename "Button Size" and "Button Gap" to "Icon Button Size" and "Icon Button Gap", respectively. Your Dialog Constants folder should now look something like this:
We established standard and header styles in part 2, but that's not going to be enough any more. Really, we want to assign a seperate style to each type of item we will be using on our dialogs. Even if the fonts end up being the same (as will be the case), we want seperate styles so that we can change them seperately if need be. This will be our new customstyles.sc2style file:
All we did was add the styles "DialogItemText", which will be used in dialog items like edit and list boxes, and "Dialog Button Text" which will be used for our OK and Cancel buttons. You'll note that DialogButtonText is the same as HeaderCenterText.
Import this new style file into your map.
Strings can be constants too. In fact, most variable types except records and arrays can be constants. We're going to start using string constants for our style names. Why? Because it significantly reduces the chance for typos. That's really the only reason, but it's a good one so I suggest you do it. Inside the Dialog Constants folder, create a new folder called "Styles". In it, add the following four constants:
We're going to start out with a simple edit box dialog:
Something like this might look familar to anyone who's every used a GUI before. We're just looking to get a single string input from the user, with the option to accept or cancel. Simple, but very reusable. We can just show this dialog anytime we need to get such input from the user. We're even going to make it so we can customize the header and the default string each time we show it. Feel free to use this directly in any map you make.
So we'll start by creating a folder for our dialog. Call it "Edit Box Dialog". This time, we'll use a record to store all our dialog data in a single place. So inside the folder, create a new record and call it "Edit Box Dialog Data":
Our record has 5 variables, a single dialog and 4 dialog items. We'll need to keep track of the OK and Cancel buttons so that we know when they've been used. We'll also need to track the header and edit box items so that we can set their values. Also, we'll need the edit box item so that we can get it's value after we're done.
So now we actually have to create a variable for our record, so let's do that. I called mine "Global Edit Box Dialog". The "Global" part might seem a little redundant, but I use Global to signify that there is one dialog for all players.
We're going to need one more variable, and this one is per player. We'll need to keep track of whether the user hit OK or cancel. We can do this with a simple boolean. So create a variable called "Player Edit Box Dialog Status" of type boolean, and make it an array with 16 elements.
Your Edit Box Dialog folder should now look like this:
Create a new action definition called "Edit Box Dialog - Create". Once again, we're going to start off with several calculations:
If you're having trouble trying to figure out how to calculate your sizes, it sometimes helps to draw them out:
cEdit Box Height
cDialog Button Gap
cDialog Button Height
Of course, you don't normally have a completed dialog to look at, but you can get about the same results on graph paper or in a Photoshop-like program. Microsoft Excel is also pretty good for drawing things out in a grid. That's what I typically use.
Our dialog height is just the sum of a bunch of numbers: cDialog Extra (which is cDialog Border * 2), cHeader Height + cRow Gap, cEdit Box Height, and cDialog Button Gap + cDialog Button Height.
Another fairly simple calculation. We could just keep a running Y variable like we did in part 1, or we could calculate our offsets ahead of time. Either way works fine, and it can depend on the sitation. Hopefully, this calculation makes sense. Since the header will be placed at cDialog Border, we want the edit box to be offset by the height of the header plus the gap.
This calculation builds on top of the last. We know where the edit box will go, so if we add cEdit Box Height and cDialog Button Gap to it, we can determine where the buttons will be placed.
Button Offset X
This one's a little trickier, and will be calculated in three parts:
The dialog inner width is something I calculate all the time. It's useful in this situation because of the way we want to center the dialog buttons, which I'll demonstrate in a second. You may notice that Inner Width will come out to equal cEdit Box Width, but this is not always the case. Also, since we may at some time change the dialog width, it's best to just keep the calculation the way it is. The inner width will always equal the dialog width minus cDialog Extra, no matter what.
An easy calculation, but it might not be immediately obvious why we need it. Let's first take a look at how we want our dialog to look:
Half Inner Width
Button Offset X
We want to center our buttons across half of the dialog, so we need to calculate this span which we will use as the "total width" in a call to the Center function we created:
Now that we have Half Inner Width, we want to center cDialog Button Width across it. Finally, we need to add back in cDialog Border, because we removed it when we calculated the Inner Width.
We calculated this is three parts for several reasons. For one, it's just easier to break up a calculation into several pieces and then each individual calcuation becomes more manageable. Also, we're going to need Half Inner Width again. You'll notice we only calculated the offset of the OK button. Cleverly, however, we can just add Half Inner Width to that and we'll have the offset of our Cancel button. Inner Width will also end up being the width of our header.
Drawing the Dialog
As our dialog drawing functions get longer, it's useful to add comments to break up individual sections and label them.
Now this part is a little bit different from what we've done before. Unlike labels, buttons, and images, there isn't a specific function for creating edit boxes. Instead, we will use the function "Create Dialog Item" and set the type to "Edit Box". Create Dialog Item doesn't have parameters for size and offset, so we'll have to move and resize the dialog item with seperate calls. We will still save our dialog item into a variable and set the dialog item style the same way as before.
More basic button creation. Since we calculated everything ahead of time, this is all pretty easy. Notice that we add Half Inner Width to Button Offset X before creating the cancel button.
In the main dialog folder, create an action definition called "Dialog Initialization". In it, call the Edit Box Dialog - Create function we just made. Open up the Melee Initialization trigger, and call Dialog Initialization from there. It's probably a good idea to move the two other calls in our Melee Initialization into our Dialog Initialization action, so that it will look like this:
Test Our Dialog
Always a good idea to test often, so let's actually add a Show Dialog line to the end of our Edit Box Dialog - Create function and test our document. Don't forget to delete this line after you are done. You should get something that look something like this:
I think you'll agree that the header looks a little off. This is partly because the font is too small, and partly because the gap is too small. We're using cRow Gap for our gap and we really shouldn't be. So I'm going to create a new constant called cHeader Gap and set it to 10. Inside Edit Box Dialog - Create, I changed every instance of cRow Gap (there are two) with cHeader Gap. Then, I changed the height of HeaderCenterText to 28 inside our style file and reimported it. It did not change the height of DialogButtonText. Since we changed our style height, we also need to change cHeader Height, which I changed to 32.
Just a few tweaks and I think our dialog looks quite a bit better:
As I said before, we're going to be creating an action definition for showing our dialog, and another one for hiding it. This isn't entirely true. Our show dialog action is actually going to be a function. This is, we want it to return something. What is it goint to return? Our function is actually going to wait until the user is done with the dialog, and return the string that they typed. If they hit cancel, it will return an empty string (""). This is what's the most useful, I think, for use in a general program, where you don't want to set up a trigger every time. You really just want to know what they typed in.
So create a new function in our Edit Box Dialog folder, and call it "Edit Box Dialog - Show". The return type will be string. It will take three parameters. The first is the player number and is very important. The second will be the header text. This will be of type text so that styles and colors can be used if desired. The third parameter will be the default string to put in the edit box. This has to be of type string, because the edit box won't accept text. You should have something like this:
What's really important to note is that we're using (Player group(Player)), which is "Convert Player to Player Group" > Parameter > Player. We are not using "All Players", which is the default. We only want to do these steps for one player. First, we set the header text to our header parameter. Then we set the edit box value to our default string parameter. Finally, we show the dialog. Then, we wait...
Oops! There's a dialog constant in there I haven't brought up yet, but we'll come back to that...
We start a wait loop, checking every "cDialog Wait Time" (0.25) seconds, to see if the dialog is still visible for the player. There's a number of ways you can check to see if a user is done with a dialog, but checking for visibility is my personal favorite. It doesn't require we track another variable or anything like that, and it's fairly simple to check.
For the next step, we check the Player Edit Box Status for this player. We'll set this variable in our trigger. If true, the user didn't hit cancel, and we want to return the edit value of our edit box dialog item. If false, the user hit cancel and the contents of the edit box are irrelevant, so we return an empty string ("").
Now, about that constant. Generally I feel that 0.25 seconds is a sufficient wait time for dialog responses, but you might disagree. Dialogs rely on user input and typically don't have to be super responsive. You do want them to be responsive enough, however, that the user doesn't really notice any lag. I think 0.25 seconds hits that sweet spot without taking up a huge amount of processor time. Different implementations might call for different times. This uncertainty alone is enough reason to create a constant. We'll add it to our dialog constants folder, and it will be of type real. Try using 0.25 for the value. If you don't like it, you can always change it.
As I said before, it's just one line but we're making it a seperate action definition anyways. Also, make sure you're changing "All Players" to be "(Player group(Player))".
There's just one more step before we can actually use our dialog, so let's just jump right in:
EditBoxDialog-CallbackEventsDialog-AnyDialogItemisusedbyPlayerAnyPlayerwitheventtypeClickedLocalVariablesEditValue=(EditvalueofGlobalEditBoxDialog.EditBoxfor(Triggeringplayer))<String>ConditionsOrConditions(Useddialogitem)==GlobalEditBoxDialog.OKButton(Useddialogitem)==GlobalEditBoxDialog.CancelButtonActionsGeneral-If(Conditions)thendo(Actions)elsedo(Actions)IfAndConditions(Useddialogitem)==GlobalEditBoxDialog.OKButtonEditValue==""ThenGeneral-SkipremainingactionsElseEditBoxDialog-Hide((Triggeringplayer))General-If(Conditions)thendo(Actions)elsedo(Actions)If(Useddialogitem)==GlobalEditBoxDialog.CancelButtonThenVariable-SetPlayerEditBoxDialogStatus[(Triggeringplayer)] = false
Variable - Set Player Edit Box Dialog Status[(Triggeringplayer)] = true
It looks a lot more complicated than it is. Since there are exactly two buttons they could have pushed for this dialog, we can check for those in the conditions using an "Or" condition. Then, we use an if statement to check if the user hit the OK button but the edit box is blank. If they do, we just ignore the fact that they hit the OK button using the action "Skip remaining actions". They need to either put something in the edit box or hit cancel. Once we know that they did one of those two things, we can hide the dialog. Finally, we set Player Edit Box Dialog Status to true or false, depending on whether the cancel button was hit or not.
Using Our Dialog
You already removed the line that shows the dialog at the end of Edit Box Dialog - Create, right? Good.
Let's go back into our Unit Selection Dialog folder, and open up the callback for that dialog. In it, we create a unit based on what the user selected. What if we wanted them to be able to name that unit? Simple.
First, create a new variable called "Unit Name" of type string. Then, immediately after the unit is created, add the following line:
We want to set Unit Name equal to the return value of our function. Triggering player will be our player parameter, and "Unit Name" will be our header. Unfortunately, we can't get the unit name or the player's name as a string to use as the default string, so we'll have to just leave it blank. You could set it to "Test" or something if you wanted to see it working.
Now, we want to actually do something with this string, so we'll just keep it easy and add a text tag to the unit we created:
Give your map a test, and you should be able to name your unit:
Taking it to the Next Level
In this situation, we probably would not want to allow the user to cancel our dialog. In other cases, we might still want to allow it. What we really need is the flexibility to do either from the same dialog. We can definitely do that.
What we could do is move the OK button and hide/show the cancel button as needed. To do this, we'd need to keep track of where we need to move the OK button to. We would need Button Offset X as well as a centered button offset to be global variables instead of local. This would definitely work, but there's a slightly easier way.
We could just create 3 buttons. We have two OK buttons, one centered and one to the left, and we only show one at a time. We would still show/hide the cancel button as needed.
We'll start by modifying our Edit Box Dialog Data record. We'll need to add a new dialog item variable to it, called "Centered OK Button". Rename "OK Button" to "Left OK Button" to avoid confusion.
Open up Edit Box Dialog - Create and add a new variable:
Then, let's jump into our Edit Box Dialog - Show function. It will need a new parameter, type boolean, called "Allow Cancel". Set the default value to true. Then, add the following code to the very beginning of the function:
We could have checked for both OK buttons, but it's easier to just check and see that it's not the cancel button. So what we did was change OK Button to Cancel Button and == was changed to !=.
Go back into the Unit Selection Dialog - Callback trigger, and set Allow Cancel to false. Give it a test, and you should see:
Because of the changes we made to the callback, you still will not be able to hit OK if the edit box is blank. Note that if you set Allow Cancel to false, you will get the normal edit box dialog.
Most of the other dialog items behave in the same way as the edit box. Remember that you will need to set the size and position of a dialog item created with "Create Dialog Item". With the edit box, we made extensive use of the "Dialog Item Edit Value", which is specific to the edit box. For other dialog items, there may be another specific function used to get it's value, and finding this may take a little trial-and-error. If you have any trouble, please feel free to post questions here, or join the sc2mapster IRC channel and ask questions there.
We also covered showing different things to different players. I spoke briefly about how you can't resize or move a dialog for only one player. To do that, you would need another method which is not covered here. The method we used, however, works in every other case which is a vast majority of dialogs. You can move, hide, show, or resize dialog items for only one player. You can also change their contents like we say with our header and with the edit box value. GE gives a great deal of flexibility to maintain a single dialog, while showing different things to different players at different times. Although it may seem confusing at first, in the long run it is a lot easier to manage.
I need to know how I can do it for :
Same as you wrote but by example : "(string text) " + "(Enter)"
If I want to write something in the "string texte" (as dialog) and after clicked Enter that thing appear in the map....
I got a little problem.
When calculating the value "Button offset X" it gives me weird answers. I checked everything thoroughly, and even made it output the values of center(Half Inner Width,cDialog Button Width) and cDialogBorder, which gave the values 12 and 40. But when I add them together, it gives the value -60 (and draws my buttons too far to the left). I guess it's a bug of GE, but if I'm making a newb mistake, I'd like to know :)