คู่มือนี้อธิบายวิธีใช้ YouTube Playables SDK กับเว็บแอป Flutter
การกําหนดค่า Flutter
เว็บแอป Flutter เริ่มต้นต้องมีการเปลี่ยนแปลงบางอย่างจึงจะทํางานเป็น Playable ได้ คุณต้องทําการเปลี่ยนแปลงต่อไปนี้ใน 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
หากต้องการแก้ไขปัญหานี้ คุณสามารถสร้างเว็บแอปด้วย Flag เพิ่มเติมต่อไปนี้
$ flutter build web --no-web-resources-cdn
โดยค่าเริ่มต้น เว็บแอป Flutter จะโหลดแบบอักษรแบบไดนามิกแทนที่จะฝังไว้ในแอป แอป Flutter เริ่มต้นใช้แบบอักษร Roboto และข้อผิดพลาดนี้จะแสดงในคอนโซล JavaScript ดังต่อไปนี้ Refused to connect to '...'
because it violates the following Content Security Policy directive:
"connect-src 'self' blob: data".
วิธีแก้ไขมีดังนี้
- สร้างโฟลเดอร์ชื่อ "fonts" ในรูทของเว็บแอป Flutter
- ดาวน์โหลดชุดแบบอักษร Roboto จาก fonts.google.com
- แตกไฟล์แบบอักษรและคัดลอก Roboto-Regular.ttf ลงในโฟลเดอร์แบบอักษร
- เพิ่มบรรทัดต่อไปนี้ลงใน pubspec.yaml ของเว็บแอป Flutter
fonts:
- family: Roboto
fonts:
- asset: fonts/Roboto-Regular.ttf
ดูข้อมูลเพิ่มเติมเกี่ยวกับการกำหนดค่าแบบอักษรได้ในเอกสารประกอบของ Flutter
การผสานรวม SDK
คุณใช้ YouTube Playables SDK จากเกมบนเว็บ Flutter ได้ผ่าน Wrapper การทํางานร่วมกันของ JS ที่คล้ายกับตัวอย่างนี้
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
- แก้ไข
web/index.html
ของแอป Flutter Web ให้รวมแท็กสคริปต์ที่จําเป็น
- แก้ไข
- เพิ่มสําเนาของ
ytgame.dart
ในsrc
ของเว็บแอป Flutter - ตรวจสอบว่าได้เพิ่มแพ็กเกจ 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
ตัวอย่างนี้แสดงการใช้งาน sendScore(int points)
ใน 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
ต่อไปนี้เป็นตัวอย่างวิธีที่เกมสามารถฟังเหตุการณ์ Pause
ที่มาจาก YT
Playables เพื่อหยุดการทำงานของเครื่องยนต์ชั่วคราวเมื่อจำเป็น
...
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();
}