Application development

There are several ways to add or customize your Kanzi application behavior and functionality:

  • Using configuration you can set the most common startup and runtime parameters for your application. For example, you can configure the application resolution, the kzb files that the application uses, and so on. See Configuring your application.

  • Use code behind to add behavior to UI elements in your application. Code behind is the easiest way to add functionality without leaving Kanzi Studio and without creating solutions or project files. You can observe the behavior that is implemented in code behind immediately in the Preview. See Programming Activity nodes with Code Behind.

  • Derive a C++ class from the Application class which is the base class for all Kanzi applications. Override the various virtual functions of the Application class to react to different points in the lifecycle of the application, such as initialization and uninitialization. For example, when the application starts, when the application loads its UI content, and so on. See Reacting to application lifecycle events.

  • Use the MainLoopScheduler class to which the Application class delegates processing of input, ticking of animations, laying out and rendering content, ending with update of the screen through repeated control flow. You can add tasks that execute in the main loop of the application to add recurring or one-time functionality relative to the stages of the main loop of the application. See Modifying the main loop logic.

  • Develop Kanzi Engine plugins which can implement functionality that you can reuse between applications.

Configuring your application

Using configuration you can set the most common startup and runtime parameters for your application. For example, you can configure the application resolution, the kzb files that the application uses, and so on.

You can configure your Kanzi application:

  • In the C++ application override the Application::onConfigure() function of your application class. Kanzi calls this function as part of application initialization before it reads the application.cfg and before it initializes the graphics subsystem. Use this function to configure application properties.

  • In application.cfg by setting the parameters for Kanzi Studio projects without recompiling your application or even without a C++ application.

The configuration you specify in application.cfg overrides the configuration you specify in Application::onConfigure().

For example, to set in application.cfg which kzb file to load

# Loads the kzb file named my_application.kzb.
BinaryName = "my_application.kzb"

To set the kzb file to load in Application::onConfigure()

// Loads the kzb file named my_application.kzb.
configuration.binaryName = "my_application.kzb";

For a list of all the configuration settings you can use for your Kanzi application, see Application configuration reference.

Reacting to application lifecycle events

Adding startup logic

You can define the startup logic of your application in these functions:

  • Kanzi calls the Application::onStartup() function once, immediately after starting the application, but before it loads the initial UI contents. Here you can add application start-up logic that requires modifying already initialized Kanzi objects.

  • Kanzi calls the Application::onProjectLoaded() function once, immediately after loading the initial UI contents.

    Usually you insert here initialization code that depends on the content that has already been loaded. For example, you can attach initial message handlers or listeners, populate the UI with data, and so on.

    For example, this code attaches a handler to a Button after Kanzi loads the node tree:

    void onProjectLoaded() override
    {
        // Get reference to the Screen node.
        ScreenSharedPtr screen = getScreen();
        kzAssert(screen);
    
        // Look up reference to a button by alias.
        Button2DSharedPtr button = screen->lookupNode<Button2D>("#Button");
        kzAssert(button);
    
        // Attach a handler to the button.
        button->addMessageHandler(Button2D::ClickedMessage, [this](auto& arguments) { this->buttonHandler(arguments); });
    }
    

This diagram shows the callbacks for startup and shutdown logic.

Observing application state

You can observe the application state to find out whether an application is running, minimized, or suspended.

A Kanzi application is always in one of these states:

  • Running state. This is the normal mode of operation of a Kanzi application. This is the initial state of an application.

  • Paused state. The platform can impose rules on application behavior when it is out of focus or minimized. To mark such state, a platform backend can set the application to the Paused state.

    In this state, Kanzi stops normal input handling and rendering, and waits to be put back to the Running state. Additionally, it is typical for applications to halt heavy work when in this state.

  • Quitting state. This state indicates that either application or the platform requested the application to quit by calling quit(). In this state, applications must avoid starting work, unless that work is related to uninitialization.

This diagram shows the states of a Kanzi application.

To find out the application state, call getState().

For example, to prevent adding a task when an application is quitting:

void doSomethingRepeatedly()
{
    doSomething();

    // If a condition is met...
    if (someCondition() &&
        // ...and the application is not quitting...
        getState() != MainLoopState::Quitting)
    {
        // ...add another task to the task dispatcher.
        getDomain()->getTaskDispatcher()->submit([this]() { this->doSomethingRepeatedly(); });
    }
}

Reacting to application state changes

Use these functions to react to application state changes:

Function

Description

onSuspend()

Kanzi calls this function in a frame where no rendering was performed, to determine whether to suspend the application and for what duration. The default implementation calculates appropriate timeout based on active animations, timers, and resources waiting for deployment.

Use this function to customize application suspension.

onPause()

Kanzi calls this function when the application main loop enters the Paused state.

onResume()

Kanzi calls this function when the application main loop returns from the Paused to the Running state.

onShutdown()

Kanzi calls this function immediately before the application uninitialization.

Modifying the main loop logic

The main loop consists of a sequence of stages, where each stage consists of a sequence of tasks. A task is any callable item, including functions, function objects, and lambdas.

using Task = function<void(chrono::nanoseconds)>;

When Kanzi executes a task, it passes the last frame duration as an argument to the task.

These are the default stages in Kanzi:

  • In the input stage Kanzi handles the input events.

  • In the user stage Kanzi handles update logic: deploys resources, loads kzb files and prefabs, and executes your task dispatcher and timer tasks.

  • In the animate stage Kanzi ticks the active animations.

  • In the layout stage Kanzi performs layout the node tree.

  • In the render stage Kanzi renders the node tree.

  • In the present stage Kanzi presents the node tree.

This diagram shows the control flow of the Kanzi main loop.

In an application you can remove, replace, or insert tasks in any main loop scheduler stage. See MainLoopScheduler.

Adding recurring tasks

When you want Kanzi to execute a task every frame, you can add it to any stage of the main loop using appendTask() and prependTask(), with option Recurring.

For example, you can use a recurring task to call the update function of an external library:

void onStartup() override
{
    // From the main loop Animate stage, tick an external physics engine.
    getMainLoopScheduler().appendTask(AnimateStage,
                                      kzMakeFixedString("TickPhysics"),
                                      MainLoopScheduler::TaskRecurrence::Recurring,
                                      [this](auto deltaTime) { this->getPhysicsEngine()->tick(deltaTime); });
}

Adding one-time tasks

When you want Kanzi to execute a task only once, you can add it to any stage of the main loop using appendTask() and prependTask(), with recurrence option OneTime.

For example, to capture a screen shot of the output when the user presses the Enter key:

void onKeyInputEvent(const KeyEvent& keyEvent) override
{
    // First invoke the base class implementation.
    Application::onKeyInputEvent(keyEvent);

    // If the user presses the Enter key:
    if (keyEvent.getKeyUp() == LogicalKey::Enter)
    {
        // Force re-rendering.
        getScreen()->invalidateDraw();

        // Add a post-rendering task to capture the screen output.
        getMainLoopScheduler().appendTask(RenderStage,
                                          kzMakeFixedString("CaptureScreenshot"),
                                          MainLoopScheduler::TaskRecurrence::OneTime,
                                          [this](auto) { this->captureScreen(); });
    }
}

Overriding tasks

When you want to modify the default approach that Kanzi uses to process input, performs layout and renders content, update the screen, and perform other repeating tasks, you can replace existing tasks using replaceTask().

For example, a typical use case of overriding the default rendering is warping. Warping distorts the output so that the application correctly projects it on a curved screen. You can achieve this by first rendering the contents to an off-screen texture with the default rendering implementation, and then copying that texture to the screen with the distortion effects applied:

void onStartup() override
{
    // Create a texture for rendering.
    auto texture = createOffscreen();

    // Get the token of the default rendering task from the render stage.
    auto& scheduler = getMainLoopScheduler();
    auto tasks = scheduler.getTaskInfo(RenderStage);
    auto itTask = find_if(cbegin(tasks), cend(tasks), [](const auto& taskInfo) { return taskInfo.name == "Render"; });
    kzAssert(itTask != cend(tasks));
    auto token = itTask->token;

    // Replace the texture with the wrapped implementation.
    scheduler.replaceTask(RenderStage,
                          token,
                          [this, texture = move(texture)](auto) {
                              auto renderer = this->getRenderer3D();
                              auto& compositionStack = renderer->getCompositionStack();

                              // Set the off-screen texture as the target.
                              compositionStack.pushComposition(*texture);

                              // Render to the off-screen texture using the default rendering logic.
                              this->render();

                              // Revert to the screen as the target.
                              compositionStack.popComposition();

                              // Render to the screen the texture with the wrapping effect applied.
                              this->renderWarped(renderer, texture);
                          });
}

As an alternative to individually replacing tasks, you can redefine the whole main loop, by overriding initializeMainLoopTasks(). Kanzi attaches the default main loop tasks in this function. To allow copying, its implementation is intentionally exposed in the header.

Adding timers

When you want Kanzi to execute a task at regular time intervals, you can add the task before a stage of the main loop using prependTimer() and after a stage of the main loop using appendTimer().

Use timer recurrence options to execute a timer only once (OneTime) or repeatedly (Recurring). When a recurring timer elapses multiple times in a single iteration of the Kanzi main loop, Kanzi executes that timer only once.

using TimerTask = function<void(chrono::nanoseconds, unsigned int)>;

When Kanzi executes a timer, it passes as arguments:

  • The elapsed duration rounded down to a multiple of the interval of the timer

  • The number of elapsed intervals

For example, to update time on a clock using a recurring timer:

void activateClock()
{
   // Add a recurring timer task that updates the clock once every second.
   m_clockTimer = getMainLoopScheduler().appendTimer(UserStage,
                                                      kzMakeFixedString("Clock"),
                                                      MainLoopScheduler::TimerRecurrence::Recurring,
                                                      chrono::seconds(1),
                                                      [this](auto elapsedDuration, auto repeatCount) {
                                                         this->updateClock(elapsedDuration * repeatCount);
                                                      });
}

void deactivateClock()
{
   // Remove the clock timer.
   getMainLoopScheduler().removeTimer(m_clockTimer);
}

Balancing between UI responsiveness and resource loading

The memory that is required to contain all of the application resources can exceed the amount of RAM on your target device. You can break down your application into parts and define the navigation so that only some parts are loaded at the same time.

When your application navigates to a part, Kanzi receives a request to load the required resources. You can tell Kanzi to load these resources in loader threads instead of the UI thread. This way you can preserve the responsiveness of your application while Kanzi loads the resources.

While resources can be loaded in the loader threads, OpenGL requires that the GPU resources, such as textures, meshes, and materials are deployed in the UI thread where the resources are copied to the GPU memory.

Kanzi keeps the resources it needs to deploy in a queue and deploys only one resource each frame. However, in doing so Kanzi trades the responsiveness of the UI for how soon all the resources needed to display the new UI can be available.

You can define your own deployment logic by overriding the progressDeploymentQueueOverride(). Use the knowledge of the assets of your application or its execution environment to modify the behavior to deploy a different number of resources on a per-frame basis.

For example, when your application contains a lot of large resources, the default implementation of deploying one resource every frame can cause uneven framerate. When you define your own deployment logic you can throttle the deployment of resources:

void progressDeploymentQueueOverride() override
{
    StopWatch stopWatch;
    ResourceManager* resourceManager = getResourceManager();

    // Deploy once and check whether any items remain in the queue.
    while (resourceManager->processDeploymentQueueItem() &&
           // Check whether the elapsed time is less than the limit.
           stopWatch.peekDeltaTime() < getMaxDeploymentTime())
    {
    };
}

To learn how to set the number of threads to use to load resources, see LoadingThreadCount.

Handling global input

Usually you use UI controls and their messages to handle input. In some cases you want to handle input that is global. Kanzi calls handleEvents() for all input events, onKeyInputEvent() for keyboard events, and onPointerInputEvent() for mouse events. Use these functions to define the event handling logic in your application. Make sure to call the base implementation in your override.

For example, when you want to implement debug mode. See Adding one-time tasks.

Measuring application performance

The Kanzi performance profiling system enables you to measure the performance of your Kanzi application. You can measure the performance of: