Massively Multiplayer
Seamless Open-World
Real Time Strategy
 

Twitter   Facebook   Google+   YouTube   E-Mail   RSS
The One Man MMO Project: A Chrome UI Part III - Javascript and CSS are Farking Borken
By Robert Basler on 2011-10-25 01:10:26
Homepage: www.onemanmmo.com email:one at onemanmmo dot com

A Chrome UI Part 1 & Part Deux.

I've been working on adding a more-complete user interface to my game. The UI screens I've done up to this point have been static pages (splash screen) or simple forms (login screen.) This week I've really gotten into it. I've been adding a HUD. The HUD is tricky because it needs to interact with the game, as well as update its internal state and display based on user input. It needs to be slick looking and responsive.

Ready for the acronyms? I'm building the HUD with HTML, CSS, Javascript and AJAX. Just one out of those four isn't borken.

Developing the page, my first design decision was that I should minimize the state information stored in the web page. The hud has a bunch of tabs on it. Rather than storing a huge whack of data for all of the tabs and building the parsing code for all that data into the web page, the web page sends every tab button press to the game which sends back just one tab worth of data to the web page to parse.

The first step in actually building the HUD was making it slick looking. I didn't think I could make it pretty enough with HTML+CSS, so I went to Corel Draw. I drew my UI, carefully laying it out, adding gradients, icons, and flourishes. The HUD picture contains several different versions of the HUD with different button states drawn on different areas of the picture. Then I exported it to a bitmap for each screen resolution I wanted to support.

Traditionally at this point you start cutting your UI bitmap into lots of teeny bitmaps. With a bunch of resolutions to support, this was looking unwieldy. Fortunately CSS presented the opportunity to avoid this tedious step. It turns out it is possible to specify a bitmap for the background of an element, such as an unordered list, and then adjust the position within that background bitmap that is displayed. So for example, my list mainbuttons is positioned 5 pixels from the top right corner of the document and has a width of 120 pixels and height of 137. The background is hud480.png and I'm displaying the background with coordinates of 0,0.

Now you'll notice that each of the list items also specifies the background bitmap and coordinates. This is where the magic happens. You can specify any coordinates you like for each list item (button), and you can change the coordinates at runtime using Javascript. Using those coordinates you can animate your button simply by changing the coordinates to the highlighted (or pressed, or whatever) version of the button.

    <ul id="mainbuttons" style="background: url(images/hud480.png) no-repeat 0px 0px;position: absolute; 
          top: 5px; right: 5px; width: 120px; height: 137px; list-style: none; margin: 0; padding: 0;">
        <li id="sellButton" style="background: url(images/hud480.png) no-repeat 0px 0px; position:absolute; 
          top: 0px; left: 0px; width: 20px; height: 22px;"   onclick="onSellButton()"><span>Sell Tooltip</span></li>
        <li id="repairButton" style="background: url(images/hud480.png) no-repeat -20px 0px; position:absolute; 
          top: 0px; left: 20px; width: 20px; height: 22px;"  onclick="onRepairButton()"><span>Repair Tooltip</span></li>
    </ul>

I'd used Javascript for some little features on websites before - cut and paste jobs from examples on the internet, but this was my first attempt at some serious scripting. For the first day or so using Javascript I suffered. Suffered. You see, Javascript is borken. It runs right up until something goes wrong, then just stops. No error message. No diagnostics. No clues. This kind of makes sense in the world of web browsers where users don't want to see error messages, but when you're developing complex web pages - it sucks hard. I figured there has to be a better way, and it turns out there is - Firebug.

Firebug is a Javascript debugger. You can set breakpoints, examine variables, step through code, all the normal stuff. But it is still borken! If you have bad code, the debugger still doesn't help you, it just spontaneously aborts whatever function you're in without any notice. There is an error tab that catches some errors, such as mismatched braces, but it is only minimally helpful.

One feature in Firebug that was super helpful was "Click an element in the page to inspect" because it would draw a rectangle around any element you put the mouse over. This was fantastic for checking the coordinates of the buttons of my HUD when positioning the <li> tags.

Chromium also has a built in Javascript debugger (which doesn't seem to work in Berkelium), but I haven't played with it much yet.

Once I had Firebug installed I started making some progress. Add a few alert("it got this far"); calls from time to time (those show up in my trace), and I was on my way.

Tooltips are important for helping new users, so I wanted to add some. To my amazement, CSS can make Tooltips - really super easy. Put the text in a <span> tag then display:none; makes the text invisible. But when you put the mouse over the button (with hover) you can draw cool translucent tooltips with absolute positioning, so I positioned my tooltips adjacent to my buttons, and they were good to go. (I have several groups of buttons, and I wanted my tooltips adjacent to the buttons, so I have several lists and styles, one for each button group.) The styles are:

    ul li span 
    {
        display:none;
    }
    ul#mainbuttons li:hover span 
    {
        display:block;
        padding: 5px;
        width:150px;
        background: #000;
        position: fixed;
        right: 127px;
        top: 5px;
        font: 11px Arial, Helvetica, sans-serif;
        opacity: .75;
        filter:alpha(opacity=75);
        color: #FFF;
        text-decoration: none;
    }

Next I wanted to get my buttons to talk to the game. I used AJAX for this. AJAX is a fancy name for a simple concept. Create an XMLHttpRequest object and you can use it to make get requests to your HTTP server without reloading the entire page. Magic!

Now like everything to do with web browsers, XMLHttpRequest is borken. You can't just create one and use it, you need a helper to create the freaking thing:

function createXMLHttpRequest() 
{
    if (typeof XMLHttpRequest == "undefined") 
    {
        try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); }
        catch (e) { }
        try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); }
        catch (e) { }
        try { return new ActiveXObject("Microsoft.XMLHTTP"); }
        catch (e) { }
        throw new Error("This browser does not support XMLHttpRequest.");
    }
    else {
        return new XMLHttpRequest();
    }
}

First we call createXMLHttpRequest to create the sellXml object. It will do this on page load. I might look into moving that into a onLoad handler at some point, but for now this is working.

        var sellXml = createXMLHttpRequest();

Then add a callback handler function for XMLHttpRequest. First it waits for readyState to be 4 (request complete) then checks the result code (0 for data loaded from disk, 200 for data loaded from a HTTPD server.) Once that passes, I call updateHudModeDisplay with the result (in this case the result is text - I'll talk more about that in a minute.)

        function onSellResponse() {
            if (sellXml.readyState == 4) {
                if (( sellXml.status == 0) || (sellXml.status == 200) ) {
                    updateHudModeDisplay(sellXml.responseText);
                }
            }
        }

Next up is the onSellButton handler (see it called from the <li> above?)

        function onSellButton() {
            sellXml.open('get', 'hudajax.xml?button=sell', true);
            sellXml.onreadystatechange = onSellResponse;
            sellXml.send(null);
        }

Open sets up the AJAX call along with the URL to get, the method to use (get/post) and true to tell the browser to make the call asynchronously. Once that's called, set the onreadystatechange callback and lastly call send to start the request to the HTTPD server.

The request goes to my game's built-in HTTPD server which sends back one text character, 0, 1, or 2 to tell the updateHudModeDisplay function how to display the Sell and Repair buttons.

While debugging in Firefox, you can store your AJAX data in a file, Firefox is quite happy to load it for you as long as the URL origins are the same.

So I said I'd talk about XML responses. I'm using XML to return the display information for a bunch of other buttons and tabs that change appearance when selected and have customized tooltip text. I won't bore you with the HTML, it looks more or less like the HTML list above. A subset of the XML for my HUD looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<info>
  <catbuttons>
    <button select="0" />
    <button select="1" />
    <button select="2" />
  </catbuttons>
  <tabs count="10" selected="4" offset="1" />
  <buttons>
    <button tt="Button 1 tooltip" />
    <button tt="Button 2 tooltip" />
    <button tt="Button 3 tooltip" />
    <button tt="Button 4 tooltip" /> 
    <button tt="Button 5 tooltip" />
  </buttons>
</info>  

AJAX gives you an XML object you can parse to pull the data from the xml. In your callback handler, use responseXML rather than responseText. Note that if your responseXML result is null, it means your XML data is wrong in some fashion (or maybe not - I'll talk about that in a sec...)

        function onUpdateResponse() {
            if (updateXml.readyState == 4) {
                if ((updateXml.status == 0) || (updateXml.status == 200)) {
                    updateCatButtonDisplay(updateXml.responseXML);
                }
            }
        }

So once you have your XMLHttpRequest object result, getElementsByTagName will pull out arrays of objects from the file. I use it to pull all the button records and process them.

        function updateCatButtonDisplay(xmlresponse) {
            var  = xmlresponse.getElementsByTagName('info');
            var catButtons = [0].getElementsByTagName('catbuttons');
            var buttons = catButtons[0].getElementsByTagName('button');
            for (var i = 0; i < buttons.length; i++) {
                var buttonWidth = -20;  // Width of an individual  button
                var selectShift = -124; // Value to shift bitmap coordinates to go to bitmaps for enabled/selected
                // select = 0 - disabled
                // select = 1 - enabled
                // select = 2 - selected
                var select = buttons[i].getAttribute('select');
                var offset = (i * buttonWidth) + ( select * selectShift );
                var newCoords = offset.toString() + "px -137px";
                switch (i) {
                    case 0:
                        document.getElementById('button0').style.backgroundPosition = newCoords;
                        if (select == 0) {
                            document.getElementById('cat0tt').innerHTML = "[[$HudCat0DTT]]";
                        }
                        else {
                            document.getElementById('cat0tt').innerHTML = "[[$HudCat0TT]]";
                        }
                        break;
                    case 1:
                        document.getElementById('button1').style.backgroundPosition = newCoords;
                        if (select == 0) {
                            document.getElementById('cat1tt').innerHTML = "[[$HudCat1DTT]]";
                        }
                        else {
                            document.getElementById('cat1tt').innerHTML = "[[$HudCat1TT]]";
                        }
                        break;
                    case 2:
                        document.getElementById('button2').style.backgroundPosition = newCoords;
                        if (select == 0) {
                            document.getElementById('cat2tt').innerHTML = "[[$HudCat2DTT]]";
                        }
                        else {
                            document.getElementById('cat2tt').innerHTML = "[[$HudCat2TT]]";
                        }
                        break;
                }
            }
        }

With the page working in Mozilla (my browser of choice) of course it wouldn't work in Berkelium. I got an error indicating that responseXML was null. Checking the responseText, it contained valid XML data. I tried loading an .html file containing the XML data in plain Chromium, and it gave me the curious error "cross origin requests are only supported for http." What? As it turns out Chromium won't parse a document named *.html containing XML even if it is told <b>Content-Type: text/xml</b> by the server. Changing the file name extension to .xml it magically started working.

I haven't said much about CSS yet. CSS is big, complicated, and completely opaque. As far as I can tell, there are no effective tools to help you debug your CSS. You can use an online CSS Validator to fix a few errors, but for everyday debugging this is really inconvenient. And it completely doesn't help you get the results you're looking for onscreen. All you can do is try something - see what happens. Doesn't work? Too bad. Try something else. And if you're supporting multiple browsers, you'll need to try your CSS on every browser and see what it does. Luckily I only have one browser to test against, but in the common case this is totally mental. Oh, and you can have CSS in external files, at the top of your file, or inline in individual tags. Ideally you're supposed to put your CSS in an external file so you can restyle your pages by changing a single file. In practice I'm putting my CSS in all three places which is just messy.

Web developers are heroes. The tools are crap.

New Comment

Cookie Warning

We were unable to retrieve our session cookie from your web browser. If pressing F5 once to reload this page does not get rid of this message, please read this to learn more.

You will not be able to post until you resolve this problem.

Comment (You can use HTML, but please double-check web link URLs and HTML tags!)
Your Name
Homepage (optional, don't include http://)
Email (optional, but automatically spam protected so please do)
Type girl. (What's this?)

Admin Log In



[The Imperial Realm :: Miranda] [Blog] [Gallery] [About]
Terms Of Use & Privacy Policy