Spellcaster Studios

Make it happen…

New UI system

In the last weeks, I’ve been working on and off in my new UI system… It’s about the 100th time I try to make an UI system for a game, since I always end up with a system that I don’t like…

First of all, I hate UI work, it’s simply not fun… Second, I’m picky about UI systems… they tend to be unwieldy or not game-oriented (so slow rendering performance).

This time, I decided to use some lessons learned with my RealJob™, in which I do a lot of Javascript/HTML/CSS frontends.

So, what I did was define a CSS-like language to express the visual appearance of elements:

.default
{
    color: white;
    interaction: false;
};

.mouse_cursor  
{ 
    image-bank: "ui";
    image-name: "cursor_normal";
}; 
  
miniframe_main
{
    position: (80,200,0.3); 
};

miniframe_base
{ 
    image-bank: "ui";
    image-name: "miniframe";
    position: (0,0,0);
};

miniframe_base_mask : miniframe_base
{
    image-name: "miniframe_portraitmask";
    image-mode: "mask";
    position: (0,0,0.02);
};

miniframe_base_portrait
{ 
    position: (-50,-50,0.02);
    interaction: true;
    image-bank: "portraits";
    image-name: "portrait_grey";
    width: 100;
    height: 100;
};

miniframe_base_bar
{ 
    image-bank: "ui";
    image-name: "miniframe_healthbar";
};

miniframe_bar : miniframe_base_bar
{
    width: 5;
    segment-count: 2;
    path: shape
    {
        line
        {
            start-pos: (0,0);
            end-pos: (122,0);
        };
    };
    texture-fill: true;
    texture-fit: true;
    color: yellow;    
};

characterframe_main
{
    position: (150,930,0.3);
};

characterframe_base
{ 
    image-bank: "ui";
    image-name: "maincharacterframe";
    position: (0,0,0);
};

characterframe_base_mask : characterframe_base
{
    image-name: "maincharacterframe_mask";
    image-mode: "mask";
    position: (0,0,0.02);
};

characterframe_base_portrait
{ 
    position: (-85,-88,0.02);
    interaction: true;
    image-bank: "portraits";
    image-name: "portrait_grey";
    width: 174;
    height: 174;
};

characterframe_resource_bar_base
{
    image-bank: "ui";
    image-name: "maincharacterframe_resourcebar";
    position: (-123,23,-0.02);
}

characterframe_base_bar
{ 
    image-bank: "ui";
    image-name: "miniframe_healthbar";
};

characterframe_health_bar : characterframe_base_bar
{
    width: 7;
    segment-count: 10;
    path: shape
    {
        arc
        {
            radius: (121,121);
            limits: (-100,6);
        };
    };
    texture-fill: true;
    texture-fit: true;
    color: green;    
    position: (2,-1,-0.04);
};

characterframe_resource_bar : characterframe_base_bar
{
    width: 7;
    segment-count: 10;
    path: shape
    {
        arc
        {
            radius: (121,121);
            limits: (-105,-217);
        };
    };
    texture-fill: true;
    texture-fit: true;
    color: yellow;    
    position: (1,-6,-0.04);
};

characterframe_action_buttons
{
    position: (0,0,-0.06);
    wheel-radius: 140;
    wheel-start-angle: -55;
    wheel-action-count: 4;
    wheel-separation: 25; 
}

lifeshape_button_base
{
    image-bank: "ui";
    interaction: true;
    position: (-30,-30,0);
};

lifeshape_button_imp : lifeshape_button_base
{
    image-name: "lifeshape_imp_normal";
    hover : class
    { 
        image-name: "lifeshape_imp_hover";
    };
    clicked : class
    { 
        image-name: "lifeshape_imp_click";
    };
}

lifeshape_button_wisp : lifeshape_button_base
{
    image-name: "lifeshape_wisp_normal";
    hover : class
    { 
        image-name: "lifeshape_wisp_hover";
    };
    clicked : class
    { 
        image-name: "lifeshape_wisp_click";
    };
}

lifeshape_button_bloodfiend : lifeshape_button_base
{
    image-name: "lifeshape_bloodfiend_normal";
    hover : class
    { 
        image-name: "lifeshape_bloodfiend_hover";
    };
    clicked : class
    { 
        image-name: "lifeshape_bloodfiend_click";
    };
}

lifeshape_button_skeleton : lifeshape_button_base
{
    image-name: "lifeshape_skeleton_normal";
    hover : class
    { 
        image-name: "lifeshape_skeleton_hover";
    };
    clicked : class
    { 
        image-name: "lifeshape_skeleton_click";
    };
}

It’s a semantic similar to CSS, but allowing for multiple inheritance of “classes” and some data of atypical types (like “curve” or “color gradient”).

Now, all elements that I create have one or more associated type, and what happens is that any type of element (for example “text” or “shape render”) will traverse the classes, looking for the applicable properties.

This works great, especially because the system supports hot-reload of styles (so I just change the style file, and it gets reflected immediately on the system, which supports fast iteration times, which is one of the most annoying points in terms of UI design).

I can instance any type of element using Lua code, but the system really shines when I use another type of file, which behaves in a similar fashion to HTML (although it’s more XML compliant):

<container>
    <container class="miniframe">
        <sprite id="frame" class="miniframe_base"/>  
        <sprite id="mask" class="miniframe_base_mask"/>
        <sprite id="portrait" class="miniframe_base_portrait"/>
        <sprite id="health_bar_base" class="miniframe_base_bar" position="(60,-15,-0.02)"/>
        <shape id="health_bar" class="miniframe_bar" position="(60,-10,-0.04)"/>
        <sprite id="resource_bar_base" class="miniframe_base_bar" position="(60,5,-0.02)"/>
        <shape id="resource_bar" class="miniframe_bar" position="(60,10,-0.04)"/>
    </container>
</container>
 

So, with just one Lua instruction, I can instance an element with this properties (which allow for override of the class properties), while preserving the hot-reload even of these components.

So, for once in my life, I’m pretty happy with the UI system… The only problem the system currently has is that the hierarchical notion of elements is not exactly simple (the 2d system of Spellbook was never designed for hierarchies), which means that while the position of an element depends on his parent element, other properties like scaling can’t be propagated properly (yet). Clamping the rendering to a specific rectangle (for scrollers, etc) is also complicated, for example (since I use the normal blit manager that’s used for normal 2d game stuff).

One possibility for the future is to add a special kind of render view (besides the 2d and 3d), which is dedicated to UI, which can encompass this type of logic, but I’m afraid that might be overkill, considering how late the game already is…

The end result of the above classes/UI elements is (zoomed):

image

Of course, there’s still a bucket of logic to be done on code (for example, the update of the bars), but that’s simple code.

The system also allows for some automatic behaviors, like “hover” or “click” behaviors (so that I register functions to be called when there’s a click, or a class to use when the cursor is over a component), so it’s very flexible at the moment…

By loading the correct style file, I can also consider tricky things like UI resolutions (with different screenspace settings, etc).

This took me in total about 12 hours or so of development, but I think this will pay itself nicely, considering it took me less than 1 hour to setup the current UI of the game (which is already fairly complex, with masks and curved progress bars, etc)…

Now, once more into the breach!

Comment