SDK web sperimentale Flutter della Sala giochi di YouTube

Questa guida spiega come utilizzare l'SDK YouTube Playables con un'app web Flutter.

Configurazione Flutter

L'app web Flutter predefinita richiede alcune modifiche per funzionare come giocabile. Le seguenti modifiche sono necessarie a partire dalla versione 3.24.5 di Flutter.

Per impostazione predefinita, Flutter è configurato per caricare le risorse dalla directory principale, mentre Playables richiede che le risorse vengano caricate in base al punto di entry. Verrà visualizzato come errore 404 o come errore visualizzato nella console JavaScript con il seguente messaggio: Refused to execute script from... because its MIME type ('text/html') is not executable. Un modo per riconfigurarlo è rimuovere il seguente tag dal file index.html:

<base href="$FLUTTER_BASE_HREF">

Per impostazione predefinita, Flutter carica alcune librerie in modo dinamico anziché incorporarle nell'app. CanvasKit è un esempio di questo. Verrà visualizzato un errore nella console JavaScript con il seguente messaggio: Refused to connect to '...' because it violates the following Content Security Policy directive: "connect-src 'self' blob: data:". Flutter potrebbe anche generare log aggiuntivi in caso di errore di caricamento, ad esempio Failed to download any of the following CanvasKit URLs. Per risolvere il problema, puoi compilare l'app web con il seguente flag aggiuntivo:

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

Per impostazione predefinita, le app web Flutter caricano i caratteri in modo dinamico anziché incorporarli nell'app. L'app Flutter predefinita utilizza il carattere Roboto, che verrà visualizzato come errore nella console JavaScript con il seguente messaggio: Refused to connect to '...' because it violates the following Content Security Policy directive: "connect-src 'self' blob: data". Per risolvere il problema, puoi procedere nel seguente modo:

  • Crea una cartella denominata "fonts" nella directory principale della tua app web Flutter
  • Scarica la famiglia di caratteri Roboto da fonts.google.com.
  • Estrai i caratteri e copia Roboto-Regular.ttf nella cartella dei caratteri
  • Aggiungi la seguente riga al file pubspec.yaml della tua app web Flutter:
  fonts:
    - family: Roboto
      fonts:
       - asset: fonts/Roboto-Regular.ttf

Puoi trovare maggiori informazioni sulla configurazione dei caratteri nella documentazione di Flutter.

Integrazione dell'SDK

L'SDK YouTube Playables può essere utilizzato da un gioco Flutter web tramite un wrapper JS-interop simile a questo:

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;

Utilizzo

  1. Segui le istruzioni per configurare e inizializzare l'SDK web.
    • Modifica il file web/index.html della tua app web Flutter per includere i tag script obbligatori.
  2. Aggiungi una copia di ytgame.dart in src della tua app web Flutter.
  3. Assicurati che il pacchetto js sia aggiunto a Flutter, ad esempio eseguendo il comando flutter pub add js.
  4. Aggiungi import 'src/location/of/ytgame.dart'; dove necessario.
  5. Utilizza l'oggetto ytgame per accedere all'SDK.

Esempi

Di seguito sono riportati un paio di esempi dell'API Dart in azione.

firstFrameReady e gameReady

Affinché il gioco possa avviarsi correttamente, dovrai chiamare firstFrameReadyquando il primo frame è pronto per essere visualizzato e gameReady quando il gioco è pronto per essere utilizzato. Un modo per farlo nell'app Flutter predefinita è aggiungere le chiamate al file main.dart:

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

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

Tieni presente che questo è solo un esempio per far funzionare il gioco. Dovrai posizionare correttamente la posizione di queste chiamate per fornire l'implementazione corretta.

sendScore

Questo esempio mostra un'implementazione di sendScore(int points) in 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

Questo è un esempio di come un gioco può ascoltare gli eventi Pause provenienti da YT Playables per mettere in pausa il motore in caso di necessità:

...
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();
}

Consulta la documentazione di riferimento completa dell'API YT Playables.