Code3Arena

PlanetQuake | Code3Arena | Articles | << Prev | Article 8 | Next >>

menu

  • Home/News
  • ModSource
  • Compiling
  • Help!!!
  • Submission
  • Contributors
  • Staff
  • Downloads

    Tutorials
    < Index >
    1. Mod making 101
    2. Up 'n running
    3. Hello, QWorld!
    4. Infinite Haste
    5. Armor Piercing Rails
    6. Bouncing Rockets
    7. Cloaking
    8. Ladders
    9. Favourite Server
    10. Flame Thrower
    11. Vortex Grenades
    12. Grapple
    13. Lightning Discharge
    14. Locational Damage
    15. Leg Shots
    16. Weapon Switching
    17. Scoreboard frag-rate
    18. Vortex Grenades II
    19. Vulnerable Missiles
    20. Creating Classes
    21. Scrolling Credits
    22. Weapon Dropping
    23. Anti-Gravity Boots
    24. HUD scoreboard
    25. Flashlight and laser
    26. Weapon Positioning
    27. Weapon Reloading
    28. Progressive Zooming
    29. Rotating Doors
    30. Beheading (headshot!)
    31. Alt Weapon Fire
    32. Popup Menus I
    33. Popup Menus II
    34. Cluster Grenades
    35. Homing Rockets
    36. Spreadfire Powerup
    37. Instagib gameplay
    38. Accelerating rockets
    39. Server only Instagib
    40. Advanced Grapple Hook
    41. Unlagging your mod


    Articles
    < Index >
    1. Entities
    2. Vectors
    3. Good Coding
    4. Compilers I
    5. Compilers II
    6. UI Menu Primer I
    7. UI Menu Primer II
    8. UI Menu Primer III
    9. QVM Communication, Cvars, commands
    10. Metrowerks CodeWarrior
    11. 1.27g code, bugs, batch


    Links

  • Quake3 Files
  • Quake3 Forums
  • Q3A Editing Message Board
  • Quake3 Editing


    Feedback

  • SumFuka
  • Calrathan
  • HypoThermia
  • WarZone





    Site Design by:
    ICEmosis Design


  •  
    ARTICLE 8 - UI Menu Primer III
    by HypoThermia

    With a good understanding of how the menu system works (UI Menu Primer I), and a look through the menu reference (UI Menu Primer II), we're now in a good position to look at the more advanced features of the menu interface. This article assume that you've at least read through parts I and II.

    If you're contemplating doing some of the things described in this article then it's assumed that you're savvy enough to be able to work through some code. Be prepared to look through the source code to improve and polish your understanding.

    Finally, we'll look at some of the things that go into good menu design, concentrating mainly on pages of controls.
     

    1. Custom menu drawing

    Custom menu drawing comes into its own when you're designing a new menu (or modifying an existing one). When you want to draw a decorative piece of art or title, then consider using this instead of creating a menu control that's disabled (doesn't take any input).

    There is a limit to the number of controls on a page, if you're reaching this limit then custom menu drawing might help you cut down on menu controls that never take input. The alternative is increasing the number of controls on a page - at the risk of creating excessive clutter.

    When it comes to drawing the menu on-screen we can intercept this by setting the draw function pointer in your menuframework_s structure for that menu. If this pointer is set then our menu drawing function will be called instead of the default menu drawing code. We must remember to call that default code ourselves, otherwise any controls we've created won't be drawn.

    The default menu drawing code is called through DrawMenu(menuframework_s*). Any background drawing should be done before calling this function, similarly any drawing done afterward will obscure what has been drawn by DrawMenu().

    The menu.draw function must be of the following type:

    static void DrawFunctionName(void); // "static" recommended
    

    and assigned when the menu controls are initialized. It's also a good idea to cache any graphics that'll be drawn.
     

    1.1 Custom menu drawing: Example

    The following example shows the bot menu background (a title and left/right brackets) being drawn without using controls. Taken from ui_addbots.c:

    #define ART_BACKGROUND "menu/art/addbotframe"
    
    typedef struct {
          menuframework_s menu;
          menubitmap_s arrows;
    
          // rest of structure snipped
    } addBotsMenuInfo_t;
    
    static addBotsMenuInfo_t	addBotsMenuInfo;
    
    
    /*
    =================
    UI_AddBotsMenu_Draw
    =================
    */
    static void UI_AddBotsMenu_Draw( void ) {
       UI_DrawBannerString( 320, 16, "ADD BOTS",
            UI_CENTER, color_white );
       UI_DrawNamedPic( 320-233, 240-166,
            466, 332, ART_BACKGROUND );
    
       // standard menu drawing
       Menu_Draw( &addBotsMenuInfo.menu );
    }
    

    The menu.draw pointer was initialized with the following code when each of the other controls were initialized:

    addBotsMenuInfo.menu.draw = UI_AddBotsMenu_Draw;
    

     

    2. Updating a menu thats already been pushed

    If you create a menu hierarchy with a parent and one or more child menus, you use UI_PushMenu() to start display of the child, and UI_PopMenu() to return to the parent. How do you update the parent menu when something relevent in a child has changed?

    You can't rely on the initialization process for the parent, because this is only called when the menu is created. When control pops to the parent menu it just picks up from where it was last drawn.

    Fortunately you can detect when this happens. The variable uis.firstdraw is always set to qtrue when a menu page replaces a previously drawn menu. This happens during either the push that first activates the menu, or the pop that re-activates the dormant menu. You can check for this during your custom draw routine: either for an individual control, or the entire menu.

    Note that this is late initialization. If you do a lot of data loading or manipulation as a result then this will cause significant delays in menu transition. Sometimes you can't avoid this, but you should at least be aware of it.

    This now gives an additional place in which you can initialize data in controls. It should be used sparingly, and is best for fixing syncronization problems caused by data changes in child menus.

    While checking uis.firstdraw offers one method of updating a dormant menu, Id have also implemented another. The single player game menu in ui_splevel.c provides a global function that forces re-initialization, but deferred until the menu is next drawn. You can track how they did it by the usage of UI_SPLevelMenu_Reinit() and the variable levelMenuInfo.reinit. Notice that the use of UI_PopMenu() in this method means that any info in your menu needs to be saved first.

    Use the method you're most comfortable with, or feel is most appropriate.
     

    3. Ownerdrawn controls

    What happens if you want something other than the seven standard controls?

    Well, short of designing a control of your own from scratch, you can use an existing control and modify how its drawn on screen. You'd best start by choosing a control closest in behaviour to what you want. For example: if you want to draw a clickable picture with some text, then you'd probably start with the menubitmap_s control (it already draws a picture and is clickable).

    When each control is initialized you can set the data member generic.ownerdraw to point to a function under your control. By default the body of this ownerdraw function takes FULL responsibility for drawing the control.

    The ownerdraw function must be of the following form:

    // self points to the control being drawn
    // and can be recast to the generic (menucommon_s*)
    // or type of control.
    // "static" qualifier recommended
    static void OwnerDrawFunction( void *self )
    

    Although there is a QMF_OWNERDRAW flag, it's not actually used. Just set the function pointer generic.ownerdraw to your OwnerDrawFunction().
     

    3.1 Fleshing out an ownerdrawn control

    If the control can be manipulated by the user then some kind of a "tactile" feedback is required. This takes the form of a graphical highlight (indicating one item from a choice), a "pulsing" effect to indicate focus (that the control is currently selected), or a "greyed out" effect (showing the control is disabled).

    The current state of the control can be accessed by looking at the state of generic.flags. The following values are tactile and should be handled as needed:

    QMF_GRAYED,QMF_PULSE, QMF_PULSEIFFOCUS, QMF_HIGHLIGHT, and QMF_HIGHLIGHT_IF_FOCUS.

    Controls have special behaviour when implementing the paired flags (PULSE and HIGHLIGHT). It is also possible for a control to "pulse" or "highlight" when only the permanent "IFFOCUS" flag is set. This happens when the mouse cursor is over the control. Example 2.2 below shows how to detect this situation.

    Most of the default draw functions for each control has been "hidden" by making the draw function "static". The two exceptions to this are Bitmap_Draw(menubitmap_s*) and ScrollList_Draw(menulist_s*), probably because they're the most complex. Make sure you've initialized your control so that these draw routines work "as expected".

    If you do want to access these "hidden" draw functions then you'll have to remove their "static" qualifier, and add their declaration to ui_local.h in the section describing things appearing in ui_qmenu.c.

    You can get a lot of good ideas by looking at how the existing 7 controls are implemented. Take a wander through ui_qmenu.c.
     

    3.2 Ownerdraw example: Handling state flags

    Although this is code from one of the 7 provided controls, it shows how most of the state flags can be handled for the drawing of text. When you're providing your own drawing routines you'll need to decide which of these you'll be using, and how to implement them.

    This menuaction_s control isn't used within the source code in its default form. It was supposed to provide a simple menu-like text control, but was replaced by menutext_s.

    For the most part we are just converting the QMF_* flags into the equivalent UI_* flags, choosing the appropriate colours, and then calling the text drawing function at the right position on-screen.

    If you want to see how state flags can be handled for bitmaps then take a look at the more complicated Bitmap_Draw() in ui_qmenu.c.

    The special consideration of the QMF_PULSEIFFOCUS type of flag is explained below.

    /*
    =================
    Action_Draw
    =================
    */
    static void Action_Draw( menuaction_s *a )
    {
          int		x, y;
          int		style;
          float*	color;
    
          style = 0;
          color = menu_text_color;
          if ( a->generic.flags & QMF_GRAYED )
          {
              color = text_color_disabled;
          }
          else if (( a->generic.flags & QMF_PULSEIFFOCUS ) &&
              ( a->generic.parent->cursor ==
              a->generic.menuPosition ))
    
          {
              color = text_color_highlight;
              style = UI_PULSE;
          }
          else if (( a->generic.flags & QMF_HIGHLIGHT_IF_FOCUS )
              && ( a->generic.parent->cursor ==
              a->generic.menuPosition ))
          {
              color = text_color_highlight;
          }
          else if ( a->generic.flags & QMF_BLINK )
          {
              style = UI_BLINK;
              color = text_color_highlight;
          }
    
          x = a->generic.x;
          y = a->generic.y;
    
          UI_DrawString( x, y, a->generic.name,
               UI_LEFT|style, color );
    
          if ( a->generic.parent->cursor ==
               a->generic.menuPosition )
          {
              // draw cursor
              UI_DrawChar( x - BIGCHAR_WIDTH, y, 13,
                   UI_LEFT|UI_BLINK, color);
          }
    }
    

    There are pre-defined colours that correspond to the existing Q3 menu colour scheme, they can be found in ui_local.h. I'd recommend that you use them where possible, this makes a colour scheme easier to change.

    Notice how the control responds to the QMF_PULSEIFFOCUS flag. It checks whether the cursor is over that control, and then provides a visual ("tactile") feedback to the user. For completeness it should give QMF_PULSE the same effect (without checking the cursor).

    There is also a function Menu_ItemAtCursor(menuframework_s*) that returns the current item under the cursor; this can be used in a similar way, as the following code fragment shows:

    // "t" can be any control data structure
    menuaction_s* t;
    
    // assign a safe value to "t" before using it
    
    if( Menu_ItemAtCursor( t->generic.parent ) == t ) {
    }
    
    // which is identical to this:
    
    if ( t->generic.parent->cursor ==
         t->generic.menuPosition ) {
    }
    

     

    3.3 Ownerdraw example: Choose a crosshair

    One of the more involved owner drawn controls, this source code is taken from ui_preferences.c. The usage of this control actually "mixes and matches" controls and types... so pay attention.

    This is essentially a new type of control. All the drawing and initialization is done outside the standard seven controls in ui_qmenu.c.

    The effect that we're looking for is a control with named text that draws the cursor graphic. When clicked upon it cycles through all the types of cursor available.

    The first decision was whether a graphic control is modified to draw text as well, or a text control is modified to draw graphics too. The "conceptual" idea is a list that draws a graphic (instead of the text choices), so menubitmap_s wasn't used.

    This leads to the control of choice as menulist_s, since we are selecting one from a list of many:

    typedef struct {
          // snipped...
          menulist_s crosshair;
          // snipped...
    } preferences_t;
    
    static preferences_t s_preferences;
    

    So far, so good. What follows is where some of the confusion may arise.

    When we look at how the crosshair structure is initialized (below), we see that it's defined as MTYPE_TEXT. This was done with the QMF_NODEFAULTINIT flag set too, so we have to set the generic parameters ourselves. This includes the mouse activated area bounded by generic.top, generic.bottom, generic.left, and generic.right.

    Why do things this way? Well this helps while the page of controls is developed. At the start you have a standard text control that acts as a "placeholder" for the final owner draw version. You can chop and change the look of the page until you're happy, without implementing (and possibly breaking) the owner draw code. You also don't have to set up or use any part specific to the list control.

    Later, when you move to the owner draw implementation, you don't trigger any of the menulist_s code (there's no list of text used, so using MTYPE_TEXT makes sure it's never assumed to be present). Finally, the crosshair.curvalue variable is available for storing the crosshair type.

    Make sure you understand what we have here: this is essentially a new type of control. All of the drawing and initialization is done outside the standard seven controls in ui_qmenu.c. So important an idea that it's worth the second mention.

    y = 144;
    s_preferences.crosshair.generic.type = MTYPE_TEXT;
    s_preferences.crosshair.generic.flags =
         QMF_PULSEIFFOCUS|QMF_SMALLFONT|
         QMF_NODEFAULTINIT|QMF_OWNERDRAW;
    s_preferences.crosshair.generic.x = PREFERENCES_X_POS;
    s_preferences.crosshair.generic.y = y;
    s_preferences.crosshair.generic.name = "Crosshair:";
    s_preferences.crosshair.generic.callback = Preferences_Event;
    s_preferences.crosshair.generic.ownerdraw = Crosshair_Draw;
    s_preferences.crosshair.generic.id = ID_CROSSHAIR;
    s_preferences.crosshair.generic.top = y - 4;
    s_preferences.crosshair.generic.bottom = y + 20;
    s_preferences.crosshair.generic.left = PREFERENCES_X_POS -
        ((strlen(s_preferences.crosshair.generic.name)+1)
        * SMALLCHAR_WIDTH);
    s_preferences.crosshair.generic.right = PREFERENCES_X_POS + 48;
    

    Now that we understand what we have here, this is how the control is drawn on the screen. Some attention is given to whether the control is greyed or has the focus. This only applies to the text... not the crosshair graphic.

    /*
    =================
    Crosshair_Draw
    =================
    */
    static void Crosshair_Draw( void *self ) {
       menulist_s *s;
       float *color;
       int x, y;
       int style;
       qboolean focus;
    
       s = (menulist_s *)self;
       x = s->generic.x;
       y = s->generic.y;
    
       style = UI_SMALLFONT;
       focus = (s->generic.parent->cursor ==
          s->generic.menuPosition);
    
       if ( s->generic.flags & QMF_GRAYED )
          color = text_color_disabled;
       else if ( focus )
       {
          color = text_color_highlight;
          style |= UI_PULSE;
       }
       else if ( s->generic.flags & QMF_BLINK )
       {
          color = text_color_highlight;
          style |= UI_BLINK;
       }
       else
          color = text_color_normal;
    
       if ( focus )
       {
          // draw cursor
          UI_FillRect( s->generic.left, s->generic.top,
                s->generic.right-s->generic.left+1,
                s->generic.bottom-s->generic.top+1,
                listbar_color );
          UI_DrawChar( x, y, 13, UI_CENTER|UI_BLINK|
                UI_SMALLFONT, color);
       }
    
       UI_DrawString( x - SMALLCHAR_WIDTH, y, s->generic.name,
          style|UI_RIGHT, color );
       if( !s->curvalue ) {
          return;
       }
       UI_DrawHandlePic( x + SMALLCHAR_WIDTH, y - 4, 24, 24,
          s_preferences.crosshairShader[s->curvalue] );
    }
    

    Notice the use of s->curvalue (in the last line of code) to decide which crosshair to draw. The following code snippet is from the event handler for the control. We have to update and wrap curvalue for the correct behaviour when the control is clicked on. This would be done for you in a menulist_s correctly identified as MTYPE_SPINCONTROL.

    static void Preferences_Event( void* ptr, int notification ) {
       if( notification != QM_ACTIVATED ) {
             return;
       }
    
       switch( ((menucommon_s*)ptr)->id ) {
       case ID_CROSSHAIR:
             s_preferences.crosshair.curvalue++;
             if( s_preferences.crosshair.curvalue == NUM_CROSSHAIRS ) {
                  s_preferences.crosshair.curvalue = 0;
             }
             trap_Cvar_SetValue( "cg_drawCrosshair",
                  s_preferences.crosshair.curvalue );
             break;
    
             // code snipped...
    
       }
    }
    

     

    4. Using the status bar

    If a control has any special behaviour, or takes special value(s), then consider using a statusbar to inform/remind the user. This additional information about a control will be displayed when the cursor hovers over the control, and makes use of the generic.statusbar callback function.

    static void StatusBarFunction( void *self )
    

    The pointer to self indicates the control that is drawing the status bar. If you don't have access to the source control then re-cast the pointer to (menucommon_s*) for the type of control in self->type.

    The status bar function is repeatedly called each time the control is drawn (which is as quickly as your graphics drivers will allow), and only if the cursor is over it; so you shouldn't set anything permanent. The information drawn is usually placed at the bottom of the screen.

    Use of the status bar function saves on creating a text control, and also avoids invoking an ownerdrawn version of the control to detect the presence of the cursor. Naturally, the area of the screen used by the status bar text should be kept free of other controls.
     

    4.1 Status bar example

    This example of a statusbar is taken from ui_startserver2.c where a single player game is setup. It's displayed when the cursor is over the timelimit or fraglimit controls. In this case the same status bar function is used for both controls.

    /*
    =================
    ServerOptions_StatusBar
    =================
    */
    static void ServerOptions_StatusBar( void* ptr ) {
    	UI_DrawString( 320, 440, "0 = NO LIMIT",
              UI_CENTER|UI_SMALLFONT, colorWhite );
    }
    

    The code for initializing the control looks like this:

    s_serveroptions.fraglimit.generic.type = MTYPE_FIELD;
    s_serveroptions.fraglimit.generic.name = "Frag Limit:";
    s_serveroptions.fraglimit.generic.flags =
         QMF_NUMBERSONLY|QMF_PULSEIFFOCUS|QMF_SMALLFONT;
    s_serveroptions.fraglimit.generic.x = OPTIONS_X;
    s_serveroptions.fraglimit.generic.y = y;
    s_serveroptions.fraglimit.generic.statusbar =
         ServerOptions_StatusBar;
    s_serveroptions.fraglimit.field.widthInChars = 3;
    s_serveroptions.fraglimit.field.maxchars = 3;
    

     

    5. Designing the menu

    I'm not going to turn this into (much of) a lecture on good design: you don't want to read that, and I'm not really qualified to give it. What I'll do is point out a few things that Id have done, how you can benefit from doing similar things, and that people do take an impression away with them about good or bad interface design.

    'Nuf arm twisting.
     

    5.1 The good, the bad, and the just plain ugly

    There are several things that jump out about the way Id have designed their menus. The most obvious thing is that they're relatively uncluttered, with a fair amount of open space.

    Take the two skirmish menu pages that help you setup a single or multi-player game. They could have been squeezed onto one page... one complicated and tightly packed page. Instead they've been split up over two pages, with controls grouped together (and separate from each other) so you can see what's related to what.

    The first skirmish page chooses the map and type of game. For team games the second page will add red/blue options for each of the bots/players. If this were all done on one page then there'd be the added confusion of these fields vanishing and re-appearing as the game type was cycled. Not exactly eye candy, now is it?

    For pages of controls that follow on from each other, there are always buttons that take you forward and take you back. The button that takes you back is in the lower left corner, while the one that takes you forward is in the bottom right corner. Think of page turning in a book to see the sense in this. You don't read books? Then think of the forward/back buttons on your web browser.

    Related controls are almost always close together. This gives two benefits: you show that they are related (duh!), and when they're changed the user needs to move only a short distance to neighbours. Moving a long distance between frequently changed controls is a pain in the wrist! You want to save your wrist something else... right?

    When choosing one action makes another control redundant, then that redundant control is hidden, disabled, or greyed out. This helps the user understand that something isn't going to do anything. And lets face it, users like you for taking decisions like that away from them.
     

    5.2 Back to pencil and paper

    Just a quick tip for those who are contemplating a new menu page full of controls. I've found that you can design a 640x480 screen of controls on an A4 page of squared paper (the kind that has squares about 5mmx5mm, you probably did maths sums in school using paper like this).

    If you take each square as 16x16 pixels then the page is just about filled. The small font gets two characters per square, the standard big font get one char per square. Screen co-ordinates are then easily and accurately recovered, and you have a hard copy of what you want the final menu page to look like.

    You can move controls around easily, and quickly see what does and doesn't work. Once you've coded something there's a lot of effort that might have to go to waste.
     

    6. Congratulations...

    Phew! If you've got this far then you've read through a lot of stuff about writing menu code. Thanks for the perseverance. I hope it's given you the infomation you needed.

    If you've followed and understood then you should be able to write exactly what you need for your mod in the way of menu code. Let me know if there's anything that needs clarifying, or if I've missed something major.

    Cheers!

    HypoThermia

    PlanetQuake | Code3Arena | Articles | << Prev | Article 8 | Next >>