In this first tutorial, I'll go over building a basic "icon button" dialog. The end results will look something like this:
The demonstration map created through this tutorial is attached to the bottom of this post.
It's real simple, but as you'll see its a very flexible dialog. This simple dialog also allows me to introduce some key concepts to dialog building without being distracted by a complex dialog.
The two most important tools to building dialogs (and they aren't immediately obvious), are:
Constants and Records
Let me tell you why.
Constants
Constants are your friends. Constants let you change every element of your dialog from one place, with ease. They take a minute to set up initially, but you will get that minute back the first time you go to change something... and you will change something.
Dialog code should never have numbers in it. It should all be constants and calculated variables. Really, there are only three numbers you should ever see anywhere near dialog code (or most code for that matter):
0: Zero is acceptable because it's zero and it's always going to be zero. You don't have to make a constant called "cZero". That's just redundant. It's perfectly OK to have zeros in your code.
1: It's ok to have a loop from 1 to n, and it's sometimes ok to add or subtract 1 from something. In such a situation, I'd still consider that if you ever wanted to change that 1 to a 2 (or anything else), it should be a constant.
2: Is only ok when multiplying or dividing. It's ok to multiply something by two (a dialog has 2 borders, always will) and it's ok to divide by two (like when you are centering something). These things are perfectly normal.
Any other time, you should be using constant or a variable. Besides the cases above, the only other time you should use a numeric literal is to initialize a constant.
If you're not already familiar with what constants are, they are basically just variables. They can be global or local, but constants are most meaningful when they are global. We will only use global constants. The only difference between a constant and a variable is that a constant's value can't change. This distinction protects you from accidently changing it in the future. Really though, variables would suit the same purpose, so long as they were all set in the same place. The point is to set a global set of numbers that will control how all your dialogs are displayed. An example:
Building a Few Dialog Constants
We're gonna start with a new map and jump straight to triggers. Delete the four steps inside the melee initialization trigger. Then, make a new folder and call it "Dialog". Although we'll only have one dialog (for now), this is good practice. Inside the Dialog folder, make another folder and call it "Dialog Constants".
Inside that folder, create a new integer variable. Call it "cDialog Border". Check off "Constant" and give it an initial value of 40.
Dialog Border will be the distance between the edge of the dialog and where we start drawing, as seen as 1 below:
cDialog Border = 40
cButton Size = 84
cIcon Size = 76
cButton Gap = 4
Repeat the steps to create the other three constants above. Your map should look like this:
Calculated "Constants"
You can perform calculations inside a constant's initial value, but the calculations can only contain other constants. Also, it's best to stick to the basic math functions. I'm not sure what other functions may work in a constant's calculation, but I've found custom functions generally don't work (even if they other perform simple math). Generally, I find it's best to not worry about which things can and can't be constants, if it's a calculated value it doesn't need to be a constant. It'd be nice, but the editor is a little weird when it comes to these sorts of things.
At any matter, we need a couple of additional values, which will actually be variables and not constants. For the most part, however, we will treat them just like all the other dialog constants.
Later, when we want to calculate how big the dialogs will be, we'll actually need to add the dialog border twice. Since this is fairly common, I find it's best to calculate this value upfront. It's also best that it be calculated, in case you later decide to change the dialog border.
So create a variable in the dialog constants folder that looks like this:
cDialogExtra=(cDialogBorder*2)<Integer>
The second value we need to know is a little harder to calculate. We want to center our icon (cIcon Size = 76) onto our button (cButton Size = 84). The end result is 4, but we want to calculate it in case we change one or both of our size constants.
Create another variable, cButton Inlay, and set up the initial value as follows:
Which is, an Arithmatic (Integer) with an Arithmatic (Integer) in each side. Then it can be set up as (cButtonSize / 2) - (cIcon Size / 2). It's not the most accurate formula, but for even numbers it's perfectly accurate.
Your map should now look like this:
and hopefully you now have a good understanding of what constants are, and are starting to understand how they might be used to build a dialog.
Records
I'm going to use records to store the data for our buttons. If you're not familiar with records, they are kind of hard to describe in the abstract. I think it's best to teach through demonstration. Just follow through and you should have a decent understanding by the time we are done.
Alright, so we have X buttons. The picture shows 6, but really it doesn't matter how many buttons we have. We want to save a little bit of information about each button somewhere so that we can use it to display the button. Specificially, we want:
A unit type, for spawning
An image, for displaying on the button
Some text, to display in the tooltip
So let's do it. Start by creating a folder inside the Dialog folder called "Unit Button". Inside it, create a new record, and call it "Unit Button Record".
Over on the right, add 3 variables:
Spawn Unit, type: Unit Type
Icon, type: File - Image
Tooltip, type: Text
Creating a record doesn't actually do anything, directly. It just defines a new type that can be used later. The advantage of records is that they can be used as arrays. So we're going to create a new variable inside our Unit Button folder, and call it "Unit Buttons". The variable is of type Record - Unit Button Record. The variable should also be an array. Make the size 10 for now. It can always be changed later.
You may have noticed that you can not set an initial value.
Initialize Our Record
We're going to initialize our record in two steps. First, we're going to build a action definition called "Add Unit Button". It will take three parameters, Spawn Unit, Icon, and Tooltip (the three variables inside our record). It will fill the next available slot in our array with those three values. It will then advance a pointer so that we can keep track of what the next available array element is. We'll then need to create another action definition in which we call Add Unit Button once for every button we create. We will call this second action definition from our Melee Initialization trigger.
Before we get two far ahead of ourselves, we'll need that counter variable to keep track of how many buttons we've added already. This variable will serve two purposes. As I said, we're going to use it as a counter to keep track of which array element is the next available one. Once we're done though, it's actually going to hold the number of buttons we added, so this will be useful when we go to draw the dialog. You'll see how this comes up later. For now, just create an integer variable called "nUnit Buttons" inside our Unit Button folder, and set it to 0. The lower case "n" at the front is a fairly standard abbreviation for 'number of':
nUnitButtons=0
Now, let's create a new action definition inside our Unit Button folder, and call it "Add Unit Button". This action definition will be fairly short:
As I said, we're just taking in three parameters, and using them to set our record variables. Notice how we use nUnit Buttons, and then add one to it.
The next step is really quite simple. Create another action definition inside our Unit Button folder, calling this one "Initialize Unit Buttons". In this function, we just call Add Unit Buttons a bunch of times:
We'll call this action from our Melee Initialization trigger, but we're going to wait a minute before setting that up.
A Word About Flexibility
It's important to mention that you don't have to have 6 buttons. You can actually have any number of buttons, but I chose 6 because I wanted there to be two rows, and I didn't want to spend a great deal of time initializing the record. To create more buttons, just call "Add Unit Button" more times. Note, however, that we only set up our record array, Unit Buttons, to hold 10 elements. If you need more than 10 buttons, you'd have to change that number as well.
The other advantage to the way we've done things is that you can reorganize buttons fairly easily. The buttons will be displayed in the order in which you initialize them. You can move buttons simply be reordering lines of code. It doesn't get much easier than that.
It would be more difficult to reogranize the buttons because you'd have to change all the numbers around. Instead, you can just cut/paste code and buttons are instantly reorganized. Try it and you'll see how easy it is.
Creating the Dialog, Finally
We're nearly there. We've done a lot of work and so far we still can't see anything. Finally comes the part where all our hard work pays off.
Start by creating a new folder inside our Dialog folder and calling it "Unit Selection Dialog":
First, we're going to need one more constant. We need to know how many columns to display across. Technically, this could be a dialog constant, but it doesn't really matter. Just create an global integer constant called "nCols" and set it to 3 (it doesn't actually have to be 3, you can play around with it). You can put it in your Dialog Constants folder or in this folder, whichever makes more sense to you. I'll be keeping it in the Unit Selection Dialog folder.
Calculating Dialog Variables
Now create a new action definition inside of that folder, and call it "Unit Selection Dialog - Create". Before we get too carried away actually drawing the dialog, we're going to need to calculate a decent number of variables. Typically I do all my calculations inside the local variables section of the function, as I will do here. I find this breaks up the code nicely, but it doesn't always work if you need conditions or loops. For this dialog it will work fine.
The first thing we need to know is, given the number of buttons and the number of columns, how many rows will there be? On paper the calculation is pretty easy, but the galaxy editor makes it a little harder than it needs to be. Make a local variable inside your action definition called "nRows", and set it up as follows:
This is the most complicated mathematical calculation we need to perform, and I've had a few questions about it, so I'd like to go over it briefly. The short answer is we need to divide the number of buttons by the number of columns, but the long answer is a bit more complicated than that. nUnit Buttons and nCols are both integers. In programming, the end result of a division between two integers is always an integer. That is, the decimal part of the result is ignored. Basically it will always round down. This is not what we want. This is actually fine for our calculation (6 / 3) because it equals exactly 2, but if we had 7 buttons instead, we would still get 2 as a result, and 2 rows is not enough for 7 buttons. We actually need to always round up so that any remainder of the division creates a whole new row. To do this, we first need to convert or integers to reals so that we can actually get a decimal result. Then we use the function "Ceiling". The purpose of the ceiling function is to always round up any decimal value. It has a complimentary function, "Floor", which always rounds down (we won't be using that). Both of these functions are quite similar to the "Round" function, which you probably already understand.
Now that we know how many rows there we be, we can determine how big the dialog will be. Consider that the width of the dialog will equal: cDialog Extra + (cButton Size * nCols) + (cButton Gap * (nCols - 1)). This might not be immediately obvious, so let's go over it real quick. cDialog Extra is equal to cDialog Border times 2. So that's our total dialog border. Then, we need nCols worth of buttons. Finally, we need gaps between those buttons, but we don't need a gap after the last button. This is why we need to subtract 1 from nCols before we multiply times cButton Gap. It can be difficult to set something up like this in Galaxy Editor, so let's look at how it's laid out:
Similarly, the height of the dialog will equal: cDialog Extra + (cButton Size * nRows) + (cButton Gap * nRows - 1):
Once you've created Dialog Width, you can copy it and rename the copy to Dialog Height, then just change nCols to nRows.
For the buttons, we're going to use some variables (x and y), and just increment them every time we make a new button. That way, whenever we go to create a new button, it will be drawn at (x, y). So we will need to create these variables, and initialize them to cDialog Border.
x=cDialogBorder<Integer>y=cDialogBorder<Integer>
There's a few other variables we'll need, but since we don't need to initialize them, we'll create them when we need them.
Drawing the Dialog, the Easy Part
If you've ever found drawing dialogs to be difficult, it's probably because you didn't perform all the setup we just did. If you don't, when you go to actually draw the dialog, suddenly you're scrambling to try and figure out how big it will be, where it will be, et cetera. All that stuff we already know. It's too much to try and figure out all that at once, I find it's a lot easier when you break it up into two pieces. After all the setup we just did, I think you'll find that actually drawing the dialog is really, really easy. When the editor asks you how large the dialog is going to be, you have the answer ready. I think you'll find that the code is also much easier to read as a result.
It's really that easy, and the beauty is that there's no complex calculations in this code. All those calculations are performed above.
The next part is a little more complex, but if you've done a decent number of loops it's actually quite simple. We do, however, need a loop inside of a loop (one for rows, one for columns), so conceptually it's a little complicated. The skeleton of the loop will look like this:
Note that I don't ever use the "pick each" functions. I like to have a variable for everything. Also, you can't put a pick each integer inside of another one, so we'd have to use at least one "for each" here no matter what. I find it's just plain good practice to use "for each" all the time, as I have done here. This means I needed to create two new variables, iRow and iCol.
We'll start adding code to the loop by thinking about the outer loop first. At the beginning of each row, we want to set x = cDialog Border. At the end of each row, we want to add cButton Size + cButton Gap to y. This is what will cause us to move down to the next row each time.
Inside the inner loop, we're going to draw the actual button and icon. But we need to know which button and icon to draw, and iRow and iCol don't exactly tell us. We could calculate (iRow * nCols) + iCol every time, or we could have a variable start with zero and increment it by one after every loop. I've chosen to do the latter. Either way we'd need a new variable, which I called "iButton", and the initial value should be zero.
Drawing the Button
If you've worked with dialogs in the past, you may be aware that buttons can have text but not images. You can however have a button with an image simply by drawing the image on top of the button. One of the things I've learned from experience is that you want to put the tooltip on the button, and not the image.
So we will draw out dialog button in two steps. Step 1:
This is the simpler of the two steps where we create the button using our pre-calculated values. Notice that we set the tooltip here, and that the button text is "" (blank text).
In this step we actually need to add cButton Inlay to our x and y values. If we needed to use this calculation more than once (or if it were much more complicated), we would have pre-calculated it. However, it is fine to do small calculations like these inline.
It's very important to note that tiled is set to false. For most images, this is what you want (and it's not the default). It's no documented very well, but your options are "tiled" or "scaled". To get the image to scale, you set tiled to false. This is a common pitfall new dialog designers run into. We want our icon to scale.
Putting it All Together
A couple more steps need to happen in our inner loop. Our loop will work fine the way we have it, but if we decided to have 7 buttons instead of 6, it would still try to draw those last two buttons on the third row (buttons 8 and 9). To prevent that, we're going to add a simple if/then/else:
This code will not go inside of the if statement we just created. It will go just below it.
Finally, at the end of our function, we need to actually show the dialog. As the last step of our action definition (outside of any loops), add the following line:
Dialog-Show(Lastcreateddialog)for(Allplayers)
When you put it all together, your action definition should look like this:
UnitSelectionDialog-CreateOptions:ActionReturnType:(None)ParametersGrammarText:UnitSelectionDialog-Create()HintText:(None)CustomScriptCodeLocalVariablesnRows=(Ceiling(((Real(nUnitButtons))/(Real(nCols)))))<Integer>DialogWidth=(+(cDialogExtra,((+(cButtonSize,cButtonGap))*nCols)))<Integer>DialogHeight=(+(cDialogExtra,((+(cButtonSize,cButtonGap))*nRows)))<Integer>x=cDialogBorder<Integer>y=cDialogBorder<Integer>iRow=0<Integer>iCol=0<Integer>iButton=0<Integer>ActionsDialog-CreateaModaldialogofsize(DialogWidth,DialogHeight)at(0,0)relativetoCenterofscreenGeneral-ForeachintegeriRowfrom1tonRowswithincrement1,do(Actions)ActionsVariable-Setx=cDialogBorderGeneral-ForeachintegeriColfrom1tonColswithincrement1,do(Actions)ActionsGeneral-If(Conditions)thendo(Actions)elsedo(Actions)IfiButton<nUnitButtonsThenDialog-Createabuttonfordialog(Lastcreateddialog)withthedimensions(cButtonSize,cButtonSize)anchoredtoTopLeftwithanoffsetof(x,y)settingthetooltiptoUnitButtons[iButton].Tooltip with button text "" and the hover image set to ""
Dialog - Create an image for dialog (Last created dialog) with the dimensions (cIcon Size, cIcon Size) anchored to Top Left with an offset of ((+ (x, cButton Inlay)), (+ (y, cButton Inlay))) setting the tooltip to "" using the image Unit Buttons[iButton].Icon as a Normal type with tiled set to false tint color White and blend mode Normal
Else
Variable - Modify x: + (+ (cButton Size, cButton Gap))
Variable - Modify iButton: + 1
Variable - Modify y: + (+ (cButton Size, cButton Gap))
Dialog - Show (Last created dialog) for (All players)
Testing Basic Functionality
We're finally ready to test our map and actually see something! ... well, almost. You'll just need to add two lines of code to our melee initialization trigger. First, you'd need to actually call the "Initialize Unit Buttons" action, then we need to call the "Unit Selection Dialog - Create" action.
Feel free to test your map at this point. It should look a lot like the demonstration image at the top of this post. However, you may notice right away that clicking a button doesn't actually do anything. We'll have to program that part shortly.
Before that, I encourage you to mess with the dialog constants to see what effect they have on the dialog. For example, by changing cDialog Border to 24 and cButton Gap to 0, I was able to pull the dialog items in much tighter to each other:
The numbers I arrived at were not by accident. They were arrived at by trying a number of different things first until I found a set up that looked good. Using constants allowed me to try new values quickly and easily.
Interaction
For this demonstration, we're going to keep the user interaction fairly simple. All users will see the same dialog. When someone selects a unit, that unit will be disabled for all other users. This will be fairly easy to do since they all see the same dialog. We just need to hide the dialog for the users who have already selected, and disable the button that they used.
Dialog Variables
Before we can do any of that, we actually need to back up a little bit. We need to save a couple of variables that we didn't save before when we were creating our dialog. First, we need to save the dialog itself as a variable. You can't use "Last Created Dialog" forever, eventually you need to save it to a variable for later retreival. In our case, in order to hide the dialog for our players, we'll need the dialog as a variable.
We'll also need to save the buttons. When we build our dialog callback, we'll have the function "Used Dialog Item" available to us, but we'll need to compare that value with our actual dialog buttons to see which ones were clicked. To do that, we'll need to save them off somewhere. We will use an array.
For pretty much any dialog you create, you will need to save the dialog and any buttons. You will also need to save any other elements that you might want to go back and change later. If you had a label that displayed HP, for example, you might need to save that label so you can go back and change its contents later. In our case, when we disable the button, we're going to need to disable the icon as well (otherwise it doesn't look right). So we're going to need to save the images as well.
In future tutorials, we'll use records to store all this, but for now we can just use normal variables. We will need a single dialog variable (we'll call it "User Selection Dialog"), and two arrays of dialog items (we'll call them "User Selection Buttons", and "User Selection Icons"). Since we made our User Buttons array size 10, we should make these size 10 as well. If we add more buttons, we will need to resize these arrays as well. (Unfortuantely, you can't use constants for array sizes). Create these variables inside your User Selection Dialog folder.
Then, add a few quick "set variable" lines to the User Selection Dialog - Creation action:
We've now successfully saved all the dialog variables we will need later.
Dialog Callback
For the last piece of the puzzle, create a new trigger inside the User Selection Dialog folder and call it "User Selection Dialog - Callback". For the event, use "Dialog Item is Used".
In this trigger, we're going to loop through all the dialog buttons to see if "Used Dialog Item" is one of them. If it is, we'll perform a few quick actions:
UnitSelectionDialog-CallbackEventsDialog-AnyDialogItemisusedbyPlayerAnyPlayerwitheventtypeClickedLocalVariablesiButton=0<Integer>ConditionsActionsGeneral-ForeachintegeriButtonfrom0tonUnitButtonswithincrement1,do(Actions)ActionsGeneral-If(Conditions)thendo(Actions)elsedo(Actions)If(Useddialogitem)==UserSelectionButtons[iButton]
Then
Dialog - Hide User Selection Dialog for (Player group((Triggering player)))
Dialog - Disable User Selection Buttons[iButton] for (All players)
Dialog - Disable User Selection Icons[iButton] for (All players)
Unit - Create 1 Unit Buttons[iButton].Spawn Unit
for player (Triggering player) at (Center of (Entire map))
using default facing (No Options)
Else
This trigger is actually quite simple. Hopefully the for each loop and the if'then/else are fairly familiar to you. The only thing we really need to analyze is what happens when we do find a button.
The first thing we do is hide the dialog for triggering player only (the player that selected something). Next, we disable the button and the icon for all players. Finally, we spawn a unit. For simplicity, I chose the center of the map as the spawn location.
Conclussion
It was a long road (and a long tutorial), but we finally got there. The dialog we built was fairly simple, but firm groundwork has been laid for all kinds of other great dialogs (which we will build in later tutorials). We've learned that constants allow us to easily tweak the properties of our dialog, which makes it easier to build great looking dialogs. We've learned the basics of using records to store similar information in one place, especially arrays of similar data.
Hopefully this tutorial was not too long, and hopefully I avoided a "wall of text" feel by breaking it up with pictures and examples. I attempted to build this tutorial from the viewpoint of someone who has never created a dialog (but has some experience with the editor), so if I went over something too quickly, please let me know and I will go back over it. I also welcome any suggestions about how I may improve future tutorials.
Hey I really like this tutorial, especially about records and constants... not really sure why Blizz uses their own names like "Real" instead of "double" or "float" and "Record" instead of "Struct"... but yeah good stuff man. I don't use constants enough!
As far as I know a double is not the same as a float or a real, the difference being the number of bits allocated in memory, but correct me if I'm wrong. I believe Blizzard calls it 'real' because this is a mathematical way of looking at it, and they will handle the size allocation on their own. Record and struct are pretty interchangeable, I've seen languages that call them structs and others that call them records. Same sort of deal.
I agree with the stuff about constants, after making a few dialogs myself I definitely found it much easier to tweak the numbers (especially with multiple buttons where you use formulas to determine positioning) with constants.
I think BLizz is trying to make it more user friendly; eg the fact that all arrays in the trigger editor have indexes 0 to their size, whereas in the real world, arrays go from 0 to size-1.
Why not put all the variables in the record if you are already calling one?
Keeps everything much cleaner, since you know which function you're referring to.
Ok, I kinda skimmed through and was wondering what you think of the idea of creating a function that lets you set your record items, rather than tediously setting them one by one?
Variable-SetUnitButtons[iButton].SpawnUnit=SentryVariable-SetUnitButtons[iButton].Icon=Assets\Textures\btn-unit-protoss-sentry.ddsVariable-SetUnitButtons[iButton].Tooltip="It's a glass robot."
setRecord(string unit, string icon, string tip);
Instead of using iButton, what I usually do is have a global integer that I use to cycle the index and increment after setting each record. This global integer is then reset after all records have been set.
I've used this method for larger records before, but I've never really considered it for the smaller ones. Really those three lines of code you listed are the same line copy/pasted 3 times. I like the initiative tho. That's good thinking and some strong logic. It would work quite well.
Instead of reseting your global integer, you could just hold it in the variable nUnit Buttons, which we need anyways.
If you don't mind, I'd like to use this method in future tutorials.
hey i followed your tutorial and my dialog is the size of one of the buttons and i cant figure it out with all these variables lol
Download the sample map and compare the two. You've obviously done something different. Since your dialog is the wrong size, your focal point should be around this line of code:
i did i look at a the dialog height and width its set the same i did have mistake in dialog height but i fixed it now the dialog is to tall and i looked at the linked variable
i did i look at a the dialog height and width its set the same i did have mistake in dialog height but i fixed it now the dialog is to tall and i looked at the linked variable
I'm not sure what to tell you, man. I can assure you that the map does work, you can try it yourself and see. So there's obviously something you've done different if your map doesn't work. I'd first make sure that everything is in the same place.
You may actually want to run back through the tutorial again. This can be a fairly common pitfall when you're using someone else's code but don't fully understand it. If something breaks, you have no idea where to start. When you fully understand something, it's much easier to analyze a bug. Perhaps if you run back through the tutorial, the bug will become more obvious. If you have something that needs more clarification, I'd be happy to do it.
On a more specific note, if you're saying that the dialog is indeed being created with the width and height set to Dialog Width and Dialog Height, respectively, and Dialog Width and Height are calculated properly, the next step would be to backup further and check all the constants that make up Dialog Width and Height. Follow the math the composes Dialog Width and Height, and do the calculations yourself. Use the function "UI - Text Message" to output those two variables to the screen and make sure they're right.
Outputting variables is a common way to divide a problem in half. If you know what a variable is at a specific time, you can determine where to look for the bug next. If the value is right, obviously the bug happens after that section of code. If the value is wrong, the bug has already happened. This is a fairly efficient way to find bugs.
I could ask you to send me the map and I could just tell you what's wrong, but I'm not going to do that. Problems like these are so normal in programming. Dealing with bugs is what divides programmers from typists. You will learn more if you figure it out yourself.
When you put it all together, your action definition should look like this:
Unit Selection Dialog - Create
Options: Action
Return Type: (None)
Parameters
Grammar Text: Unit Selection Dialog - Create()
Hint Text: (None)
Custom Script Code
Local Variables
nRows = (Ceiling(((Real(nUnit Buttons)) / (Real(nCols))))) <Integer>
Dialog Width = (+ (cDialog Extra, ((+ (cButton Size, cButton Gap)) * nCols))) <Integer>
Dialog Height = (+ (cDialog Extra, ((+ (cButton Size, cButton Gap)) * nRows))) <Integer>
x = cDialog Border <Integer>
y = cDialog Border <Integer>
iRow = 0 <Integer>
iCol = 0 <Integer>
iButton = 0 <Integer>
Actions
Dialog - Create a Modal dialog of size (Dialog Width, Dialog Height) at (0, 0) relative to Center of screen
General - For each integer iRow from 1 to nRows with increment 1, do (Actions)
Actions
Variable - Set x = cDialog Border
General - For each integer iCol from 1 to nCols with increment 1, do (Actions)
Actions
Dialog - Create a button for dialog (Last created dialog) with the dimensions (cButton Size, cButton Size) anchored to Top Left with an offset of (x, y) setting the tooltip to Unit Buttons[iButton].Tooltip with button text "" and the hover image set to ""
Dialog - Create an image for dialog (Last created dialog) with the dimensions (cIcon Size, cIcon Size) anchored to Top Left with an offset of ((+ (x, cButton Inlay)), (+ (y, cButton Inlay))) setting the tooltip to "" using the image Unit Buttons[iButton].Icon as a Normal type with tiled set to false tint color White and blend mode Normal
Variable - Modify x: + (+ (cButton Size, cButton Gap))
Variable - Modify iButton: + 1
Variable - Modify y: + (+ (cButton Size, cButton Gap))
Dialog - Show (Last created dialog) for (All players)
<</quote>>
ok notice that thees 2 are different but its when you first show us how to do that variable then you review it nrows is different so i got confused there and i set dialog size 500/500 and it was still small... nothing changed
Thank you for catching that. I did fix a small bug with the way nRows was calculated, and I neglected to change it in both places. I have fixed it.
If we have 6 buttons, nButtons will actually be 5. Ceiling of (5/3) is 2, which is the proper number of rows, so it actually worked ok. But if you had 7 buttons, ceiling of (6/3) is 2, which is one less row than we need. So you need to add that 1 to nButtons in order for the math to work right. Ceiling of (6+1/3) is 3; the proper number of rows.
This shouldn't effect anything, however. This bug only applies when you have 1 more button than is required for a row. That last button would go off that dialog (and possibly not show at all).
Still, I think you are on the right track. Maybe output nRows to the screen to make sure it's the right value?
Rollback Post to RevisionRollBack
Pocket Warriors - A pokemon-style game with SC2 units and full banking. New demo coming soon!
Great tutorial - well written! Note, however, that you seem to change your terminology from "Unit Selection" to "User Selection" near the end...was this intended?
Basic Dialog, Introduction to Constants and Records
Introduction
In this first tutorial, I'll go over building a basic "icon button" dialog. The end results will look something like this:
The demonstration map created through this tutorial is attached to the bottom of this post.
It's real simple, but as you'll see its a very flexible dialog. This simple dialog also allows me to introduce some key concepts to dialog building without being distracted by a complex dialog.
The two most important tools to building dialogs (and they aren't immediately obvious), are:
Constants and Records
Let me tell you why.
Constants
Constants are your friends. Constants let you change every element of your dialog from one place, with ease. They take a minute to set up initially, but you will get that minute back the first time you go to change something... and you will change something.
Dialog code should never have numbers in it. It should all be constants and calculated variables. Really, there are only three numbers you should ever see anywhere near dialog code (or most code for that matter):
Any other time, you should be using constant or a variable. Besides the cases above, the only other time you should use a numeric literal is to initialize a constant.
If you're not already familiar with what constants are, they are basically just variables. They can be global or local, but constants are most meaningful when they are global. We will only use global constants. The only difference between a constant and a variable is that a constant's value can't change. This distinction protects you from accidently changing it in the future. Really though, variables would suit the same purpose, so long as they were all set in the same place. The point is to set a global set of numbers that will control how all your dialogs are displayed. An example:
Building a Few Dialog Constants
We're gonna start with a new map and jump straight to triggers. Delete the four steps inside the melee initialization trigger. Then, make a new folder and call it "Dialog". Although we'll only have one dialog (for now), this is good practice. Inside the Dialog folder, make another folder and call it "Dialog Constants".
Inside that folder, create a new integer variable. Call it "cDialog Border". Check off "Constant" and give it an initial value of 40.
Dialog Border will be the distance between the edge of the dialog and where we start drawing, as seen as 1 below:
Repeat the steps to create the other three constants above. Your map should look like this:
Calculated "Constants"
You can perform calculations inside a constant's initial value, but the calculations can only contain other constants. Also, it's best to stick to the basic math functions. I'm not sure what other functions may work in a constant's calculation, but I've found custom functions generally don't work (even if they other perform simple math). Generally, I find it's best to not worry about which things can and can't be constants, if it's a calculated value it doesn't need to be a constant. It'd be nice, but the editor is a little weird when it comes to these sorts of things.
At any matter, we need a couple of additional values, which will actually be variables and not constants. For the most part, however, we will treat them just like all the other dialog constants.
Later, when we want to calculate how big the dialogs will be, we'll actually need to add the dialog border twice. Since this is fairly common, I find it's best to calculate this value upfront. It's also best that it be calculated, in case you later decide to change the dialog border.
So create a variable in the dialog constants folder that looks like this:
The second value we need to know is a little harder to calculate. We want to center our icon (cIcon Size = 76) onto our button (cButton Size = 84). The end result is 4, but we want to calculate it in case we change one or both of our size constants.
Create another variable, cButton Inlay, and set up the initial value as follows:
Which is, an Arithmatic (Integer) with an Arithmatic (Integer) in each side. Then it can be set up as (cButtonSize / 2) - (cIcon Size / 2). It's not the most accurate formula, but for even numbers it's perfectly accurate.
Your map should now look like this:
and hopefully you now have a good understanding of what constants are, and are starting to understand how they might be used to build a dialog.
Records
I'm going to use records to store the data for our buttons. If you're not familiar with records, they are kind of hard to describe in the abstract. I think it's best to teach through demonstration. Just follow through and you should have a decent understanding by the time we are done.
Alright, so we have X buttons. The picture shows 6, but really it doesn't matter how many buttons we have. We want to save a little bit of information about each button somewhere so that we can use it to display the button. Specificially, we want:
So let's do it. Start by creating a folder inside the Dialog folder called "Unit Button". Inside it, create a new record, and call it "Unit Button Record".
Over on the right, add 3 variables:
Creating a record doesn't actually do anything, directly. It just defines a new type that can be used later. The advantage of records is that they can be used as arrays. So we're going to create a new variable inside our Unit Button folder, and call it "Unit Buttons". The variable is of type Record - Unit Button Record. The variable should also be an array. Make the size 10 for now. It can always be changed later.
You may have noticed that you can not set an initial value.
Initialize Our Record
We're going to initialize our record in two steps. First, we're going to build a action definition called "Add Unit Button". It will take three parameters, Spawn Unit, Icon, and Tooltip (the three variables inside our record). It will fill the next available slot in our array with those three values. It will then advance a pointer so that we can keep track of what the next available array element is. We'll then need to create another action definition in which we call Add Unit Button once for every button we create. We will call this second action definition from our Melee Initialization trigger.
Before we get two far ahead of ourselves, we'll need that counter variable to keep track of how many buttons we've added already. This variable will serve two purposes. As I said, we're going to use it as a counter to keep track of which array element is the next available one. Once we're done though, it's actually going to hold the number of buttons we added, so this will be useful when we go to draw the dialog. You'll see how this comes up later. For now, just create an integer variable called "nUnit Buttons" inside our Unit Button folder, and set it to 0. The lower case "n" at the front is a fairly standard abbreviation for 'number of':
Now, let's create a new action definition inside our Unit Button folder, and call it "Add Unit Button". This action definition will be fairly short:
As I said, we're just taking in three parameters, and using them to set our record variables. Notice how we use nUnit Buttons, and then add one to it.
The next step is really quite simple. Create another action definition inside our Unit Button folder, calling this one "Initialize Unit Buttons". In this function, we just call Add Unit Buttons a bunch of times:
We'll call this action from our Melee Initialization trigger, but we're going to wait a minute before setting that up.
A Word About Flexibility
It's important to mention that you don't have to have 6 buttons. You can actually have any number of buttons, but I chose 6 because I wanted there to be two rows, and I didn't want to spend a great deal of time initializing the record. To create more buttons, just call "Add Unit Button" more times. Note, however, that we only set up our record array, Unit Buttons, to hold 10 elements. If you need more than 10 buttons, you'd have to change that number as well.
The other advantage to the way we've done things is that you can reorganize buttons fairly easily. The buttons will be displayed in the order in which you initialize them. You can move buttons simply be reordering lines of code. It doesn't get much easier than that. It would be more difficult to reogranize the buttons because you'd have to change all the numbers around. Instead, you can just cut/paste code and buttons are instantly reorganized. Try it and you'll see how easy it is.
Creating the Dialog, Finally
We're nearly there. We've done a lot of work and so far we still can't see anything. Finally comes the part where all our hard work pays off.
Start by creating a new folder inside our Dialog folder and calling it "Unit Selection Dialog":
First, we're going to need one more constant. We need to know how many columns to display across. Technically, this could be a dialog constant, but it doesn't really matter. Just create an global integer constant called "nCols" and set it to 3 (it doesn't actually have to be 3, you can play around with it). You can put it in your Dialog Constants folder or in this folder, whichever makes more sense to you. I'll be keeping it in the Unit Selection Dialog folder.
Calculating Dialog Variables
Now create a new action definition inside of that folder, and call it "Unit Selection Dialog - Create". Before we get too carried away actually drawing the dialog, we're going to need to calculate a decent number of variables. Typically I do all my calculations inside the local variables section of the function, as I will do here. I find this breaks up the code nicely, but it doesn't always work if you need conditions or loops. For this dialog it will work fine.
The first thing we need to know is, given the number of buttons and the number of columns, how many rows will there be? On paper the calculation is pretty easy, but the galaxy editor makes it a little harder than it needs to be. Make a local variable inside your action definition called "nRows", and set it up as follows:
This is the most complicated mathematical calculation we need to perform, and I've had a few questions about it, so I'd like to go over it briefly. The short answer is we need to divide the number of buttons by the number of columns, but the long answer is a bit more complicated than that. nUnit Buttons and nCols are both integers. In programming, the end result of a division between two integers is always an integer. That is, the decimal part of the result is ignored. Basically it will always round down. This is not what we want. This is actually fine for our calculation (6 / 3) because it equals exactly 2, but if we had 7 buttons instead, we would still get 2 as a result, and 2 rows is not enough for 7 buttons. We actually need to always round up so that any remainder of the division creates a whole new row. To do this, we first need to convert or integers to reals so that we can actually get a decimal result. Then we use the function "Ceiling". The purpose of the ceiling function is to always round up any decimal value. It has a complimentary function, "Floor", which always rounds down (we won't be using that). Both of these functions are quite similar to the "Round" function, which you probably already understand.
Now that we know how many rows there we be, we can determine how big the dialog will be. Consider that the width of the dialog will equal: cDialog Extra + (cButton Size * nCols) + (cButton Gap * (nCols - 1)). This might not be immediately obvious, so let's go over it real quick. cDialog Extra is equal to cDialog Border times 2. So that's our total dialog border. Then, we need nCols worth of buttons. Finally, we need gaps between those buttons, but we don't need a gap after the last button. This is why we need to subtract 1 from nCols before we multiply times cButton Gap. It can be difficult to set something up like this in Galaxy Editor, so let's look at how it's laid out:
Similarly, the height of the dialog will equal: cDialog Extra + (cButton Size * nRows) + (cButton Gap * nRows - 1):
Once you've created Dialog Width, you can copy it and rename the copy to Dialog Height, then just change nCols to nRows.
For the buttons, we're going to use some variables (x and y), and just increment them every time we make a new button. That way, whenever we go to create a new button, it will be drawn at (x, y). So we will need to create these variables, and initialize them to cDialog Border.
There's a few other variables we'll need, but since we don't need to initialize them, we'll create them when we need them.
Drawing the Dialog, the Easy Part
If you've ever found drawing dialogs to be difficult, it's probably because you didn't perform all the setup we just did. If you don't, when you go to actually draw the dialog, suddenly you're scrambling to try and figure out how big it will be, where it will be, et cetera. All that stuff we already know. It's too much to try and figure out all that at once, I find it's a lot easier when you break it up into two pieces. After all the setup we just did, I think you'll find that actually drawing the dialog is really, really easy. When the editor asks you how large the dialog is going to be, you have the answer ready. I think you'll find that the code is also much easier to read as a result.
So let's draw the dialog:
It's really that easy, and the beauty is that there's no complex calculations in this code. All those calculations are performed above.
The next part is a little more complex, but if you've done a decent number of loops it's actually quite simple. We do, however, need a loop inside of a loop (one for rows, one for columns), so conceptually it's a little complicated. The skeleton of the loop will look like this:
Note that I don't ever use the "pick each" functions. I like to have a variable for everything. Also, you can't put a pick each integer inside of another one, so we'd have to use at least one "for each" here no matter what. I find it's just plain good practice to use "for each" all the time, as I have done here. This means I needed to create two new variables, iRow and iCol.
We'll start adding code to the loop by thinking about the outer loop first. At the beginning of each row, we want to set x = cDialog Border. At the end of each row, we want to add cButton Size + cButton Gap to y. This is what will cause us to move down to the next row each time.
Inside the inner loop, we're going to draw the actual button and icon. But we need to know which button and icon to draw, and iRow and iCol don't exactly tell us. We could calculate (iRow * nCols) + iCol every time, or we could have a variable start with zero and increment it by one after every loop. I've chosen to do the latter. Either way we'd need a new variable, which I called "iButton", and the initial value should be zero.
Drawing the Button
If you've worked with dialogs in the past, you may be aware that buttons can have text but not images. You can however have a button with an image simply by drawing the image on top of the button. One of the things I've learned from experience is that you want to put the tooltip on the button, and not the image.
So we will draw out dialog button in two steps. Step 1:
This is the simpler of the two steps where we create the button using our pre-calculated values. Notice that we set the tooltip here, and that the button text is "" (blank text).
Step 2:
In this step we actually need to add cButton Inlay to our x and y values. If we needed to use this calculation more than once (or if it were much more complicated), we would have pre-calculated it. However, it is fine to do small calculations like these inline.
It's very important to note that tiled is set to false. For most images, this is what you want (and it's not the default). It's no documented very well, but your options are "tiled" or "scaled". To get the image to scale, you set tiled to false. This is a common pitfall new dialog designers run into. We want our icon to scale.
Putting it All Together
A couple more steps need to happen in our inner loop. Our loop will work fine the way we have it, but if we decided to have 7 buttons instead of 6, it would still try to draw those last two buttons on the third row (buttons 8 and 9). To prevent that, we're going to add a simple if/then/else:
In the "Then" section is where we'll actually put the code we just created to draw our buttons. Nothing will go inside of the "Else" section.
After each button is displayed, we need to increase x by cButton Size + cButton Gap. Also, we will need to increment iButton by 1:
This code will not go inside of the if statement we just created. It will go just below it.
Finally, at the end of our function, we need to actually show the dialog. As the last step of our action definition (outside of any loops), add the following line:
When you put it all together, your action definition should look like this:
Testing Basic Functionality
We're finally ready to test our map and actually see something! ... well, almost. You'll just need to add two lines of code to our melee initialization trigger. First, you'd need to actually call the "Initialize Unit Buttons" action, then we need to call the "Unit Selection Dialog - Create" action.
Feel free to test your map at this point. It should look a lot like the demonstration image at the top of this post. However, you may notice right away that clicking a button doesn't actually do anything. We'll have to program that part shortly.
Before that, I encourage you to mess with the dialog constants to see what effect they have on the dialog. For example, by changing cDialog Border to 24 and cButton Gap to 0, I was able to pull the dialog items in much tighter to each other:
The numbers I arrived at were not by accident. They were arrived at by trying a number of different things first until I found a set up that looked good. Using constants allowed me to try new values quickly and easily.
Interaction
For this demonstration, we're going to keep the user interaction fairly simple. All users will see the same dialog. When someone selects a unit, that unit will be disabled for all other users. This will be fairly easy to do since they all see the same dialog. We just need to hide the dialog for the users who have already selected, and disable the button that they used.
Dialog Variables
Before we can do any of that, we actually need to back up a little bit. We need to save a couple of variables that we didn't save before when we were creating our dialog. First, we need to save the dialog itself as a variable. You can't use "Last Created Dialog" forever, eventually you need to save it to a variable for later retreival. In our case, in order to hide the dialog for our players, we'll need the dialog as a variable.
We'll also need to save the buttons. When we build our dialog callback, we'll have the function "Used Dialog Item" available to us, but we'll need to compare that value with our actual dialog buttons to see which ones were clicked. To do that, we'll need to save them off somewhere. We will use an array.
For pretty much any dialog you create, you will need to save the dialog and any buttons. You will also need to save any other elements that you might want to go back and change later. If you had a label that displayed HP, for example, you might need to save that label so you can go back and change its contents later. In our case, when we disable the button, we're going to need to disable the icon as well (otherwise it doesn't look right). So we're going to need to save the images as well.
In future tutorials, we'll use records to store all this, but for now we can just use normal variables. We will need a single dialog variable (we'll call it "User Selection Dialog"), and two arrays of dialog items (we'll call them "User Selection Buttons", and "User Selection Icons"). Since we made our User Buttons array size 10, we should make these size 10 as well. If we add more buttons, we will need to resize these arrays as well. (Unfortuantely, you can't use constants for array sizes). Create these variables inside your User Selection Dialog folder.
Then, add a few quick "set variable" lines to the User Selection Dialog - Creation action:
We've now successfully saved all the dialog variables we will need later.
Dialog Callback
For the last piece of the puzzle, create a new trigger inside the User Selection Dialog folder and call it "User Selection Dialog - Callback". For the event, use "Dialog Item is Used".
In this trigger, we're going to loop through all the dialog buttons to see if "Used Dialog Item" is one of them. If it is, we'll perform a few quick actions:
This trigger is actually quite simple. Hopefully the for each loop and the if'then/else are fairly familiar to you. The only thing we really need to analyze is what happens when we do find a button.
The first thing we do is hide the dialog for triggering player only (the player that selected something). Next, we disable the button and the icon for all players. Finally, we spawn a unit. For simplicity, I chose the center of the map as the spawn location.
Conclussion
It was a long road (and a long tutorial), but we finally got there. The dialog we built was fairly simple, but firm groundwork has been laid for all kinds of other great dialogs (which we will build in later tutorials). We've learned that constants allow us to easily tweak the properties of our dialog, which makes it easier to build great looking dialogs. We've learned the basics of using records to store similar information in one place, especially arrays of similar data.
Hopefully this tutorial was not too long, and hopefully I avoided a "wall of text" feel by breaking it up with pictures and examples. I attempted to build this tutorial from the viewpoint of someone who has never created a dialog (but has some experience with the editor), so if I went over something too quickly, please let me know and I will go back over it. I also welcome any suggestions about how I may improve future tutorials.
Hey I really like this tutorial, especially about records and constants... not really sure why Blizz uses their own names like "Real" instead of "double" or "float" and "Record" instead of "Struct"... but yeah good stuff man. I don't use constants enough!
@OneTwoSC: Go
As far as I know a double is not the same as a float or a real, the difference being the number of bits allocated in memory, but correct me if I'm wrong. I believe Blizzard calls it 'real' because this is a mathematical way of looking at it, and they will handle the size allocation on their own. Record and struct are pretty interchangeable, I've seen languages that call them structs and others that call them records. Same sort of deal.
I agree with the stuff about constants, after making a few dialogs myself I definitely found it much easier to tweak the numbers (especially with multiple buttons where you use formulas to determine positioning) with constants.
Good tutorial.
@KnightsOfTables: Go
I think BLizz is trying to make it more user friendly; eg the fact that all arrays in the trigger editor have indexes 0 to their size, whereas in the real world, arrays go from 0 to size-1.
Interesting.....I fully understand dialogs but never really thought to use constants to set them up. Nice tut.
@obliviron: Go
THIS IS TALK OF HERESY!! BURN HIM!!!
@OneTwoSC: Go
Ok, I kinda skimmed through and was wondering what you think of the idea of creating a function that lets you set your record items, rather than tediously setting them one by one?
setRecord(string unit, string icon, string tip);
Instead of using iButton, what I usually do is have a global integer that I use to cycle the index and increment after setting each record. This global integer is then reset after all records have been set.
@FuzzYD: Go
I've used this method for larger records before, but I've never really considered it for the smaller ones. Really those three lines of code you listed are the same line copy/pasted 3 times. I like the initiative tho. That's good thinking and some strong logic. It would work quite well.
Instead of reseting your global integer, you could just hold it in the variable nUnit Buttons, which we need anyways.
If you don't mind, I'd like to use this method in future tutorials.
hey i followed your tutorial and my dialog is the size of one of the buttons and i cant figure it out with all these variables lol
Download the sample map and compare the two. You've obviously done something different. Since your dialog is the wrong size, your focal point should be around this line of code:
Which means you may also want to look and ensure that Dialog Width and Dialog Height are calculated properly.
I tried to make the variable names a self-explanatory as possible. Try reading out the code, and it may become more obvious where something is wrong.
i did i look at a the dialog height and width its set the same i did have mistake in dialog height but i fixed it now the dialog is to tall and i looked at the linked variable
I'm not sure what to tell you, man. I can assure you that the map does work, you can try it yourself and see. So there's obviously something you've done different if your map doesn't work. I'd first make sure that everything is in the same place.
You may actually want to run back through the tutorial again. This can be a fairly common pitfall when you're using someone else's code but don't fully understand it. If something breaks, you have no idea where to start. When you fully understand something, it's much easier to analyze a bug. Perhaps if you run back through the tutorial, the bug will become more obvious. If you have something that needs more clarification, I'd be happy to do it.
On a more specific note, if you're saying that the dialog is indeed being created with the width and height set to Dialog Width and Dialog Height, respectively, and Dialog Width and Height are calculated properly, the next step would be to backup further and check all the constants that make up Dialog Width and Height. Follow the math the composes Dialog Width and Height, and do the calculations yourself. Use the function "UI - Text Message" to output those two variables to the screen and make sure they're right.
Outputting variables is a common way to divide a problem in half. If you know what a variable is at a specific time, you can determine where to look for the bug next. If the value is right, obviously the bug happens after that section of code. If the value is wrong, the bug has already happened. This is a fairly efficient way to find bugs.
I could ask you to send me the map and I could just tell you what's wrong, but I'm not going to do that. Problems like these are so normal in programming. Dealing with bugs is what divides programmers from typists. You will learn more if you figure it out yourself.
nRows = (Ceiling(((Real((nUnit Buttons + 1))) / (Real(nCols))))) <Integer>
When you put it all together, your action definition should look like this: Unit Selection Dialog - Create Options: Action Return Type: (None) Parameters Grammar Text: Unit Selection Dialog - Create() Hint Text: (None) Custom Script Code Local Variables nRows = (Ceiling(((Real(nUnit Buttons)) / (Real(nCols))))) <Integer> Dialog Width = (+ (cDialog Extra, ((+ (cButton Size, cButton Gap)) * nCols))) <Integer> Dialog Height = (+ (cDialog Extra, ((+ (cButton Size, cButton Gap)) * nRows))) <Integer> x = cDialog Border <Integer> y = cDialog Border <Integer> iRow = 0 <Integer> iCol = 0 <Integer> iButton = 0 <Integer> Actions Dialog - Create a Modal dialog of size (Dialog Width, Dialog Height) at (0, 0) relative to Center of screen General - For each integer iRow from 1 to nRows with increment 1, do (Actions) Actions Variable - Set x = cDialog Border General - For each integer iCol from 1 to nCols with increment 1, do (Actions) Actions Dialog - Create a button for dialog (Last created dialog) with the dimensions (cButton Size, cButton Size) anchored to Top Left with an offset of (x, y) setting the tooltip to Unit Buttons[iButton].Tooltip with button text "" and the hover image set to "" Dialog - Create an image for dialog (Last created dialog) with the dimensions (cIcon Size, cIcon Size) anchored to Top Left with an offset of ((+ (x, cButton Inlay)), (+ (y, cButton Inlay))) setting the tooltip to "" using the image Unit Buttons[iButton].Icon as a Normal type with tiled set to false tint color White and blend mode Normal Variable - Modify x: + (+ (cButton Size, cButton Gap)) Variable - Modify iButton: + 1 Variable - Modify y: + (+ (cButton Size, cButton Gap)) Dialog - Show (Last created dialog) for (All players)
<</quote>>
ok notice that thees 2 are different but its when you first show us how to do that variable then you review it nrows is different so i got confused there and i set dialog size 500/500 and it was still small... nothing changed
wow what a fail sorry just look at nrows in both parts
Thank you for catching that. I did fix a small bug with the way nRows was calculated, and I neglected to change it in both places. I have fixed it.
If we have 6 buttons, nButtons will actually be 5. Ceiling of (5/3) is 2, which is the proper number of rows, so it actually worked ok. But if you had 7 buttons, ceiling of (6/3) is 2, which is one less row than we need. So you need to add that 1 to nButtons in order for the math to work right. Ceiling of (6+1/3) is 3; the proper number of rows.
This shouldn't effect anything, however. This bug only applies when you have 1 more button than is required for a row. That last button would go off that dialog (and possibly not show at all).
Still, I think you are on the right track. Maybe output nRows to the screen to make sure it's the right value?
Thanks for posting this, it really helped me with making my race selector dialog :)
how would i out put n rows
Using UI - Text Message, display the number to the screen.
Great tutorial - well written! Note, however, that you seem to change your terminology from "Unit Selection" to "User Selection" near the end...was this intended?