Flutter Web SDK ของฟีเจอร์เล่นเกมบน YouTube รุ่นทดลอง

คู่มือนี้อธิบายวิธีใช้ 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;

การใช้งาน

  1. ทําตามวิธีการเพื่อตั้งค่าและเริ่มต้นใช้งาน Web SDK
    • แก้ไข web/index.html ของแอป Flutter Web ให้รวมแท็กสคริปต์ที่จําเป็น
  2. เพิ่มสําเนาของ ytgame.dart ใน src ของเว็บแอป Flutter
  3. ตรวจสอบว่าได้เพิ่มแพ็กเกจ js ลงใน Flutter แล้ว เช่น โดยการเรียกใช้คำสั่ง flutter pub add js
  4. เพิ่ม import 'src/location/of/ytgame.dart'; ตามต้องการ
  5. ใช้ออบเจ็กต์ 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();
}

ดูข้อมูลอ้างอิงเต็มของ YT Playables API