npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@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

4

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