本指南介绍了如何将 YouTube Playables SDK 与 Flutter Web 应用搭配使用。
Flutter 配置
默认的 Flutter Web 应用需要进行一些更改才能作为可玩内容运行。从 Flutter 版本 3.24.5 开始,必须进行以下更改。
默认情况下,Flutter 配置为从根目录加载资源,而 Playables 要求相对于入口点加载资源。这将显示为 404 错误,或者可能显示为 JavaScript 控制台中显示的错误,内容为:Refused to execute script
from... because its MIME type ('text/html') is not executable.
重新配置此问题的方法之一是从 index.html 文件中移除以下代码:
<base href="$FLUTTER_BASE_HREF">
默认情况下,Flutter 会动态加载某些库,而不是将其嵌入到应用中。CanvasKit 就是一个例子。这将在 JavaScript 控制台中显示为以下错误:Refused to connect to '...' because it
violates the following Content Security Policy directive: "connect-src 'self'
blob: data:".
在加载失败时,Flutter 可能还会输出其他日志,例如 Failed to download any of the following CanvasKit URLs
。如需解决此问题,您可以使用以下额外标志构建 Web 应用:
$ flutter build web --no-web-resources-cdn
默认情况下,Flutter Web 应用会动态加载字体,而不是将其嵌入到应用中。默认的 Flutter 应用使用 Roboto 字体,这将在 JavaScript 控制台中显示以下错误:Refused to connect to '...'
because it violates the following Content Security Policy directive:
"connect-src 'self' blob: data".
如需解决此问题,您可以执行以下步骤:
- 在 Flutter Web 应用的根目录中创建一个名为“fonts”的文件夹
- 从 fonts.google.com 下载 Roboto 字体系列。
- 解压缩字体,然后将 Roboto-Regular.ttf 复制到字体文件夹中
- 将以下行添加到 Flutter Web 应用的 pubspec.yaml 中:
fonts:
- family: Roboto
fonts:
- asset: fonts/Roboto-Regular.ttf
如需详细了解字体配置,请参阅 Flutter 文档。
SDK 集成
您可以通过类似于以下 JS 互操作封装容器在 Flutter Web 游戏中使用 YouTube Playables SDK:
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;
用法
- 按照说明设置和初始化 Web SDK。
- 修改 Flutter Web 应用的
web/index.html
,使其包含所需的脚本标记。
- 修改 Flutter Web 应用的
- 在 Flutter Web 应用的
src
中添加ytgame.dart
的副本。 - 确保已将 js 软件包添加到 Flutter,例如通过运行
flutter pub add js
命令。 - 根据需要添加
import 'src/location/of/ytgame.dart';
。 - 使用
ytgame
对象访问 SDK。
示例
下面列出了一些 Dart API 的实际运作示例。
firstFrameReady
和gameReady
为了让游戏能够正确启动,您需要在第一帧准备好显示时调用 firstFrameReady
,并在游戏准备好接受互动时调用 gameReady
。在默认的 Flutter 应用中执行此操作的一种方法是将调用添加到 main.dart 文件中:
...
import 'src/ytgame.dart';
...
void main() {
ytgame.game.firstFrameReady();
ytgame.game.gameReady();
runApp(const MyApp());
}
请注意,这只是一个用于让游戏运行的示例,您需要正确放置这些调用的位置,以便提供正确的实现。
sendScore
以下示例展示了 Dart 中的 sendScore(int points)
实现:
...
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
以下示例展示了游戏如何监听来自 YT Playables 的 Pause
事件,以便在需要时暂停其引擎:
...
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();
}
请参阅完整的 YT Playables API 参考文档。