Version expérimentale du SDK Web Flutter pour Jeux intégrés

Ce guide explique comment utiliser le SDK YouTube Playables avec une application Web Flutter.

Configuration Flutter

L'application Web Flutter par défaut nécessite quelques modifications pour fonctionner en tant que jeu intégré. Les modifications suivantes sont requises à partir de la version 3.24.5 de Flutter.

Par défaut, Flutter est configuré pour charger les ressources à partir du répertoire racine, tandis que les éléments Playables nécessitent que les ressources soient chargées par rapport au point d'entrée. Cela s'affichera sous la forme d'une erreur 404 ou d'une erreur qui s'affiche dans la console JavaScript et qui se lit comme suit: Refused to execute script from... because its MIME type ('text/html') is not executable. Pour reconfigurer cela, vous pouvez supprimer la balise suivante du fichier index.html:

<base href="$FLUTTER_BASE_HREF">

Par défaut, Flutter charge certaines bibliothèques de manière dynamique au lieu de les intégrer à l'application. CanvasKit en est un exemple. Cela s'affichera sous forme d'erreur dans la console JavaScript avec le message suivant: Refused to connect to '...' because it violates the following Content Security Policy directive: "connect-src 'self' blob: data:". Flutter peut également générer des journaux supplémentaires en cas d'échec du chargement, par exemple Failed to download any of the following CanvasKit URLs. Pour résoudre ce problème, vous pouvez créer votre application Web avec l'option supplémentaire suivante:

$ flutter build web --no-web-resources-cdn

Par défaut, les applications Web Flutter chargent les polices de manière dynamique au lieu de les intégrer à l'application. L'application Flutter par défaut utilise la police Roboto, ce qui s'affiche comme une erreur dans la console JavaScript qui se lit comme suit: Refused to connect to '...' because it violates the following Content Security Policy directive: "connect-src 'self' blob: data". Pour résoudre ce problème, procédez comme suit:

  • Créez un dossier nommé "fonts" dans le répertoire racine de votre application Web Flutter.
  • Téléchargez la famille de polices Roboto sur fonts.google.com.
  • Extrayez les polices et copiez Roboto-Regular.ttf dans le dossier de polices.
  • Ajoutez la ligne suivante au fichier pubspec.yaml de votre application Web Flutter:
  fonts:
    - family: Roboto
      fonts:
       - asset: fonts/Roboto-Regular.ttf

Pour en savoir plus sur la configuration des polices, consultez la documentation Flutter.

Intégration d'un SDK

Le SDK YouTube Playables peut être utilisé à partir d'un jeu Web Flutter via un wrapper d'interopérabilité JavaScript semblable à celui-ci:

ytgame.dart

// Copyright 2023 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

@JS()
library ytgame_api;

import 'package:js/js.dart';
import 'package:js/js_util.dart' as js_util;

enum PlayablesErrorType {
  unknown('UNKNOWN'),
  apiUnavailable('API_UNAVAILABLE'),
  invalidParams('INVALID_PARAMS'),
  sizeLimitExceeded('SIZE_LIMIT_EXCEEDED');

  const PlayablesErrorType(String type) : _type = type;
  final String _type;

  @override
  String toString() => _type;

  static PlayablesErrorType fromString(String errorType) {
    return values.firstWhere(
      (element) => element._type == errorType,
    );
  }
}

@JS()
@staticInterop
class PlayablesError {}

extension PlayablesErrorExtension on PlayablesError {
  @JS('errorType')
  external String get _errorType;
  PlayablesErrorType get errorType {
    return PlayablesErrorType.fromString(_errorType);
  }
}

@JS()
@anonymous
@staticInterop
class PlayablesScore {
  external factory PlayablesScore({
    required num value,
  });
}

extension PlayablesScoreExtension on PlayablesScore {
  external num get value;
}

// engagement
@JS()
@staticInterop
class PlayablesEngagement {}

extension PlayablesEngagementExtension on PlayablesEngagement {
  @JS('sendScore')
  external Object _sendScore(PlayablesScore score);
  Future<void> sendScore(PlayablesScore score) {
    return js_util.promiseToFuture(_sendScore(score));
  }
}

// game
@JS()
@staticInterop
class PlayablesGame {}

extension PlayablesGameExtension on PlayablesGame {
  external void firstFrameReady();
  external void gameReady();

  @JS('loadData')
  external Object _loadData();
  Future<String?> loadData() {
    return js_util.promiseToFuture<String?>(_loadData());
  }

  @JS('saveData')
  external Object _saveData(String? data);
  Future<void> saveData(String? data) {
    return js_util.promiseToFuture<void>(_saveData(data));
  }
}

// health
@JS()
@staticInterop
class PlayablesHealth {}

extension PlayablesHealthExtension on PlayablesHealth {
  external void logError();
  external void logWarning();
}

// system
typedef PlayablesUnsetCallback = void Function();
typedef PlayablesOnAudioEnabledChange = void Function(bool isAudioEnabled);
typedef PlayablesOnPause = void Function();
typedef PlayablesOnResume = void Function();

@JS()
@staticInterop
class PlayablesSystem {}

extension PlayablesSystemExtension on PlayablesSystem {
  external bool isAudioEnabled();

  @JS('onAudioEnabledChange')
  external PlayablesUnsetCallback _onAudioEnabledChange(
      PlayablesOnAudioEnabledChange callback);
  PlayablesUnsetCallback onAudioEnabledChange(
      PlayablesOnAudioEnabledChange callback) {
    return _onAudioEnabledChange(allowInterop(callback));
  }

  @JS('onPause')
  external PlayablesUnsetCallback _onPause(PlayablesOnPause callback);
  PlayablesUnsetCallback onPause(PlayablesOnPause callback) {
    return _onPause(allowInterop(callback));
  }

  @JS('onResume')
  external PlayablesUnsetCallback _onResume(PlayablesOnResume callback);
  PlayablesUnsetCallback onResume(PlayablesOnResume callback) {
    return _onResume(allowInterop(callback));
  }
}

@JS()
@staticInterop
class Playables {}

extension PlayablesExtension on Playables {
  @JS('SDK_VERSION')
  external String get sdkVersion;
  external PlayablesEngagement get engagement;
  external PlayablesGame get game;
  external PlayablesHealth get health;
  external PlayablesSystem get system;
}

@JS('ytgame')
external Playables get ytgame;

Utilisation

  1. Suivez les instructions pour configurer et initialiser le SDK Web.
    • Modifiez le web/index.html de votre application Web Flutter pour inclure les balises de script requises.
  2. Ajoutez une copie de ytgame.dart dans le src de votre application Web Flutter.
  3. Assurez-vous que le package js est ajouté à Flutter, par exemple en exécutant la commande flutter pub add js.
  4. Ajoutez import 'src/location/of/ytgame.dart'; si nécessaire.
  5. Utilisez l'objet ytgame pour accéder au SDK.

Exemples

Vous trouverez ci-dessous quelques exemples de l'API Dart en action.

firstFrameReady et gameReady

Pour que votre jeu démarre correctement, vous devez appeler firstFrameReady lorsque le premier frame est prêt à être affiché et gameReady lorsque le jeu est prêt à être utilisé. Pour ce faire dans l'application Flutter par défaut, vous pouvez ajouter les appels au fichier main.dart:

...
import 'src/ytgame.dart';
...

void main() {
  ytgame.game.firstFrameReady();
  ytgame.game.gameReady();
  runApp(const MyApp());
}

Notez qu'il ne s'agit que d'un exemple pour lancer votre jeu. Vous devrez placer correctement l'emplacement de ces appels afin de fournir l'implémentation appropriée.

sendScore

Cet exemple montre une implémentation de sendScore(int points) en Dart:

...
import 'src/ytgame.dart';
...

/// Sends [points] as score to YT Playables
Future<void> sendScore(int points) async {
  // Create a score object...
  final PlayablesScore score = PlayablesScore(
    value: points,
  );
  // Submit the score to ytgame
  await ytgame.engagement.sendScore(score);
}

onPause

Voici un exemple de la façon dont un jeu peut écouter les événements Pause provenant de YT Playables pour suspendre son moteur si nécessaire:

...
import 'src/ytgame.dart';
...

/// The instance of your game.
late Game myGame;

/// Callback to stop listening for Pause events (cleanup).
PlayablesUnsetCallback? _unsetOnPause;
...

/// Sets a [callback] to respond to Pause events.
void registerOnPauseListener(PlayablesOnPause callback) {
  _unsetOnPause = ytgame.system.onPause(callback);
}

/// Stops listening to Pause events, and clears [_unsetOnPause].
void unsetOnPauseListener() {
  if (_unsetOnPause != null) {
    _unsetOnPause!();
    _unsetOnPause = null;
  }
}

...

/// Initialization for your game
void initGame() {
  ...
  myGame = ...
  registerOnPauseListener(_onPause);
}

void _onPause() {
  // This is called when a Pause event is received from YT Playables.
  myGame.pauseEngine();
}

/// Teardown for your game
void disposeGame() {
  ...
  unsetOnPauseListener();
}

Consultez la documentation de référence complète de l'API YT Playables.