ブラウザゲームの中で Unreal Engine 製「ゲーム内ゲーム」を遊ぶために Amazon GameLift Streams を使ってみた
はじめに
本記事では Amazon GameLift Streams を活用した「Next.js 製のシンプルな 2D ゲームの画面の奥で Unreal Engine のゲームをストリーミングするデモ」の構成を紹介します。

ゲーム内ゲームとは
ゲーム内ゲームとは、あるゲームの中に別のゲーム画面やミニゲームが埋め込まれている構成のことです。今回は 2D のミニゲーム画面の中央に携帯ゲーム機風の端末を描き、その画面部分だけを GameLift Streams による Unreal Engine の映像で差し替える構成にしました。
Amazon GameLift Streams とは
Amazon GameLift Streams は、クラウド上で実行しているゲームを、 WebRTC を使ってブラウザやネイティブアプリにストリーミングできるサービスです。 GPU 付きインスタンスのプールを抽象化した「ストリームグループ」と、 S3 に配置した実行ファイルから構成される「アプリケーション」を組み合わせて、高品質なゲーム画面をオンデマンドに配信できます。
なぜ Amazon GameLift Streams を使うのか
今回やりたいことは、ひとつのゲームの中から別のゲームを遊べるようにすること です。もしこれをローカル実行前提で実現しようとすると、ゲーム内ゲームの数だけ実行ファイルとアセットを同梱する必要があり、配布サイズがあっという間に数十 GB 単位で膨らんでしまいます。
Amazon GameLift Streams を使うと、この問題をクラウド側に逃がせます。 2D 側のゲームはあくまで軽量な Web アプリのまま保ち、 Unreal Engine などの大きなゲーム本体とアセットはすべてクラウド側で管理します。プレイヤーがゲーム内の端末を操作したタイミングだけストリーミングを開始すれば、ローカルに巨大なアセットを抱え込むことなく、ひとつのゲームの中から別の重いゲームを呼び出す体験を実現できます。
対象読者
- Unreal Engine のゲームをクラウド側で実行し、ブラウザにストリーミングしたい方
- Web フロントエンドと Amazon GameLift Streams を組み合わせた PoC やデモ構成を試してみたい方
- ゲーム内ゲーム的な演出をクラウドゲーム技術で実現してみたい方
参考情報
- Getting started with Amazon GameLift Streams
- Starting your first stream in Amazon GameLift Streams
- Setting up a web server and client with Amazon GameLift Streams
全体構成
今回のデモでやりたいのは、Next.js 製の 2D ゲームの中で、携帯ゲーム機風の画面だけを Amazon GameLift Streams 経由で Unreal Engine のゲームに差し替える ことです。登場するコンポーネントと通信の流れを整理すると次のようになります。
-
Next.js アプリ
- 2D のミニゲームと携帯ゲーム機風 UI を描画します。
- GameLift Streams Web SDK を使い、
<video>と<audio>要素にストリームを流し込みます。
-
Next.js API Route
- フロントエンドから送られてきた
signalRequestを受け取り、バックエンドの publisher service に橋渡しします。
- フロントエンドから送られてきた
-
GameLiftStreams publisher service
- Amazon が提供しているサンプルの Node.js サーバです。
- Amazon GameLift Streams の
StartStreamSessionAPI を呼び出し、signalResponseをフロントエンドに返します。
-
Amazon GameLift Streams
- S3 上のビルドフォルダから Unreal Engine の実行ファイルを起動し、 GPU 付きインスタンスで動かします。
- WebRTC を通じてブラウザに映像と音声を配信します。
ブラウザ側から見ると、 2D のプレイヤーキャラクターが画面中央の携帯ゲーム機に近づき、ボタンを押すとクラウド上の Unreal Engine ゲームが起動する、という体験になります。
Unreal Engine 実行ファイルの準備
最初に、クラウド側で動かすゲームの準備をします。なるべくリッチなほうがインパクトがあると考え、Unreal Engine 5 のサンプルゲームである Lyra Starter Game を使用しました。手順の概要は次の通りです。
- Unreal Engine 5 をインストールします。
- Epic Games Launcher から Lyra Starter Game プロジェクトを取得します。
- プロジェクトを開き、
Windows向けにShippingビルドを行います。

ビルドが完了すると、 Windows 用の実行ファイルと各種アセットを含んだフォルダが生成されます。Amazon GameLift Streams では zip ではなくフォルダそのものを S3 に配置する必要があるため、ビルドフォルダ全体をアップロードできるようにしておきます。

LyraGame.exe を起動し、ゲームがローカルで正しく起動することを確認します。

Amazon GameLift Streams 側の設定
S3 への配置
Unreal Engine のビルドフォルダを Amazon S3 にアップロードします。
- GameLift Streams のリージョンと同じリージョンに S3 バケットを作成します。
- ビルドフォルダの中身をフォルダ構造ごとアップロードします。

- 後続のアプリケーション作成時に、このパスを「ベースパス」として指定します。
アプリケーションの作成
次に、 S3 に配置したビルドフォルダを元にアプリケーションを作成します。
- ランタイム設定:
Windows Server 2022

- ベースパス:
s3://your-bucket/lyra-buildのようなフォルダパス - 実行可能な起動パス:
s3://your-bucket/lyra-build/Windows/LyraGame.exeのように実行ファイルを指定

作成後、ステータスが「準備完了」になるまで待ちます。ここでパス指定を間違えていると、後述のテストストリームで起動に失敗するため注意します。

ストリームグループの作成
続いて、ゲームを実際に動かすためのストリームグループを作成します。

- ストリームクラス:
gen5n_win2022(クラウド側はなるべくリッチな構成にしてインパクトを持たせたかったため) - リンク先のアプリケーション: 前述のアプリケーションを紐付けます。
- 場所と容量:
- 常時稼働容量: 1
- オンデマンド容量: 0
コンソールからのテストストリーム
Next.js との連携に進む前に、 GameLift Streams コンソールから単体でストリームを開始できるか確認します。
- 左ペインメニュー「テストストリーム」から、作成したストリームグループをチェックして「ストリームグループを選択する」ボタンをクリックします。

- ブラウザに Lyra のロゴが表示され、その後ゲーム画面が立ち上がり、マウスとキーボードで操作できることを確認します。

ここまで動作すれば、 GameLift Streams 側の準備は完了です。
Next.js 側から GameLift Streams を呼び出す
Next.js 側の役割は、次の二つだけです。
- 2D のゲーム画面と携帯ゲーム機風 UI を描画する
- GameLift Streams Web SDK を使って、ストリームの開始と入力の送信を行う
この記事では 2D ゲームのロジック部分は深掘りせず、 GameLift Streams との連携に必要な箇所だけに絞って紹介します。
publisher service とのブリッジ API
Getting started with Amazon GameLift Streams で提供されている GameLiftStreams publisher service をそのまま利用しました。 Next.js の Route Handler では publisher service との橋渡しだけを担当します。
src/app/api/gamelift/route.ts
import { NextRequest, NextResponse } from "next/server";
const PUBLISHER_ENDPOINT =
process.env.GAMELIFT_PUBLISHER_ENDPOINT ?? "http://localhost:8000";
type StartStreamRequestBody = {
signalRequest: string;
};
export async function POST(request: NextRequest) {
const body = (await request.json()) as StartStreamRequestBody;
if (!body.signalRequest) {
return NextResponse.json(
{ error: "signalRequest is required" },
{ status: 400 }
);
}
const res = await fetch(`${PUBLISHER_ENDPOINT}/start-stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ signalRequest: body.signalRequest }),
});
if (!res.ok) {
console.error("Failed to call publisher service", await res.text());
return NextResponse.json(
{ error: "Failed to start stream session" },
{ status: 502 }
);
}
const json = await res.json();
return NextResponse.json({
signalResponse: json.signalResponse,
streamSessionId: json.streamSession?.id,
});
}
ブラウザから送られてきた signalRequest をそのまま publisher service に渡し、返ってきた signalResponse をフロントエンドに返すだけのシンプルな構成です。 publisher service 側で StartStreamSession の呼び出しや Location 指定などを行うため、 Next.js 側では GameLift Streams の認証情報を直接扱う必要がありません。
GameLift Streams Web SDK と携帯ゲーム機風コンポーネント
携帯ゲーム機風のコンポーネント StreamMachine では、 Web SDK を使ってストリームの開始と入力の紐付けを行います。外側のフレームは単なる HTML と CSS で描画し、その中央に配置した <video> と <audio> 要素にストリームを流し込みます。
src/components/StreamMachine.tsx
"use client";
import { useRef, useState } from "react";
import { loadGameLiftStreams } from "@/lib/gamelift-websdk";
type StreamStatus = "idle" | "connecting" | "streaming";
export function StreamMachine() {
const videoRef = useRef<HTMLVideoElement | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const [status, setStatus] = useState<StreamStatus>("idle");
async function handleStart() {
if (!videoRef.current || !audioRef.current) return;
setStatus("connecting");
const GameLiftStreams = await loadGameLiftStreams();
const gls = new GameLiftStreams({
videoElement: videoRef.current,
audioElement: audioRef.current,
inputConfiguration: {
autoKeyboard: true,
autoMouse: true,
autoGamepad: true,
},
});
const signalRequest = await gls.generateSignalRequest();
const res = await fetch("/api/gamelift", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ signalRequest }),
});
if (!res.ok) {
setStatus("idle");
console.error("Failed to start stream", await res.text());
return;
}
const { signalResponse } = await res.json();
await gls.processSignalResponse(signalResponse);
gls.attachInput();
setStatus("streaming");
}
return (
<div className="stream-machine">
<div className="console-body">
<div className="console-header">
<span className="logo-text">MATRIOSHKA</span>
<span className={`power-indicator power-${status}`} />
</div>
<div className="console-screen">
<video ref={videoRef} autoPlay muted playsInline />
<audio ref={audioRef} autoPlay />
{status === "connecting" && (
<div className="screen-overlay">CONNECTING...</div>
)}
{status === "streaming" && (
<div className="screen-overlay streaming">STREAMING</div>
)}
</div>
<div className="console-controls">
{/* 実際の 2D ゲームからは、プレイヤーがこのボタンを押したタイミングで handleStart を呼び出します */}
<button className="button-a" onClick={handleStart}>
A
</button>
</div>
</div>
</div>
);
}
2D ゲーム側では、プレイヤーキャラクターが携帯ゲーム機の前まで歩いていき、決定ボタンを押したタイミングで handleStart を呼び出すだけです。 GameLift Streams に関する処理は StreamMachine 内に閉じ込めておくことで、 2D ゲームのロジックとクラウドゲーム部分をきれいに分離できます。
検証結果
最終的な見た目は次のようになりました。
- 画面中央には MATRIOSHKA と書かれた携帯ゲーム機風の端末
- 端末の画面内に 2D のキャラクターや背景が描画されたシンプルなドット絵のシーン
- 端末を起動すると Lyra のロゴやゲームプレイ画面がストリーミングされる

2D 側の描画と 3D 側のストリーミングはいずれもブラウザ上で行われるため、ローカルマシンの CPU とネットワーク帯域にはそれなりの負荷がかかります。ただし、 Unreal Engine のゲームをローカルで直接実行しているわけではないため、 GPU 能力が控えめなマシンでも比較的スムーズに動作する印象でした。(GIF アニメは容量削減のためフレームレートを落としています。)
まとめ
本記事では、 Amazon GameLift Streams を使って Next.js 製 2D ゲームの画面の奥に Unreal Engine のゲームをストリーミングするデモ構成を紹介しました。ゲーム内ゲームという文脈では、 2D 側とクラウド側のゲームを完全に別プロジェクトとして扱えるため、既存タイトルのアセットをそのまま流用しながら新しい体験を作りやすいです。今後は複数人で同時接続する構成や、ストリーム開始前後の演出を工夫することで、クラウドゲーム技術を活かしたデモの幅をさらに広げていきたいと思います。






