[Treasure Hunt] Radar UI

radar3

How to make Radar UI

In this tutorial you will learn how to use html and css to build
game UI, which will show a radar, which later could be enhanced
to display relative position to other entities.

Tutorial does not include scripting part, it focuses only on
visual part of UI and which techniques one can use to build
something that have a nice look, while being prepared for later
integration with the rest of the game.

Initial html

Widgets usually come bootstrapped with some code, but for the sake
of tutorial I will skip things, which are not related to UI itself.

Let’s start with some simple html page:

<html>
    <head>
        <style></style>
    </head>
    <body>
    </body>
</html>

The style would be where our CSS later go and elements will be in body.

Radar Screen

Let’s describe a radar class inside of style block
and then add corresponding element in the body:

<html>
    <head>
        <style>
            .radar {
        
                /* We use absolute position to place our radar anywhere on the screen. */
                position: absolute;
                /* Absolute positioning ignores standard top-to-bottom flow elements in 
                websites and istead positions element relative to either the screen or
                the closest parent with same absolute or `relative` positioning. */
        
                right: 2rem;
                bottom: 0rem;
                /* `right` and `bottom` properties would locate the element in a way, that
                it's right edge would be on a specified distance from right edge of parent
                element and same for bottom edge. Basically here we set that there is small
                distance from element to the right and it sticks to the bottom with its bottom
                edge. This will work regardless of the size of element. */
        
                width: 12rem;
                height: 12rem;
                /* For now we just set a fixed size of element: 10 on 10 rems */
        
                /* Configuration for border is a mixed set:
                */
                border: 0.3rem black solid;
                /* 
                    Firstly we define that border would be 0.3rem, black and solid
                    At this point that config works on all borders - top, bottom, etc
                */
                border-bottom-width: 0rem;
                /* 
                    Then we override part of previous style to define that on 
                    bottom edge there will be no border, or rather border is of 
                    width 0, which is essentially no border.
                */
                border-top-left-radius: 5%;
                border-top-right-radius: 5%;
                /* 
                    Then on two corners we would define that border has to shaped
                    as a circle quarters with the radius, which is 5 percent of element size.
                */
                
                background: black;
                /*
                    We would want a screen of our radar to be simply black.
                */
            }
        </style>
    </head>
    <body>
        <div class="radar">
        </div>
    </body>
</html>

Grid

For the radar to have this cool nice lamp-style look we would want to
add a bit of green color to it. Radars you usually see in films have
that grid made of green lines and circles to distinguish distances
and angles to the located objects.

With CSS we can imitate the same effect by using gradients. Gradient
is a way to describe how to color elements based on several colors, which
transition from one to another. There are three types of gradients -
linear, radial and conic. Since the grid we want to display would consist
of lines and circles, we would only need linear and radial gradients.

The trick we are going to use here is transparent color. The thing is
that each color in CSS has in addition to the color itself, also how
much of background behind it, the color should let through. This is a
bit like a colored window. You can choose how thick the glass would be
besides choosing the color of each part of it.
transparent color is basically a color, which lets everything through
it and that would help us to define a places between grid lines.

Let’s start with placing grid under our main radar screen element:

    <div class="radar">
        <div class="grid">
        </div>
    </div>

Now that we have grid underneath radar, they are bound with parent-child
relationships. It does not mean they rememeber each other birthdays, but
that child element coordinates depends on parent coordinates.

    .grid {
        /* We again use absolute positioning */
        position: absolute; 
        /* 
            However, in this case, position would be defined within
            parent element, because of how `absolute` position works
            related to the closest parent with same or `relative`
            positioning.
        */
        width: 12rem; 
        height: 12rem; 
        /* 
            We are setting the same size on the grid as the screen itself,
            so it is neatly fit to it.
        */

        /* 
            Border is configured with 100 percent radius, so that all 
            corners are rounded as much as possible, which effectively 
            makes the element a circle.
        */
        border-radius: 100%;

        /* 
            Background property helps us to configure how the element insides look like.
            In here we use a bunch of gradients, each of them works in addition to others.
        */
        background:
            linear-gradient(0deg, transparent 49.6%, #03b627 50%, transparent 50.4%),
            linear-gradient(90deg, transparent 49.6%, #03b627 50%, transparent 50.4%),
            /* 
                These two first gradients are two lines: horizontal and vertical.
                Firstly we describe the horizontal line. Horizontal line is defined as a gradient
                going bottom to top, so it firstly defines the angle at which it lies to the y axis:
                0 degrees. Basically 0 degrees means from bottom to top.
                Next three parameters are color points. We define that we want transparent color
                from the start of gradient up to 49.6 percent of the lenght of the element.
                Next point at 50 percent defines that the greenish color needs to be at that 
                location and in between - color would be gradually changed from transparent 
                to that greenish color. Then we again define that 0.4 percent later, the color
                should be transparent again and up to the end of element it would remain transparent.
                That definition makes up a line in the middle with that green color.

                Secondly we define the same kind of line, but rotated on 90 degrees, which would
                be a vertical line.
            */

            linear-gradient(45deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            /*
                Each of the following linear gradients have slightly smalleer size so that
                lines under not strict angles, would be a bit less wide. 
            */
            linear-gradient(-45deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            linear-gradient(30deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            linear-gradient(-30deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            linear-gradient(60deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            linear-gradient(-60deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            linear-gradient(75deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            linear-gradient(-75deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            linear-gradient(15deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            linear-gradient(-15deg, transparent 49.9%, #03b627 50%, transparent 50.2%),
            
            repeating-radial-gradient(transparent 5.8%, transparent 18%, #03b627 18.6%, transparent 18.9%);
            /*
                Final gradient is a repeated radial gradient. This would create a bunch
                of circles due to the way repeating a gradient works. Basically we
                define that in places where gradient is not defined - it would repeat
                itself over and over again in circles instead of just continueing the way
                that latest color point was set.
                Two transparent points at 18 and 18.9 percent are outline edges of the line
                with greenish color as point between them at 18.6 defines.
                Point at 5.8 is basically used to setup a distance between circular line, 
                where grid is transparent. This helps with defining how far circles would
                be spaced apart. 
            */
    }

Scanning ray

Now that we have nice grid inside of our screen, let’s place there something
that looks like a radar scanning ray.

    <div class="radar">
        <div class="grid"></div>
        <div class="scanning"></div>
    </div>

We have placed scanning ray inside of base radar screen element and now
we need to style that new block, knowing that its position depends on
position of the radar.

    .scanning {
        /*
            Relatively to radar screen we would want to place the scanning 
            ray at exactly the top left corner of it.
        */
        position: absolute;
        top: 0px;
        left: 0px;

        width: 50%;
        height: 50%;
        /* 
            Now we set the size of this element to be a half that of the parent.
        */

        border-top-left-radius: 100%;
        background: linear-gradient(45deg, transparent 50%, #6ae01c 100%);
        /*
            This here uses gradient again, however this time we point it 
            at an angle between 0 and 90 and make it slowly change from 
            transparent in the middle to completely green at the end.
            At the same time we set only one of angles to be a 100 percent
            rounded, which would leave us with a quarter of a circle.
        */

        animation: rotating 4s infinite;
        animation-timing-function: linear;
        transform-origin: bottom right;
        /* 
            This part is animating scanning ray, so it would be rotating 
            around the center of the radar screen.
            We define name of animation (which would be defined a bit later)
            to use, then how much time it takes to make a full turn - 4 seconds,
            then in which patern to repeat that animation - we set it to be
            infinitely repeated.
            Timing function of animation sets it to always animate at the same
            linear speed without speeding up or slowing down at any point.
            
            Finally we define the pivot point of transformation. Since we want
            to rotate the ray around he center of screen, which is for the ray 
            itself is the bottom right corner - that is what we have to set
            for `transform-origin`.
        */
    }

    @keyframes rotating {
        from { transform: rotate(0deg); }
        to { transform: rotate(360deg); }
    }
    /* 
        Now to animation definition - we describe that it changes from transformation
        without any rotation to transformation, which rotates the element 360 degrees
        around the pivot point set before.
    */

At this point you should have something looking like this:

radar1

Point container

This already looks interesting, but we need now to add some of points of interest,
which is radar created to show in the first place.

Now there are of ways to position items within another one - you have seen
several so far - it is possible to simply use absolute positioning and then
adjust how edges are distanced from each other. It is also possible to adjust
that using transformations.

However, this is the part, where we should start thinking in the direction of how
later we would need to control and change positions of points - whenever the angle
at which player is rotated is changes - all points would have to rotate.
When player is moved - angles and distances would change as well. When points
themselves change their positions, we would need to modify where points are located
on the radar, etc etc.
The relatively easy and native for radar way to define position of the point of it,
would be to use polar coordinates. In polar coordinate there is a single point called
pole, which in our case would be a center location where radar is placed and every
other points are defined by two numbers - distance to that pole and angle between
line from the pole to point and some predefined fixed axis.

CSS transform can easily use polar coordinates to position anything we want
with two operators: translate and rotate. We have already used rotate just now,
translate is even simpler - it just moves element to other position relative to
the place it was before.

Let’s start by modifying our html once again:


        <div class="radar">
            <div class="grid"></div>
            <div class="scanning"></div>
            <div class="point-container" style="transform: translate(0rem, -4rem) rotate(120deg)">
            </div>
                
        </div>

Note that now apart from defining class for our new element point-container,
we have also defined a style. Now style allows to bind a certain styles
to the element the same way we do with a separate CSS class, however placing
styles inside of class is not needed, because we would want eventually
to use data-bind-style-transform attribute to setup the transform based
on the dynamically supplied data from Lua side, which is not covered by this
tutorial.
However, I usually find it easier to do most of UI related things, as supplying
data to the widgets is in most situations a trivial thing.

In the style transform: translate(0rem, -4rem) rotate(120deg) we define that
element is distanced from its usual position on 4rem along y axis - above. The way
screen coordinates work is that y axis points top to bottom, x axis - left to right.
We are going to use y axis to place element at a certain distance to represent how far
the object is from radar, then rotation places it at a certain angle.
Later we would need to scale the distance, knowing that the maximum in widget
coordinates is 12rem, while maximum in the world coordinates would be whatever
the range of the radar we would deem necessary in the game.

Now the part of style, which is not going to be changed dynamically we would include
as CSS class:


    .point-container {
        position: absolute; 
        left: 50%; 
        top: 50%;
        /*
            This are similar positions as for the ray before, basically
            we want to place the starting point at the center of the screen,
            from which it is moved using transform described before.
        */

        background: red;
        width: 1rem;
        height: 1rem;
        /* 
            These styles here are temporary only for now to represent
            where the element is located.
        */
    }

You should see the red square now on our radar.
Try changing the rotation angle in style of element to be 0 degrees.

The squary does not seem to be located exactly where we want the point to be, however
its left top corner is where we want it, which means we would need to move the point
itself within the coordinates of the container which is created for this purpose.

Point

Now let us add the point inside of container we have just created:

    <div class="radar">
        <div class="grid"></div>
        <div class="scanning"></div>
        <div class="point-container" style="transform: rotate(15deg) translate(0rem, -4rem)">
            <div class="point" >
            </div>
        </div>
    </div>

And describe some styles for it:


    .point {
        width: 1rem;
        height: 1rem;
        /* 
            We set the fixed size of point. We might later need
            to make it dynamic if for the purpose of game it would
            require to be different by some reason.
        */

        position: absolute;
        left: -0.5rem;
        top: -0.5rem;
        /* 
            To make sure that the point is a circle around the top
            left corner of container - we move it relative to it
            by a distance, which is half of the size of point.
        */

        background: radial-gradient(#03b627 20%, transparent 60%);
        /* 
            Radial gradient would help us make this point a nice 
            looking circle of greenish color.
        */
    }

Now you might want to remove unnecessary styles we made on container,
since we already see the point itself:

    .point-container {
        position: absolute; 
        left: 50%; 
        top: 50%;
    }

radar2

Now one of the things we could add to the point to make it more exciting
is to make it pulsing with CSS like this:

    .pulse {
        animation-name: pulse;
        animation-duration: 1s;
        animation-iteration-count: infinite;
        animation-timing-function: ease-in;
        /* 
            This animation has different duration - 1 second. It also
            would repeat infintely. You might notice that here we split 
            animation definition in a different properties, this oftenly 
            happens in CSS that there is several properties to specify 
            different parts of the same thing (here - animation) and then
            one shortcut to simply define all of them in one property,
            how we did before with `animation: rotating 4s infinite`
            
            The timing function we use here is different, because we want
            the pulsing to look more alive, for each it would change speed
            in a no-linear manner. This would look more smooth and natural.
        */
    }

    @keyframes pulse {
        0% { transform: scale(1) }
        50% { transform: scale(1.8) }
        100% { transform: scale(1) }
    }

    /* 
        Pulsing would be transforming the element by scaling it.
        It starts with 1, then increases element size by 1.8 and
        then returns back to 1. 
        Note how in this animation we define the time points using 
        percentage rather than names `from` and `to`, since we want
        to define intermediate point - 1.8, while corner points
        are the same - 1.
    */

And we could check how it looks like this:

    <div class="radar">
        <div class="grid"></div>
        <div class="scanning"></div>
        <div class="point-container" style="transform: rotate(15deg) translate(0rem, -4rem)">
            <div class="point pulse" >
            </div>
        </div>
    </div>

This would make point to look more important too, so you might later
decide to make this class optional by use of data-bind-class-toggle.
For now this is just UI development, so may choose to use it or not later.

Now let’s try to make a small box around the item, which would only show
several corners around it. As it often happens, there are several ways to do it
with CSS, I would prefer to define each of the corners separately - top left,
top right, bottom left and bottom right.

Let’s make it in html like this:

<div class="radar">
    <div class="grid"></div>
    <div class="scanning"></div>
    <div class="point-container" style="transform: rotate(15deg) translate(0rem, -4rem)">
        <div class="point pulse" >
        </div>

        <div style="transform: rotate(-15deg);">
            <div class="point-box-corner top left"></div>
            <div class="point-box-corner top right"></div>
            <div class="point-box-corner bottom left"></div>
            <div class="point-box-corner bottom right"></div>
        </div>
    </div>
</div>

As we see here the whole box is rotated back to match the container rotation,
so the corners are shown without being rotated themselves.
This would have to match point rotation with data-bind-style-transform later.

Note how each corner has now more than one class. We are going to use now
one of the most powerful parts of CSS design: composition of classes.
Basically we are going to define four different classes: top, left, bottom and right
in addition to fifth class point-box-corner, which is there to bind them all.
Then each of elements only uses three of those rings classes, effectively
combining specific classes like top and left with the fifth class defining
something shared between all elements.

Now we define each of the CSS classes we used in elements


    .point-box-corner {
        border-color: #a9ffa9; 
        border-style: solid; 
        border-width: 0px;
        /* 
            We define here how corner would have a border.
            The color, width and style of border are defined separately
            here, not with a shortcut `border`, as we did earlier.

            Now this seems like there won't be any border at all, since
            we set width to 0px.. However, in the later classes we are going
            to override this for the specific edges: top, left, right and bottom.
        */

        position: absolute; 
        /* 
            We define absolute positioning here, but do not place
            any specific coordinates. Those would be defined by later classes.
        */

        width: 0.15rem; 
        height: 0.15rem;
        /* 
            We need to give element some size, so that border would be around it.
        */
        
        --point-box-offset: -0.7rem;
        --point-box-border-width: 0.05rem;
        /* 
            This here we start using some advanced CSS feature: variables.
            To avoid repeating later the same thing in different places and allow
            to change it eaesier, we are defining two variables - for width and for
            offset, which we would later refer to.
        */
    }

    .point-box-corner.top {
        border-top-width: var(--point-box-border-width); 
        top: var(--point-box-offset);
    }
    /* 
        Both of top corners - top left and top right are going
        to have something in common: we want them to be positioned
        relative to the top edge - at the offset specified in the 
        variable earlier as well as we want both of them to 
        have non-zero top width.

        Following are three classes each with the similar structure.
        Html code that you have seen before uses pairs of them, but 
        each pair is unique: there is only one top bottom corner.
        There are four combinations and we use all of them combining
        with the shared `point-box-corner`.
    */

    .point-box-corner.bottom {
        border-bottom-width: var(--point-box-border-width); 
        bottom: var(--point-box-offset);
    }

    .point-box-corner.right {
        border-right-width: var(--point-box-border-width); 
        right: var(--point-box-offset); 
    }

    .point-box-corner.left {
        border-left-width: var(--point-box-border-width); 
        left: var(--point-box-offset); 
    }

Here is our final UI look:

radar3

The next steps to make a complete working widget from this UI would be:

  • calculate angle and distance to each of points you want to show in Lua script on client
  • make up model for widget, update data in it constantly
  • “templatize” the UI and add bindings to the model
  • test it in different scenarios, fix all the bugs, etc

How to do each of those points above is opiniated and might also somewhat
depend on the kind of game you are making.

How often to update the points for example might be different for the game,
where you want to slightly complicate hunting for the treasure and for the
game, where you only use radar to show where is the next objective.
You might want to use Schedule for one and LocalOnTick for the other.
There is also the question of how fast points are actually moving and whether
updating on tick is actually improving anything.

Deciding on those things is outside of the purpose of this guide, but you
are always welcome to ask for help in Discord, f.e. in #game-design-help
or #lua-help and #ui-help regarding the coding part.