ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [이인용] 프로젝트 회고
    project 2020. 8. 19. 18:44

     

     

    0px

     

    이인용

    2020. 07. 14 - 08. 11

     

     

    concept

    <canvas> + Socket.io = Online Game

    추억의 이인용 오락실 게임을 온라인으로 즐기자!

    출처 - 한겨레 http://www.hani.co.kr/arti/PRINT/724210.html

     

    position

    프런트엔드

     

    프런트엔드로서 게임 화면을 구성하는 작업을 맡았습니다.

    게임을 구현하며 겪게 된 두 가지 문제와 발견을 정리해봅니다.

     

    문제

    1. 두 유저의 다른 클라이언트 사이즈

    2. 캔버스와 소켓 조합의 무거움

     

    발견

    1. 어려움 속에서 발견된 게임

    2. 간단한 기능의 커다란 즐거움

     

     

     

    첫 번째 문제

    두 유저의 다른 클라이언트 사이즈

     

      제작되는 게임 중에는 순발력이 있어야 하는 두더지 잡기, 슈팅 게임이 있습니다. 이런 게임은 플레이 화면이 조금이라도 다르면 안 됩니다. 한 끗 차이로 승부가 갈리는 게임이기 때문이지요. 그렇기 때문에 크기의 문제는 아주 중요했습니다. 

    <!-- index.html -->
    <body style="height: 50vw; width: 100vw;">
    // 화면크기 재설정 함수
    resize() {
      this.stageWidth = document.body.clientWidth;
    
      this.canvas.width = Math.floor(this.stageWidth / 4);
      this.canvas.height = Math.floor(this.stageWidth / 2.4);
    
      this.setState({ width: this.canvas.width, height: this.canvas.height });
    }

      캔버스의 크기는 resize 함수로 어떠한 크기의 클라이언트라도 같은 비율로 정해집니다. body의 크기를 기준으로 canvas의 크기가 설정되고 사이즈 값으로 state를 바꿉니다. resize함수는 최초로 컴포넌트가 마운트 되는 시점에서만 실행됩니다. 'resize' 이벤트 리스너로 유저가 클라이언트의 크기를 변경할 때마다 캔버스의 크기도 변경될 수도 있습니다. 그러나 게임 중인 캔버스의 요소들이 변경되는 사이에 감당할 수 없는 많은 오류를 발생시키기 때문에 마운트 되는 순간의 사이즈를 기준으로 고정됩니다. 

     

      캔버스 안의 요소들은 state의 width 값에 의존하게 되었습니다. 핑퐁게임의 공에 대한 값을 살펴보면 width 값을 기준으로 ballRadius와 ballSpeed가 정해집니다.

    // PongGame.js
    this.ballRadius = this.state.width / 20;
    this.ballSpeed = this.state.width / 100;
    
    this.ball = new Ball(this.state.width, this.state.height, this.ballRadius, this.ballSpeed)
    // Ball.js
    export class Ball {
      constructor(stageWidth, stageHeight, radius, speed) {
        this.radius = radius;
        this.speed = speed;		// 기본속도
        this.vx = speed;		// 증가값
        this.vy = speed;
        this.x = stageWidth / 2;	// 시작위치
        this.y = stageHeight / 2;	
        this.stop = true;		// 움직임 제어
      }
      ...
    }

     

      이런 방법은 width를 나누는 값에 신경을 써야 했습니다. 캔버스 안의 요소들의 크기는 state의 width값으로 조절하면 큰 문제가 없습니다. 하지만 움직임이 있는 경우 소수점 밑의 숫자가 쌓여서 격차를 만들기 때문에 큰 문제가 됩니다. 핑퐁게임의 경우에는 공 속도의 데이터가 시간이 갈수록 누적되기 때문에 소수점 자리의 숫자들이 큰 영향을 주었습니다. 이를 위해서 Math.floor() 함수를 사용하고 소수점 아래 숫자를 적게 만드는 수로 나누어야 했습니다.

     

      블록 움직임에 대한 데이터는 주고받아야 하는 데이터이기 때문에 조금 다른 방식으로 위치가 결정되었습니다. canvas에서의 마우스 위치( e.layerX )를 받아서 블록의 위치를 정하고 상대에게 보내기 위한 데이터는 캔버스 화면에 대한 블록 위치의 비율을 계산하여 보내줍니다. 상대 클라이언트에서는 블록 위치의 비율을 자신의 클라이언트에 맞게 계산하여 블록의 위치를 정합니다.

    // 자신의 데이터를 보낸다
    this.canvas.addEventListener('mousemove', (e) => {
      if(e.movementX !== 0){
        this.blockPosX = Math.floor(e.layerX - this.blockSizeX)
      }
      let posPercent = Number((this.blockPosX / this.state.width).toFixed(2));
      if(this.prePercent !== posPercent){
        socket.emit('mouseMove', posPercent);
        this.prePercent = posPercent
      }
    });
    // 상대의 데이터를 받는다
    socket.on('rivalMove', (e) => {
      this.RivalPosX = ((1 - e) * this.state.width) - this.RivalSizeX
    });

     

     

     

    두 번째 문제

    캔버스와 소켓 조합의 무거움

     

      캔버스는 window.requestAnimationFrame() 메서드로 애니메이션을 업데이트하는 함수를 반복적으로 호출합니다. 콜백 함수는 캔버스의 요소들을 지우고 그리는 과정을 거치는데 메서드로 호출되는 수는 일반적으로 디스플레이 주사율과 일치하여 보통 1초에 60회입니다. 캔버스 화면에 하나의 점이 있더라도 바쁘게 함수를 실행하는 것입니다. 그렇기 때문에 많은 데이터를 실시간으로 주고받는 일을 피해야 했습니다. 

      마우스 움직임의 데이터가 큰 문제였습니다. 마우스로 블록을 움직일 때 조금씩 버벅거리는 문제 때문에 마우스 데이터를 확인해보았더니 역시나 필요 없는 데이터가 보내지고 있었습니다. 이를 마우스 움직임의 조건문 ( e.movementX !== 0 ) 으로 분별하고 연산 후에 중복을 피하는 조건문 ( this.prePercent !== posPercent ) 으로 소켓 통신되는 데이터를 줄였습니다.

     

    this.canvas.addEventListener('mousemove', (e) => {
      if(e.movementX !== 0){  // 조건1
        this.blockPosX = Math.floor(e.layerX - this.blockSizeX)
      }
      let posPercent = Number((this.blockPosX / this.state.width).toFixed(2));
      if(this.prePercent !== posPercent){  // 조건2
        socket.emit('mouseMove', posPercent);
        this.prePercent = posPercent
      }
    });

     

     

      슈팅 게임에서는 발사각의 데이터를 최적화했습니다. 유저가 마우스로 정하는 발사각을 좌측(-1), 중앙(0), 우측(1)으로 제한시킵니다. 한 명의 유저가 여러개의 총알을 발사할 수 있기 때문에 총알의 데이터를 클라이언트에서 따로 관리합니다. 소켓 통신으로 발사각에 대한 간단한 정보를 받은 클라이언트는 총알을 생성합니다. 발사각이 적용된 총알은 생성된 그대로 반대편까지 날아가게 됩니다. 

     

    // 조준
    this.canvas.addEventListener('mousemove', (e) => {
      let moveRight = e.layerX + this.state.width / 15;
      let moveLeft = e.layerX - this.state.width / 15;
    
      // 처리할 연산 줄이기
      if (moveRight < this.preMousePos || moveLeft > this.preMousePos) {
        this.mouseX = e.layerX;
        this.mouseY = e.layerY;
        this.angle = this.calc();
        // 왼쪽 조준
        if (this.angle > -40 && this.angle < 60) {
          this.moveX = this.BulletSpeed * -1;
          this.moveY = this.BulletSpeed;
          this.aim = -1;
        }
        // 중앙 조준
        else if (this.angle >= 60 && this.angle <= 120) {
          this.moveX = 0;
          this.moveY = this.BulletSpeed * 2;
          this.aim = 0;
        }
        // 오른쪽 조준
        else if ((this.angle > 120 && this.angle < 180) || this.angle < -140) {
          this.moveX = this.BulletSpeed;
          this.moveY = this.BulletSpeed;
          this.aim = 1;
        }
        this.preMousePos = e.layerX;
      }
    });
    // 발사각 계산 함수
    calc() {
      let BulletX = this.blockPosX + this.blockSizeX / 2;
      let BulletY = this.state.height - 40;
      let width = BulletX - this.mouseX;
      let height = BulletY - this.mouseY;
      let angle = Math.floor((Math.atan2(height, width) * 180) / Math.PI);
      return angle;
    }

     

     


     

     

     

    첫번째 발견

    어려움 속에서 발견된 게임

     

      핑퐁 게임은 기획 단계에서부터 데이터 최적화를 생각했습니다. "두 유저의 클라이언트에서 블록 위치가 같다면 튕겨져 나가는 공의 움직임도 같을 것이다."라는 가설을 세우고 블록 위치 데이터만 소켓 통신이 일어나도록 만들기 시작한 겁니다. 그러나 프로젝트가 진행될수록 가설이 잘못되었다는 것을 깨닫습니다. 두 클라이언트에서 공의 움직임을 완벽히 같게 만들 수 없었습니다. 또한 시간이 지체되어 다른 방법을 찾아야 했습니다.

     

      공의 가속도가 가장 문제였습니다. 두 클라이언트에서 가속도의 양을 완전히 같게 만들 수 없었습니다. 그렇다고 핑퐁게임에서 중요한 요소인 가속도를 없앨 수도 없었습니다. 대신에 가속도가 필요없는 게임이 생각납니다. 공이 일방적으로 움직여서 다시 되돌아오지 않는 슈팅 게임입니다. 슈팅 게임의 구현은 핑퐁 게임의 경험을 바탕으로 빠르게 구현되었습니다. 구현을 마치고 보니 핑퐁 게임보다 재미있는 요소가 많았습니다.

     

     

     

     

    두번째 발견

    간단한 기능의 커다란 즐거움

      

    게임 도중에 빠르고 효과적인 감정표현을 위해서 GIF 이모지를 구현했습니다.

     

     

      이 기능은 정말 간단한 기술로 빠르게 만들어졌지만 가장 반응이 좋습니다. 온라인 게임이기 때문에 도중에 이모지로 자신의 감정을 표현하고 상대방을 약올리는 용도로 사용되어 게임의 즐거움을 더해주었습니다. 생각보다 좋은 반응을 통해서 새로운 관점을 얻게 되었습니다. 우리가 엄청난 기술을 선보이는 것도 중요하지만 때론 감정이 담긴 간단한 기능이 유저들에게 강한 인상을 준다는 사실을 알게 되었습니다. 

     

     

     

     

     

    댓글

Designed by CHANUL