본문 바로가기
공허의 유산/사상의 도구

[Unity3D/WebGL] 브라우저 스크립팅과 상호 작용(2)

by 바른생활머시마 2023. 10. 7.
728x90
반응형

이제 웹 페이지(편의상 index.html이라고 가정하고..)에서 Unity 내부의 함수를 호출하는 방법을 알아보겠습니다. 예를 들면,  웹 페이지에 있는 어떤 버튼을 누르면 유니티 내부의 탱크가 발사를 하는 형식이 되겠네요.

 

 그러자면, 일단 Build and Run처럼 새로 생성 되는 index.html을 그대로 쓰면 안되기 때문에 한번 생성 된 index.html을 수정해서 별도의 웹서버에서 확인을 해봐야되겠네요.

 

 그리고, 브라우져에 유니티 플레이어가 너무 크게 그려지면 웹에 뭐 넣을 공간이 부족하니 크기도 좀 작게 즐여보겠습니다.

 

유니티 내부 제어 코드는 따로 수정 할 것이 없을 것 같고, 발사(FIre) 함수 호출을 위해 연결 해야 할 인터페이스 구현과 관련 된 코드가 있다면 수정을 해야 할 것 같습니다.

 

1. VSCode의 Live Server로 실행 해 보기

앞에서 만들었던 결과물 폴더를 VSCode로 열고 index.html을 GoLive로 실행 시켜 봅니다.

환경에 따라 다르겠지만, 저는 아래와 같은 오류가 뜹니다. Build Setting에 콘텐츠 압축 형식 지정하는 것이 있는데 아마도 그것과 관련이 있었던 것 같은 기억이 어렴풋이 나네요.

아래 블로그 포스트에 깔끔하게 정리 해 두셨네요.

https://scvtwo.tistory.com/210

 

[Unity] Unable to parse Build/xxxxx.framework.js.gz! 에러

안녕하세요 unity Webgl로 빌드하여, 서버에 올리면 다음과 같이 에러가 발생합니다. Unable to parse Build/xxxxx.framework.js.gz! This can happen if build compression was enabled but web server hosting the content was misconfigured

scvtwo.tistory.com

 

조치를 하고 다시 실행 해 보면, 아래와 같이 잘 뜹니다. 웹 페이지에 발사 버튼을 넣어야 하는데 공간이 없죠.

 

2. 버튼 공간 확보를 위해 Player 크기 줄이기

창 크기를 줄이면 함께 작아지지 않고, 원래 크기가 그대로 유지되는 것도 볼 수 있습니다. 웹 페이지를 구성하는 콤포넌트로 사용되러면, 화면이나 Embed 된 오브젝트의 크기에 따라 조정되는 방법도 체크 해야 할 사항인 것 같습니다. 그건 또 다른 주제 같으니 일단 이번에는 플레이어 크기 자체를 작게 해보겠습니다.

Player 크기 조정

 Project settings/ Player/ Resolution and Presentation 항목을 보면 Player 해상도 설정이 있습니다. 일단, 연동 여부 확인이 목적이니 반으로 확 줄이도록 하겠습니다. 그리고, 저 속성 이름을 보면 Default Canvas Width/Height입니다. 즉 기본값이므로 변경이 가능하다는 뜻이죠. HTML에 Canvas 형태로 들어가게 되므로, Javascript로 Canvas의 크기는 얼마든지 조절 할 수 있을 것입니다. 그건 또 다음에 알아 볼 일이니 패스~ 

아래와 같이 적당한 여백이 보이는 형태로 만들어졌습니다. 당연히, Unity Player에 포커싱이 된 상태에서 스페이스키를 누르면 발사가 되고, 이 발사에 의해서 내부에 포함 된 Javascript 코드를 실행하여 alert 창이 뜹니다.

3. 웹 페이지 => Player로의 I/F

발사를 Unity player 내부에서 실행 시키지 않고, 외부로부터 명령을 받아 실행 시키도록 해보겠습니다.

웹 페이지에 버튼을 하나 추가하고, 그 버튼이 눌려지면 웹 페이지의 특정 함수가 실행되도록 하고, 그 함수 내부에서 Unity로 제어 명령을 내리도록 하면 될 것 같습니다.

 

Button 추가

 이제부터는 출력 된 index.html을 좀 살펴봐야 합니다. 직접 만든 것이 아니므로 흐름 분석을 해야 하고, 또 제대로 보려면, 이것저것 필요한 것들이 많아서 그리 간단하지는 않은 일이죠.필요한 것만 체크해서 살펴보기로 하고, 일단 전체 코드를 스윽 훑어 보는 정도는 해 보겠습니다. 전체 코드는 아래와 같습니다.

 

<!DOCTYPE html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Unity WebGL Player | BrowserScriptInteraction</title>
    <link rel="shortcut icon" href="TemplateData/favicon.ico">
    <link rel="stylesheet" href="TemplateData/style.css">
  </head>
  <body>
    <div id="unity-container" class="unity-desktop">
      <canvas id="unity-canvas" width=640 height=360 tabindex="-1"></canvas>
      <div id="unity-loading-bar">
        <div id="unity-logo"></div>
        <div id="unity-progress-bar-empty">
          <div id="unity-progress-bar-full"></div>
        </div>
      </div>
      <div id="unity-warning"> </div>
      <div id="unity-footer">
        <div id="unity-webgl-logo"></div>
        <div id="unity-fullscreen-button"></div>
        <div id="unity-build-title">BrowserScriptInteraction</div>
      </div>
    </div>
    <script>

      var container = document.querySelector("#unity-container");
      var canvas = document.querySelector("#unity-canvas");
      var loadingBar = document.querySelector("#unity-loading-bar");
      var progressBarFull = document.querySelector("#unity-progress-bar-full");
      var fullscreenButton = document.querySelector("#unity-fullscreen-button");
      var warningBanner = document.querySelector("#unity-warning");

      // Shows a temporary message banner/ribbon for a few seconds, or
      // a permanent error message on top of the canvas if type=='error'.
      // If type=='warning', a yellow highlight color is used.
      // Modify or remove this function to customize the visually presented
      // way that non-critical warnings and error messages are presented to the
      // user.
      function unityShowBanner(msg, type) {
        function updateBannerVisibility() {
          warningBanner.style.display = warningBanner.children.length ? 'block' : 'none';
        }
        var div = document.createElement('div');
        div.innerHTML = msg;
        warningBanner.appendChild(div);
        if (type == 'error') div.style = 'background: red; padding: 10px;';
        else {
          if (type == 'warning') div.style = 'background: yellow; padding: 10px;';
          setTimeout(function() {
            warningBanner.removeChild(div);
            updateBannerVisibility();
          }, 5000);
        }
        updateBannerVisibility();
      }

      var buildUrl = "Build";
      var loaderUrl = buildUrl + "/output.loader.js";
      var config = {
        dataUrl: buildUrl + "/output.data.unityweb",
        frameworkUrl: buildUrl + "/output.framework.js.unityweb",
        codeUrl: buildUrl + "/output.wasm.unityweb",
        streamingAssetsUrl: "StreamingAssets",
        companyName: "DefaultCompany",
        productName: "BrowserScriptInteraction",
        productVersion: "1.0",
        showBanner: unityShowBanner,
      };

      // By default, Unity keeps WebGL canvas render target size matched with
      // the DOM size of the canvas element (scaled by window.devicePixelRatio)
      // Set this to false if you want to decouple this synchronization from
      // happening inside the engine, and you would instead like to size up
      // the canvas DOM size and WebGL render target sizes yourself.
      // config.matchWebGLToCanvasSize = false;

      if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
        // Mobile device style: fill the whole browser client area with the game canvas:

        var meta = document.createElement('meta');
        meta.name = 'viewport';
        meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes';
        document.getElementsByTagName('head')[0].appendChild(meta);
        container.className = "unity-mobile";
        canvas.className = "unity-mobile";

        // To lower canvas resolution on mobile devices to gain some
        // performance, uncomment the following line:
        // config.devicePixelRatio = 1;


      } else {
        // Desktop style: Render the game canvas in a window that can be maximized to fullscreen:

        canvas.style.width = "640px";
        canvas.style.height = "360px";
      }

      loadingBar.style.display = "block";

      var script = document.createElement("script");
      script.src = loaderUrl;
      script.onload = () => {
        createUnityInstance(canvas, config, (progress) => {
          progressBarFull.style.width = 100 * progress + "%";
              }).then((unityInstance) => {
                loadingBar.style.display = "none";
                fullscreenButton.onclick = () => {
                  unityInstance.SetFullscreen(1);
                };
              }).catch((message) => {
                alert(message);
              });
            };

      document.body.appendChild(script);

    </script>
  </body>
</html>

 

가장 앞쪽에는 화면에 필요한 요소들을 div로 먼저 정의를 해두고 있고, 그 중에 Unity Player용 Canvas도 있습니다.

그 후에 Javascript 로직들이 나옵니다. 

설정 관련 부분을 보니, 화면 크기 조정에 대한 옵션 설정을 할 수 있네요. 

주석 처리 된 설명의 가장 마지막에 config.matchWebGLToCanvasSize = true/false로 설정 할 수 있군요.

Canvas의 크기를 고정으로 할 것이냐, 화면 크기에 따라 조정 할 것이냐는 설정과 이 옵션을 쓰면 되겠습니다.

      var buildUrl = "Build";
      var loaderUrl = buildUrl + "/output.loader.js";
      var config = {
        dataUrl: buildUrl + "/output.data.unityweb",
        frameworkUrl: buildUrl + "/output.framework.js.unityweb",
        codeUrl: buildUrl + "/output.wasm.unityweb",
        streamingAssetsUrl: "StreamingAssets",
        companyName: "DefaultCompany",
        productName: "BrowserScriptInteraction",
        productVersion: "1.0",
        showBanner: unityShowBanner,
      };

      // By default, Unity keeps WebGL canvas render target size matched with
      // the DOM size of the canvas element (scaled by window.devicePixelRatio)
      // Set this to false if you want to decouple this synchronization from
      // happening inside the engine, and you would instead like to size up
      // the canvas DOM size and WebGL render target sizes yourself.
      // config.matchWebGLToCanvasSize = false;

 

버튼을 Canvas 바로 아래에 추가하고, 버튼을 누르면 alert가 뜨도록 해두고,

  <body>
    <div id="unity-container" class="unity-desktop">
      <canvas id="unity-canvas" width=640 height=360 tabindex="-1"></canvas>
      <button onclick="alert('hello');">Fire</button>
      <div id="unity-loading-bar">
        <div id="unity-logo"></div>
        <div id="unity-progress-bar-empty">
          <div id="unity-progress-bar-full"></div>
        </div>
      </div>
      <div id="unity-warning"> </div>
      <div id="unity-footer">
        <div id="unity-webgl-logo"></div>
        <div id="unity-fullscreen-button"></div>
        <div id="unity-build-title">BrowserScriptInteraction</div>
      </div>
    </div>

제대로 되는지 확인해 봤습니다. 잘 되네요.

 

UnityPlayer Instance

가이드 문서를 보면, Unity 내부의 함수를 호출하려면 Instance를 얻고, 그 Instance에 메세지를 보내는 방법을 사용해야 한다고 합니다.

Instance를 얻는 방법을 알려주는 샘플 코드는 아래와 같습니다. 전역 변수로 instance를 받아두면 될 것 같습니다. 요걸 버튼이 눌려지면 실행 되는 함수에서 참조하면 되겠죠?? Javascript는 hoisting이 있으니 위치에 상관 없을 것 같네요.

 var myGameInstance = null;
  script.onload = () => {
    createUnityInstance(canvas, config, (progress) => { /*...*/ }).then((unityInstance) => {
      myGameInstance = unityInstance;

메세지를 보내는 방법은 아래와 같습니다.

 

MyGameInstance.SendMessage(objectName, methodName, value);

 

인자들 중, objecrName은 CompleteTank를 쓰면 될 것 같고,

MethodName은 당연하겠지만, 외부로 I/F가 뚫린 함수만 호출 되겠죠??

별다른 I/F가 없는 Fire를 직접 호출하는 것과, Export 된 Hello를 호출하는 것을 둘 다 해봐야겠습니다.

Fire는 private로 되어 있으니, 행여 private 때문에 안될 수도 있으니, 미리 public으로 변경 해 두고 테스트 해보겠습니다. 요걸 변경했으니, 다시 빌드 해줘야겠네요. 새로 빌드하면 버튼 코드가 삭제되니까 미리 백업 잘 해두고~ html 파일도 새로 만들어 지는 줄 알았는데 그대로 유지 되네요. @_@a

스크립트의 가장 앞쪽에 Instance용 변수를 먼저 선언합니다. (꼭 그래야 할 필요는 없지만..)

    <script>

      var UnityPlayerInstance;

      var container = document.querySelector("#unity-container");
      var canvas = document.querySelector("#unity-canvas");
      var loadingBar = document.querySelector("#unity-loading-bar");
      var progressBarFull = document.querySelector("#unity-progress-bar-full");
      var fullscreenButton = document.querySelector("#unity-fullscreen-button");
      var warningBanner = document.querySelector("#unity-warning");

그리고, Instance가 생성 된 후 실행 되는 함수 내부에서 Instance를 받아 둡니다.

      var script = document.createElement("script");
      script.src = loaderUrl;
      script.onload = () => {
        createUnityInstance(canvas, config, (progress) => {
          progressBarFull.style.width = 100 * progress + "%";
              }).then((unityInstance) => {

                UnityPlayerInstance = unityInstance;
                
                loadingBar.style.display = "none";
                fullscreenButton.onclick = () => {
                  unityInstance.SetFullscreen(1);
                };
              }).catch((message) => {
                alert(message);
              });
            };

      document.body.appendChild(script);

뭐...잘 되었겠죠???ㅋㅋ

사실 좀 더 완성도 있게(?) 하자면, 로딩이 끝나고 Instance가 할당 된 후 Fire 버튼이 보여지면 더 좋겠지만, 그런 것을 만드는 것이 목적이 아니므로, 충분히 로딩 된 후 버튼을 눌러보도록 하겠습니다.

 

 

SendMessage

 

이제 Instance가 잘 되었다고 생각하고, Instance를 이용해서 호출하는 방법은 아래와 같습니다.

MyGameInstance.SendMessage('MyGameObject', 'MyFunction');
MyGameInstance.SendMessage('MyGameObject', 'MyFunction', 5);

MyGameInstance.SendMessage('MyGameObject', 'MyFunction', 'MyString');

 

아래 코드를 버튼에 할당하고 실행~!

      <canvas id="unity-canvas" width=640 height=360 tabindex="-1"></canvas>
      <button onclick="
        UnityPlayerInstance.SendMessage('CompleteTank','Hello','Call from WebPage');
      ">Fire</button>
      <div id="unity-loading-bar">

...했지만 아무런 반응이 없었습니다.

 

콘솔을 살펴보니 그 오브젝트에는 Hello가 없다네요.

일단, CompleteTank라는 오브젝트로 가서 찾아보기는 했나봅니다. 

가이드 문서를 다시 살펴봅시다.

 

Where objectName is the name of an object in your scene; methodName is the name of a method in the script, currently attached to that object; value can be a string, a number, or can be empty. For example:

 

GameObject라는 말은 없으니, 다른 오브젝트를 한번 살펴보죠. 

Unity에서도 버튼 이벤트를 전달 할 때, 어떤 GameObject의 특정 스크립트에 연결 된 함수를 실행 하려고 하면, 그 GameObject가 아니라 그 스크립트 클래스를 지정하는 경우가 있는데, 여기서도 CompleteTank 말고 스크립트 클래스를 지정해보겠습니다.

 

TankShooting이라는 클래스에 코드를 구현해두었었죠~

클래스 이름도 TankShooting이 맞습니다. 음..namespace가 조금 마음에 걸리는데...일단 그냥 고고!!

namespace Complete
{
    public class TankShooting : MonoBehaviour
    {

        [DllImport("__Internal")]
        private static extern void Hello();

        [DllImport("__Internal")]
        private static extern void HelloString(string str);

실행 코드도 아래와 같이 수정하고~

      <button onclick="
        UnityPlayerInstance.SendMessage('TankShooting','Hello','Call from WebPage');
      ">Fire</button>
      <div id="unity-loading-bar">

기대를 했지만....

 

 

그럼 오브젝트는 원래처럼 CompleteTank가 맞는데 함수 이름을 다르게 했어야 할까요??

namespace도 아니고,

접근제어자도 아니고...

...

다 안는데...

...

이렇게 고생하다가 딱 문제가 풀리는 그 맛에 디버깅에 중독되기는 하는데.ㅋㅋ

...

 

이것저것 막 하다가...

그냥 C# 스크립트에 있는 함수 호출이나 한번 해 보려고, Fire 함수를 호출하도록 했더니.. 헐헐헐~~~

Fire 함수가 실행 되네요.

앞에서 계속 Javascript 함수 호출을 해와서 계속 그 범위에서 생각을 했었는데,  그냥 그 GameObject에 노출 되어 있는 함수 호출이 되는거네요. Message 방식이라 훨씬 자유도가 높군요.

 

그럼 이제 일단, 브라우져에서 Unity 내부의 함수를 호출하는 방법은 확인이 된 것 같습니다.

 다음은, Unity 내부에서 밖에 있는 함수를 호출하는 방법을 알아봐야겠습니다. Document 객체를 통해서 어떻게 접근 해 보면 되지 않을까 싶기도 한데, 일단 관련 자료를 좀 살펴보도록 하고~~~

 

728x90
반응형

댓글