Open4

Discord ActivityをUnityで作る(備忘録)

Hirosuke KayabaHirosuke Kayaba

DiscordでUnity(WebGLビルド)が動くまで

基本環境

Windows 10
WSL2 (Ubuntu)
Unity2023.2.20f1(Win)
React Unity WebGL
VSCode

Unityビルド

Unity環境構築

Unity Hubを使用。Unityエンジニアを対象として記事のため省略。
Unity2022LTS以降なら恐らく問題ないはず。

Unityビルド

先駆者を参考に作成。チュートリアル時はもう少し変える

環境構築

WSLの環境構築

WSLのインストールは下記参照。今回はUbuntuを使用。
https://www.tohoho-web.com/ex/wsl.html

VSCodeを使用して基本作業するので下記を参考にRemote Development拡張機能パックをインストール
https://learn.microsoft.com/ja-jp/windows/wsl/tutorials/wsl-vscode
下記画像のようにUbuntuをWSLから操作できるようになればOK
WSL環境構築

node.jsの環境構築

node.jsのバージョンを切り替える可能性を考慮してnvmを使用(rbenv的なヤツ)
https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
nvm --version
nvm install --lts
node -v

Cloudflareの環境構築

以下でシステム種別を確認(筆者はx86_64)

uname -m

Cloudflare Zero Trustダウンロードページから、対象のシステム種別の.debのダウンロードリンクをコピーしてwgetでダウンロード。
下記はその例

wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
cloudflared --version

ブラウザでの実行

Reactプロジェクトの作成

Viteでのプロジェクト作成

mkdir discord-unity-app
cd discord-unity-app
npm create vite@latest client -- --template react-swc-ts
cd client
npm i
npm i react-unity-webgl
npm i @discord/embedded-app-sdk

追記予定:この時点でのフォルダ構造

Unity WebGLビルドの移動

以下のフォルダ構造になるように

discord-unity-app/client/public/
├── Build/
├── TemplateData/
├── index.html
└── vite.svg #vite生成時に生成されたアイコン

Reactプロジェクトの編集

  1. discord-unity-app/client/src/下にUnityComponent.tsxという名前でコンポーネントを作成
  2. UnityComponent.tsxに下記を記述
UnityComponent.tsx
import { useEffect } from "react";
import { Unity, useUnityContext } from "react-unity-webgl";

interface UnityCompoonentProps {
  userName: string;
}

const UnityComponent = (props: UnityCompoonentProps) => {
  // UnityContextを準備、表示するUniyアプリを指定
  const { unityProvider, sendMessage, isLoaded } = useUnityContext({
    loaderUrl: "Build/DiscordApp.loader.js",
    dataUrl: "Build/DiscordApp.data",
    frameworkUrl: "Build/DiscordApp.framework.js",
    codeUrl: "Build/DiscordApp.wasm",
  });

  // useEffectの対象にisLoadedを含めない場合
  // 環境によってはsendMessageが動作しない問題がある
  useEffect(() => {
    if (isLoaded) {
      // Unityアプリに対してメッセージを送信
      // sendMessage("オブジェクト名", "関数名", 引数)
      sendMessage("Canvas", "SetText", props.userName);
    }
  }, [isLoaded]);

  return <Unity id="unity-canvas" unityProvider={unityProvider} />;
};

export default UnityComponent;
  1. client/src/App.tsxを下記のように変更
App.tsx
import UnityComponent from "./UnityComponent";

function App() {
  return <UnityComponent userName={"test"} />;
}

export default App;
  1. client/src/index.cssに下記のように変更
index.css
body {
  margin: 0;
  overflow: hidden;
}

#unity-canvas {
  width: 100vw;
  height: 100vh;
}
  1. clientディレクトリで下記を実行
npm install
npm dev
  1. http://localhost:5173/など、表示されたURLにアクセス。
    ブラウザで実行出来れば成功
    ブラウザ実行

Discordアプリの作成

チームの作成

Discordの開発者ポータルへアクセス
https://discord.com/developers/applications
アプリ単位でテストユーザーを最大50人まで追加できるが、チームを作成しそのチームのアプリとした方がユーザー管理/アプリの共同管理が楽なのでチームを作成することを推奨。

複数人でのテストのため、この段階で自分のサブアカウント、あるいは協力者を招待しておくのが吉(RoleはRead Onlyで良い)
チーム招待

アプリケーションの作成

Applications->New Application
チーム選択するのを忘れないように気を付ける
チーム選択
続いて、アクティビティの設定をする。
サイドバーから「ACTIVITIES」→「Getting Started」をクリック。

認証情報(OAuth2)の確認

認証形式には OAuth2 が使われている。
サイドバーから「OAuth2」をクリックし、認証情報画面を開く。

  1. Redirects->Add Redirectでhttp://localhostを記入
  2. Client ID, Client Secretをコピー、メモに残しておく
    OAuth2

アクティビティでUnityを実行する

アクティビティ実行のための事前準備

  1. 下記リポジトリをクローンする
    https://github.com/discord/getting-started-activity
  2. クローンしたリポジトリから、serverディレクトリとexample.envをdiscord-unity-app/に配置する
  3. example.envを.envに名前変更する
    ここまで行うと以下のようなフォルダ構造になっている
discord-unity-app
├── client/
├── server/
└── .env
  1. .envに先ほどメモしたClient ID, Client Secretを記載する。
VITE_DISCORD_CLIENT_ID=<client_id>
DISCORD_CLIENT_SECRET=<client_secret>

アクティビティのバックエンドの起動

  1. アクティビティサーバー用のターミナルを新規で立ち上げる
  2. serverにディレクトリを移動
cd server
  1. バックエンドの起動
npm install
npm run dev

起動したままにする。認証などDiscord APIでやり取りするのに使用する。

フロントエンドの修正

  1. discord-unity-app/client/vite.config.tsを下記のように変更
vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  envDir: "../",
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:3001",
        changeOrigin: true,
        secure: false,
        ws: true,
      },
    },
    hmr: {
      clientPort: 443,
    },
  },
});
  1. discord-unity-app/client/App.tsxを下記のように変更
App.tsx
import { useEffect, useState } from "react";
import UnityComponent from "./UnityComponent";
import { DiscordSDK } from "@discord/embedded-app-sdk";

function App() {
  let auth;

  const discordSdk = new DiscordSDK(import.meta.env.VITE_DISCORD_CLIENT_ID);

  const [userName, setUserName] = useState("");

  useEffect(() => {
    setupDiscordSdk();
  }, []);

  async function setupDiscordSdk() {
    await discordSdk.ready();

    // Discordクライアントの認証
    const { code } = await discordSdk.commands.authorize({
      client_id: import.meta.env.VITE_DISCORD_CLIENT_ID,
      response_type: "code",
      state: "",
      prompt: "none",
      scope: [
        "identify",
        "guilds"
      ],
    });

    // サーバーからaccess_tokenを取得
    const response = await fetch("/api/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        code,
      }),
    });
    const { access_token } = await response.json();

    // access_tokenを用いた認証
    auth = await discordSdk.commands.authenticate({
      access_token,
    });

    if (auth == null) {
      console.log("Authenticate command failed");
      throw new Error("Authenticate command failed");
    }

    // チャンネル名の取得
    let activityChannelName = 'Unknown';
    // Requesting the channel in GDMs (when the guild ID is null) requires
    // the dm_channels.read scope which requires Discord approval.
    if (discordSdk.channelId != null && discordSdk.guildId != null) {
      // Over RPC collect info about the channel
      const channel = await discordSdk.commands.getChannel({channel_id: discordSdk.channelId});
      if (channel.name != null) {
        activityChannelName = channel.name;
      }
    }
    console.log(`[Debug]チャンネル:${activityChannelName}`);
  
    await discordSdk.commands.getInstanceConnectedParticipants()

    // ユーザー情報の取得
    const user: { global_name: string } = await fetch(
      `https://discord.com/api/users/@me`,
      {
        headers: {
          Authorization: `Bearer ${auth.access_token}`,
          "Content-Type": "application/json",
        },
      }
    ).then((reply) => reply.json());

    console.log(`[Debug]名前:${user.global_name}`);

    // ユーザー名の設定
    //setUserName(user.global_name);
  }

  return <UnityComponent userName={userName} />;
}

export default App;

フロントエンドをcloudflareを使用して実行

  1. フロントエンドを実行(clientディレクトリ)
npm run dev

以下のようにURLが表示される

VITE v5.0.12  ready in 100 ms

➜  Local:   http://localhost:5173/
➜  Network: use --host to expose
➜  press h + enter to show help
  1. cloudflare用の新規ターミナルを作成
  2. ネットワークトンネルを先ほどのURLで開始(別ターミナル)
cloudflared tunnel --url http://localhost:5173

以下のようなURLが表示されるのでURLをコピーする

Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):
https://funky-jogging-bunny.trycloudflare.com
  1. URLマッピングの設定
    Application->URL Mappings->Target
    URLマッピング

Discordでの確認

  1. 開発者コンソールのため、Canary版Discordをダウンロードする
  2. 開発者モードを有効にする
    ユーザー設定->詳細設定->開発者モード
    開発者モード
  3. ボイスチャットに参加
  4. アクティビティを起動
    起動に時間がかかるが、しばらく待ってUnityが起動したら成功。
Hirosuke KayabaHirosuke Kayaba

トラブルシューティング

ブラウザで実行できない

前半のブラウザ表示に失敗する

  • URLをミスしていないか
  • フロントエンド=clientは実行(nvm run dev)されているか
  • Unityのビルド名とReact内でのUnity名にミスがないか
    • 本記事ではDiscordAppというディレクトリにUnityビルドする前提です
  • F12でブラウザのコンソールで表示されるエラーにヒントがないか

アクティビティでの確認後実行できない

終盤のApp.tsx改変後はブラウザでは実行できず、真っ白になるのが正しいです。

アクティビティで実行できない

  • Discordのサーバー設定でアクティビティを有効にしているか
  • cloudflareは実行されているか
  • URLマッピングを忘れていないか
    • cloudflareで発行後のURLと一致することを確認する
  • 500が出る場合、フロントエンド=clientは実行(nvm run dev)されているか

認証ダイアログが出てこない、ユーザー名が取得できない

  • バックエンド(server)は実行されているか
  • 許可ダイアログは許可をしたか

マルチプレイヤー関係

他のユーザーがアクティビティを起動できない

  • 「権利がない」と表示される場合、アクティビティを起動しようとしているユーザーは内部テストユーザー、あるいはチームのメンバーか確認する