Monday, January 14, 2019

battleMETAL - And Yet, Somehow it all works - part 1 - CSQC



CSQ-what?

It's hard to go into complete detail about this powerful module for Darkplaces (though I feel obligated to say this module is not exclusive to Darkplaces, and is available in other modern source ports of Quake such as FTE).

I probably already mentioned this earlier in the series, Client-Side Quake C, that is. Recall that Quake C compiles to file of bytecode called progs.dat. This file contains all of the custom game code that the Quake engine runs when the player starts up the game. Client-side Quake C (hereafter referred to as CSQC ) takes this concept and applies it to the client code during gameplay. What this means is that the programmer gets the full power of Quake C - spawning entities, File I/O, player state, player input and more; on the client-side. Want to load custom UI data but don’t want to have to broadcast over the network? CSQC. Custom sound effects played only for the specific client? CSQC. You get the idea.

One of the most powerful features CSQC has is the ability to draw images on the player’s Heads up display, or HUD. Like most features of the source ports of Quake, this is a series of api hooks that rely on the programmer to leverage effectively. The design ambiguity offers freedom however. Using the basic image draw calls, and some input tracking code, I was able to implement a basic Graphical User Interface (GUI) that runs during gameplay. I was also able to make a wonderful mech-style HUD that shows the player most of the information they’ll need to know when piloting the big stompy robots. This post deals mostly with implementing the in-game GUI which ironically is different from the Main Menu system.

CSQC has the following methods that define the skeleton of the system -

    CSQC_Init()

    CSQC_InputEvent()

    CSQC_UpdateView()

    CSQC_Parse_StuffCmd()

There are a few more, but these are outside the scope of this post. A quick breakdown of these functions. _init() is the first function called, specifically once the client connects to the server. Its when the CSQC context begins, semantically it's a great place to put any code you want to be initialized as soon as possible. The InputEvent() handles all player input during gameplay. It returns a boolean where TRUE means the input event was handled by CSQC and thus does not need to be passed onto the server. UpdateView() is the big one, this is where all render calls for custom GUI / HUD / anything else you want drawn get placed. You can also modify the view angles, location, and Field of View for the client as well. Lastly, StuffCmd() is used for handling text-based console commands that come from the server.

I built the in-game menus off a few basic presumptions: tracking player mouse input, the on-screen location of the mouse, a modest set of UI functions, and some variables to track the state of the menu. The trickiest implementations were the modest set of ui functions - end result wasn’t so modest. I was able to get things like lists to work; you can see examples in the Mech Hangar and Arming menus. However I never got ‘scrolling’ lists to work especially due to not feeling the feature was needed in the first place. I was able to ensure that the UI scaled to the player’s display resolution...though my approach was hacky.


I chose a resolution, 1240 x 960, and created all the UI’s to this scale including their screen coordinates. Next, I converted the size or location value into a percentage of (x / 1240), (y / 960). Then, these percentages are applied to the player’s screen resolution. Therefore, a UI element that is 75% of the total screen width will always be 75% of the total screen width regardless of the actual number that screen width is. Its brute-force, but it worked solidly enough to base a menu system on.

The core of any given menu is a set of functions that in a real language would be an ‘interface’, a contract of functions that each menu object would have to implement. Each menu has a _DrawFrame(), _Listener() functions. The menu system uses a switch() control and a global variable MENU_CHOICE to determine which _DrawFrame and Listener function to call. Let’s use the hangar example, we see mechHangarDrawFrame() and mechHangarListener(). Inside each of these functions, we put all the UI elements we’d like to draw for that menu; mechHangarDrawFrame() ends up looking like

     mechHangarDrawFrame(){

         local vector topleftroot;
         topleftroot = VIEW_ORG;

        drawpic(topleftroot, UI_DEF_BOX_512, VIEW_MAX, CLR_DEF_UI_HANGAR, 1,0);

        menu_hangar_MechDisplay(topleftroot + gui_percentToPixelRawVec('0 24'));
        menu_hangar_MechList(topleftroot + gui_percentToPixelRawVec('0 24'));
        menu_hangar_MechInfo(topleftroot);

         menu_hangar_MechFluff(topleftroot);

         menu_hangar_MechHPoints(topleftroot);

     }


drawPic() is a CSQC function for drawing 2D images onto the player’s screen. The other functions are made by me, and each renders a different part of the Mech Hangar GUI so that the end result looks like:

 

The second part is reading player input when the menu is active. For each menu, there’s a distinct function, and in our example its mechHangarListener(). This function contains all the input-related behavior that the code will call when the player inputs something.

     mechHangarListener(){
         mechSelectListener()
     }

Where mechSelectListener() is ‘listens’ for player input on a specific UI element. Quake C is fairly rudimentary, so the means for capturing player input in-context of a GUI was crude. For starters, the code I wrote breaks the screen up into rectangles. The specific Listener function will give the coordinates for the rectangle. When the player clicks the mouse, the listener code then checks to see if the mouse’s screen location is inside the rectangle that it defined. If the mouse is inside the rectangle, it returns the TRUE value, and again it's up to the listener function to handle this result. For lists of objects, such as the list of playable mechs, the ListListener function actually returns the index number of the item on the list. So if the player chose the second Light Mech, then the listener return value would be 2.

There are some drawbacks to such a simple menu setup; the biggest being items cannot be layered on top of each other. Each menu is only ever 1 ‘layer’ of UI elements deep. Another one is scrolling lists, I’m sure with some extra effort, getting the scrolling lists wouldn't be too bad. Finally, there is no way to drag-and-drop items (because there aren’t any ‘layer’ of menu). If I had more time for polish, I would have added some drag-and-drop functionality, especially for the Arming menu.



No comments:

Post a Comment