showTitles(duration, title, subtitle, titlePosition, subtitlePosition) {
    return new Promise((resolve) => {
      if (duration == null) { duration = 0.0; }

      if (this._titlesAction != null) {
        this.stopAction(this._titlesAction);
        this._titlesAction = null;
      }

      // title label
      if (title != null && title.length > 0) {
        var titleLabel = this.getOrCreateTitleLabel();
        titleLabel.setString(title.toUpperCase());
      } else {
        // remove title
        if (this._titleLabel != null) {
          this._titleLabel.destroy(duration);
          this._titleLabel = null;
        }
      }

      // subtitle label
      let subtitleLabel;
      if (subtitle != null && subtitle.length > 0) {
        // dual positions
        if (titlePosition == null) { titlePosition = cc.p(0.0, -UtilsEngine.getGSIWinHeight() * 0.5 + 180); }
        if (subtitlePosition == null) { subtitlePosition = cc.p(0.0, -UtilsEngine.getGSIWinHeight() * 0.5 + 120); }

        subtitleLabel = this.getOrCreateSubtitleLabel();
        subtitleLabel.setString(subtitle);
      } else {
        // single position
        if (titlePosition == null) { titlePosition = cc.p(0.0, -UtilsEngine.getGSIWinHeight() * 0.5 + 140); }

        // remove subtitle
        if (this._subtitleLabel != null) {
          this._subtitleLabel.destroy(duration);
          this._subtitleLabel = null;
        }
      }

      // setup sequence
      const sequence = [];

      // show title
      if (titleLabel != null) {
        titleLabel.setScale(1.0);
        if (duration > 0.0) {
          titleLabel.setPosition(titlePosition.x, titlePosition.y - 20.0);
          titleLabel.fadeTo(0.0, 0.0);
          const titleAction = cc.spawn(
            cc.show(),
            cc.fadeIn(duration),
            cc.moveBy(duration, cc.p(0, 20)).easing(cc.easeCubicActionOut()),
          );
          titleAction.setTag(CONFIG.FADE_TAG);
          sequence.push(cc.targetedAction(titleLabel, titleAction));
        } else {
          titleLabel.setPosition(titlePosition);
          titleLabel.fadeTo(0.0, 255.0);
        }
      }

      // show subtitle
      if (subtitleLabel != null) {
        subtitleLabel.setScale(1.0);
        if (duration > 0.0) {
          subtitleLabel.setPosition(subtitlePosition.x, subtitlePosition.y - 10.0);
          subtitleLabel.fadeTo(0.0, 0.0);
          const subtitleSequence = [];
          if (titleLabel != null) {
            subtitleSequence.push(cc.delayTime(0.1));
          }
          subtitleSequence.push(cc.spawn(
            cc.show(),
            cc.fadeIn(duration),
            cc.moveBy(duration, cc.p(0, 10)).easing(cc.easeCubicActionOut()),
          ));
          let subtitleAction;
          if (subtitleSequence.length > 1) {
            subtitleAction = cc.sequence(subtitleSequence);
          } else {
            subtitleAction = subtitleSequence[0];
          }
          subtitleAction.setTag(CONFIG.FADE_TAG);
          sequence.push(cc.targetedAction(subtitleLabel, subtitleAction));
        } else {
          subtitleLabel.setPosition(subtitlePosition);
          subtitleLabel.fadeTo(0.0, 255);
        }
      }

      if (sequence.length > 0) {
        this._titlesAction = cc.sequence(
          cc.spawn(sequence),
          cc.callFunc(() => {
            this._titlesAction = null;
            resolve();
          }),
        );
        this.runAction(this._titlesAction);
      } else {
        resolve();
      }
    })
      .catch((error) => { EventBus.getInstance().trigger(EVENTS.error, error); });
  }
Shows titles, optionally animating if duration is provided.
@param {Number} [duration=0.0]
@param {String} [title=null]
@param {String} [subtitle=null]
@param {Vec2} [titlePosition=auto]
@param {Vec2} [subtitlePosition=auto]
@returns {Promise}