React에서 Phaser 사용하기

hjnoh·2023년 1월 31일
4
post-thumbnail

사용 계기

현재도 개발중인 게임 스크린샷

지난해 하반기, 제 2회 웅진씽크빅 게임 개발 챌린지에 참여하면서, 친구와 함께 수학 게임을 개발하게 되었다.
친구도 나도 React를 공부하고 있었고 웹 개발자를 목표하고 있었기 때문에 React로 웹 게임 만들기라는, 우리로서는 다소 일반적이지 않은 도전을 하게 되었다.
기획 단계에서는 모든 부분을 오직 React 컴포넌트만으로만 제작할 생각이었으나, 위 이미지와 같은 소위 "마이룸"이라 불리는 방 꾸미기 컨텐츠를 위해 이미지를 배치하려니 다음과 같은 부분을 생각해야 했다.

  • 마감까지 시간이 얼마 남지 않아 빠른 시간 안에 다양한 크기의 여러 이미지를 배치하고, 변경해도 프레임이 크게 떨어지지 않아야 한다.
  • 캐릭터와 상호작용 등 파티클 이펙트가 발생한다. (파티클 이펙트는 보통 프레임을 많이 떨어뜨린다)

결국에는 게임이라는 컨텐츠 특성상 프레임이 조금이라도 더 떨어지면 UX에 매우 큰 영향을 미치기 때문에 짧은 시간 안에 최적의 퍼포먼스를 구현하기 가장 좋은 도구로서 게임 엔진을 고르게 되었다. 다른 JS 게임 엔진 중에서 Phaser를 고르게 된 이유는 아무래도 시간이 문제다보니 레퍼런스가 많은 점을 크게 평가했다.

아무튼 React와 Phaser를 함께 사용하는 레퍼런스가 은근히 적어 같은 문제를 직면하는 분들에게 조금이나마 도움도 되었으면 하는 마음으로 글을 작성해본다.

Phaser 추가

현시점에서 최신 버전인 Phaser3을 사용한다.

npm install phaser
// or
yarn add phaser

게임 Scene 세팅하기

Scene은 유니티와 같은 게임 엔진을 다뤄본 사람이라면 익숙할만한 단어이다.
게임 엔진에서는 보통 Scene 단위로 개발을 한다.
하나의 게임에는 메인 화면, 게임 화면, 설정 화면 등 여러 화면이 있다.
이런 것들을 별개의 Scene 으로 나누어 개발하는 것이라고 보면 된다.
원하는 모습에 따라 여러 개의 Scene 혹은 하나의 Scene만 사용할 수도 있다.

이 프로젝트에서는 오직 하나의 기능을 위해 하나의 Scene을 사용했다.


const Room = () => {
  const game = useRef(null);
  const phaserConfig = {
    type: Phaser.AUTO,
    // 배경을 투명하게 설정(디폴트 검정색)
    transparent: true,
    // 씬을 관리하는 메소드.
    scene: {
      preload,
      create,
      update,
    }
  };

  // 씬을 만들기 전에 실행
  function preload() {
  }

  // 씬을 생성하기 위해 실행
  function create() {
  }

  // 씬을 갱신하기 위해 실행
  function update() {
  }

  useEffect(() => {
    // game 레퍼런스에 phaserConfig 로 씬을 생성
    // 씬은 game 레퍼런스에 HTMLcanvas를 그리는 식으로 생성된다.
    game.current = new Game(phaserConfig);
    // 주의!! 단 한 번만 실행될 수 있도록 신경써야 한다. 
    // 두 번 실행되면 가차없이 두 개의 게임 화면이 생긴다.
    // 여기서는 useEffect 의 dependency array에 []를 넣어서 한 번만 실행되도록 했다.
  }, []);

  return(
    // 화면이 출력될 element
    <div ref={game} />
  )
}

게임 화면 크기 지정 + 반응형

그냥 씬을 생성하면 부모 div(편의상 div로 언급하지만 어떠한 태그든 상관없다)의 크기와는 관계없이 고정 크기로 적용된다.
씬 출력용 div에 id를 지정해주고 다음과 같이 phaserConfig에 추가해주면 된다.
<div ref={game} id="gamediv"/>

  const phaserConfig = {
    ...
    scale: {
      // 게임 div의 id
      parent: "gamediv",
      // 높이를 꽉 채우고, 비율에 맞게 가로를 조정한다.
      // css로 치면 height 100% width auto 의 기능을 한다.
      // WIDTH_CONTROLS_HEIGHT 도 가능하다.
      mode: Phaser.Scale.HEIGHT_CONTROLS_WIDTH,
      // 게임이 아래의 해상도로 렌더링된다.
      // 모든 좌표, 크기 설정은 이 크기를 기본으로 계산된다.
      width: 600,
      height: 300,
    },
  }

이미지 배치

const carpet = useRef()

function preload () {
  this.load.image(
      // 이미지 key (Phaser 안에서 이미지를 다룰 때에 사용한다.)
      "carpet_image",
      // 이미지 소스
      "https://velog.velcdn.com/images/imhjnoh/post/3a11b731-60ba-4095-a3b3-606f8a7956bb/image.png"
    );
}

function create () {
  ...
  	// 여기서 this는 게임 Scene이다.
  	// 이 프로젝트의 경우 카펫 이미지를 Phaser가 아닌 외부 컴포넌트에서 컨트롤하기 때문에 ref를 이용했다.
    carpet.current =
      this.make
        .image({
    	  // 이미지가 생성되는 중심 좌표 위치이다.
     	  // 위에서 600 * 300으로 설정했으니 해당 범위 안으로 지정해줘야 화면에 보인다.
          x: 300,
          y: 150,
          // preload에서 지정한 이미지 키를 입력한다.
          key: "carpet_image",
        })
  		// 이미지 크기를 지정한다.
  		// phaserConfig에서 가로를 600으로 지정했고, x좌표가 300으로 600의 중앙이니 이 이미지는 게임 canvas의 가로를 꽉 채우게 될 것이다.
        .setDisplaySize(600, 100);
  ...
}

외부에서 Phaser 요소 관리하기

원래 게임엔진의 개발 의도대로라면 preload, create, update 안에서 모든 게임 로직이 돌아가는 것이 맞으나, 부득이하게 외부의 제어가 필요할 때가 있다.
Phaser는 이벤트를 통해 외부 제어가 가능하다.

  function update() {
    // "CHANGE_FURNITURE" 이벤트 핸들러
    game.current.events.on("CHANGE_FURNITURE", (e) => {
      // 이벤트에서 받아온 이미지로 변경한다.
      carpet.current.setTexture(e.carpet);
    });
  }

  useEffect(() => {
    // furnitures (외부에서 들어오는 props) 가 변경될 때에 
    // game ref에 게임이 세팅되어 있다면(ts의 경우 인스턴스 여부를 체크해도 좋을 듯 하다.)
    // game 인스턴스 내부에 "CHANGE_FURNITURE" 이벤트를 발생시키고, furnitures 데이터를 함께 보낸다.
    if (game.current) game.current.events.emit("CHANGE_FURNITURE", furnitures);
  }, [furnitures]);

preload에서 비동기 처리

Phaser의 preload에서 axios를 사용해 서버로부터 이미지를 받아와서 등록하고 싶은데, preload에서는 비동기 처리를 기다려주지 않는다.
플러그인을 설치해야 한다.
https://rexrainbow.github.io/phaser3-rex-notes/docs/site/index.html#rex-plugins

npm install phaser3-rex-plugins
// or
yarn add phaser3-rex-plugins

설치 후 phaserConfig에 플러그인을 추가한다.

const phaserConfig = {
  ...
    plugins: {
      global: [
        {
          key: "rexAwaitLoader",
          plugin: AwaitLoaderPlugin,
          start: true,
        },
      ],
    },
  ...
}

preload에서 비동기처리를 원하는 부분에 다음과 같이 적용한다.

function preload () {
    this.load.rexAwait(function (successCallback, failureCallback) {
      // axios 등 비동기 코드를 이곳에 작성하면 된다.
    });
}

Phaser 문서

0개의 댓글