Version: 2021.3
Create a Custom Inspector
视觉树

Create your first runtime UI

This page will guide you through the steps to set up a simple character selection screen using UI Toolkit. It covers the creation of the UI elements and templates, the scene setup, and how to connect scripting logic to the UI. This guide won’t cover styling through USS, and use only default styles and theming.

您可以在本页底部此处找到本指南的最终源代码。

Topics covered: UI Builder, ListView, Label, PanelSettings, UIDocument, selection handling

本指南将指导您完成以下步骤:

  • Create the main UI view
  • Setup the scene
  • Create sample data to display
  • Create a controller for the main view
  • Create a list entry UI template
  • Create a controller for a list entry
  • React to user selection

Create the main UI view

The final UI screen is composed of two individual UI templates (UXML). The main view template contains the list with the character names, a smaller panel to display the selected character’s details, and a button. In this section you will set up this template using UI Builder.

Note: If you are familiar with UI Builder and want to skip this step, you can copy the UXML code for the main view from the bottom of this page and paste it into a new file directly. Save it as Assets/UI/MainView.uxml.

The UI layout setup for the main view
The UI layout setup for the main view

Open the UI Builder window via the menu Window > UI Toolkit > UI Builder. Create a new UXML document using the file menu at the top left of the viewport.

UI Builder file menu
UI Builder file menu

When developing game UI, always make sure to select the Unity Default Runtime Theme at the top right of the UI Builder’s viewport. Default font sizes and colors differ between Editor and Runtime themes and that affects your layout.

Create new elements by dragging from the Library
Create new elements by dragging from the Library

Select the new UXML file in the Hierarchy and enable the Match Game View checkbox. You might need to set your Unity Editor to a landscape resolution if you haven’t already.

Enabling Match Game View
Enabling Match Game View

Now it’s time to create UI elements! Create a new VisualElement by dragging it from the Library into the Hierarchy.

Create new elements by dragging from the Library
Create new elements by dragging from the Library

The new element needs to cover the entire screen, so you need to set the flex-grow property to 1. Select the element from the hierarchy and find the foldout labeled Flex in the Inspector panel on the right. Change the value for Grow from 0 to 1.

Setting the Flex property
Setting the Flex property

To center all children of this VisualElement in the middle of the screen, change the Align properties of the VisualElement. You need to set both Align Items and Justify Content to center.

Centering children
Centering children

Lastly, you can pick a background color under Background > Color. This step is optional. This example uses #732526 as the color.

Root element background color
Root element background color

Next, create a new VisualElement underneath the existing one. This will become the parent container for the left and right sections of the UI.

Add a child VisualElement
Add a child VisualElement

Set the flex-direction property to row for this new element (it defaults to column). You also need to set a fixed height of 350 pixels.

Center container properties
Center container properties

This is what the current UI should look like. Note that your screen might look different depending on the resolution and aspect ratio of your Game View.

Background container with empty element inside
Background container with empty element inside

To create the list for the character names, select a ListView control from the Library and add it as a child underneath the VisualElement you just created. Select the element and assign it the name CharacterList in the inspector. This is necessary so you can access this list via the controller script later.

Background container with empty element inside
Background container with empty element inside

Set the list to have a fixed width of 230 pixels. Also give it a 6 px wide margin on the right, to crate some distance to the next elements that you are going to create.

Size and Margin foldouts for the character list
Size and Margin foldouts for the character list

You can also assign a background color and set a rounded border for the list. This guide uses #6E3925 for the background and #311A11 for the border color, with a 4px wide border and a 15px radius. This step is optional.

Styled character list
Styled character list

Add a new VisualElement under the same parent as the CharacterList. This will hold the character details panel and the button. Under the Align foldout, change the setting for Align Items to flex-end, and Justify Content to space-between.

Justify content property
Justify content property

Add a new VisualElement to this new container. This will become the character details panel. When the user selects a character from the list on the left, it will display the character’s portrait, name, and class.

Set a fixed width of 276 pixels for the element, and switch Align Items and Justify Content to center. Also add an 8 pixels wide padding for the element, so that the children will keep a minimum distance to the borders of the container.

Properties of the charcter details container
Properties of the charcter details container

You can style the panel by setting a background color of #AA5939 and a border color of #311A11 with a 4px wide border and a 15px radius. This step is optional.

Your UI layout should now look similar to the image below.

Empty character details panel
Empty character details panel

Next you’ll add the individual Ui controls to the character details. First is the character portrait. This consists of two elements - a frame in the background and an image in the foreground.

Add a new VisualElement to the character details container for the background frame first. Assign it a fixed size of 120x120 pixels, and a padding of 4 pixels so that the contained image won’t directly touch the border.

You can use a 2px-wide, 15px radius border with a color of #311A11 and a background color of #FF8554 to style the element. Feel free to apply your own colors and styling instead.

Background frame for the character portrait
Background frame for the character portrait

For the actual image add a new VisualElement as a child to the frame you just created. Name it CharacterPortrait so that you can access it in the controller script later.

Set Flex > Grow to 1, so that the image makes use of all the available space. Also make sure to change the scaling mode under Background > Scale Mode to scale-to-fit, so that the image can be scaled up or down to match the element size, while keeping the correct aspect ratio.

VisualElement for the portrait image
VisualElement for the portrait image

Next, add two label controls to the the character details container, which you will later use to display the selected character’s name and class. Name them CharacterName and CharacterClass.

Add labels for name and class
Add labels for name and class

To make the character’s name stand out more than the class, change that label’s font size to 18, and set the style to bold.

Change font settings
Change font settings

Your UI screen should now look similar to the image below.

Finished character details panel
Finished character details panel

Lastly, add a Button control to the right side UI container. You will later access this button in the controller script and enable or disable it when a character is selected or deselected. Name the button SelectCharButton and give it a fixed width of 150px. You should also set the label text of the button to Select Character.

Add button for character selection
Add button for character selection

To style the button, set a background color of #FF8554, and a 2px border with a color of #311A11. This step is optional.

Your finished main view should look similar to the image below.

Final main view layout
Final main view layout

Save the UXML template as Assets/UI/MainView.uxml. You can also find the final UXML code for this template at the bottom of the page here.

Setup the scene

In this section you will learn how to load and display the UI template you created in the previous section in your game at runtime.

To start you need to create a PanelSettings asset. This asset will define the settings of your screen, such as scaling mode and rendering order. It will also determine the name under which your UI will appear in the UI Toolkit Debugger.

Create a new panel settings asset
Create a new panel settings asset

Create a new Panel Settings Asset by right-clicking in the project view. Choose Create > UI Toolkit > Panel Settings Asset. Name the newly created file GameUI_Panel. For this guide you can leave all settings at their default values.

No need to change the default PanelSettings
No need to change the default PanelSettings

To display the main view UI template from the previous section you need to create a new GameObject in the scene. Attach a UIDocument component to it.

The UIDocument will automatically load the assigned VisualTreeAsset when entering Play mode in Unity. A VisualTreeAsset is a UXML template. Assign both the MainView.uxml and the new GameUI_Panel panel settings to the component.

The UI Document component
The UI Document component

Note: If you do not assign a PanelSettings asset to your UI Document component, it will automatically search the project and use the first Panel Settings Asset it finds automatically. Keep this in mind when renaming or moving assets.

You can now enter Play mode in the Unity Editor and see your UI displayed in the game view.

UI displayed at runtime
UI displayed at runtime

Note: If you have multiple UI Documents in your scene, you can assign the same panel settings asset to all. This will cause all UI to be rendered on the same panel, optimizing performance.

Create sample data to display

In this section you will create some sample data that will be used to fill the character list in the UI with data.

For the character list you need a simple class that holds a character name, class, and a portrait image. Create a new ScriptableObject script Assets/Scripts/CharacterData.cs and paste the following code into the file:

using UnityEngine;

public enum ECharacterClass
{
    Knight, Ranger, Wizard
}

[CreateAssetMenu]
public class CharacterData : ScriptableObject
{
    public string m_CharacterName;
    public ECharacterClass m_Class;
    public Sprite m_PortraitImage;
}

The [CreateAssetMenu] attribute will automatically add an entry to the Create menu. Right-click into a folder in the Project view to create instances of the new ScriptableObject.

New Create menu entry
New Create menu entry

Now you need to create a few CharacterData instances and fill them with random data. Place all of them in a folder Resources/Characters. You will write a script which automatically parses and loads all character data from this folder later.

Create a few sample characters
Create a few sample characters

Create a list entry UI template

In this section you will create a UI template for the individual entries in the list. At runtime, a controller script will create an instance of this UI for each character and add it to the list. The UI for a character list entry consists of acolored background frame and the character name.

List entry showing the character name
List entry showing the character name

Note: If you want to skip this step, you can copy the UXML code for the list entry from the bottom of this page and paste it into a new file directly. Save it as Assets/UI/ListEntry.uxml.

Open the UI Builder window via the menu Window > UI Toolkit > UI Builder. Create a new UXML template by selecting File > New.

Create a new UXML template in UI Builder
Create a new UXML template in UI Builder

Add a VisualElement for the background, and set a fixed heigth of 41px. Since the text inside the entry should be left aligned and placed in the middle of the element, open the Align foldout and set Align Items to left, and Justify Content to center. Also set a left padding of 10px, to make the label have a minimum distance to the left border of the frame.

For the styling, you can use #AA5939 for the background color and add a 2px wide border, with a 15px radius and a color of #311A11. This step is optional and you can apply your own colors and styling.

The background VisualElement
The background VisualElement

Add a label as a child to the existing VisualElement and name it CharacterName, so that you can access it later in the controller script. Set the Font Style to bold and the font size to 18.

Add label for the characters name
Add label for the character’s name

Save the UXML template as Assets/UI/ListEntry.uxml. You can also find the final UXML code for this template at the bottom of the page here.

Create a controller for a list entry

In this section you will create a controller script for a list entry. The purpose of the script is to display the data of a character instance in the UI of the list entry. It needs to access the label for the character name and set it to display the name of the given character instance.

Create a new script Assets/Scripts/UI/CharacterListEntryController.cs and paste the following code into it:

using UnityEngine.UIElements;

public class CharacterListEntryController
{
    Label m_NameLabel;

    public void SetVisualElement(VisualElement visualElement)
    {
        m_NameLabel = visualElement.Q<Label>("CharacterName");
    }

    public void SetCharacterData(CharacterData characterData)
    {
        m_NameLabel.text = characterData.m_CharacterName;
    }
}

There are two functions in this class, and both of them are Set functions.

SetVisualElement(VisualElement visualElement) This function will receive a visual element that is an instance of the ListEntry UI template you created in the previous section. The main view controller will create this instance. The purpose of this function is to retrieve a reference to the character name label inside the UI element.

SetCharacterData(CharacterData characterData) This function receives the character whose name this list element is supposed to display. Because the elements list in a ListView are pooled and reused, it’s necessary to have a Set function to change which character’s data to display.

Note that the CharacterListEntry class is not a MonoBehaviour. Since the visual elements in UI Toolkit aren’t GameObjects, you can’t attach components to them. Instead, this class will be attached to the userData property in the next section.

Create a controller for the main view

In this section you will create a controller script for the character list in the main view, and a MonoBehaviour script that instantiates and assignes it to the visual tree.

To start, create a new script under Assets/Scripts/UI/CharacterListController.cs and paste the following code into it.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public class CharacterListController
{
    public void InitializeCharacterList(VisualElement root, VisualTreeAsset listElementTemplate)
    {
    }
}

You’re going to fill in the InitializeCharacterList() method late on, but it’s important to add the empty method now, so that you can call it in the next section.

Attach the controller script to the main view

Just like the CharacterListEntryController, the CharacterListController is not a MonoBehaviour, and needs to be attached to the visual tree in a different manner. You need to create a MonoBehaviour script that you can attach to the same GameObject as the UIDocument. It will instantiate the CharacterListController and attach it to the Visual Tree.

Create a new script Assets/Scripts/UI/MainView.cs and paste the following code into it:

using UnityEngine;
using UnityEngine.UIElements;

public class MainView : MonoBehaviour
{
    [SerializeField]
    VisualTreeAsset m_ListEntryTemplate;

    void OnEnable()
    {
        // The UXML is already instantiated by the UIDocument component
        var uiDocument = GetComponent<UIDocument>();

        // Initialize the character list controller
        var characterListController = new CharacterListController();
        characterListController.InitializeCharacterList(uiDocument.rootVisualElement, m_ListEntryTemplate);
    }
}

Go into the Unity Editor and attach the script to the same GameObject that the UIDocument is on. Assign the ListEntry.uxml to the List Entry Template property.

Add main view script and assign reference
Add main view script and assign reference

There’s no need for your script component to instantiate the MainView UXML, as this is done automatically in the UIDocument component on the same GameObject. The MainView script accesses the UIDocument component to get a reference of the already instantiated Visual Tree. It then creates an instance of the CharacterListController and passes in the root element of the visual tree and the UXML template used for the individual list elements.

Note: When the UI is reloaded, companion MonoBehaviour components on the same GameObject containing the UIDocument component will be disabled prior to the reload, and then re-enabled after the reload. Therefore it’s a good practice to place code that interacts with the UI in the OnEnable and OnDisable methods of these MonoBehaviours.

Enumerate all character data instances

The first functionality you should add to the controller script is a function that enumerates all the character data instances you created earlier. These will be used to fill the list.

Copy the code below into the CharacterListController class.

List<CharacterData> m_AllCharacters;

void EnumerateAllCharacters()
{
    m_AllCharacters = new List<CharacterData>();
    m_AllCharacters.AddRange(Resources.LoadAll<CharacterData>("Characters"));
}

Note: This code assumes that you created the character instances in the Resources/Characters folder. You might need to adjust the folder name accordingly if you placed the characters in a different folder.

Now you need to call the EnumerateAllCharacter method during initialization. Add a call to it to the top of the InitializeCharacterList method:

public void InitializeCharacterList(VisualElement root, VisualTreeAsset listElementTemplate)
{
    EnumerateAllCharacters();
}

Get references to the UI elements

In this section you will fill in the content of the InitializeCharacterList method. The first thing this method needs to do is obtain references to all the individual UI controls it needs to access to display information. Use the UQuery family of APIs to retrieve individual UI controls by name, USS class, type, or a combination of these.

Extend the code inside the CharacterListController class with the code below:

// UXML template for list entries
VisualTreeAsset m_ListEntryTemplate;

// UI element references
ListView m_CharacterList;
Label m_CharClassLabel;
Label m_CharNameLabel;
VisualElement m_CharPortrait;
Button m_SelectCharButton;

public void InitializeCharacterList(VisualElement root, VisualTreeAsset listElementTemplate)
{
    EnumerateAllCharacters();

    // Store a reference to the template for the list entries
    m_ListEntryTemplate = listElementTemplate;

    // Store a reference to the character list element
    m_CharacterList = root.Q<ListView>("CharacterList");

    // Store references to the selected character info elements
    m_CharClassLabel = root.Q<Label>("CharacterClass");
    m_CharNameLabel = root.Q<Label>("CharacterName");
    m_CharPortrait = root.Q<VisualElement>("CharacterPortrait");

    // Store a reference to the select button
    m_SelectCharButton = root.Q<Button>("SelectCharButton");
}

Fill the list with entries

Next you need to fill the list on the screen with the characters you enumerated and loaded earlier. To do so you need to create a new method FillCharacterList inside the CharacterListController class.

Filling a ListView with elements takes 4 steps:

  1. Create a makeItem function
  2. Create a bindItem function
  3. Set the item height
  4. Set the item source

The purpose of a makeItem callback function is to create a small visual tree representing the UI a single list item, and returning the root VisualElement of this tree.

In this case, the makeItem callback needs to instantiate the UXML template you created for the list entries. It also needs to create an instance of the CharacterListEntryController controller script, which takes care of filling the UI with the data from the CharacterData.

Create a FillCharacterList method inside the class and paste the code below.

void FillCharacterList()
{
    // Set up a make item function for a list entry
    m_CharacterList.makeItem = () =>
    {
        // Instantiate the UXML template for the entry
        var newListEntry = m_ListEntryTemplate.Instantiate();

        // Instantiate a controller for the data
        var newListEntryLogic = new CharacterListEntryController();

        // Assign the controller script to the visual element
        newListEntry.userData = newListEntryLogic;
    
        // Initialize the controller script
        newListEntryLogic.SetVisualElement(newListEntry);

        // Return the root of the instantiated visual tree
        return newListEntry;
    };
}

As part of the makeItem callback, you’re storing the controller script inside the userData property of the instantiated visual element. This allows you to access the script at a later time and assign different characters to the list element.

// Assign the controller script to the visual element
newListEntry.userData = newListEntryLogic;

As a memory and performance optimization, a ListView reuses list elements instead of instantiating one element for every entry in the list. It creates only enough visual elements to fill the visible area, and then pools and reuses them as the list is scrolled.

For this reason you need to provide a bindItem callback which binds an instance of your data (in this case CharacterData) to an individual list element.

Extend the FillCharacterList method by adding the code below at the bottom.

// Set up bind function for a specific list entry
m_CharacterList.bindItem = (item, index) =>
{
    (item.userData as CharacterListEntryController).SetCharacterData(m_AllCharacters[index]);
};

The bindItem callback receives a reference to the root visual element for the visual tree of the list entry, and an index for the data. Because you stored a reference to the CharacterListEntryController in the userData property of the visual element, the code can access it and directly set the CharacterData.

Lastly you need to set the item height of the element and provide a reference to the data source for the list. This tells the list how many elements it contains.

Extend the FillCharacterList method by adding the code below at the bottom.

// Set a fixed item height
m_CharacterList.fixedItemHeight = 45;

// Set the actual item's source list/array
m_CharacterList.itemsSource = m_AllCharacters;

Finally, you need to call the FillCharacterList method at the end of initialization. Add a call to the bottom of the InitializeCharacterList method as shown below:

FillCharacterList();

If you enter Play Mode now, the character list will fill up with the names of the characters you created.

The character list is no longer empty
The character list is no longer empty

You can find the final code for the CharacterListController script at the bottom of this guide, here

React to user selection

When the user selects a character, the character’s details - namely portrait, full name and class - need to be displayed in the character details section on the right side of the screen. Also, when a character is selected, the selection button needs to be enabled. When no character is selected, the button should disable again.

Note that you can already click and select characters in your list. The functionality for selection and highlighting are part of the ListView control. All you need to add is a callback function to react when a user changes the selection in the list. The ListView control contains an onSelectionChange event for this purpose:

Add the following code to the bottom of the InitializeCharacterList method:

// Register to get a callback when an item is selected
m_CharacterList.onSelectionChange += OnCharacterSelected;

Now you need to implement the callback function OnCharacterSelected that you set up in the code above. This function will receive a list of all selected items in the list. However, because you list only allows single item selection, you can access the selected item directly through the list’s selectedItem property.

Copy the code below into your class:

void OnCharacterSelected(IEnumerable<object> selectedItems)
{
    // Get the currently selected item directly from the ListView
    var selectedCharacter = m_CharacterList.selectedItem as CharacterData;
}

The selectedItem property could return null. This is the case if nothing is selected, or the user presses the ESC key to deselect everything. This case needs to be handled first.

Extend the OnCharacterSelected method as shown below:

void OnCharacterSelected(IEnumerable<object> selectedItems)
{
    // Get the currently selected item directly from the ListView
    var selectedCharacter = m_CharacterList.selectedItem as CharacterData;

    // Handle none-selection (Escape to deselect everything)
    if (selectedCharacter == null)
    {
        // Clear
        m_CharClassLabel.text = "";
        m_CharNameLabel.text = "";
        m_CharPortrait.style.backgroundImage = null;

        // Disable the select button
        m_SelectCharButton.SetEnabled(false);

        return;
    }
}

If the selection is valid, you need to display the character’s details in the UI. You can access to the labels and the portrait image visual elements via the references you retrieved in the InitializeCharacterList method of your class.

Copy the code below into the OnCharacterSelected method:

// Fill in character details
m_CharClassLabel.text = selectedCharacter.m_Class.ToString();
m_CharNameLabel.text = selectedCharacter.m_CharacterName;
m_CharPortrait.style.backgroundImage = new StyleBackground(selectedCharacter.m_PortraitImage);

// Enable the select button
m_SelectCharButton.SetEnabled(true);

You can now enter Play Mode and see your character selection list in action. Press the Escape key to deselect a character.

Final runtime UI
Final runtime UI

Final scripts

Below you can find the full source code for all files created in this guide.

MainView.uxml

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
    <ui:VisualElement style="flex-grow: 1; align-items: center; justify-content: center; background-color: rgb(115, 37, 38);">
        <ui:VisualElement style="flex-direction: row; height: 350px;">
            <ui:ListView focusable="true" name="CharacterList" style="width: 230px; border-left-color: rgb(49, 26, 17); border-right-color: rgb(49, 26, 17); border-top-color: rgb(49, 26, 17); border-bottom-color: rgb(49, 26, 17); border-left-width: 4px; border-right-width: 4px; border-top-width: 4px; border-bottom-width: 4px; background-color: rgb(110, 57, 37); border-top-left-radius: 15px; border-bottom-left-radius: 15px; border-top-right-radius: 15px; border-bottom-right-radius: 15px; margin-right: 6px;" />
            <ui:VisualElement style="justify-content: space-between; align-items: flex-end;">
                <ui:VisualElement style="align-items: center; background-color: rgb(170, 89, 57); border-left-width: 4px; border-right-width: 4px; border-top-width: 4px; border-bottom-width: 4px; border-left-color: rgb(49, 26, 17); border-right-color: rgb(49, 26, 17); border-top-color: rgb(49, 26, 17); border-bottom-color: rgb(49, 26, 17); border-top-left-radius: 15px; border-bottom-left-radius: 15px; border-top-right-radius: 15px; border-bottom-right-radius: 15px; width: 276px; justify-content: center; padding-left: 8px; padding-right: 8px; padding-top: 8px; padding-bottom: 8px;">
                    <ui:VisualElement style="border-left-color: rgb(49, 26, 17); border-right-color: rgb(49, 26, 17); border-top-color: rgb(49, 26, 17); border-bottom-color: rgb(49, 26, 17); border-left-width: 2px; border-right-width: 2px; border-top-width: 2px; border-bottom-width: 2px; height: 120px; width: 120px; border-top-left-radius: 13px; border-bottom-left-radius: 13px; border-top-right-radius: 13px; border-bottom-right-radius: 13px; padding-left: 4px; padding-right: 4px; padding-top: 4px; padding-bottom: 4px; background-color: rgb(255, 133, 84);">
                        <ui:VisualElement name="CharacterPortrait" style="flex-grow: 1; -unity-background-scale-mode: scale-to-fit;" />
                    </ui:VisualElement>
                    <ui:Label text="Label" name="CharacterName" style="-unity-font-style: bold; font-size: 18px;" />
                    <ui:Label text="Label" display-tooltip-when-elided="true" name="CharacterClass" style="margin-top: 2px; margin-bottom: 8px; padding-top: 0; padding-bottom: 0;" />
                </ui:VisualElement>
                <ui:Button text="Select Character" display-tooltip-when-elided="true" name="SelectCharButton" style="width: 150px; border-left-color: rgb(49, 26, 17); border-right-color: rgb(49, 26, 17); border-top-color: rgb(49, 26, 17); border-bottom-color: rgb(49, 26, 17); background-color: rgb(255, 133, 84); border-left-width: 2px; border-right-width: 2px; border-top-width: 2px; border-bottom-width: 2px;" />
            </ui:VisualElement>
        </ui:VisualElement>
    </ui:VisualElement>
</ui:UXML>

ListEntry.uxml

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
    <ui:VisualElement style="height: 41px; align-items: flex-start; justify-content: center; padding-left: 10px; background-color: rgba(170, 89, 57, 255); border-left-color: rgba(49, 26, 17, 255); border-right-color: rgba(49, 26, 17, 255); border-top-color: rgba(49, 26, 17, 255); border-bottom-color: rgba(49, 26, 17, 255); border-left-width: 2px; border-right-width: 2px; border-top-width: 2px; border-bottom-width: 2px; border-top-left-radius: 15px; border-bottom-left-radius: 15px; border-top-right-radius: 15px; border-bottom-right-radius: 15px;">
        <ui:Label text="Label" display-tooltip-when-elided="true" name="CharacterName" style="-unity-font-style: bold; font-size: 18px;" />
    </ui:VisualElement>
</ui:UXML>

CharacterData.cs

using UnityEngine;

public enum ECharacterClass
{
    Knight, Ranger, Wizard
}

[CreateAssetMenu]
public class CharacterData : ScriptableObject
{
    public string m_CharacterName;
    public ECharacterClass m_Class;
    public Sprite m_PortraitImage;
}

CharacterListEntryController.cs

using UnityEngine.UIElements;

public class CharacterListEntryController
{
    Label m_NameLabel;

    public void SetVisualElement(VisualElement visualElement)
    {
        m_NameLabel = visualElement.Q<Label>("CharacterName");
    }

    public void SetCharacterData(CharacterData characterData)
    {
        m_NameLabel.text = characterData.m_CharacterName;
    }
}

MainView.cs

using UnityEngine;
using UnityEngine.UIElements;

public class MainView : MonoBehaviour
{
    [SerializeField]
    VisualTreeAsset m_ListEntryTemplate;

    void OnEnable()
    {
        // The UXML is already instantiated by the UIDocument component
        var uiDocument = GetComponent<UIDocument>();

        // Initialize the character list controller
        var characterListController = new CharacterListController();
        characterListController.InitializeCharacterList(uiDocument.rootVisualElement, m_ListEntryTemplate);
    }
}

CharacterListController.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public class CharacterListController
{
    // UXML template for list entries
    VisualTreeAsset m_ListEntryTemplate;

    // UI element references
    ListView m_CharacterList;
    Label m_CharClassLabel;
    Label m_CharNameLabel;
    VisualElement m_CharPortrait;
    Button m_SelectCharButton;

    public void InitializeCharacterList(VisualElement root, VisualTreeAsset listElementTemplate)
    {
        EnumerateAllCharacters();

        // Store a reference to the template for the list entries
        m_ListEntryTemplate = listElementTemplate;

        // Store a reference to the character list element
        m_CharacterList = root.Q<ListView>("CharacterList");

        // Store references to the selected character info elements
        m_CharClassLabel = root.Q<Label>("CharacterClass");
        m_CharNameLabel = root.Q<Label>("CharacterName");
        m_CharPortrait = root.Q<VisualElement>("CharacterPortrait");

        // Store a reference to the select button
        m_SelectCharButton = root.Q<Button>("SelectCharButton");

        FillCharacterList();

        // Register to get a callback when an item is selected
        m_CharacterList.onSelectionChange += OnCharacterSelected;
    }

    List<CharacterData> m_AllCharacters;

    void EnumerateAllCharacters()
    {
        m_AllCharacters = new List<CharacterData>();
        m_AllCharacters.AddRange(Resources.LoadAll<CharacterData>("Characters"));
    }

    void FillCharacterList()
    {
        // Set up a make item function for a list entry
        m_CharacterList.makeItem = () =>
        {
            // Instantiate the UXML template for the entry
            var newListEntry = m_ListEntryTemplate.Instantiate();

            // Instantiate a controller for the data
            var newListEntryLogic = new CharacterListEntryController();

            // Assign the controller script to the visual element
            newListEntry.userData = newListEntryLogic;

            // Initialize the controller script
            newListEntryLogic.SetVisualElement(newListEntry);

            // Return the root of the instantiated visual tree
            return newListEntry;
        };

        // Set up bind function for a specific list entry
        m_CharacterList.bindItem = (item, index) =>
        {
            (item.userData as CharacterListEntryController).SetCharacterData(m_AllCharacters[index]);
        };

        // Set a fixed item height
        m_CharacterList.fixedItemHeight = 45;

        // Set the actual item's source list/array
        m_CharacterList.itemsSource = m_AllCharacters;
    }

    void OnCharacterSelected(IEnumerable<object> selectedItems)
    {
        // Get the currently selected item directly from the ListView
        var selectedCharacter = m_CharacterList.selectedItem as CharacterData;

        // Handle none-selection (Escape to deselect everything)
        if (selectedCharacter == null)
        {
            // Clear
            m_CharClassLabel.text = "";
            m_CharNameLabel.text = "";
            m_CharPortrait.style.backgroundImage = null;

            // Disable the select button
            m_SelectCharButton.SetEnabled(false);

            return;
        }

        // Fill in character details
        m_CharClassLabel.text = selectedCharacter.m_Class.ToString();
        m_CharNameLabel.text = selectedCharacter.m_CharacterName;
        m_CharPortrait.style.backgroundImage = new StyleBackground(selectedCharacter.m_PortraitImage);

        // Enable the select button
        m_SelectCharButton.SetEnabled(true);
    }
}
Create a Custom Inspector
视觉树