Facing navigation flow with Wave Engine

The navigation flow in games is a very important part that can be difficult to deal with. In this article we will introduce a possible strategy to solve this.

Wave Engine provides generic services and tools to manage scene transitions and basic navigation. However, distributing the navigation logic between different scenes and behaviors can be tricky at times and difficult to scale.

To solve this, an interesting approach is to use a service that contains all the navigation logic, managing transitions and deferred loading between scenes. Later, a simple and generic component can be used to link navigation events to buttons in the scene and hardware buttons.

Navigation Map

The first step is to define the necessary navigation map for our game.  The following diagram shows a simplified navigation map for a level-based mobile game.

Dashed lines represent a default forward path (i.e. Loading to Gameplay), while solid lines represent optional forward path (i.e. Pause to Level Selection). Also some paths have an arrow on both sides indicating that the user can navigate back in those screens.

This navigation map can be clearly defined as a finite state machine inside our Navigation Service, where blue boxes will be states and the labels over the lines will be possible command to execute in those states. This allows us to ask the service if the user can navigate back or forward from the current state, and the service will be the only one who knows what can be done. Also, we can define on this service other useful things, like the transitions between scenes, or the asset preloading of incoming scenes before navigate on this service.

The interface of the navigation service can be:

public interface INavigationService<TCommands> where TCommands : struct
{
  bool IsPerformingNavigation { get; }

  void StartNavigation();
  void Navigate(TCommands command);
  bool CanNavigate(TCommands command);
}

IsPerformingNavigation is active while navigation is performed. During this period, no other navigation will take effect. This avoid multiple button navigation in screens with several navigation options.

StartNavigation method is called on startup to navigate to the first screen and initialize the state machine. It’s useful to load global assets at this point while a loading scene is shown.

Navigate method must be used to perform a navigation. If the command is invalid for the current state, an exception should be raised to avoid unintentional behaviors.

CanNavigate method is used to check if navigation with the command can be performed. Useful for controlling the visibility or effect of a button under different conditions. (i.e. device hardware back button, visibility of “NextLevel” when no more levels are available, multiplayer access with no internet connection, etc.)

The possible commands for the sample navigation map shown here should be:

public enum NavigateCommands
{
  Back = 0,
  DefaultForward,
  Quit,
  Play,
  ChooseLevel,
  NextLevel,
  ReturnMainMenu,
}

Configuring buttons

Another approach of this system is the creation of a simple Wave Component to configure navigation actions for buttons in a simple way. This component will invoke the service to perform the navigation.

A possible implementation of this component can be:

[DataContract]
public class NavigationComponent : Component
{
  [DataMember]
  public NavigateCommands NavigationCommand { get; set; }

  protected override void DefaultValues()
  {
    base.DefaultValues();
    this.NavigationCommand = NavigateCommands.Back;
  }

  public void DoNavigation()
  {
    var navService = WaveServices.GetService<NavigationService>();
    if (navService != null)
    {
      navService.Navigate(this.NavigationCommand);
    }
  }
}

This component can be configured from Wave Visual Editor to choose the command to use when navigation is performed.

In the NavigationFlow sample there are 3 components that make use of the one above:

  • KeyboardNavigationComponent: Listen to key press changes from the keyboard and invoke the navigation when a certain key is pressed.
  • TimerNavigationComponent: It will start a timer and invoke the navigation when the timer ends.
  • TouchNavigationComponent: It will invoke the navigation when a touch tap is detected on the entity.

Check the full sample available in the GitHub repository.

Leave a Reply

Your email address will not be published. Required fields are marked *