@futureverse/react-unity-viewer
v0.1.2
Published
Futureverse React Unity Viewer provides a modern solution for embedding Unity WebGL builds in your React Application while providing advanced APIs for two way communication and interaction between Unity and React.
Downloads
3
Readme
@futureverse/react-unity-viewer
When bringing your Unity Application to the web, you might need to communicate with Components on a webpage, build interactive interfaces or might want to implement functionality using Web APIs which Unity does not expose. Combining Unity with React is a great way to achieve these goals. Unity Viewer provides a modern solution for embedding Unity WebGL builds in your React Application while providing advanced APIs for two way communication and interaction between Unity and React.
Unity Build
Please use the MessageHandler
script available on Futureverse - Asset Viewer
package for Unity in order to receive messages from React.
Installation
Get started by installing Unity Viewer using the Node Package Manager or Yarn in your JavaScript or TypeScript React project.
npm install @futureverse/react-unity-viewer
# or
yarn install @futureverse/react-unity-viewer
# or
pnpm install @futureverse/react-unity-viewer
Examples
The following examples are also available on /examples
folder.
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
Open http://localhost:3000 with your browser to see the result.
Basic usage:
Import the Unity Viewer Styles to your project using the command below on your css file.
@import url("./node_modules/@futureverse/react-unity-viewer/dist/style.css");
In order to use the unity viewer you will need the context <UnityViewerContextProvider>
and the viewer <UnityViewer>
, viewer requires the context to be able to control the state of the application such as loading, open and close states and also to receive react messages.
Common context hooks:
const {
openUnity,
closeUnity,
isLoaded,
isReady,
loadingProgression,
dispatchPlayerData,
} = useUnityViewerContext();
Basic usage example:
import {
UnityViewerContextProvider,
UnityViewer,
} from "@futureverse/react-unity-viewer";
<UnityViewerContextProvider>
<UnityViewer unityHostURL="UNITY_HOST_URL" unityFileName="UNITY_FILE_NAME" />
</UnityViewerContextProvider>;
by default the application won't load the unity player straight away, to load the unity player you need to use the openUnity
hook.
Basic usage example for opening the unity application
import {
UnityViewerContextProvider,
UnityViewer,
UnityViewerControls,
UnityViewerPreloader,
useUnityViewerContext,
} from "@futureverse/react-unity-viewer";
const CustomViewerStarter = () => {
const { openUnity } = useUnityViewerContext();
const handleLoadUnity = () => {
openUnity({
initialMessageFunctionName: "INITIAL_FUNCTION_NAME_CALLED_ON_UNITY",
initialMessageData: "INITIAL_FUNCTION_DATA_CALLED_ON_UNITY",
});
};
return (
<div className="absolute inset-0 flex items-center justify-center">
<button
onClick={handleLoadUnity}
className="bg-black text-white rounded-md px-3 py-1.5 border border-white"
>
Click here to load Unity Application
</button>
</div>
);
};
const UnityViewerWithCustomUnityStarter = () => {
return (
<main className="absolute inset-0 bg-gray-50">
<UnityViewerContextProvider>
<CustomViewerStarter />
<UnityViewer
unityHostURL="https://fv-goblins.s3.us-west-2.amazonaws.com/gag-viewer-dev"
unityFileName="gag"
/>
<UnityViewerControls />
<UnityViewerPreloader />
</UnityViewerContextProvider>
</main>
);
};
Basic usage example with default preloader and controls:
import {
UnityViewerContextProvider,
UnityViewerStarter,
UnityViewer,
UnityViewerControls,
UnityViewerPreloader,
} from "@futureverse/react-unity-viewer";
<UnityViewerContextProvider>
<UnityViewerStarter
initialMessageFunctionName="INITIAL_FUNCTION_NAME_CALLED_ON_UNITY"
initialMessageData="INITIAL_FUNCTION_DATA_CALLED_ON_UNITY"
/>
<UnityViewer unityHostURL="UNITY_HOST_URL" unityFileName="UNITY_FILE_NAME" />
<UnityViewerControls />
<UnityViewerPreloader />
</UnityViewerContextProvider>;
Basic usage example for Gods and Goblins project with default preloader and controls:
import {
UnityViewerContextProvider,
UnityViewerStarter,
UnityViewer,
UnityViewerControls,
UnityViewerPreloader,
} from "@futureverse/react-unity-viewer";
<UnityViewerContextProvider>
<UnityViewerStarter
initialMessageFunctionName="update_model"
initialMessageData={{
URL: "https://fv-goblins.s3.us-west-2.amazonaws.com/goblins/000-34_050-18__010-5078_1V0-4922__0C1-0/glb/000-34_050-18__010-5078_1V0-4922__0C1-0.glb",
}}
/>
<UnityViewer
unityHostURL="https://fv-goblins.s3.us-west-2.amazonaws.com/gag-viewer-dev"
unityFileName="gag"
/>
<UnityViewerControls />
<UnityViewerPreloader />
</UnityViewerContextProvider>;
Custom preloader example:
import { IconFutureVerse } from "@/icons/IconFutureVerse";
import { IconGodsAndGoblins } from "@/icons/IconGodsAndGoblins";
import {
UnityViewerContextProvider,
UnityViewerStarter,
UnityViewer,
UnityViewerControls,
useUnityViewerContext,
} from "@futureverse/react-unity-viewer";
const CustomViewerPreloader = () => {
const { isUnityOpened, isLoaded, loadingProgression } =
useUnityViewerContext();
return (
<>
{isUnityOpened && (
<>
{!isLoaded && (
<div className="absolute inset-0 flex items-center justify-center bg-black text-xs text-white">
<div className="flex w-full max-w-[500px] flex-col gap-10 px-10">
<div className="flex justify-center">
<IconGodsAndGoblins />
</div>
<div className="w-full overflow-hidden">
<div className="relative w-full overflow-hidden">
<div className="relative h-[3px] w-full bg-white opacity-10"></div>
<div
className="absolute left-0 top-0 w-full h-full transform bg-white"
style={{
width: `${loadingProgression * 100}%`,
}}
></div>
</div>
</div>
<div className="flex justify-center">
<a
href="https://www.futureverse.com/"
target="_blank"
className="flex items-center gap-1.5"
rel="noreferrer"
>
Powered by
<IconFutureVerse />
</a>
</div>
</div>
</div>
)}
</>
)}
</>
);
};
const UnityViewerWithCustomPreloader = () => {
return (
<main className="absolute inset-0 bg-gray-50">
<UnityViewerContextProvider>
<UnityViewerStarter
initialMessageFunctionName="update_model"
initialMessageData={{
URL: "https://fv-goblins.s3.us-west-2.amazonaws.com/goblins/000-34_050-18__010-5078_1V0-4922__0C1-0/glb/000-34_050-18__010-5078_1V0-4922__0C1-0.glb",
}}
/>
<UnityViewer
unityHostURL="https://fv-goblins.s3.us-west-2.amazonaws.com/gag-viewer-dev"
unityFileName="gag"
/>
<UnityViewerControls />
<CustomViewerPreloader />
</UnityViewerContextProvider>
</main>
);
};
Custom controls example:
import {
UnityViewerContextProvider,
UnityViewerStarter,
UnityViewer,
UnityViewerPreloader,
useUnityViewerContext,
} from "@futureverse/react-unity-viewer";
const CustomViewerControls = () => {
const { isReady, closeUnity, dispatchPlayerData } = useUnityViewerContext();
const handleFullBody = () => {
dispatchPlayerData("camera_composition_default", {});
};
const handleUpperBody = () => {
dispatchPlayerData("camera_composition_key", {
CompositionKey: "head",
});
};
return (
<>
{isReady && (
<div className="absolute left-0 right-0 bottom-0 p-2">
<div className="flex items-center justify-center gap-2 text-sm">
<button
className="bg-black text-white rounded-md px-3 py-1.5 border border-white"
onClick={closeUnity}
>
Exit
</button>
<button
className="bg-black text-white rounded-md px-3 py-1.5 border border-white"
onClick={handleFullBody}
>
Full body
</button>
<button
className="bg-black text-white rounded-md px-3 py-1.5 border border-white"
onClick={handleUpperBody}
>
Upper body
</button>
</div>
</div>
)}
</>
);
};
const UnityViewerWithCustomControls = () => {
return (
<main className="absolute inset-0 bg-gray-50">
<UnityViewerContextProvider>
<UnityViewerStarter
initialMessageFunctionName="update_model"
initialMessageData={{
URL: "https://fv-goblins.s3.us-west-2.amazonaws.com/goblins/000-34_050-18__010-5078_1V0-4922__0C1-0/glb/000-34_050-18__010-5078_1V0-4922__0C1-0.glb",
}}
/>
<UnityViewer
unityHostURL="https://fv-goblins.s3.us-west-2.amazonaws.com/gag-viewer-dev"
unityFileName="gag"
/>
<CustomViewerControls />
<UnityViewerPreloader />
</UnityViewerContextProvider>
</main>
);
};
Sending mouse position to unity example:
import { useEffect } from "react";
import {
UnityViewerContextProvider,
UnityViewerStarter,
UnityViewer,
UnityViewerPreloader,
useUnityViewerContext,
} from "@futureverse/react-unity-viewer";
const CustomMousePosition = () => {
const { dispatchPlayerData } = useUnityViewerContext();
useEffect(() => {
const onMouseMove = (event: MouseEvent) => {
const target = event.target as HTMLElement;
const localX = event.clientX - target.offsetLeft;
const localY = event.clientY - target.offsetTop;
const localMousePosition = { x: localX, y: localY };
const globalMousePosition = { x: event.clientX, y: event.clientY };
dispatchPlayerData(`mouse_move_position`, {
globalMousePosition,
localMousePosition,
});
};
window.addEventListener("mousemove", onMouseMove);
return () => {
window.removeEventListener("mousemove", onMouseMove);
};
}, [dispatchPlayerData]);
return <></>;
};
const CustomPanel = () => {
const { isReady } = useUnityViewerContext();
return <>{isReady && <CustomMousePosition />}</>;
};
const UnityViewerWithCustomControls = () => {
return (
<main className="absolute inset-0 bg-gray-50">
<UnityViewerContextProvider>
<UnityViewerStarter
initialMessageFunctionName="update_model"
initialMessageData={{
URL: "https://fv-goblins.s3.us-west-2.amazonaws.com/goblins/000-34_050-18__010-5078_1V0-4922__0C1-0/glb/000-34_050-18__010-5078_1V0-4922__0C1-0.glb",
}}
/>
<UnityViewer
unityHostURL="https://fv-goblins.s3.us-west-2.amazonaws.com/gag-viewer-dev"
unityFileName="gag"
/>
<CustomPanel />
<UnityViewerPreloader />
</UnityViewerContextProvider>
</main>
);
};
Sending container (div) position to unity example:
import { useEffect, useRef } from "react";
import {
UnityViewerContextProvider,
UnityViewerStarter,
UnityViewer,
UnityViewerPreloader,
useUnityViewerContext,
} from "@futureverse/react-unity-viewer";
const CustomContainerPosition = ({
children,
}: {
children: React.ReactNode | React.ReactNode[],
}) => {
const containerRef = useRef < HTMLDivElement > null;
const { dispatchPlayerData } = useUnityViewerContext();
useEffect(() => {
let animationRequest: number;
const getContainerRect = () => {
return containerRef.current?.getBoundingClientRect();
};
const onRender = () => {
const containerRect = getContainerRect();
if (containerRect) {
dispatchPlayerData(`div_position`, {
x: Math.round(containerRect.x) * window.devicePixelRatio,
y: Math.round(containerRect.y) * window.devicePixelRatio,
width: Math.round(containerRect.width) * window.devicePixelRatio,
height: Math.round(containerRect.height) * window.devicePixelRatio,
});
}
animationRequest = requestAnimationFrame(onRender);
};
onRender();
window.addEventListener("resize", onRender);
window.addEventListener("scroll", onRender);
return () => {
window.removeEventListener("resize", onRender);
window.removeEventListener("scroll", onRender);
cancelAnimationFrame(animationRequest);
};
}, [dispatchPlayerData]);
return (
<>
<div
ref={containerRef}
style={{
position: "relative",
width: "100%",
}}
>
<div>{children}</div>
</div>
</>
);
};
const CustomPanel = () => {
const { isReady } = useUnityViewerContext();
return (
<>
{isReady && (
<div className="absolute inset-0 overflow-auto h-[2000px] gap-6 flex flex-col justify-between">
<div className="bg-red-500 opacity-20 h-[200px]"></div>
<div className="bg-red-500 opacity-20 h-[200px]"></div>
<div className="bg-red-500 opacity-20 h-[200px]"></div>
<div className="bg-red-500 opacity-20 h-[200px]"></div>
<div className="bg-red-500 opacity-20 h-[200px]"></div>
<CustomContainerPosition>
<div className="bg-red-500 opacity-20 h-[200px]"></div>
</CustomContainerPosition>
</div>
)}
</>
);
};
const UnityViewerWithCustomControls = () => {
return (
<main className="absolute inset-0 bg-gray-50">
<UnityViewerContextProvider>
<UnityViewerStarter
initialMessageFunctionName="update_model"
initialMessageData={{
URL: "https://fv-goblins.s3.us-west-2.amazonaws.com/goblins/000-34_050-18__010-5078_1V0-4922__0C1-0/glb/000-34_050-18__010-5078_1V0-4922__0C1-0.glb",
}}
/>
<UnityViewer
unityHostURL="https://fv-goblins.s3.us-west-2.amazonaws.com/gag-viewer-dev"
unityFileName="gag"
/>
<UnityViewerPreloader />
<CustomPanel />
</UnityViewerContextProvider>
</main>
);
};
Updating NPM Package
The following tasks are available for npm run
or yarn
:
dev
: Run Vite in host mode for a local development environment (not included in production build)watch
: Run Vite in watch mode to detect changes to files during developmentbuild
: Run Vite to build a production release distributable
Development
Vite features a host mode to enables development with real time HMR updates directly from the library via the start
script.
To test your library from within an app:
- From your library: run
npm link
oryarn link
command to register the package - From your app: run
npm link "@futureverse/react-unity-viewer"
oryarn link "@futureverse/react-unity-viewer"
command to use the library inside your app during development
Development Cleanup
Once development completes, unlink
both your library and test app projects.
- From your app: run
npm link "@futureverse/react-unity-viewer"
oryarn link "@futureverse/react-unity-viewer"
command to use the library inside your app during development - From your library: run
npm unlink
oryarn unlink
command to register the package
Release Publishing
Update your package.json
to next version number, and remember to tag a release.
Once ready to submit your package to the NPM Registry, execute the following task via npm
(or yarn
):
npm run build
— Build the package
Assure the proper npm login:
npm login
Submit your package to the registry:
npm publish --access public
https://www.npmjs.com/package/@futureverse/react-unity-viewer