鳥小屋.txt

主に自作ゲームをつくったりしているよ。制作に関することやそうじゃないことのごった煮ブログ

Electron + RPGツクールMZ なゲームで Steam の実績に対応する

【注意】こんなタイトルですが、真面目にツクールを使ってる多くの人にとっては参考になりません

Steam には実績機能があります。正確にはこの辺りの機能には、

  • ゲームのスコア的な内部数値を格納できる Stats
  • PS系で言うところのトロフィー的な Achievements

の2つがありますが、今回の話は Achievements のほうです。

せっかく Steam でリリースするからには実績機能に対応したいため、いろいろやった記事です。

前提

Electron

RPGツクールMZのゲームは内部的にはHTML5なブラウザゲームとして動作します。そのため Steam でリリースをするためには、まず何らかの方法で Windows 等向けのアプリケーションにする必要があります

RPGツクール側の標準では NW.js が使われています。RPGツクール上でテストプレイを押したときに開くのも、この NW.js です。

しかし一般的にはこういったWebアプリケーションのアプリ化には Electron が使われることが多く、インターネットで得られる記事やツールも Electron を対象としたものが多いと思われます。そのため、僕も NW.js ではなく Electron を使うことにしました。

なお、RPGツクールMZのゲームを Electron を使ってアプリ化する方法は、トリアコンタンさんが詳しい記事を書いているためそちらを見ると良いです。

Steamworks.js

Steamの機能を使うためには、Steamworks SDK の機能を呼び出す必要があります。しかし Steamworks SDK は C++ のコードのため、JavaScript の世界……というか node.js の世界からそのまま呼び出すことはできません。

以前は node.js から Steamworks SDK を呼び出す方法として Greenworks というライブラリが有名でした。ただ結構使うのが難しかった印象が強いです……前の僕は挫折した><;
また、現在は Greenworks は開発が停止状態になっており、他のライブラリの利用が推奨されています。

Steamworks.js は Greenworks のページから移行先の1つとして紹介されているライブラリです。 Rust で書かれたブリッジのライブラリになっており、とてもシンプルでわかりやすい印象があったため使うことにしました。

Steamworks APIを呼び出す

プロジェクトのフォルダ構成

過去の記事 でも触れましたが、僕のゲームのプロジェクトのフォルダ構成はざっくり以下のようになっています。

├── app/     …… RPGツクールのプロジェクトフォルダ
│   ├── audio/
│   (略)
│   └── game.rmmzproject
├── android/ …… Capacitor Android の作成するフォルダ
├── ios/     …… Capacitor iOS の作成するフォルダ
├── windows/ …… Electron 用のフォルダ
│   ├── main/     …… Electron用のJavaScriptが入るフォルダ
│   ├── public/   …… ゲームが入るフォルダ(後述) 
│   └── package.json 
└── package.json

※説明に不要な部分は割愛

Capacitor

僕はスマホアプリも作りたいので Capacitor を使っています。 Capacitor はプロジェクトフォルダの直下にプラットフォーム名(android / ios)のフォルダを作るため、それに習って Electron 用のフォルダに windows という名前をつけています。

Capacitor では cap sync というコマンドを実行すると、 android / ios のそれぞれのフォルダの中にゲームのデータがコピーされるようになっています。そのため、同じように windows/public のフォルダの中にもゲームのデータが自動的にコピーされるようにしています。

Steamworks.js を組み込む

Steamworks.js の README では BrowserWindow を nodeIntegration: true にする方法が記載されていますが、あまり nodeIntegration を有効にはしたくないため、以下のようにして組み込みます。

// main.js

const { app, BrowserWindow, ipcMain } = require('electron');
const steamworks = require('steamworks.js');

// Steamで発行される App ID
const STEAM_APP_ID = xxxxxxx;

let browserWindow = null;

const createWindow = async () => {
  if (browserWindow) return;

  const win = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,  // 強い意志で false にする!
      contextIsolation: true,   // 代わりに contextIsolation は使います
    },
    show: false,
  });

  // ウィンドウサイズをいい感じに
  win.removeMenu();
  win.setContentSize(1280, 720);
  win.setMinimumSize(...win.getSize());

  // ページを読み込む
  await win.loadFile(path.join(__dirname, '..', 'public', 'index.html'));
  win.show();

  browserWindow = win;
};

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

(async () => {
  // Steam経由起動でない場合はゲームを終了する
  if (steamworks.restartAppIfNecessary(STEAM_APP_ID)) {
    app.exit();
    return;
  }

  // Steamworksの初期化&オーバーレイメニューの有効化
  const steamClient = steamworks.init(STEAM_APP_ID);
  steamworks.electronEnableSteamOverlay();

  await app.whenReady();

  // ゲーム内から実績獲得の呼び出し用の口を作る
  ipcMain.handle('activate-steam-achievement', (_event, name) => {
    steamClient.achievement.activate(name);
  });

  // ウィンドウを作る
  await createWindow();
})();

ポイントは steamworks.restartAppIfNecessary steamworks.electronEnableSteamOverlay() を呼ぶタイミングです。

この2つはアプリケーションが立ち上がってすぐに呼び出さないとうまく動かない場合があります。そのため BrowserWindow の作成よりも先に呼び出すようにします。

また、 ゲーム内から呼び出すためのコードを preload.js に追加します。以下は window.steam.activateAchievement(実績名) で先程用意したSteam実績を追加するための口を呼び出すようにしています。

// preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('steam', {
  activateAchievement(name) {
    return ipcRenderer.invoke('activate-steam-achievement', name);
  },
});

あとはゲーム内から良きタイミングで window.steam.activateAchievement(実績名) を呼ぶだけです!

その他

パッケージ化するときの注意

Electronのアプリをパッケージ化する際に、ASAR アーカイブ を使用する場合は1点注意が必要です。

Steamworks.js の中には DLL などのバイナリを含むため、それらがASARアーカイブの中に含まれてしまうと正常に動作しなくなってしまいます。そのため node_modules/steamworks.js/dist/ 以下のファイルは ASAR アーカイブに含まないようにします。

僕はパッケージ化に electron-forge を使っているので、 unpackDir の設定に steamworks.js のディレクトリを指定しました。もしくは Auto Unpack Native Modules Plugin でもできるのかも? 試してないけど。

おまけ:実績プラグインと連携する

Torigoya_Achievement2 というRPGツクールに実績機能を追加するプラグインがありますね(ダイマ)。

Torigoya_Achievement2 には「実績獲得時に実行される処理を追加する」機能があるため、以下のようなアドオンプラグインを用意すると、実績獲得時に自動的に Steam 上の実績も獲得状態にできます。

(() => {
  Torigoya.Achievement2.Manager.on(({achievement}) => {
    if (!window.steam) return;
    window.steam.activateAchievement(achievement.key);
  });
})();

なんて便利なプラグインなんだ!!!!!!1111(自画自賛)

できた!

左上と右下から実績が出てくるゲーム。

僕は他に少し色々とやりたいことがあるので実際はもうちょっとややこしいコードを書いていますが、大筋はこんな感じになっています。

Steamworks.js はとてもシンプルで扱いやすいので、Steamの機能をElectron + HTML5ゲームから呼び出すときには手軽で便利です。特に Steamworks SDK のビルド環境なども用意しなくて良いため、今後もお世話になりたいですね。


昔、Steamで公開した『天翔と剣のウィッチクラフト』というゲームはこの辺りがうまく解決できず、Steam実績に未対応だったりしました。少しずつ世界は便利になっていく。