Unity で作った WebGL のアプリを React + MUI で良い感じに置く
はじめに
GitHub Pages を使ってサイトを公開する方法を共著で記事を書いた。
この記事を作るためにいろいろ調べていたとき「Unity で作ったちょっとしたものをどうやってデプロイするのか」という話が出てきたので、やってみた。
イメージがわかるように以下に成果物としてページリンクを貼っておく。
なお、GitHub Pages を使って公開する方法は冒頭に載せた二つの記事を参考にするか、調べてみてほしい。
Unity で作った WebGL のアプリをどうやって React で扱うか
ありがたいことに React Unity WebGL という神ツールがすでにあるのでこれを使う。
package.json
と同じ階層にpublic
ディレクトリを作り、そこに Unity からビルドしたファイル群を入れる。あとはpublic
ディレクトリ以降のパスを React Unity WebGL 側に渡してあげれば良い。公式にある例であればpublic/build
ディレクトリに入れる。
import React from "react";
import { Unity, useUnityContext } from "react-unity-webgl";
function App() {
const { unityProvider } = useUnityContext({
loaderUrl: "build/myunityapp.loader.js",
dataUrl: "build/myunityapp.data",
frameworkUrl: "build/myunityapp.framework.js",
codeUrl: "build/myunityapp.wasm",
});
return <Unity unityProvider={unityProvider} />;
}
ただし、Unityでの出力設定には注意が必要で、Player Settings > Player
にある WebGL
のタブを開いた中のPulishing Settings > Compression Format
をDisabled
にしておく。Brotli
などが設定されている場合、ビルドファイルが圧縮ファイルとなり、React Unity WebGL 側で読み込んでくれなかった。
読み込み中はローディング画面を表示する
無圧縮のファイルを読み込んでこなければならないため、それに少し時間がかかる。また読み込んでくる間は何も表示されず、フィードバックがないためユーザー体験としては悪い。
そこで MUI の<CircularProgress/>
を利用して読み込んでいる間はローディング中であることを明示し、読み込み終わったら Unity の画面を表示できれば良い。
まずは以下のような<AppLoading/>
を用意する。
import { Box, CircularProgress, SxProps, Theme } from "@mui/material";
export type AppLoadingProps = {
sx?: SxProps<Theme>;
loadingSize?: string | number;
};
export function AppLoading({ sx, loadingSize = 100 }: AppLoadingProps) {
return (
<Box
sx={sx}
width="100%"
height="100%"
color="#ffffff"
display="flex"
justifyContent="center"
alignItems="center"
>
<CircularProgress size={loadingSize} color="inherit" />
</Box>
);
}
あとはローディング中は<AppLoading/>
を表示するようにすれば良い。React Unity WebGL のuseUnityContext
からローディングの状態を取得できるようになっていて、今回はisLoaded
を使って調整する。
ここで、ただ単純にisLoaded ? <AppLoading/> : <Unity/>
のような構図でやってしまうとうまくいかなかった。どうやら<Unity/>
コンポーネントは条件によらず常にDOM上に存在しないといけないらしい。ということで重ねて表示して、visibility
をisLoaded
でオンオフできれば良い。意外と"重ねて表示"に手こずったが、親要素にposition: "relative"
, 子要素にはposition: "absolute"
で場所を指定することで成功した。
import { Box, SxProps, Theme } from "@mui/material";
import { Unity, useUnityContext } from "react-unity-webgl";
import { AppLoading } from "./AppLoading";
export type UnityAppProps = {
sx?: SxProps<Theme>;
};
export function UnityApp({ sx }: UnityAppProps) {
const { unityProvider, isLoaded } = useUnityContext({
loaderUrl: "build/myunityapp.loader.js",
dataUrl: "build/myunityapp.data",
frameworkUrl: "build/myunityapp.framework.js",
codeUrl: "build/myunityapp.wasm",
});
return (
<Box sx={sx} bgcolor={"#000000"} position="relative">
<AppLoading
sx={{
position: "absolute",
top: 0,
left: 0,
visibility: isLoaded ? "hidden" : "visible",
}}
/>
<Unity
unityProvider={unityProvider}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 1,
visibility: isLoaded ? "visible" : "hidden",
}}
/>
</Box>
);
}
フルスクリーンモードに対応する 【2024/02/25 追記】
React Unity WebGL にはゲーム画面をフルスクリーンで表示する機能が提供されている。requestFullscreen
を使えば良い。
- import { Box, SxProps, Theme } from "@mui/material";
+ import { Box, Stack, Button, SxProps, Theme } from "@mui/material";
+ import FullscreenIcon from "@mui/icons-material/Fullscreen";
import { Unity, useUnityContext } from "react-unity-webgl";
import { AppLoading } from "./AppLoading";
export type UnityAppProps = {
sx?: SxProps<Theme>;
};
export function UnityApp({ sx }: UnityAppProps) {
- const { unityProvider, isLoaded } = useUnityContext({
+ const { unityProvider, isLoaded, requestFullscreen } = useUnityContext({
loaderUrl: "build/myunityapp.loader.js",
dataUrl: "build/myunityapp.data",
frameworkUrl: "build/myunityapp.framework.js",
codeUrl: "build/myunityapp.wasm",
});
+ function handleOnFullScreenRequest() {
+ requestFullscreen(true);
+ }
return (
+ <Stack gap={2}>
<Box sx={sx} bgcolor={"#000000"} position="relative">
<AppLoading
sx={{
position: "absolute",
top: 0,
left: 0,
visibility: isLoaded ? "hidden" : "visible",
}}
/>
<Unity
unityProvider={unityProvider}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 1,
visibility: isLoaded ? "visible" : "hidden",
}}
/>
</Box>
+ <Stack width="100%" justifyContent="center" alignItems="flex-end">
+ <Box>
+ <Button
+ variant="outlined"
+ color="secondary"
+ startIcon={<FullscreenIcon />}
+ disabled={!isLoaded}
+ onClick={() => handleOnFullScreenRequest()}
+ >
+ Full Screen
+ </Button>
+ </Box>
+ </Stack>
+ </Stack>
);
}
ローディング進捗を表示する 【2024/12/14 追記】
Unityを表示するまでに容量によってはそれなりに時間がかかる。グルグルが続くと仮に内部でローディングが進んでいたとしてもサイト離脱率が上がるかもしれない。
ということで、useUnityContext
フックにはローディングの進捗をloadingProgression
という値で取得できるので、それを利用して進捗を表示できるようにする。
まずは<AppLoading/>
コンポーネントを以下のように修正する。
- import { Box, CircularProgress, SxProps, Theme } from "@mui/material";
+ import {
+ Box,
+ CircularProgress,
+ SxProps,
+ Theme,
+ Typography,
+ } from "@mui/material";
export type AppLoadingProps = {
sx?: SxProps<Theme>;
loadingSize?: string | number;
+ loadingProgression?: number;
+ progressVariant?: "determinate" | "indeterminate";
};
- export function AppLoading({ sx, loadingSize = 100 }: AppLoadingProps) {
+ export function AppLoading({
+ sx,
+ loadingSize = 100,
+ loadingProgression,
+ progressVariant = "determinate",
+ }: AppLoadingProps) {
+ const enableProgress = loadingProgression != undefined;
+ const value = 100 * (loadingProgression ?? 0);
return (
<Box
sx={sx}
width="100%"
height="100%"
color="#ffffff"
display="flex"
justifyContent="center"
alignItems="center"
>
- <CircularProgress size={loadingSize} color="inherit" />
+ <CircularProgress
+ size={loadingSize}
+ color="inherit"
+ variant={enableProgress ? progressVariant : "indeterminate"}
+ value={value}
+ />
+ <Box
+ sx={{
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+ position: "absolute",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ }}
+ >
+ <Typography variant="caption" component="div">
+ {enableProgress ? `${Math.round(value)}%` : ""}
+ </Typography>
+ </Box>
</Box>
);
}
この修正によって、loadingProgression
をパーセントで枠内中央に表示できるようになった。
あとはこれを<UnityApp/>
コンポーネント側からloadingProgression
を与えるようにすれば良い。
import { Box, Stack, Button, SxProps, Theme } from "@mui/material";
import FullscreenIcon from "@mui/icons-material/Fullscreen";
import { Unity, useUnityContext } from "react-unity-webgl";
import { AppLoading } from "./AppLoading";
export type UnityAppProps = {
sx?: SxProps<Theme>;
};
export function UnityApp({ sx }: UnityAppProps) {
- const { unityProvider, isLoaded, requestFullscreen } = useUnityContext({
+ const { unityProvider, isLoaded, loadingProgression, requestFullscreen } = useUnityContext({
loaderUrl: "build/myunityapp.loader.js",
dataUrl: "build/myunityapp.data",
frameworkUrl: "build/myunityapp.framework.js",
codeUrl: "build/myunityapp.wasm",
});
function handleOnFullScreenRequest() {
requestFullscreen(true);
}
return (
<Stack gap={2}>
<Box sx={sx} bgcolor={"#000000"} position="relative">
<AppLoading
sx={{
position: "absolute",
top: 0,
left: 0,
visibility: isLoaded ? "hidden" : "visible",
}}
+ loadingProgression={loadingProgression}
/>
<Unity
unityProvider={unityProvider}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 1,
visibility: isLoaded ? "visible" : "hidden",
}}
/>
</Box>
<Stack width="100%" justifyContent="center" alignItems="flex-end">
<Box>
<Button
variant="outlined"
color="secondary"
startIcon={<FullscreenIcon />}
disabled={!isLoaded}
onClick={() => handleOnFullScreenRequest()}
>
Full Screen
</Button>
</Box>
</Stack>
</Stack>
);
}
Discussion