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:
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%;
}
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:
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.