실험용 YouTube 게임 룸 Flutter 웹 SDK

이 가이드에서는 Flutter 웹 앱에서 YouTube Playables SDK를 사용하는 방법을 설명합니다.

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이 그 예입니다. 이는 자바스크립트 콘솔에 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와 같은 추가 로그를 출력할 수도 있습니다. 이 문제를 해결하려면 다음 추가 플래그를 사용하여 웹 앱을 빌드하면 됩니다.

$ 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". 오류로 표시됩니다. 이 문제를 해결하려면 다음 단계를 따르세요.

  • Flutter 웹 앱의 루트에 'fonts'라는 폴더를 만듭니다.
  • fonts.google.com에서 Roboto 글꼴 모음을 다운로드합니다.
  • 글꼴을 추출하고 Roboto-Regular.ttf를 글꼴 폴더에 복사합니다.
  • flutter 웹 앱의 pubspec.yaml에 다음 줄을 추가합니다.
  fonts:
    - family: Roboto
      fonts:
       - asset: fonts/Roboto-Regular.ttf

글꼴 구성에 관한 자세한 내용은 Flutter 문서를 참고하세요.

SDK 통합

YouTube Playables SDK는 다음과 유사한 JS 상호 운용성 래퍼를 통해 Flutter 웹 게임에서 사용할 수 있습니다.

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. 안내에 따라 웹 SDK를 설정하고 초기화합니다.
    • 필요한 스크립트 태그를 포함하도록 Flutter 웹 앱의 web/index.html를 수정합니다.
  2. Flutter 웹 앱의 srcytgame.dart 사본을 추가합니다.
  3. flutter pub add js 명령어를 실행하는 등 js 패키지가 Flutter에 추가되었는지 확인합니다.
  4. 필요한 위치에 import 'src/location/of/ytgame.dart';를 추가합니다.
  5. ytgame 객체를 사용하여 SDK에 액세스합니다.

다음은 Dart API가 작동하는 몇 가지 예입니다.

firstFrameReadygameReady

게임이 올바르게 시작되려면 첫 번째 프레임을 표시할 준비가 되면 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 참조를 확인하세요.