Pixi.js를 사용해서 캐릭터 이동 구현해보기

krkorklo·2022년 10월 29일
0

지난번에 canvas를 사용해서 캐릭터 이동을 구현해봤었다.
이번에는 라이브러리를 사용해서 캐릭터 이동을 구현해보자~!~

WebGL

  • Web Graphics Library의 약자로 웹에서 2D 및 3D 그래픽을 렌더링하기 위한 JavaScript API
  • 코드를 기반으로 점, 선, 삼각형 등을 그림
  • 웹 브라우저에서 GPU를 사용해 렌더링
  • GPU에서 실행되기 위한 코드는 함수(shader) 쌍 형태로 제공
    • vertex shader : 정점의 모양과 위치를 계산
    • fragment shader : fragment의 각 픽셀에 대한 색상을 계산

PixiJS

  • 2D WebGL Renderer로 canvas나 WebGL로 화면을 그림

핵심 개념

  • Application : PixiJS를 사용해 간편하게 그릴 수 있도록 renderer, stage 등을 만드는 역할
  • stage: 화면에 그려질 요소들을 가지고 있는 도화지
  • renderer: stage를 받아서 그리는 역할
  • Sprite : 화면에 렌더링되는 object로 texture에 기반을 두고 만들어진다
  • Texture : 이미지를 나타내는 정보를 저장한다

설치하기

yarn add pixi.js

배경 및 캐릭터 그리기

Application 선언하기

canvas만 사용할때는 canvas 태그를 선언해줬는데, pixiJS를 사용하기 위해서는 Application을 선언해줘야했다.

import { Application, Sprite } from "pixi.js";
import { createRef, useEffect, KeyboardEvent } from "react";

const app = new Application({
  backgroundColor: 0x000000,
  width: window.innerWidth,
  height: window.innerHeight,
});
const backgroundImage = Sprite.from("/map.png");
const characterImage = Sprite.from("/character_ArrowDown.png");
app.stage.addChild(backgroundImage);
app.stage.addChild(characterImage);

function Map() {
  const canvasRef = createRef<HTMLDivElement>();

  useEffect(() => {
    canvasRef.current?.appendChild(app.view);
    app.start();

    return () => {
      app.stop();
    };
  }, []);
  useEffect(() => {
    backgroundImage.position.set(-900, -900);
  }, []);

  const handleKeyDown = (e: KeyboardEvent) => {
    // 캐릭터 이동!
  };

  return <div ref={canvasRef} onKeyDown={handleKeyDown} tabIndex={0}></div>;
}

export default Map;

위 코드는 PixiJS를 사용해서 map.png, character_ArrowDown.png라는 이미지를 띄우는 코드이다.

const app = new Application({
  backgroundColor: 0x000000,
  width: window.innerWidth,
  height: window.innerHeight,
});
const backgroundImage = Sprite.from("/map.png");
const characterImage = Sprite.from("/character_ArrowDown.png");
app.stage.addChild(backgroundImage);
app.stage.addChild(characterImage);

앞에서 말했다시피 Application을 먼저 선언해주고 canvas에 올라갈 이미지를 Sprite object를 선언한다. Application에서는 width와 height를 설정해주지 않으면 default로 800x800으로 설정된다. Sprite에서는 from(source)를 사용해서 원하는 화면을 띄울 수 있다.

source는 위와 같은 type을 허용한다고 한다.

useEffect(() => {
  canvasRef.current?.appendChild(app.view);
  app.start();

  return () => {
    app.stop();
  };
}, []);

view는 renderer의 canvas element를 리턴해준다. 따라서 app.view를 canvasRef에 appendChild해주면 div 태그 내에 canvas 태그가 생성되어있는 것을 볼 수 있다.
start()는 render를 시작한다는 의미, stop()은 render를 끝낸다는 의미이다. 없어도 문제없이 돌아갔지만 혹시나 의도적으로 render를 멈춰야하는 로직이 필요할때 사용할 수 있을 것 같아서 우선 넣어뒀다.

useEffect(() => {
  BackgroundImage.position.set(-900, -900);
}, [BackgroundImage]);

Sprite의 position은 defalt가 (0, 0)이라 원하는 위치에 이미지를 그려주려면 렌더링될때 position을 설정해주어야한다. 추가로 Sprite.position.set(x, y)Sprite.x = x; Sprite.y = y로 대체할 수 있다.

여기까지 하면 canvas에 이미지가 그려지는걸 볼 수 있다! 그런데

이렇게 묘하게 기분나쁜 여백이 있었다.

알아보니까 canvas의 default display가 inline-block으로 설정되어있어서 그렇다고 한다. inline-block은 태그들을 가로로 정렬해주는데 태그들 사이에 enter가 들어가게 되면서 font-size에 의한 글자 폭을 가져서 여백이 생성되게 된다.

결국 block으로 display를 수정해주니까 여백이 사라졌다.

캐릭터 움직이기

캐릭터 움직이는건 canvas에서 움직이는 것과 크게 다르지 않았다. 그런데 Pixi를 사용했을때 편한 점은 drawImage를 호출해줄 필요가 없다는 점이었다. 캐릭터가 추가되거나 움직일때 계속해서 drawImage를 해줄 필요가 없이 Sprite를 add해주거나 position을 조정해주면 알아서 draw를 해준다.

const handleKeyDown = (e: KeyboardEvent) => {
  if (
    e.key !== "ArrowDown" &&
    e.key !== "ArrowUp" &&
    e.key !== "ArrowLeft" &&
    e.key !== "ArrowRight"
  )
    return;

  setCharacterPosition({ ...characterPosition, dir: e.key });

  let cnt = 0,
      requestId;

  const animation = () => {
    setCharacterPosition((prevCharacter) => {
      return {
        ...prevCharacter,
        x: prevCharacter.x + MOVE_LENGTH[e.key].x / 10,
        y: prevCharacter.y + MOVE_LENGTH[e.key].y / 10,
      };
    });
    setBackgroundPosition((prevBackground) => {
      return {
        ...prevBackground,
        x: prevBackground.x + -MOVE_LENGTH[e.key].x / 10,
        y: prevBackground.y + -MOVE_LENGTH[e.key].y / 10,
      };
    });

    requestId = requestAnimationFrame(animation);
    cnt++;

    if (cnt === 10) cancelAnimationFrame(requestId);
  };

  requestAnimationFrame(animation);
};

handleKeyDown()을 다음과 같이 수정하면 캐릭터가 이동하는 로직이 완성된다. 생략된 부분을 설명하자면 캐릭터의 절대적인 좌표를 저장하는 characterPosition state를 선언해줬고, 배경의 좌표를 이동시키기 위한 좌표 backgroundPosition state를 선언해줬다.

if (
  e.key !== "ArrowDown" &&
  e.key !== "ArrowUp" &&
  e.key !== "ArrowLeft" &&
  e.key !== "ArrowRight"
)
  return;

KeyboardEvent는 key를 속성으로 가지고 있다. 사용자가 누른 key의 value를 리턴하게 되는데, 여기서 방향키는 위와 같은 네 가지 이름을 가지고 있다. IE/Edge를 위해서는 Up, Down 등의 key도 고려해야 한다고 하는데 우선은,,,

setCharacterPosition({ ...characterPosition, dir: e.key });

캐릭터가 이동하는 방향에 따라 다른 이미지를 띄워주기 위해서는 direction 상태가 필요해서 다음과 같이 저장해줬다. (더 섬세한 캐릭터 움직임 이미지를 위해서는 연속된 이미지를 저장해서 보이는 이미지를 조정해주는 것 같은데 다음에 도전해볼것!!!!)

let cnt = 0, requestId;

const animation = () => {
  setCharacterPosition((prevCharacter) => {
    return {
      ...prevCharacter,
      x: prevCharacter.x + MOVE_LENGTH[e.key].x / 10,
      y: prevCharacter.y + MOVE_LENGTH[e.key].y / 10,
    };
  });
  setBackgroundPosition((prevBackground) => {
    return {
      ...prevBackground,
      x: prevBackground.x + -MOVE_LENGTH[e.key].x / 10,
      y: prevBackground.y + -MOVE_LENGTH[e.key].y / 10,
    };
  });

  requestId = requestAnimationFrame(animation);
  cnt++;

  if (cnt === 10) cancelAnimationFrame(requestId);
};

requestAnimationFrame(animation);

여기서 MOVE_LENGTH는 입력된 key에 따라 x좌표는 얼만큼, y좌표는 얼만큼 움직여야하는지 정의해놓은 것이다. 예를 들어서 ArrowLeft가 입력되면 character는 { x: -40, y: 0 }만큼 움직이게 되고, background는 character의 반대로 움직이게 보여져야하므로 { x: 40, y: 0 }으로 움직이게 된다.

canvas로 구현할 때는 setInterval로 캐릭터의 자연스러운 움직임을 구현했는데, 이번에는 requestAnimationFrame을 사용해보았다.

requestAnimationFrame?

  • 리렌더링이 끝나지 않았는데 애니메이션을 수행하는 명령이 내려진다면 원하는대로 애니메이션이 부드럽게 진행되지 않는다. 리페인트가 끝난 후 적용할 애니메이션을 requestAnimationFrame의 콜백으로 넣어주면 자연스러운 애니메이션이 생성된다.
  • setInterval이나 setTimeout과 달리 프레임 생성 초기 단계에 맞춰 애니메이션이 호출되어서 더 부드러운 동작이 가능하다.
  • 콜백의 수는 보통 1초에 60회이고, 대부분의 브라우저에서는 W3C 권장사항에 따라 디스플레이 주사율과 일치하게 된다고 한다.
  • cancelAnimationFrame으로 requestAnimationFrame을 취소할 수 있다.

움직여야하는 거리를 10만큼 나눠서 움직이게 만들어 애니메이션을 수행하게 해주었다.

뾰로롱

다음에는 socket.io을 사용해서 여러명의 캐릭터를 움직이게 만들어볼거다.


참고자료
https://webglfundamentals.org/webgl/lessons/ko/
https://pixijs.download/dev/docs/PIXI.html
https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame
https://blog.logrocket.com/getting-started-pixijs-react-create-canvas/

0개의 댓글