Skip to content

chwar/XRUI

Repository files navigation

XRUI Framework

release Unity 2021.2+ MIT Coverage

2dlandscape 2dportrait 3d
2D Landscape (PC) 2D Portrait (Android) World Space (Meta Quest 2)

XRUI is a responsive UI framework for making cross-platform XR applications with the Unity 3D editor. Its purpose is to assist users in creating efficient and adaptive UIs that can easily be adjusted to be rendered in 2D (for environments with a 2D screen, e.g. PC, smartphone, tablet) and 3D (i.e., rendered in world space, required to render UI in XR (AR/MR/VR)). This way, XRUI users only need to design and implement their UI once for all platforms. This can also provide memorability and familiarity to end-users that use XRUI enhanced apps on different platforms, thus increasing usability.

XRUI is based on Unity's new UI system, UI Toolkit. Internally, it uses UXML and USS, so a basic knowledge and understanding of these technologies are required to use this framework.

Getting Started

  1. In the package manager, click on Add package from git URL and insert the repository's URL: https://github.com/chwar/XRUI.git
  • Alternatively, you can unzip and import the package manually.
  1. Add the XRUI controller to your scene by navigating to XRUI > Add XRUI Controller. You can also create an empty game object and attach the main XRUI script (XRUI.cs). This script is a singleton flagged as DontDestroyOnLoad. It contains the main API that can be easily accessed through the instance:
using com.chwar.xrui;

void Start(){
    // XRUI.Instance...
}
  1. The package uses a default configuration that references the default UXML templates for UI elements. You can create your own by navigating to Assets > Create > XRUI > Create XRUI Configuration asset. You can then override the default templates for UI elements with your own (see Custom UI Elements). Don't forget to reference your own XRUI configuration asset to the XRUI controller.
  2. You can have a look at the provided Demo scenes to get a better idea of how XRUI works.

UI Elements

2dlandscape 3d
2D Landscape Overview 3D (World Space) Overview

XRUI provides a few UI Elements. The style is minimalistic and inspired from Bootstrap. You can add them in your project by navigating from the Unity menu to XRUI > Add XRUI Element. This creates a game object containing a UIDocument (which contains the UXML template and USS styles) and an XRUI script that matches the element. Add your own scripts to this object with a reference to the XRUI script to define the behaviour of the UI.

XRUI elements are thought as basic containers for user content. Given the hierarchic nature of UXML, it is easy to append content within the UI elements at runtime. To easily access your UXML contents and append them into various XRUI elements, reference them in the intended UI Elements list within the XRUI controller:

xruiController

The list of UI elements is accessible within the XRUI controller's instance. Use the GetUIElement method for easy access:

// Use the name of the VisualTreeAsset you put in the inspector list 
VisualTreeAsset myElement = XRUI.Instance.GetUIElement("MyElement");

XRUI Element

Click to expand!

When adding UI Elements through the XRUI menu in Unity, the system uses the template referenced in the XRUI Configuration asset (see Installation). Each XRUI element script inherits from the XRUIElement class, which comes with some useful generic methods.

To add or remove visual elements from the UI element, call these methods:

VisualElement myElement = someVisualTreeAsset.Instantiate();
XRUICard card = GetComponent<XRUICard>();

// Appends a visual element inside a parent element
card.AddUIElement(myElement, "xrui-card__container");
card.RemoveUIElement(myElement); 

Get an XRUI related visual element from the UI element:

XRUICard card = FindObjectOfType<XRUICard>();
XRUIMenu menu = FindObjectOfType<XRUIMenu>();
        
// Get a generic Visual Element from the card
var cardHeader = card.GetXRUIVisualElement("xrui-card__header");

// Get the title Label from the menu 
var menuTitle = menu.GetXRUIVisualElement<Label>("xrui-menu__title");

You can also show or hide XRUI elements at any time:

card.Show(true);    // Display.Flex, enables MeshRenderer and Collider for world UI
card.Show(false);   // Display.None, disables MeshRenderer and Collider for world UI

card.Show(myElement, false); // Hides myElement

Note: Keep in mind that hidden elements will not be found with a regular QML query, as they are hidden. You can still find them by either keeping a reference to the visual element in your code, or by querying it like this:

card.Query<TemplateContainer>().Where(ve => 
  ve.style.display.value.Equals(DisplayStyle.None)).First();

XRUI Menu

Click to expand!
2dlandscape 2dportrait 3d
2D Landscape 2D Portrait World Space

The provided XRUI Menu template is designed as a side menu that collapses out of the view frustum. It can be configured in the inspector (see screenshot above).

The list element template is the UXML template that is used to create entries. You can provide a template with a simple button, or more complex compositions with images, text, buttons, etc. to suit your needs.

Add entries to your menu:

var menu = GetComponent<XRUIMenu>();

// The menu returns the created entry to be configured
var element = menu.AddElement();
element.Q<Label>("MyElementLabel").text = "myLabelTitle";

XRUI List

Click to expand!
2dlandscape 2dportrait 3d
2D Landscape 2D Portrait World Space

The XRUI List works in the same way as the menu:

var list = GetComponent<XRUIList>();

// The list returns the created entry to be configured
var element = list.AddElement();
element.Q<Label>("MyElementLabel").text = "myLabelTitle";

XRUI Navbar

Click to expand!
2dlandscape 2dportrait 3d
2D Landscape 2D Portrait World Space

The provided navbar is a very simple dark top bar. Since XRUI does not provide any third-party assets, it is provided empty. However, the default template contains a row of buttons (three justified on the left side, one justified on the right side) to get you started. Since navbar designs can be very different, the adopted solution was to propose a very generic template to fit the most users. You could use the template as a base to add your own elements (buttons, dropdowns, labels) to tailor the navbar to your needs.

XRUI Card

Click to expand!
2dlandscape 2dportrait 3d
2D Landscape 2D Portrait World Space

The XRUI Card is floating on the right corner in the 2D landscape format, and sticks to the bottom of the screen in portrait format. Use the AddUIElement method (see XRUI Element) to fill the card with content.

XRUI Modals

Click to expand!
2dlandscape 2dportrait 3d
2D Landscape 2D Portrait World Space

XRUI creates modals at runtime rather than requiring you to create all of them in the editor in order to save resources.

Given the hierarchic nature of UXML, modals are easy to reproduce. XRUI provides a modal template, which consists of a title, empty container, two buttons (main and secondary) sticking at the bottom, and a closing button in the top right corner. You can use this template and fill its container dynamically at runtime.

In Unity, you can reference your modals in the intended list:

modals

The name given to each modal entry can be used to find the matching template and create a modal from it, with the CreateModal method:

// Adapt the namespace to your own
Type t = Type.GetType("myModalScript");
XRUI.Instance.ShowModal("DemoModal", t);

Note: The user script type has to be passed outside of the XRUI package, because Unity packages can't access the Assembly-CSharp assembly, i.e. can't find user namespaces, and hence, can't find user scripts located in the Assets automatically. It's also not possible to reference it through the inspector, as it only accepts instances of a script and not the script itself.

This creates a modal game object on which the XRUIModal script is attached, as well as a UIDocument script that contains the main template. You can access the modal system's API through the XRUIModal script. The user script type is used to create an instance of said script when the modal is created. This lets you define the behaviour of your elements. One approach is to create one method per page, and to setup event handlers on your buttons to navigate them. To create modal pages, use the UpdateModalFlow method. Its last parameter is a callback function that is fired once upon the page's creation.

private XRUIModal _xruiModal;
private UIDocument _uiDocument;

void Start() {
    _xruiModal = GetComponent<XRUIModal>();
    _uiDocument = GetComponent<UIDocument>();
    StartPage();
}

void StartPage() {
    _xruiModal.UpdateModalFlow("MyModalPage", "MainContainer", () =>
    {
        // This callback is only fired once, when the page is created for the first time
        // Put here initialization code, event subscriptions, etc. 

        Button myButton = RootElement.Q<Button>("myButton");
        _myButton.clicked += MyPage;
    });

    // Content to execute every time this page is opened.
}

void MyPage() {
    // ...
}

Using the Default Modal Template

You can use the default modal template that comes with the package and fill it with your own content. It consists of a title, empty container, two buttons (main and secondary) sticking at the bottom, and a closing button in the top right corner. You can add your content to the container by referencing it by its USS class (xrui-modal__container) to the UpdateModalFlow method, as per the example above. You can manipulate the buttons and change the title through the XRUIModal API.

Access the modal's public fields to change the title of the modal, the text of the buttons, or to set the icon of the top right close button:

_xruiModal.ModalTitle.text = "Create a new project";
_xruiModal.CancelButton.text = "Cancel";
_xruiModal.ValidateButton.text = "Finish";
_xruiModal.ValidateButton.visible = true;

Change the justification of the bottom buttons with the SetButtonsPlacement method:

// Supported options: FlexStart, FlexEnd, Center, Space Between, Space Around
_xruiModal.SetButtonsPlacement(Justify.Center);

Set the action of the cancel and validation buttons:

// These methods take an action as parameter, but you can provide a function call

_xruiModal.SetCancelButtonAction(XRUIModal.Destroy);
_xruiModal.SetValidateButtonAction(CreateProject);

Note: prefer these methods to the direct access to ValidationButton.clicked or CancelButton.clicked, as the methods replace any other event subscription with the provided action. This means that a click on either button can have only one action on a given page.

Destroy the modal:

_xruiModal.Destroy();

Form Validation

XRUI supports basic form validation by letting you define required fields. For now, only text fields are supported, i.e. XRUI determines if required text fields are empty or not.

_xruiModal.SetRequiredFields(_fieldOne, _fieldTwo, _fieldThree);

You can pass as many fields as you want in one call. Internally, XRUI checks the page where each indicated field is contained in. When a user is on a page containing required fields, the modal's validation button is disabled until all required fields contain an input. Additionally, you can subscribe your own validation methods to the validation button and flag text fields with errors to indicate users the fields to correct.

_xruiModal.SetFieldError(_fieldWithError);

XRUI Alerts

Click to expand!
2dlandscape 2dportrait 3d
2D Landscape - Primary 2D Portrait - Success World Space - Warning

Peek 2021-08-03 00-03 Peek 2021-08-03 00-05

The provided alert template creates floating cards in 2D landscape and 3D formats, and as notifications at the top of the screen in 2D portrait mode. They also come with animations to attract the attention of users. You can show alerts for different purposes; the types of alerts are inspired from Bootstrap.

Show alerts using the ShowAlert method:

XRUI.Instance.ShowAlert(XRUIAlert.AlertType.Primary, "Primary message.");
XRUI.Instance.ShowAlert(XRUIAlert.AlertType.Success, "Success message.");
XRUI.Instance.ShowAlert(XRUIAlert.AlertType.Warning, "Warning message.");
XRUI.Instance.ShowAlert(XRUIAlert.AlertType.Danger, 	"Error message.");
XRUI.Instance.ShowAlert(XRUIAlert.AlertType.Info, 	"Info message.");

You can also provide a title:

XRUI.Instance.ShowAlert(XRUI.AlertType.Primary, "Title", "Primary message.");

You can also give a callback, which will be triggered upon clicking the alert:

 XRUI.Instance.ShowAlert(XRUI.AlertType.Primary, "Click me!", "Click to trigger callback", () => MyCallback());

Or, you can set a countdown after which the alert will disappear:

 XRUI.Instance.ShowAlert(XRUI.AlertType.Primary, "Title", "This alert will disappear in 5 seconds", 5);

XRUI Contextual Menu

Click to expand!
2dlandscape 2portrait
2D Landscape 2D Portrait

XRUI can create contextual menus dynamically. The contextual menu is shown as a floating list. Similarly to the menu and list templates, a menu element template is also given to create entries in the contextual menu. Because the entries are context-dependent, they need to be generated dynamically at runtime.

The ShowContextualMenu method needs at least the x and y coordinates of the parent element (i.e. the element that was interacted which caused the contextual menu to appear), and a boolean to indicate whether or not the styling should include an arrow pointing at the parent element. A first overload gives the possibility to provide a custom template. A second overload lets developers provide left and right offsets for finer tuning of the menu’s position. The method returns a XRUIContextualMenu instance, which is required to add entries to the menu.

var myElement = GetComponent<UIDocument>();
var myBtn = myElement.rootVisualElement.Q<Button>("myBtn");
	
// Define parent coordinates for menu positioning
Vector2 parentCoordinates = new Vector2(myBtn.worldBound.x, myBtn.worldBound.y);
	
// For using a custom template
VisualTreeAsset myTemplate = Resources.Load<VisualTreeAsset>("myTemplate");
	
myBtn.clicked +=
{
	var menu;
	// Use one of these overloads
	// Call with 2 parameters: parent coord. and whether to show a pointing arrow
	menu = XRUI.Instance.ShowContextualMenu(parentCoordinates, false);
	
	// 3 parameters: provide a custom template as 1st param.
	menu = XRUI.Instance.ShowContextualMenu(myTemplate, parentCoordinates, true);
	
	// 5 parameters: provide left and right offsets in pixels
	menu = XRUI.Instance.ShowContextualMenu(myTemplate, parentCoordinates, true, 50,
	100);
}
	
// To override the default menu entry template
menu.menuElementTemplate = Resources.Load<VisualTreeAsset>("myEntryTemplate");
	
// Add entries to the contextual menu
var entry = contextualMenu.AddMenuElement();

In addition, the contextual menu considers the available space on screen. By default, contextual menus will attempt to display on the right of the parent element. However, if there is no available space, they are displayed on the left instead.

XR Adaptation

Global XRUI Format

XRUI's main functionality is to provide responsiveness for different XR variants. This is done by setting the chosen XRUI format during the app's initialization, which all XRUI Elements (both static and dynamic) adopt thanks to USS styles.

To change the global XRUI format, change the related value in the XRUI controller:

The XRUI API provides methods to assess the current XRUI format. You can use it to do target-specific manipulations like so:

if(XRUI.IsGlobalXRUIFormat(XRUI.XRUIFormat.ThreeDimensional)) {
    // 3D UI specific code here
}

XRUIFormat format = XRUI.GetGlobalXRUIFormat();

Overriding the Global XRUI Format for Specific Elements

It is also possible for specific UI elements to override the global format defined in the XRUI controller. This way, hybrid 2D and 3D / World UI elements can be rendered in the same scene. Each XRUI Element possesses the xruiFormatOverride property, which can be set to the following values:

  • UseGlobal: uses the value set in the XRUI controller (default value)
  • TwoDimensional: overrides the value set in the XRUI controller by setting this UI element format to 2D
  • ThreeDimensional: overrides the value set in the XRUI controller by setting this UI element format to 3D

The API provides methods to check the format of any XRUI Element:

var card = GetComponent<XRUICard>();
if(card.IsXRUIFormat(XRUI.XRUIFormat.ThreeDimensional) {
    // 3D UI specific code for this given element
}

XRUIFormat format = card.GetXRUIFormat(); 

Note: When integrating World UI elements while using 2D as a global format, don't forget to change the Panel Settings of the UI Document to an asset that is fit for World UI (you can use the provided DefaultWorldUIPanelSettings asset).

Two Dimensional Format

For 2D UI, additional USS styles are provided to adapt for both landscape and portrait orientations. These classes are automatically added when the device (i.e., a smartphone) changes orientation. For ease of use, you can force the portrait mode by checking the Force Two Dimensional Format to Portrait checkbox in the XRUI controller.

Three Dimensional Format (World Space UI)

When XRUI is set to Three Dimensional format, UI is rendered on panels in world space. Each XRUI Element contains a set of World UI Parameters which can alter the way it is rendered in world space.

worlduiparameters

  • The Bend Panel property will slightly bend the panel, which is a common practice in VR apps.
  • The Anchor Panel To Camera property makes the panel follow the gaze of the camera, with a slight delay.
  • The Camera Follow Threshold property defines the minimum distance that needs to be between the panel and the camera gaze before the panel recenters itself.
  • The Custom Panel Dimensions overrides the size of the panel, which is otherwise calculated from the ratio of the width and height of the UI element defined in the USS sheet.
  • The Panel Scale property lets you alter the scale of the panel. By default, the size of panels tend towards one world space unit.

XRUI Grid System

In order to organize easily and efficiently UI elements on screen, XRUI makes use of a grid system. You can use it by navigating to XRUI > Add XRUI Grid. In the Unity editor, you can group UI components inside rows through the scene hierarchy. The XRUIGridController component is attached to the root of the grid, and contains the list of all rows. A weighting system allows you to define which rows should take which amount of space (this uses the flex-grow attribute of CSS/USS Flexbox).

For example, a top navbar can be setup in one row, with a weight of 0, i.e., it should not "grow"--as in, take space--more than its initial size. A second row containing the rest of the on-screen UI can have a weight of 1, i.e. it should take more of the available space than what its initial size requires. Since there are two rows and the first row has a weight of 0, this results in the second row using all remaining screen space. Horizontally, elements are contained in absolute containers, which mean they all take the entire horizontal space and can therefore overlap.

pc

pc

Note: In case all UI elements within a row are absolute, the row's height becomes zero, because its USS property is set to height: auto. You should then indicate a minimum height in the indicated field to obtain the expected behaviour.

Custom UI Elements

You can create your own UXML templates and refer them in the XRUI Configuration asset. You should however be careful in naming your elements, should you want to inherit the functionalities provided by the default UI elements. You can check them with Unity's UI Builder, or you can simply duplicate the UXML files and start working from here.

Also, the root visual element of your custom templates must have the .xrui USS class.

USS Styles

XRUI comes with its own set of styles that are imported just after Unity's in UI Toolkit's pipeline. They are imported through a theme file which is used in the provided Panel Settings assets (also linked in the XRUI Configuration asset). You can add your own root styles to this theme file, override the root XRUI styles, or remove some of the imported assets if you don't need them. Should you want to inherit some of the XRUI styles for your own UI elements, you can add the related USS classes to the desired visual elements.

Additionally, when creating your custom elements based on existing ones, it is recommended that you add the following USS classes to keep the XRUI functionalities (e.g., updating the title from the inspector). They are the following:

XRUI Element Root USS Class Sub USS Classes
Menu .xrui-menu .xrui-menu__title
.xrui-menu__subtitle
.xrui-menu__container
.xrui-menu__btn-container
.xrui-menu__close-btn
.xrui-menu__main-btn
Menu item .xrui-menu-item
List .xrui-list .xrui-list__title
.xrui-list__add-btn
.xrui-list__container
List item .xrui-list-item .xrui-list-item__icon
.xrui-list-item__text
Navbar .xrui-navbar
Card .xrui-card .xrui-card__title
.xrui-card__subtitle
.xrui-card__container
.xrui-card__close-btn
Alert .xrui-alert .xrui-alert__title
.xrui-alert__content
Modal .xrui-modal .xrui-modal__title
.xrui-modal__close-btn
.xrui-modal__container
.xrui-modal__btn-container
.xrui-modal__cancel-btn
.xrui-modal__validate-btn
Contextual Menu .xrui-contextual-menu .xrui-contextual-menu__arrow
.xrui-contextual-menu__container
Contextual Menu Element .xrui-contextual-menu-element .xrui-contextual-menu-element__text
Icons .xrui-icon .xrui-icon--white
.xrui-icon--black
Templates .xrui-templates__btn
.xrui-templates__separator
.xrui-templates__textfield
Backgrounds .xrui-background--primary
.xrui-background--secondary
.xrui-background--warning
.xrui-background--success
.xrui-background--danger
.xrui-background--info
.xrui-background--light-grey
.xrui-background--dark

XR Interactions

See the XRUIDemoInteraction scene in the Demo folder. To enable interactions with world UI (i.e., make your UI react to MR/VR pointers), you need to add a few objects from the XR Interaction package in your scene:

  • An Event Handler component
  • The Input Action Manager component from the XRI package
  • The XR Interaction Manager from the XRI package
  • The XR UI Input Module from the XRI package

Your controllers need:

  • The XR Controller component from the XRI package (preferably with the XRI input actions configured)
  • The XR Ray Interactor component from the XRI package
    • You must check the "Enable Interaction with UI GameObjects" checkbox

XRUI automatically adds the Tracked Device Physics Raycaster component to World UI game objects.

You can also specifically disable XR interactions on specific XRUI Elements, to avoid adding unnecessary Game Objects to your scene at runtime. To do so, check the Disable XR Interaction checkbox in the World UI Parameters section of the XRUI Element.

Acknowledgements

  • Thanks to katas94 for the inspiration on interfacing XRUI with Unity Event Handlers and the XR Interaction package.
  • Thanks to mattvr for their gist to create curved panels.
  • Thanks to swifter14 for the fix on Android auto-rotation lock.
  • Thanks to NormandErwan for DocFxForUnity.

Roadmap

  • Grid system for World UI
  • Animation mechanism for all XRUI Elements
  • Custom inspectors for ease of use

Known bugs

  • Raycasts on World UI Interactions do not entirely match the visuals shown to the users when using the Oculus SDK. Collisions are detected on the left of panels although they should not, and they stop too early before the right border of the panel.
  • When scripts are recompiled in the Editor, 2D UI Elements will sometimes not update properly. This is not really problematic as going into play mode re-renders all UI Elements correctly.