Minecraft の速通コミュニティはいかにして Expo で効率を保っているか

Minecraft 速通コミュニティが Expo を使ってモバイルアプリとプッシュ通知の基盤を構築し、速通の進捗をリアルタイムでユーザーに知らせる仕組みを紹介します。

日本語
コピー
How the Minecraft Speedrunning Community stays fast with Expo

この記事は Chitraksh Tarun によるゲスト投稿です。彼はソフトウェア開発エンジニアで、現在 ClubzFM で SWE インターンとして働いています。また Minecraft のスピードラン走者でもあり、2021 年 4 月からこのゲームを走っており、PaceMan.gg モバイルアプリのメンテナーも務めています。

...

Minecraft スピードランコミュニティのテンポは速い。驚くほど速い。スピードランは世界中でいつでも行われていて、コミュニティは記録を更新しそうな走りをリアルタイムで追いたいと考えている。

PaceMan.gg は、進行中の Minecraft スピードランを追跡するコミュニティ主導のツールだ。進行中の走りを目立たせ、リアルタイムのリーダーボードで進捗を更新する。

PaceMan.gg のモバイルアプリは Expo SDK で構築され、モバイル画面からリアルタイムのスピードラン更新を届ける。最近このアプリにプッシュ通知サポートを追加した。要は、走りが期待できるペースに入ったときユーザーに通知が届き、アプリで進捗を追ったり、Twitch で配信中ならそのまま視聴したりできる。

この記事では、高速でリアルタイムなアプリを作るために私たちが下したアーキテクチャと開発上の判断を分解して説明する。通知基盤、API セキュリティ、そしてネイティブ UI の一部を expo-notifications@expo/app-integrityexpo-glass-effect でどう組み立てたかも紹介する。

PaceMan.gg モバイルアプリ

本質的にこのアプリは、シンプルでリアルタイムな Minecraft スピードランのダッシュボードだ。初回起動時、ホームタブには現在走っているすべてのスピードラン走者とその split が表示される。リーダーボードタブでは、指定した期間(日次、週次、月次、歴代)で最速の記録を並べる。統計タブでは、走者ごとの各 split のデータをより細かく分解して見られる。何が起きているかをひと目で把握できる、シンプルなインターフェースであることが狙いだ。

モバイルアプリのUI

アプリは完全に Expo で開発されており(v1.2.0 時点で 99.5% が TypeScript、0.5% が Other 👀)、Expo のサービスを使って iOS と Android で同じ機能を簡単に実現している。実際、最初のいくつかのバージョンは Mac なしで開発した。WSL が動く Windows マシン、1 台の iPhone、そして古い Android 端末だけだ。Expo のクラウドサービスがあったおかげで、Mac なしでもビルドしてリリースできた。

Expo Notifications でプッシュ通知を実装する

このアプリの中核機能のひとつは、あるスピードランが盛り上がるペースに入ったときに通知を受け取れることだ。ユーザーは進行中の走りをリアルタイムで把握でき、Twitch で配信中ならそのまま見に行ける。

express.js のマイクロサービス/バックエンド(PushNotificationsService)が通知を管理し、別のマイクロサービス/バックエンド(ActiveRunsService)が現在アクティブなスピードランを管理する。各スピードランイベントは ActiveRunsService から WebSocket イベントで PushNotificationsService に送られ、後者がイベントを解析して通知に値するかどうか(要は、その走りのペースが十分に良いか)を判断する。良ければプッシュトークンを取り出して通知を送る。この部分は expo-server-sdk-node を統合しており、トークンやその他の情報の保存は MySQL データベースと Redis インスタンスで行っている。

以下はこの仕組みを簡略化したフロー図だ:

フローチャート

クライアント側は <NotificationsProvider /> を実装しており、Beto のチュートリアルに着想を得ている。expo-notifications を使って、トークンの登録からアプリがフォアグラウンド/バックグラウンドにあるときの通知イベント処理まで、すべてを担う。

App Integrity でセキュリティを確保する

PushNotificationsService のバックエンドには、モバイルアプリが CRUD 操作(トークンといくつかの設定の保存)を行うための API ルートもいくつかある。@expo/app-integrity パッケージの登場に加え、これらの CRUD はそもそもモバイルアプリからしか呼ばれない設計だったため、App Integrity でこれらのルートに保護層をかけることにした。

API リクエストを送る前に、モバイルアプリは getIntegrityHeaders() 関数を実行し、一意の challenge に対して App Attest(iOS)または Play Integrity(Android)の検証を行い、その結果をリクエストと一緒に送る。バックエンドにはルートの前に走るミドルウェア関数 verifyIntegrity() があり、整合性を検証して、通れば CRUD を続行し、失敗すれば 401 - Unauthorized を返す。バックエンドはこれらの検証に node-app-attest(iOS)と @googleapis/playintegrity(Android)の 2 つのライブラリを使っている。

以下は、簡素化と整理を経た getIntegrityHeaders() 関数のコード断片です:

export const getIntegrityHeaders = async () => {
  if (Platform.OS !== "android" && Platform.OS !== "ios") return;
  // Get unique challenge
  const challenge = await getChallenge();
	// Handle Android
  if (Platform.OS === "android") {
    await waitForIntegrityProviderReady();
    const integrityToken = await requestIntegrityCheckAsync(challenge);
    return integrityToken;
  }
  // Handle iOS
  if (Platform.OS === "ios") {
    let keyId = await getItemAsync("app-attest-key");       
    // Attest first time, if no attestation available
    if (!keyId || typeof keyId !== "string" || keyId.trim().length === 0) {
      keyId = await generateKeyAsync();
      await setItemAsync("app-attest-key", keyId);
      const attestation = await attestKeyAsync(keyId, challenge);
      return attestation;
    }
		// Assert if attestation exists
    try {
      const request = {
        expoToken,
        challenge,
      };
      const assertion = await generateAssertionAsync(keyId, JSON.stringify(request));
      const rawAuthentication = JSON.stringify({
        keyId,
        assertion,
      });
      const authentication = Buffer.from(rawAuthentication).toString("base64");
      return authentication;
    } catch {
	    // Re-attest, if current attestation key fails for whatever reason.
      const newKeyId = await generateKeyAsync();
      await setItemAsync("app-attest-key", newKeyId);
      const attestation = await attestKeyAsync(newKeyId, challenge);
      return attestation;
    } 
  }
  return;
};

この特有の課題を App Integrity で処理し、さらに expo-secure-store で key ID を keychain に安全に保存すれば、正規にインストールされたアプリだけがバックエンドにアクセスできる。安全かつシンプルだ。Notifications Provider は再登録と token 更新のロジックも担う。これらも App Integrity で保護されるため、ユーザーが通知を見逃すことはない。

expo-glass-effect でネイティブな Header を実現する

UI の見た目と操作感は、できる限りネイティブに近づけたい。Header はその一例だ。header に適切なぼかしとスタイル効果を加えると、スクロールの感触がネイティブプラットフォーム本来の挙動と一致する。対象は Android(単色 header)、iOS 26 より前の iOS(header は半透明ぼかし)、iOS 26 以降の iOS(header は透明ぼかし)だ。

Anurabh Verma の実装にヒントを得て、expo-glass-effect パッケージの isLiquidGlassAvailable() ハンドラも併用しつつ、ページ間で header を一貫させる useScreenOptions() hook を実装した:

// @/hooks/use-screen-options.ts 
import { useColorsForUI } from "@/hooks/use-colors-for-ui";
import type { NativeStackNavigationOptions } from "@react-navigation/native-stack";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { useColorScheme } from "nativewind";
import { Platform } from "react-native";

export const useScreenOptions = (): NativeStackNavigationOptions => {
  const { colorScheme } = useColorScheme(); 
  const { backgroundColor } = useColorsForUI(); // Custom hook to retrieve certain hex codes
  return {
    headerShadowVisible: false,
    headerTransparent: Platform.select({
      ios: true,
      android: false,
    }),
    headerStyle: {
      backgroundColor: Platform.select({
        android: backgroundColor,
      }),
    },
    headerBlurEffect: !isLiquidGlassAvailable()
      ? colorScheme === "light"
        ? "systemChromeMaterialLight"
        : "systemChromeMaterialDark"
      : "none",
    headerBackButtonDisplayMode: "minimal",
  };
};

使い方は次のとおり:

// @/app/_layout.tsx
import { useScreenOptions } from "@/hooks/use-screen-options";
export default function RootLayout() {
	// ...
  const screenOptions = useScreenOptions();
  // ...
  return (
	  <Stack screenOptions={screenOptions}>
		  {/* Remaining Stack Elements */}
		</Stack>
	)

これで header をスクロールするときもネイティブな感触が得られる。

ヘッダーのぼかし効果

パッケージ内の <GlassView /> コンポーネントにはさらに注力し、アプリに Liquid Glass の UI 要素を少しずつ増やしていく予定だ。段階的に進め、アプリの UI は控えめでありながら滑らかさを保ちたい。

PaceMan.gg の今後の予定

このアプリには Widgets をはじめ、まだまだ予定している機能があり、改善と磨き込みの余地も多い。Expo とコミュニティが提供する周辺技術をより深く使いこなし、Minecraft Speedrunning コミュニティに優れたモバイル体験を提供し続けたい。

P.S. このモバイルアプリは私が Expo で開発したが、このプロジェクトは Minecraft Speedrunning コミュニティの開発者たちの優れた仕事なしには成り立たなかった。彼らがこのツールの周辺コンポーネント、ユーティリティ、インターフェースを作り、私はその上にアプリを構築している。特に SpecnrBoyennSaanviDuncanJojoeRedLimeCylo、そしてこのプロジェクトを実現させた他の多くの開発者に感謝する。

注意:PaceMan.gg はコミュニティ主導のリアルタイム速通進捗トラッキングアプリだ。本アプリは Minecraft、Mojang、Microsoft のいずれとも提携関係になく、承認も受けていない。また Minecraft 使用ガイドラインに準拠している。

出典: Expo Blog← ホームへ戻る