Bỏ qua tới nội dung
Các trang tài liệu

Nhà phát triển · Hướng dẫn

Engine và framework

Tích hợp SDK từ Phaser, canvas hay JavaScript thuần, và cách gọi SDK từ Unity WebGL, Construct, Godot qua cầu nối JavaScript.

Trên trang này

SDK là JavaScript thuần gắn ở window.WSG, nên engine nào xuất được ra HTML5 cũng dùng được. Cách làm chung: nhúng /sdk/v1/wsg-sdk.js vào index.html của bản export, rồi từ code của engine gọi sang JavaScript ở đúng các thời điểm của vòng đời game.

Phaser 3

Với template trong repo

Game làm trong repo WebSuperGames dùng WsgPhaserPlugin của package nội bộ @wsg/game-kit: plugin tự gọi loadingProgress/loadingFinished theo scene Preload/Menu, gameplayStart/gameplayStop theo scene Game, ngủ vòng lặp và tắt tiếng khi có pause, và cung cấp this.wsg.submitScore() không ném lỗi. Xem Bắt đầu nhanh, phần B.

Phaser không dùng game-kit

@wsg/game-kit không phát hành ra ngoài, nhưng tự nối Phaser với SDK chỉ cần vài dòng. Ví dụ dưới chạy được với Phaser 3 tải bằng thẻ script (phaser.min.js đặt trong bundle) hoặc qua bundler:

JavaScriptgame.js
const GAME_ID = 'my-phaser-game';
let loadingDone = false;
let userMuted = false;

class Preload extends Phaser.Scene {
  constructor() {
    super('Preload');
  }
  preload() {
    WSG.loadingStart();
    this.load.on('progress', (value) => WSG.loadingProgress(value * 100));
    // this.load.image('player', 'assets/player.png'); // chỉ dùng đường dẫn tương đối
  }
  create() {
    this.scene.start('Menu');
  }
}

class Menu extends Phaser.Scene {
  constructor() {
    super('Menu');
  }
  create() {
    if (!loadingDone) {
      loadingDone = true;
      WSG.loadingFinished(); // chỉ một lần, khi menu đầu tiên hiện ra
    }
    this.add.text(360, 640, 'Tap to play', { fontSize: '48px' }).setOrigin(0.5);
    this.input.once('pointerdown', () => this.scene.start('Game'));
  }
}

class Game extends Phaser.Scene {
  constructor() {
    super('Game');
  }
  create() {
    this.score = 0;
    this.scoreText = this.add.text(24, 24, '0', { fontSize: '40px' });
    this.input.on('pointerdown', () => this.scoreText.setText(String(++this.score)));
    this.time.delayedCall(10000, () => this.gameOver());
    WSG.gameplayStart();
    this.events.once('shutdown', () => WSG.gameplayStop());
  }
  async gameOver() {
    WSG.gameplayStop();
    this.scene.pause();
    const result = await WSG.leaderboard.submit(this.score);
    this.scene.launch('GameOver', { score: this.score, result });
  }
}

class GameOver extends Phaser.Scene {
  constructor() {
    super('GameOver');
  }
  create({ score, result }) {
    const line = result.accepted ? `#${result.rank}` : result.reason;
    this.add.text(360, 600, `${score}\n${line}`, { fontSize: '40px', align: 'center' }).setOrigin(0.5);
    this.input.once('pointerdown', async () => {
      await WSG.ads.interstitial({ placement: 'between_rounds' });
      this.scene.start('Game');
    });
  }
}

async function boot() {
  await WSG.init({ gameId: GAME_ID });
  const game = new Phaser.Game({
    type: Phaser.AUTO,
    parent: 'game',
    backgroundColor: '#171430',
    scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, width: 720, height: 1280 },
    scene: [Preload, Menu, Game, GameOver],
  });
  WSG.on('pause', () => {
    game.loop.sleep();
    game.sound.mute = true;
  });
  WSG.on('resume', () => {
    game.loop.wake();
    game.sound.mute = userMuted;
  });
  WSG.on('audio', (enabled) => {
    game.sound.mute = !enabled || userMuted;
  });
}

boot();
  • game.loop.sleep() dừng hẳn vòng lặp (đồng hồ, tween, vật lý); game.loop.wake() chạy lại.
  • Menu có thể được tạo nhiều lần, nên dùng cờ để loadingFinished() chỉ gọi một lần.
  • Với Vite, đặt base: './' và giữ thẻ <script src="/sdk/v1/wsg-sdk.js"> trong index.html; Vite sẽ cảnh báo không gộp được thẻ script này, đó là điều mong muốn.

Canvas và JavaScript thuần

Ví dụ đầy đủ ở Bắt đầu nhanh. Những điểm cần giữ:

JavaScript
let paused = false;
let last = 0;

WSG.on('pause', () => (paused = true));
WSG.on('resume', () => {
  paused = false;
  last = 0; // bỏ qua quãng đã dừng
});

function frame(ts) {
  const dt = last ? Math.min(ts - last, 100) : 0; // kẹp dt để không "nhảy cóc" sau khi máy lag
  last = ts;
  if (!paused) update(dt);
  render();
  requestAnimationFrame(frame);
}

Engine khác qua JavaScript

Với Unity, Construct, Godot hay engine khác, hãy giữ phần nói chuyện với SDK trong JavaScript và chỉ để engine gọi vài hàm đơn giản. Ba việc cần làm trong mọi trường hợp:

  1. Thêm <script src="/sdk/v1/wsg-sdk.js"></script> vào index.html của bản export, trước script của engine.
  2. Gọi WSG.init() sớm, loadingFinished() khi màn hình đầu tiên sẵn sàng, gameplayStart()/gameplayStop() theo lượt chơi, leaderboard.submit() khi hết lượt.
  3. Chuyển pause/resume/audio của SDK thành lệnh dừng vòng lặp và tắt tiếng của engine.

Kiểm tra kích thước bản export: file .wasm và dữ liệu của engine thường chiếm phần lớn bundle, mà giới hạn là 20 MB và 300 file (tính theo kích thước chưa nén). Tắt các module, ngôn ngữ, font không dùng; nếu vẫn vượt, trao đổi với đội vận hành trước khi nộp.

Lưu ý

Tên hàm, tuỳ chọn build và cách export của mỗi engine thay đổi theo phiên bản. Các đoạn dưới đây chỉ minh hoạ cách gọi qua lại với JavaScript; đối chiếu với tài liệu của bản engine bạn dùng.

Unity WebGL

  • C# gọi JavaScript qua một plugin .jslib (đặt trong thư mục Plugins của dự án) và các hàm khai báo [DllImport("__Internal")]. Chuỗi từ C# đến JavaScript dưới dạng con trỏ, đọc bằng UTF8ToString().
  • JavaScript gọi ngược về C# bằng SendMessage("TênGameObject", "TênHàm", giáTrị).
  • Thêm thẻ script SDK vào index.html của WebGL template (template tuỳ chỉnh) hoặc sửa index.html sau khi build.
  • Origin game không gửi Content-Encoding, nên build với nén Brotli/Gzip sẽ không tải được trừ khi bật cơ chế giải nén dự phòng của Unity; đơn giản nhất là tắt nén khi build cho portal.
JavaScriptAssets/Plugins/WebGL/WsgBridge.jslib
mergeInto(LibraryManager.library, {
  WsgInit: function (gameIdPtr) {
    WSG.on('pause', function () {
      SendMessage('WsgBridge', 'OnWsgPause');
    });
    WSG.on('resume', function () {
      SendMessage('WsgBridge', 'OnWsgResume');
    });
    WSG.init({ gameId: UTF8ToString(gameIdPtr) }).then(function (ctx) {
      SendMessage('WsgBridge', 'OnWsgReady', ctx.environment);
    });
  },
  WsgLoadingFinished: function () {
    WSG.loadingFinished();
  },
  WsgGameplayStart: function () {
    WSG.gameplayStart();
  },
  WsgGameplayStop: function () {
    WSG.gameplayStop();
  },
  WsgSubmitScore: function (score) {
    WSG.leaderboard.submit(score).then(function (result) {
      SendMessage('WsgBridge', 'OnWsgScore', JSON.stringify(result));
    });
  },
  WsgShowRewarded: function () {
    WSG.ads.rewarded({ placement: 'revive' }).then(function (result) {
      SendMessage('WsgBridge', 'OnWsgRewarded', result.rewarded === true ? 'true' : 'false');
    });
  },
});
C#Assets/Scripts/WsgBridge.cs
using System.Runtime.InteropServices;
using UnityEngine;

// Gắn vào một GameObject tên đúng "WsgBridge" trong scene đầu tiên.
public class WsgBridge : MonoBehaviour
{
    [DllImport("__Internal")] private static extern void WsgInit(string gameId);
    [DllImport("__Internal")] private static extern void WsgLoadingFinished();
    [DllImport("__Internal")] private static extern void WsgGameplayStart();
    [DllImport("__Internal")] private static extern void WsgGameplayStop();
    [DllImport("__Internal")] private static extern void WsgSubmitScore(int score);
    [DllImport("__Internal")] private static extern void WsgShowRewarded();

    void Start()
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        WsgInit("my-unity-game");
#endif
    }

    // Các hàm dưới được JavaScript gọi bằng SendMessage
    void OnWsgReady(string environment) { WsgLoadingFinished(); }
    void OnWsgPause() { Time.timeScale = 0f; AudioListener.pause = true; }
    void OnWsgResume() { Time.timeScale = 1f; AudioListener.pause = false; }
    void OnWsgScore(string resultJson) { Debug.Log("Kết quả nộp điểm: " + resultJson); }
    void OnWsgRewarded(string rewarded) { if (rewarded == "true") { /* cấp thưởng */ } }
}

Gọi WsgGameplayStart() khi người chơi bắt đầu một lượt, WsgGameplayStop() rồi WsgSubmitScore(score) khi hết lượt. Bản build chạy thử trong Unity Editor không có WSG, nên bọc lời gọi bằng #if UNITY_WEBGL && !UNITY_EDITOR.

Construct 3

  • Xuất bằng Web (HTML5), rồi thêm thẻ script SDK vào index.html của bản xuất, trước các script của Construct.
  • Script trong Construct chạy trong Web Worker khi thuộc tính dự án Use worker đang bật, và khi đó không thấy window.WSG. Đặt Use worker thành No để script gọi được SDK.
  • Gọi SDK từ khối script trong event sheet; biến toàn cục của dự án đọc qua runtime.globalVars:
JavaScriptScript trong event sheet
// Khi bắt đầu lượt
WSG.gameplayStart();

// Khi game over (Score là biến toàn cục của dự án)
WSG.gameplayStop();
WSG.leaderboard.submit(Math.floor(runtime.globalVars.Score)).then((result) => {
  runtime.globalVars.Rank = result.rank ?? 0;
});

Gọi WSG.init() một lần khi dự án khởi động (ví dụ trong script chạy ở layout đầu tiên) và WSG.loadingFinished() khi màn hình đầu tiên hiện ra. Với pause/resume, đặt time scale của dự án về 0 rồi trả lại 1 và tắt tiếng bằng các hành động tương ứng.

Godot

  • Xuất bằng preset Web; thêm thẻ script SDK qua tuỳ chọn chèn mã vào <head> của preset hoặc bằng HTML shell tuỳ chỉnh.
  • Origin game không gửi các header cross-origin isolation (COOP/COEP), nên bản export cần SharedArrayBuffer (bật hỗ trợ thread) không chạy được. Dùng bản export không thread (Godot 4.3 trở lên có tuỳ chọn này) hoặc Godot 3.
  • Godot 4 nói chuyện với JavaScript qua singleton JavaScriptBridge (Godot 3 là JavaScript): eval() chạy một đoạn mã, get_interface() lấy object JavaScript, create_callback() tạo hàm để JavaScript gọi lại. Giữ tham chiếu tới callback, nếu không nó bị thu hồi.
GDScriptwsg.gd
extends Node

var _on_result  # giữ tham chiếu tới callback

func _ready() -> void:
	if not OS.has_feature("web"):
		return
	JavaScriptBridge.eval("WSG.init({ gameId: 'my-godot-game' }).then(() => WSG.loadingFinished())", true)

func round_started() -> void:
	JavaScriptBridge.eval("WSG.gameplayStart()", true)

func round_finished(score: int) -> void:
	var wsg = JavaScriptBridge.get_interface("WSG")
	wsg.gameplayStop()
	_on_result = JavaScriptBridge.create_callback(_on_submit_result)
	wsg.leaderboard.submit(score).then(_on_result)

func _on_submit_result(args: Array) -> void:
	var result = args[0]
	print("accepted: ", result.accepted, " rank: ", result.rank)