Skip to content

@futureverse/react-unity-viewer ​

Embed Unity WebGL builds inside React with two-way message bridging.

Version
0.1.2
Published
2023-05-29
License
ISC
Status
fv-archived
npm
https://www.npmjs.com/package/@futureverse/react-unity-viewer
Repository
https://github.com/futureversecom/react-unity-viewer
Homepage
https://github.com/futureversecom/react-unity-viewer#readme
Types
dist/index.d.ts
Maintainers
admin-futureverse, garethdainesnpm, jcsanpedro
Depends on
react · react-dom
Recent versions
0.1.1 · 0.1.2
## Technical notes

Why use it ​

You drop a Unity-built scene (avatar viewer, mini-game) into a React page and need React↔Unity messaging.

When to skip it ​

No Unity content.

Alternatives ​

  • react-unity-webgl (community) — general-purpose alternative

Gotchas ​

  • Last published 2023; verify Unity LTS compatibility.

Upstream 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.

sh
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:

bash
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.

css
@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: ​

javascript
const {
  openUnity,
  closeUnity,
  isLoaded,
  isReady,
  loadingProgression,
  dispatchPlayerData,
} = useUnityViewerContext();

Basic usage example: ​

javascript
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 ​

javascript
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: ​

javascript
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: ​

javascript
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: ​

javascript
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: ​

javascript
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: ​

javascript
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: ​

javascript
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 development
  • build: 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 or yarn link command to register the package
  • From your app: run npm link "@futureverse/react-unity-viewer" or yarn 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" or yarn link "@futureverse/react-unity-viewer" command to use the library inside your app during development
  • From your library: run npm unlink or yarn 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

Curated independently by Codeology. Source-attributed reference for The Root Network. Not affiliated with Futureverse / TRN Labs.