Introduction to Unity: Creating a 3D Gauge in Unity

Friday Aug 4th 2017 by Rob Bogue
Share:

Walk through a successful Unity spike and create a tangible deliverable that's a working 3D gauge.

I've been developing software for more than 25 years now. I've learned dozens of platforms and frameworks. I expect, at some point, the process of learning a new platform will get easier. Each time it does, to some degree, but it's never enough.

In this article, I'm going to walk you through my first successful Unity spike. The goal is three-fold: to demonstrate how to do something useful in Unity, to provide a framework that you can use to learn Unity—or any other environment, and to develop a small component that you might be able to leverage in something you're building.

The tangible deliverable for this project is a working 3D gauge. It has 18 segments and scales from 0 to 100%. I decided on this as my first project, because it allowed me to familiarize myself with the platform with a challenge that was somewhere between "Hello World" and SkyNet. Ultimately, this 3D gauge created the need for learning several fundamentals.

Graphics Files

Unity is a compositing environment. That is, it is used to stitch together elements. It allows you to integrate graphics, models, and scripting to create interactive solutions. It is not, however, a modeling engine. For the gauge, our starting point in Unity is a model.

Getting to a model for Unity can take a few steps. Sometimes, when working on a component, you will start with a raster-based image—either drawn or downloaded from the public domain on the Internet. This is manipulated and ultimately converted into a vector-based—and therefore scalable—image. Adobe Illustrator's image trace feature can do this conversion for you. Illustrator can also output a scalable vector graphic (SVG) file that can be imported into 3D modeling software.

Blender is a free 3D modeling software that can import SVG files, and its output files are directly importable to Unity. For a simple graphic like our gauge, it's possible to simply extrude (add depth to) the 2D drawing.

Completed gauge model
Figure 1: Completed gauge model

Unity Projects and Objects

Unlike most development environments, Unity manages projects as folders. There is no inherent solution file. There's a directory tree that contains objects. The objects are things such as the artwork and models you've created, plus scripts and scenes that use the .unity file extension.

In laying out the landscape of any new environment, it's important to recognize the landmarks. For developers, those are some of the core objects that make up the structure of the environment. Here are a few of the key objects that are the framework for Unity projects:

  • GameObject: Everything in Unity is a GameObject. You can get the current GameObject by using the GameObject instance that's available in every script.
  • Mesh: The representation of a 3D object. This is the model of the object.
  • MeshFilter: The instance of the 3D object in the scene.
  • Canvas: A container for UI elements.

Unity further organizes the experience into a set of scenes that contains all of the objects that are in the environment. Scenes in games are often called "levels." In creating the gauge, you'll need only one scene.

Creating a Project

You can create a Unity project by starting Unity and clicking in the upper-right of the opening dialog on the New icon. From there, you can name your project (such as Gauge) and specify the location to store the project. By default, Unity will create a 3D project.

Unity New dialog
Figure 2: Unity New dialog

Importing the Gauge

The building process in Unity starts with importing the gauge graphic by creating a folder under assets, called "Gauge." This can be done by right-clicking in the project area and then clicking Create and Folder. Then, drag the .blend file from Blender into the Gauge folder that you just created.

Note: You can get the code for this article via GitHub at https://github.com/rlbogue/Rbogue/tree/master/Unity/Gauge-Art. It includes the Blender file.

Unity will automatically create a materials folder to hold the surface materials for the object created in Blender. Unity also will create a main camera and directional light. With this, everything is present to bring a scene to life, including the gauge, a light, and a point of view. There are a few changes that must be made to position things so that the gauge can be seen. Everything in Unity (when in 3D mode) is oriented with three dimensions (X, Y, and Z). To make the gauge visible, we need to orient the gauge and the camera.

After clicking the gauge in the hierarchy, the inspector panel automatically displays the gauge and the transform section. In this area, we need to verify that the position is 0, 0, 0 (X, Y, Z) and change the rotation to 90, 90, and -90. This causes the gauge to stand up and look right from the point of view of the camera. We can also set the scale to 2, 2, 2 to make the gauge bigger.

In the hierarchy, we click the main camera and the inspector changes. Here, we can change the position to 0.2, -0.25, and -0.4. The X and Y settings are because the gauge's reference isn't from the center, so it's offset a bit based on its size. The Z location provides some space between the camera and the object.

Adding Text and a Slider

For our gauge, we want to have text that appears in the inside of it. To do that, we need to create a text UI element. In the hierarchy, right-click the gauge object, select UI, and then Text. When you do this, Unity creates two objects. The first object is a canvas. All UI objects must be contained by a canvas, so Unity automatically added it for you. We want this text and the canvas to be a part of our gauge, so click and drag the canvas into the gauge object in the hierarchy.

Hierarchy of the gauge
Figure 3: Hierarchy of the gauge

To get the text in the center of our gauge, we're going to move it. Click Text in the hierarchy and then, in the inspector pane's Rect Transform component, enter 0, 0, 0 for the X, Y, and Z positions. This will put the text roughly in the center of the gauge.

Other than some stitching together with code, our gauge is done. However, we want to be able to interact with it for testing. For that, we're going to add a slider. This is a standard UI component that uses a pull handle on a line to configure a value. Right-click in the hierarchy window on the canvas object and select UI, then Slider.

Adding Code

It's great that we have the gauge and we can see it. However, it doesn't do anything. There are no scripts to handle updating of values—and no way for us to set the value of the gauge. We'll do that by attaching scripts to the gauge object—and to the slider.

Code Component

GameObjects in Unity have components attached to them. One of the components that you can attach is a code component—also called a "script." This code will be initialized by calling the Startup() method when the game object is created and Update() for each frame.

Obviously, it's important to optimize the code in the Update() function, because this code will run frequently. In fact, unless where necessary, I'd recommend staying out of that function. This is particularly true for resource-constrained platforms like the Microsoft HoloLens.

To add the code file, you can click the gauge object in the hierarchy, and in the inspector panel, click Add Component, then New Script. In the name field, enter Gauge. Because the new script is dumped in the root folder, we'll want to move it. In the project folder, right-click Create and then click Folder. Name the folder "Scripts" and drag the Gauge.cs script into the Scripts folder you just created. This Gauge script will be used to manage the updating of the gauge.

We're going to do the same thing for our slider. Click the slider in the hierarchy, and in the inspector panel, click Add Component, then New Script. In the name field, enter SliderToGauge. Like our Gauge.cs file, SliderToGauge.cs is in the root, so drag it into the Scripts folder. We'll use this script to signal changes to the gauge.

Let's get started with the coding. Click into the Scripts folder and double-click Gauges.cs. This will open Visual Studio.

Code Structure

Before writing actual code, we should talk about the problems to be solved. First, we want to understand the bounds for our values. The slider that we're using for our test user interface by default operates on values from 0 to 1. This works nicely as a percentage, so we'll use that value range. However, the gauge has 18 segments, so we'll need to convert the percentage into the number of segments to be displayed. That's a bit of math, but not particularly challenging.

The real challenge will be navigating the object model to be able to get the segments that you want to be able to turn on and off. The same "find the object" problem exists for the text item, for which we want to display the actual value of the percentage. Luckily, there are methods that we can use for locating child objects.

Start() Code

In the Start() method, we've got a few things to accomplish. First, we need to get references to the objects we are going to manipulate, so that we can refer to them quickly later. Every script attached to a GameObject has a member GameObject that can be used to refer to the object. The GameObject has a set of methods for fetching children. To get our MeshFilters for each of the segments, we're going to call gameObject.GetComponentsInChildren<MeshFilter>();. This returns an array of MeshFilter objects. This is okay, except the problem is that it's not necessarily sorted the way that we need it. Each of the child meshes has a sequential name, but there's no guarantee that they're ordered in the correct order when we fetch them.

To sort the array, we're going to leverage Linq. That means adding a reference to System.Linq. Then, it's a simple one-line to get an array sorted by name. That is meshSegments.OrderBy(x=> x.name).ToArray<MeshFilter>(). With that, we now have an array sorted by name.

Getting the text element is like getting the MeshFilters. Instead of GetComponentsInChildren<>()—which is plural—we call GetComponentInChildren<Text>() because we know we only have one Text object.

With those things stored away, there's one more component that we need before we can start to do our initial update. For the text in the center, I want to allow for some formatting, including the ability to prepend and append text to the actual number. Additionally, it would be nice to control the number of decimal places to be displayed. Ultimately, in the update, we'll put things into a single string to get stuffed in the text object. Literally, it will look like string.Format(formatString, labelPrefix, Percent * 100, labelSuffix);.

For that to work, we need a format string, so we'll need to create that before we do our update. We'll put that in a function call (GetFormatString). We'll call that, and then call our method that handles updating the objects (UpdateDisplay). You'll notice that we're not using the Update() method, because we don't want the overhead of the call on every frame refresh.

Updating the Display

There are two components to updating our display: the first is updating the gauge, and the second is updating the text. In terms of updating the gauge, we need to determine how many items should be lit. Determining the right number of segments starts with determining how many percentage points each gauge item is equivalent to. That's (100/ MaxItemsVisible)—then we need to take the percentage to display and divide it by the percentage for each part of the gauge. The end statement looks like this:

int itemsVisible = Mathf.FloorToInt(Percent * 100 /
   (100/maxItemsVisible));

Mathf.FloorToInt() converts our floating-point math problem to an integer using only the integer component of the number. Now that we know the number of items, we can loop through the array of MeshFilters and set whether they are "active" or not—Unity's word for whether they should be active in the scene. If you've done other programming, you might be expecting "visible"—but in a 3D game, there are many things that may not be visible but are still active in the interface.

To set whether a segment is active, we loop through and get our gauge segment, navigate to the GameObject attached, and then set whether it's active. The Boolean math gets a bit odd here. The segments are ordered from lowest name to highest name. When the object was imported into Unity, the highest segments of the gauge are the first numbers. This means that, when we loop through, we must turn off segments first, then turn on segments. Ultimately, the statement looks like this:

child.gameObject.SetActive((visible >
   (maxItemsVisible-itemsVisible)));

This returns false for the first few segments (depending on percentage) and true when the later segments are displayed.

The second part, updating the text, is easier, because it's just a call to string.format, with the formatter string, the prefix, the percentage (multiplied by 100), and the postfix.

Setting the Percentage

To this point, I've ignored how the percentage is set in our code. However, because we look to make it possible for other objects to set the percentage, it's necessary to revisit how this is handled. It's standard in .NET to use properties over fields to take an action when the value is set. In this case, we can use a property called Percent and a field named percent (notice the lower case) with the getter and setter of the property getting and setting from and to percent. The setter also calls UpdateDisplay() to force an update of the display.

This works well inside the object and from anything that gets an instance of the object directly; however, this means that the other interface must exactly get to this script and set that property—something that's not necessarily easy. As a result, we've added a secondary way of setting the percentage of that object through the Unity messaging system. We added a SetPercent() method that takes a float of the new percentage to set the gauge to. The inside of this function is a single line, even when protecting against duplicate updates:

If (percent != newPercent) Percent = newPercent;

This updates via the property—which will call our update method—only if the percentage being set is a change. With this public method in place, it's possible to message our gauge from another object.

Parents and Messaging

The gauge itself is done. It supports updates but we don't have a way of testing it. That's what the SliderToGauge.cs file is for. In it, we're going to create a method we can use to send the gauge a message. There are two pieces to this puzzle. First, and most challenging, is getting the gauge. Second, we send the message.

Getting the parent of an object isn't easy. There's no parent member on the GameObject directly. However, there's a neat trick of using the parent object of the transform object. Every GameObject has a transform object attached to it. The transform object has a parent transform object—and a reference to the GameObject that it relates to. In our case, we have our slider under our gauge. Because it's a UI component, it's also under a canvas. To get a reference to the gauge, we need to walk up two levels of parents. That looks like this:

gameObject.transform.parent.parent.gameObject

Then, with the game object we can call the SendMessage() method. This will call methods with the same name on all of the objects below the game object we send the message to. In our case, we'll call the SetPercentage method and also pass the percent parameter that the method needs. The whole method looks like this:

public void SetGauge(float percent)
{
   GameObject gaugeGO =
      gameObject.transform.parent.parent.gameObject;
   gaugeGO.SendMessage("SetPercentage", percent);
}

You'll notice that our method accepts a float to pass along. This we'll stitch up in the user interface to provide the value from the slider. To do this, click the slider in the hierarchy and, in the On Value Changed (single) section, click the plus sign. Change the first drop down to Editor and Runtime, so we can test our changes from the editor. In the None (Object) drop down, drag the slider from the hierarchy and release it. In the No Function drop down, select the SliderToGauge, then under dynamic float, select SetGauge.

You may have noticed that we're passing the slider back to itself for On Value Changed. This is because we need an instance of our class—which is attached to the slider itself. By selecting the slider for the object to work with, we can call to the attached SliderToGauge script and the method we created.

Testing

All that's left is to test. To do this, first click File-Save Scenes and call the scene "Main." Then, press the play button at the top center of the interface. After a few seconds, you can grab the slider in the game window and drag it back and forth to see the changes in the gauge and the text.

Here is a video of the entire project being created. (You'll want to maximize the video to full screen.)

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved