FMF:Custom Menu 2
It's time to learn how to use the more complex Slices and Format Args. Oh, we'll also cover cascading input and hooking up listeners. You'd be surprised just how easy it is to add all this. Now that you've got a basic understanding of Menu Slices, the rest will just fall into place. Rather than just another arbitrary spatter of components, let's make something useful. Here's our prototype sketch:
Here's the basic idea:
- Show some cube on the left, and have a "hand" cursor pointing to one of the character's names.
- Rotate the cube (just show a new image) when the hand is moved.
- The cube should be connected to "James" at the VCENTER. We could directly code this... or we could throw all three names into a giant "wrapper" slice.
One key benefit of "slicing" up Menus like this (sorry, couldn't resist) is that we can tackle each problem independently. So let's start by simply mocking up our menu in code.
Start With What You Know[edit]
The code below will display our new menu system using simple boxes. We've added one novelty: a Menu Slice can call "setTopLeftChild()" to declare a "primary" sub-component. In fact, this function is a misnomer; the fromAnchor and toAnchor fields are still used to determine how to attach child Slices. But we digress. There are two invisible (fill=NONE) menu slices with children: the one that holds the Bob/James/Dusty boxes and a "width x height" one that contains our entire menu. Note that having such a fullscreen box is highly recommended.
Oh, there is something special: we set mfClear.borderPadding to "22". This way, we get an internal "padding" between our transparent box and its first child element. This wasn't really necessary (we could have just set blueGraphic's x and y to 22), but it comes in handy when paired with WIDTH_MAXIMUM.
//Large overlay MenuFormatArgs mfClear = new MenuFormatArgs(); mfClear.fillType = MenuSlice.FILL_NONE; mfClear.borderColors = new int[]{0xFF0000}; mfClear.widthHint = width; mfClear.heightHint = height; mfClear.borderPadding = 22; MenuSlice largeClearBox = new MenuSlice(mfClear); //Blue background "circle" MenuFormatArgs mf = new MenuFormatArgs(); mf.bgColor = 0x0000BB; mf.borderColors = new int[]{0x0000FF}; mf.fillType = MenuSlice.FILL_SOLID; mf.widthHint = 77; mf.heightHint = 77; MenuSlice blueGraphic = new MenuSlice(mf); //Our smaller collection slice mfClear.fromAnchor = GraphicsAdapter.VCENTER|GraphicsAdapter.RIGHT; mfClear.toAnchor = GraphicsAdapter.VCENTER|GraphicsAdapter.LEFT; mfClear.xHint = 20; mfClear.widthHint = MenuFormatArgs.WIDTH_MINIMUM; mfClear.heightHint = MenuFormatArgs.HEIGHT_MINIMUM; mfClear.borderPadding = 00; MenuSlice smallClearCollection = new MenuSlice(mfClear); //Bob's Name mf.bgColor = 0xA8E61D; mf.borderColors[0] = 0x22B14C; mf.widthHint = 80; mf.heightHint = 20; MenuSlice greenBox = new MenuSlice(mf); //James's Name mf.bgColor = 0xFFC20E; mf.borderColors[0] = 0xFF7E00; mf.fromAnchor = GraphicsAdapter.BOTTOM|GraphicsAdapter.LEFT; mf.toAnchor = GraphicsAdapter.TOP|GraphicsAdapter.LEFT; mf.yHint = 10; mf.widthHint = 50; MenuSlice orangeBox = new MenuSlice(mf); //Dusty's Name mf.bgColor = 0xB5A5D5; mf.borderColors[0] = 0x6F3198; mf.widthHint = 45; MenuSlice purpleBox = new MenuSlice(mf); //Connect & set Children largeClearBox.setTopLeftChild(blueGraphic); blueGraphic.connect(smallClearCollection, MenuSlice.CONNECT_RIGHT, MenuSlice.CFLAG_PAINT); smallClearCollection.setTopLeftChild(greenBox); greenBox.connect(orangeBox, MenuSlice.CONNECT_BOTTOM, MenuSlice.CFLAG_CONTROL|MenuSlice.CFLAG_PAINT); orangeBox.connect(purpleBox, MenuSlice.CONNECT_BOTTOM, MenuSlice.CFLAG_CONTROL|MenuSlice.CFLAG_PAINT); purpleBox.connect(greenBox, MenuSlice.CONNECT_BOTTOM, MenuSlice.CFLAG_CONTROL); //Some bookkeeping MetaMenu.topLeftMI = largeClearBox;
As you can see, this follows from the Zen of Java: "Make it really wordy." Since MetaMenu.java is procedural in nature (and "your code, not mine"), I wouldn't worry about it. Our prototype looks like this when we load it; note the red lines used to denote the otherwise-invisible boxes.
Image Slices[edit]
The Menu Slice mentality really begins to shine with images and text. We'll start with our box picture. We'll need an image ("box1.png", see below) which we can load using that "adaptGen" argument we ignored earlier:
//Absolute path String workingDir = Meta.pathToGameFolder; ImageAdapter box1Pic = adaptGen.createImageAdapter(workingDir + "box1.png");
Now, we simply create a new Image Slice using this Adapter. We also change the width and height hints, and background colors. Here's a code snippet:
//Blue background "circle" MenuFormatArgs mf = new MenuFormatArgs(); mf.fillType = MenuSlice.FILL_NONE; mf.borderColors = new int[]{}; mf.widthHint = MenuFormatArgs.WIDTH_MINIMUM; mf.heightHint = MenuFormatArgs.HEIGHT_MINIMUM; ImageSlice blueGraphic = new ImageSlice(mf, box1Pic);
Note that the MINIMUM hints here are handled in a very different manner than for the invisible box. If a Menu Slice has child Slices, its MINIMUM size is fairly easy to compute. For a single component, though, custom behavior is required. Essentially, the MINIMUM width of an Image Slice is equal to:
min_width = number_of_borders*2 + border_padding*2 + width_of_image;
To satisfy your curiosity: tying to create a default MenuSlice object with MINIMUM width and no children will crash GAME_FMF.
Also, for the record, just a reminder that you have to calll doLayout() before getPosX()/Y()/Width()/Height() will return anything useful. This may seem unfair for single-use components:
MetaMenu.currCursor.doLayout();
...and it may change in the future. Until now, just remember to do this!
Text Slices[edit]
In contrast to Image Slices, Text Slices figure out their minimum size based on a very complex word-wrapping algorithm. Essentially, if WIDTH is minimum, the Text Slice is assumed to be one line long. For our labels, this is just what we need!
//Bob's Name mf.bgColor = 0xA8E61D; mf.borderColors = new int[]{0x22B14C}; mf.widthHint = MenuFormatArgs.WIDTH_MINIMUM; mf.heightHint = MenuFormatArgs.HEIGHT_MINIMUM; TextSlice greenBox = new TextSlice(mf, "Bob T. H.", rpg.font, true, true, false);
The first two arguments make sense. The last three flags are defined as follows:
- skipNLSymbol? - If false, the circle/arrow is displayed whenever a line is forcibly wrapped.
- makeShadow? - If true, each letter gets a black shadow offset by 1 pixel.
- autoDiet? - If true, our text box is constantly "skinny". Er.... moving on.....
In general, "true, true, false" is what you want for any Text Slice used in the Menu Engine. Finally, the font will always be "rpg.font".
How Input Works[edit]
Calling MetaMenu.topLeftMI.processInput(input)will actually cascade the input according to this pattern:
- Try sending this input to your "active" child (your top-left one, initially)
- If this fails, process it yourself. (That is, first handle it internally --e.g., pressing Left/Right on a "List" will change the listed item, and "consume" the input.)
- If the input remains un-consumed, check your CONTROL connections. If a MenuSlice exists in the appropriate direction, "move to" that object. If you trigger a move to a sibling child, your parent now recognizes the new item, not you, as "active".
This makes it very easy to track which item is currently "active", even if it's embedded several layers deep.
Practical Listeners: A Cursor[edit]
When the "active" child changes, a "Focus Gained" listener is triggered. This listener is defined by the user, and set using "menuSlice.addFocusGainedListener(listener)". It extends the "Action" interface. For those of you who aren't Java-savvy, this is just Java's way of firing off what other languages call "Events", "Signals", or "Messages". (To purists: I realize they're actually quite different.) So, let's use our new-found knowledge of input and listeners to make a cursor that points to the name of the hero who is currently selected. Here's the code for our "Action" that occurs when Bob's menu item gains the focus:
//Listener for Bob Action bobFocusGained = new Action() { public boolean perform(Object caller) { MenuSlice bobBox = (MenuSlice)caller; int newX = bobBox.getPosX()-MetaMenu.currCursor.getWidth()+4; int newY = bobBox.getPosY()+3; MetaMenu.currCursor.forceToLocation(newX, newY); return true; //Return anything; doesn't matter } }; //Assign the listener to Bob greenBox.addFocusGainedListener(bobFocusGained);
A few things of importance here. Firstly, the "perform" method of an Action is what does all the work of that Action. When Bob's slice receives the focus, this "perform" is called. And it's called with him as the "caller". So, we can get his location fairly easily.
Next, although it may seem a bit hackish, "forceToLocation()" can be quite useful. This function sets the cursor Slice's x and y co-ordinates manually, to somewhere left-ish of Bob's menu item. The cursor will continue to be drawn here until the next call to doLayout(). (In case you were curious, it's not necessary to attach the cursor to anything at all; we handle that in the Menu Engine. If you wanted it to be fully affected by layouts, you could attach it to the RIGHT of the "large clear" container.)
Faux-Rotating the Cube[edit]
Recall that our cube should "rotate" to show a certain color based on the current player selected. We won't even try to do this in code --swapping three different images will do. Since we've actually defined all required images to this point, I'm going to throw them up here in this nifty reference picture. If you want to copy them and save them locally, copy everything inside the dotted lines. I've added one extra picture for use in the next tutorial; save them all in the ohrrpgce/games/ directory.
Now, rotation just becomes a matter of switching in images 1, 2, or 3, respectively. There are many ways to do this, but we'll opt for a quick fix. You see, each Menu Slice has a "data" field that can be set to virtually anything, at the programmer's discretion. It was added for just such an occasion. Let's add an array containing all three pictures to our Image Slice:
ImageAdapter[] picsArray = new ImageAdapter[3]; picsArray[0] = box1Pic = adaptGen.createImageAdapter(workingDir + "box1.png"); picsArray[1] = adaptGen.createImageAdapter(workingDir + "box2.png"); picsArray[2] = adaptGen.createImageAdapter(workingDir + "box3.png"); blueGraphic.setData(picsArray);
Then, in Bob's "focus gained" method, we just add:
ImageAdapter[] imgArray = (ImageAdapter[])blueGraphic.getData(); blueGraphic.setImage(imgArray[0]);
All you java users will recognize that this means we have to make blueGraphic a static variable, right?
And that's all... except for one tiny detail. You see, since blueGraphic is the top-left most menu item, it's the one that'll gain focus when the menu first activates. We could hack it so that our "small clear" container is the top-left item, but that'd complicate our simple layout. An easier solution is to just do this:
//Never allow this item to have focus blueGraphic.addFocusGainedListener(new Action() { public boolean perform(Object caller) { MenuSlice calledBy = (MenuSlice)caller; MenuSlice rightConnect = calledBy.getConnect(MenuSlice.CONNECT_RIGHT, MenuSlice.CFLAG_PAINT); rightConnect.moveTo(); return true; //Return anything. Doesn't matter } });
The purpose of this is to immediately shift focus to "the thing right of the blue graphic" whenever the blue graphic gets focus (which is only once, when the menu first opens).
End Result[edit]
Here's a few screenshots of our new menu in action. It's rather powerful, rather sleek, and -if you think about it- rather hassle-free to set up. By the way, we had to give our menu a gray background, because the game doesn't actually redraw when the menu's running. So you'd see the hand in three locations at once. If you like the "HUD" look, you might try only drawing the cursor onto the drawable area of the Slices you create. Or somehow save the current background of GAME (how, I have no idea...)