VG3, Quest 1 - UI Forms and Render Targets

Download Files

Supporting files for VG3 quests are part of a single archive that you can download here.

Create Project

*Your Unity version may differ from what is pictured. You MUST use the Unity version required by the instructor for your particular course semester.

You will use this one project for MULTIPLE solo assignments throughout the entire semester. Replace my name with your own last name and first name. You will have a second separate project for your semester project.

Replace my name with your own last name and first name.

Unity provides different templates for starting your project. All of the projects we do this semester will be "Universal 3D" SRP template games.

Create Scene

To keep our project organized for the semester, we will create a new scene to represent the start of this assignment. Use the File > New Scene and select "Basic (URP)". "URP" stands for "Universal Render Pipeline" which is a collection of operations for how Unity produces graphics on your screen.

Use the File > Save menu and name your new scene as "CharacterBuilder" which will be saved in the project /Assets/Scenes/ folder.

Create Canvas

From the Hiearchy tab, click + > UI > Canvas.

On the Canvas Inspector, set Canvas Scaler to a Reference Resolution of 1280 by 720 with Match set to full Height.

Add a UI Panel as a child of the Canvas and name it "Panel - Character Creator". Center the Panel with a size of 800 by 600. Remove the transparency from its image. In the game preview you should see a centered UI Panel that takes up most the screen with a solid background.

Since this assignment's scene focuses on UI, there is no need to render an environmental skybox as the background. Select the Main Camera in the hierachy. (You may notice compared to older assignments that the URP Camera has a lot of new features.) In the Inspector, scroll down to the Environment section and set the Background Type to a Solid Color of black.

Add Title and Buttons

We'll start with the non-interactive UI elements that should be familiar from previous expertise. To keep this menu organized, create an empty child gameobject within the panel and name it "Character". This Character menu should expand to the full size of its parent with 0 spacing relative to the Left/Top/Right/Bottom.

Within this Character menu, add a "Text - TextMeshPro" object and name it "Text - Title". (You may have to confirm "Import TMP Essentials")

"Text - Title" will be anchored to the top of its parent stretching the full width with a spacing of 20 relative to all sides. Because this is title content, it should be styled with boldness and sizing appropriate for a header. Set a title text of "Create a Character" for this content. Instead of the default white on light background which is unreadable, use a black font color.

Anchored to the upper-right with 20 pixels of spacing, create a "Home" button. (We will make this button functional in a future assignment.) The button text should also be styled as portrayed in these instructions.

Anchored to the bottom-right with 20 pixels of spacing, create a "Save" button. (We will make this button functional in a future assignment.) The button text should also be styled as portrayed in these instructions.

Add Input Field

A new kind of UI we will explore is the Input Field which behaves like a text object but allows users to type in and edit their own text.

Anchored to the upper-left, add an "Input Field - TextMeshPro" object as a child of the Character menu. Name it "InputField - Player Name".

Input Fields are very diverse and dynamic with a variety of options to customize for short-form and long-form text input. Like any piece of text content, we want to configure appropriate visual presentation. We also should adjust how text selection is handled (such as deactivating selecting all the text on focus) to ease our intended user experience.

Input Fields work in concert with multiple child objects. By inspecting the Placeholder child, we can customize what text shows up when no user text has been input yet.

Playing the game at this point reveals that the Input Field is already functional enough to accept user input text or show the placeholder text.

Add Scroll View

UI Scroll Views allow users to view and pan across content that would otherwise be too large or long to fit within a confined display space. They are great for displaying scrolling lists or exploring endless content.

We will use a Scroll View to offer the player a choice from a collection of character models.

Add a UI Scroll View as the next child in the Character menu hierarchy. Anchor it top-left positioned below the player name input field.

By default, Scroll Views are highly customizable and omnidirectional to support a wide variety of content possibilities. The only setting we need to customize for now is to make sure our Scroll View only scrolls vertically. (The scene preview will still show the horizontal scroll bar.)

Expanding the Scroll View object reveals that it functions through the coordination of several children objects. While most of these parts work as-is, we will give special attention to the "Content" child. The "Content" gameObject acts as the full-size container for anything we might want to show inside the scroll view. It's parent gameObject "Viewport" can be thought of as the window we look into to see only a portion of the "Content" at any given time. We must add additional objects as children of "Content" in order to see them within the overall scroll view.

While you can freely arrange gameObjects within the scroll view "Content", we will instead use some components to automate our content layout and sizing.

On the scroll view "Content" object, add a component for Vertical Layout Group. This component automates positioning the children within the "Content". Use the settings as portrayed.

Also add a component for Content Size Fitter. This component automates resizing the "Content" gameObject based on how much content is inside. With this component, our "Content" view will grow very tall for a long list of items, or shrink smaller when fewer items are present. This ensures we can pan the full length of our content without any missing or excess scroll distance.

Now that our scroll view is ready to render content, we can add multiple buttons as children to the "Content" game object.

Create a single Button as a child of the "Content" gameObject. With appropriate sizing, the Button can make good use of the space available within the scroll view. (Notice how some Rect Transform settings are unavailable because they are being controlled by the Layout component in the parent object.)

Our buttons are actually going to render images instead of text, so delete the "Text (TMP)" child of the button. Replace it with a RawImage (NOT Image) object instead. Stretch the RawImage to expand to fit the parent button with a spacing of 15 in all directions.

We are using RawImage instead of Image because the content that portrays our character model choices will be rendered dynamically. We do not have a static pre-rendered image file available that would fit the more familiar Image component.

For now, our buttons portray a placeholder white space.

We will have 3 character model options in this prototype. Duplicate the Button until there are three sibling gameObjects. You can see how each button automatically takes the next position vertically in the content layout, while the scroll viewport cuts off rendering of any content that is past the scroll view boundary. We will hook up the missing images and button functionality in a future step.

Add Character Preview

Similar to our character selection buttons in the scroll view, we will have a big RawImage in our UI so the player can preview their character design.

Create a UI Panel named "Preview - Frame" as a child of the Character menu. Anchor it to the upper-right below the Home button with the settings as potrayed in these instructions. Remove the transparency of the background Image so it renders with an opaque frame.

As a child of the Frame, add a UI RawImage named "RawImage - Character" stretched to fill the frame with 15 spacing on all sides. We will code this to dynamically portray player character selections in a future step.

Create Color Sliders

Create an empty object named "Colors" as a child of the Character menu anchored to the bottom-left. Position and size it as portrayed in the instructions. This UI view will contain 3 rows for manipulating the separate Red, Green, and Blue color channels of our character model. To keep these rows organized, add Vertical Layout Group component to the "Colors" gameObject.

Add an empty object named "Red" as a child of the "Colors" object. Since most of the settings are controlled by the parent layout, the main setting in the RectTransform is the height. Each row in the Colors UI is divided into three columns. A Horizontal Layout Group component helps automate positioning in this kind of layout.

Each color picker UI is composed of a text label, UI Slider, and text Input Field. These are the three columns previously mentioned.

Start by adding a "Text - TextMeshPro" object named "Text - Label" as a child of the Red object. Size appropriately as portrayed in the instructions. Use the text "R" for this label and style as portrayed in the images. Note that this text is both horizontally and vertically centered in the Text Alignment settings.

Next, we will introduce the UI Slider which allows users to input data selecting from a continuum between two extremes. This is a useful UI when a text input field would be too open-ended.

Add a UI Slider object named "Slider - Red" as a child of the Red object. Size appropriately as portrayed in the instructions.

In the Slider component, we can customize the data the user can select through this UI.

Expand the Slider gameObject to reveal all the children objects that allow it to work. To avoid distorting the UI, we will need to adjust the Handle width to match our customized Slider height.

For the third column of the Color UI, add a Text InputField object named "InputField - Red" as a child of the Red object. Size appropriately as portrayed in the instructions.

Further customize the Input Field component with the appropriate starting value, font size, character limit, and content type. We will not use the placeholder text functionality for this particular UI interaction, so clear the "Placeholder" field.

Fully expand the InputField gameObjet and its children. Delete the Placeholder child. On the Text child, customize the Text component so that both its horizontal and vertical alignment are centered. At this point, you should have a completed color customization UI for Red.

Thanks to the Vertical Layout component on our "Colors" object, we can simply duplicate the "Red" child object until we have three rows of Color Picker UI. To make sure it remains clear in future steps which objects we are referencing, be sure that the various children are named appropriately to match their colors.

The completed color picker UI features three separate color channels for red, blue, and green.

Download Assets

We will be using a selection of public domain assets sourced from https://kenney.nl/assets/prototype-kit and https://kenney.nl/assets/emotes-pack. While you can access these assets from their original credited source, any necessary assets have been provided to you in the course files to maintain the integrity and organization of these instructions.

In the course files, find the "Q1" folder and add its "Characters" and "Textures" folders to the root of your project assets. You should be able to find three character models from the Kenney Prototype Kit in the Characters folder and a collection of emotes and frames from the Kenney Emotes Pack in the Textures folder.

Ensure that the Emote Icons have the proper Import Settings.

Also make sure the Emote Frames have the proper Import Settings.

The menus we are building will use these assets to customize a player character.

Set Up Characters

Players can choose from three character models.

To view these 3D models as part of our UI, we will use a technique known as a Render Texture. In this approach, the content seen by a camera can be shown shown in UI as if it were an image.

Render Textures are saved as Project Assets that can then be accessed during runtime gameplay execution. To prepare our Render Textures, use the Project tab to navigate to the /Assets/Textures/ folder. Click the + in the Project tab, and select Rendering > Render Texture.

Repeat this process until you have three Render Textures. Name the file for our three character model options of "figurine", "figurine-cube", and "figurine-large".

Next, a Camera is hooked up to a target Render Texture. We want to leave the existing Main Camera available to render the background and any other content for the scene. Main Camera will not be used as part of our steps. Instead, we will add additional cameras. Start by adding a new Camera to the scene hierarchy. Name is "Camera-figurine" and customize its Environment settings to render a transparent background instead of the default Skybox. In the Output settings, drag the "figurine" Render Texture into the Output property. This camera now dynamically stores its perspective in the figurine image.

Previewing what your new Camera is seeing can be difficult because it is outputting to a Render Texture instead of the display. In the Scene Tools, there is an option to turn on a Cameras tool which will let you preview the "Camera-figurine".

In order for our camera to see our 3D model, we must add the model to the scene. Drag the "figurine" 3D model into the scene as a child of "Camera-figurine". Having the model as a child of the camera helps ensure that wherever we move or organize the camera, the character model will stay in the proper position.

Use the following position values on the child "figurine" object to place it just right in front of the camera.

We want to repeat this process for the other two character models, creating two additional cameras and positioning a child model inside each of them.

Remove the AudioListener component from each of our three figurine cameras. You do NOT want multiple AudioListeners in your scene, and our Main Camera already has one.

If your cameras and models are too close together, it is possible that a render texture may show multiple characters. To fix this, spread your cameras apart so that they do not overlap.

Make sure each Camera is outputting to the matching Render Texture. The thumbnails should portray each of your characters.

Now that our cameras and models are set up, we can add the render textures to our UI. Within the Scroll View content, we previously made three buttons each with a RawImage inside.

For clarity, rename each RawImage to match the RenderTexture it references. Hook up each of the three RawImages.

If you play and preview the game now, you should be able to swipe up through the scroll view ot reveal all three character designs.

To better convey how dynamic the content of render textures can be, we will set up animations for all three of our characters. Because the character models were not animated with a shared skeleton, we will need to set up each character model independently.

Under /Assets/Characters/ use the Project tab to create a subfolder named "Figurine" and in that subfolder create an Animation > Animator Controller asset also named "Figurine". Add an Animator component to the figurine gameObject in the Hierarchy. Use the newly created Figurine animator controller as the Animator component's Controller asset.

Repeat this process for both the "figurine-cube" and "figurine-large" gameObjects paying special attention to make sure each object has a different animator controller of its own.

Examine the Figurine Animator Controller in the Animator tool. Expand the original "figurine" asset in the Project tab. Drag the "walk" animation bundled within the "figurine" asset into the Animator. It is important that you do NOT mix up applying animations from one model onto another model. The "figurine" model can only use the animations from "figurine". Because this is the only animation hooked up in the animator, it will play by default.

Inspect the figurine asset in the Project tab. Switch to the Animation tab in the inspector and examine the "walk" animation from the list. Scroll down to the animation details, enable "Loop Time" so that the walk cycle repeats, and click Apply

Playing the game at this point reveals that the thumbnail for the "figurine" continuously loops through a walk animation.

Repeat the previous steps for "figurine-cube" and "figurine-large" ensuring that each thumbnail continuously loops through the walk animation of each respective model. Be especially careful not to mix up animations from different models.

Code Character Menu

In the Project tab, add a "Scripts" subfolder within the root /Assets/ folder. Create a new C# script (now called "MonoBehaviour Script" in the newer versions of Unity) and name the file MenuCharacterCreator.

Attach the MenuCharacterCreator component to the "Panel - Character Creator" gameObject.

Code Model Picker

When the player clicks any of the three character buttons, that model should be shown in the larger character preview. To accomplish this, we need a couple of code outlets that will allow us to reference the appropriate render textures and raw images. The RawImage type is part of the UnityEngine.UI class.

We will also write a SwitchCharacter function which accept an index number representing the character model we would like to choose.

MenuCharacterCreator.cs

using UnityEngine;
using UnityEngine.UI;

public class MenuCharacterCreator : MonoBehaviour
{
	// Outlets 
	public RenderTexture[] characterRenders;
	public RawImage characterImage;

	// Methods
	public void SwitchCharacter(int index) {
		// Validate Input
		if(index >= 0) {
			// Switch active character preview
			if(characterRenders.Length > index) {
				characterImage.texture = characterRenders[index];
			}
		}
	}
}
		

Fill in the outlets for the MenuCharacterCreator component. The three CharacterRenders are the Render Textures in the Project tab. They must be added in the same order as the buttons. (Button0 is figurine, while Button1 is figurine-cube, and Button2 is figurine-large.) The Character Image outlet references the "RawImage - Character" gameObject already in the Hierarchy tab.

Each of the character Buttons will have a click event that triggers the SwitchCharacter() function available on "Panel - Character Creator". Each button should pass in the index representing its matching character model. (Button0 is index 0 for figurine, Button1 is index 1 for figurine-cube, and Button2 is index 2 for figurine-large.)

Now, when you play the game, selecting each of the 3 buttons will cause that character to appear in the larger character preview.

It helps to provide visual feedback that tells the player which button in the scrollview is already selected. We can achieve this by deactivating the button for the active character model. (Because that character is already selected, there is no need to let the player click the button.) Deactivating the button also greys it out, providing a visual distinction contrasting with the other buttons.

We need to add an outlet that references each of the character buttons so we can change which buttons are interactable. In this updated code, Line 9 adds the new reference to the buttons, while the SwitchCharacter function has new code (Lines 15-21) which deactivates the interactable property of the appropriate button.

MenuCharacterCreator.cs

using UnityEngine;
using UnityEngine.UI;

public class MenuCharacterCreator : MonoBehaviour
{
	// Outlets 
	public RenderTexture[] characterRenders;
	public RawImage characterImage;
	public Button[] characterButtons;

	// Methods
	public void SwitchCharacter(int index) {
		// Validate Input
		if(index >= 0) {
			// The active character button is not interactable
			if(characterButtons.Length > index) {
				foreach(Button button in characterButtons) {
					button.interactable = true;
				}
				characterButtons[index].interactable = false;
			}
			
			// Switch active character preview
			if(characterRenders.Length > index) {
				characterImage.texture = characterRenders[index];
			}
		}
	}
}
		

Remember to fill in the Character Buttons outlet following a consistent numbering scheme as earlier steps. (Button0 is index 0 for figurine, Button1 is index 1 for figurine-cube, and Button2 is index 2 for figurine-large.)

Now when you test the game, the button for the active character model is greyed out while the other buttons can still be clicked. Selecting any other character model coordinates all the other button states appropriately.

You may have noticed that when you first start the game, the character preview is blank. We always want to have one of the character models selected by default, even if the player will decide to change it later. At the start of execution, we can manually call SwitchCharacter to select a default model.

MenuCharacterCreator.cs

	// Methods
	void Start() {
		SwitchCharacter(0);
	}
		

Now, when you first start the game, you will always see the figurine selected by default.

Code Color Sliders

The default texturing on these models does not make it easy for us to customize the coloring. To fix this, we will create and assign our own material that we can tint using the RGB sliders in our UI.

In the Project tab, create a "Materials" subfolder under /Assets/Characters/ and in that subfolder, create a Material named Character.

Inspect the Character material and change it's Base Map tint to a different default color such as green. (The player will have the freedom to customize their own RGB values later through the game UI.)

Each of the three character models needs to be updated to use this new material. Our characters are composed of multiple Mesh Renderers that all need to switch materials. You can select all the Mesh Renderers of all three character models at the same time to update their Material.

Now when you play the game, you'll see that all three character models have the default color from the new material.

To help coordinate the colors of our models, we'll start a new CharacterModel component. CharacterModel will organize access to all of the MeshRenderers on a given character and provide functions for setting and getting the colors.

CharacterModel.cs

using UnityEngine;

public class CharacterModel : MonoBehaviour
{
	// Outlets
	MeshRenderer[] renderers;
	
	// Methods
	void Awake() {
		renderers = GetComponentsInChildren<MeshRenderer>();
	}

	public void SetColor(Color color) {
		foreach(MeshRenderer renderer in renderers) {
			renderer.material.color = color;
		}
	}

	public Color GetColor() {
		Color result = Color.black;
		if(renderers.Length > 0) {
			result = renderers[0].material.color;
		}
		return result;
	}
}
		

Adding an outlet to MenuCharacterCreator for referencing the Character Models will allow us to write logic coordinating the colors of the models.

MenuCharacterCreator.cs

using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class MenuCharacterCreator : MonoBehaviour
{
	// Outlets 
	public CharacterModel[] characters;
		

Fill in the Characters property maintaining the sequence that index 0 is figurine, index 1 is figurine-cube, and index 2 is figurine-large.

To help keep track of our color choices, we will add a state tracking variable to MenuCharacterCreator. (Keep in mind this line of code is temporary, and we will rearrange some of this logic in a future assignment when we implement save data. We will also add references to our various UI sliders and input fields to display current color measurements.

MenuCharacterCreator.cs

using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class MenuCharacterCreator : MonoBehaviour
{
	// Outlets 
	public CharacterModel[] characters;
	public RenderTexture[] characterRenders;
	public RawImage characterImage;
	public Button[] characterButtons;

	public Slider[] colorSliders;
	public TMP_InputField[] colorInputFields;
	
	// State Tracking
	Color characterColor; // TEMPORARY. Will be replaced with save data.
		

Fill in the new Slider and Input Field properties following an order in which index 0 is Red, index 1 is Green, and index 2 is Blue.

For any possible Color, we want our Sliders and Input Fields to display the matching RGB measurements. Add a SetColorUI function to MenuCharacterCreator to manage the display of color information. It's worth pointing out that some objects store color data from zero to one (where one is 100% strength), while other formats store color from 0 to 255. We convert between these ranges as appropriate throughout the code.

MenuCharacterCreator.cs

	void SetColorUI(Color color) {
		colorSliders[0].value = color.r * 255f;
		colorSliders[1].value = color.g * 255f;
		colorSliders[2].value = color.b * 255f;
		
		colorInputFields[0].text = Mathf.RoundToInt(color.r * 255f).ToString();
		colorInputFields[1].text = Mathf.RoundToInt(color.g * 255f).ToString();
		colorInputFields[2].text = Mathf.RoundToInt(color.b * 255f).ToString();
	}
		

Right now, the UI displays color RGB values of 0, even though our character has a default color. Let's add code that measures the default character color during the Start event of MenuCharacterCreator.

MenuCharacterCreator.cs

	// Methods
	void Start() {
		SwitchCharacter(0);

		// Read default character color
		if(characters.Length > 0) {
			// (All characters have the same color)
			characterColor = characters[0].GetColor();
		}
		SetColorUI(characterColor);
	}
		

At the start of the game, the Sliders and Input Fields now show the correct RGB measurements. (Changing these values does not yet affect the model.)

Players can change the color of the model by manipulating three separate Sliders and three Input Fields. Although this offers six possible interactions, we will only be altering one color channel at a time. To respond to player input, we'll start with a function that accepts a number 0 through 255 for controlling the Red color channel. Like a button event, this function is public so that outside components can access it.

MenuCharacterCreator.cs

	public void ChangeColorRed(float value) {
		characterColor.r = value / 255f;
		SetColorUI(characterColor);

		foreach(CharacterModel model in characters) {
			model.SetColor(characterColor);
		}
	}
		

The Red Slider will use this function for its OnValueChanged event.

Unlike the Slider, InputField needs a function that receives strings instead of numbers. To use the ChangeColorRed function we already wrote, we'll add two more functions. ConvertToColor255 will convert a color string to a color number. ChangeColorTextRed will use ConvertToColor255 to call the ChangeColorRed function we wrote previously.

MenuCharacterCreator.cs

	float ConvertToColor255(string value) {
		// Convert string to number. Protect against empty or non-number strings
		float.TryParse(value, out float result);
		
		// Constrain to 0-255 color range 
		if(result < 0) {
			result = 0;
		}
		if(result > 255) {
			result = 255;
		}
		
		return result;
	}

	public void ChangeColorTextRed(string value) {
		ChangeColorRed(ConvertToColor255(value));
	}
		

Similar to Slider, our Red InputField will use ChangeColorTextRed for its OnValueChanged event.

Playing the game now will reveal that you have full control for manipulating the Red color channel. The model changes color dynamically in response to live values from the Slider and the Input Field.

Repeat the previous steps for the Green and Blue channels. Remember to hook up component events for each color's Sliders and Input Fields.

MenuCharacterCreator.cs

	public void ChangeColorRed(float value) {
		characterColor.r = value / 255f;
		SetColorUI(characterColor);

		foreach(CharacterModel model in characters) {
			model.SetColor(characterColor);
		}
	}

	public void ChangeColorGreen(float value) {
		characterColor.g = value / 255f;
		SetColorUI(characterColor);

		foreach(CharacterModel model in characters) {
			model.SetColor(characterColor);
		}
	}

	public void ChangeColorBlue(float value) {
		characterColor.b = value / 255f;
		SetColorUI(characterColor);

		foreach(CharacterModel model in characters) {
			model.SetColor(characterColor);
		}
	}
		

MenuCharacterCreator.cs

	public void ChangeColorTextRed(string value) {
		ChangeColorRed(ConvertToColor255(value));
	}

	public void ChangeColorTextGreen(string value) {
		ChangeColorGreen(ConvertToColor255(value));
	}

	public void ChangeColorTextBlue(string value) {
		ChangeColorBlue(ConvertToColor255(value));
	}
		

Test all 6 combinations of Slider and Input Fields interactions to make sure the character model changes colors in response to all of them.

Save and Test

Play your scene and ensure that all your features and interactions perform as demonstrated throughout the tutorial.

Submit Assignment

SAVE any open files or scenes and close Unity to ensure any temp files are finalized.

Submit your assignment for grading following the instructions supplied for your particular classroom.