Front-End 개발

파일 분리

  • 파일들을 효율적으로 관리하기 위해 index.html에 존재하는 script/css 들을 기능별로 분리해준다!

script / css 분리

  • 현재 index.html에 존재하는 js(script)와 css 를 분리해준다!
<!DOCTYPE html>
    <html lang="ko">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Vampire Survivors Prototype</title>
        <link rel = "stylesheet" href="assets/css/main.css"/>
    </head>
    <body>
        <div id="score">Score: 0</div>
        <canvas id="gameCanvas"></canvas>
        <script src = "./index.js" type="module"></script>
  </body>
</html>
  • 파일 분리로 인해 생기는 CORS 오류들은 웹서버를 통해 서비스하는 것으로 해결했다

class 분리

  • index.js로 script들을 분리했으나 이들을 세분화하지 않았기에 class를 통해 역할별로 분리를 해준다.

    => 중간에 상수로 이용했던 수들을 변수로 지정해주어 확장성을 높여준다.

  • 모든 분리 방법을 설명할 순 없으므로 Player로 분리 과정을 설명한다!

  1. 생성자 생성

    • 일단 기초 스탯과 Player 기능에 필요한 것들을 합쳐주었다.
     class Player {
         constructor(ctx, width, height, health,damage, speed) {
             // 메서드에 이용하기위해 부여받음
             this.ctx = ctx
             this.canvas = ctx.canvas
    
             // 위치는 중앙 고정
             this.x = this.canvas.width / 2,
             this.y = this.canvas.height / 2,
             // size를 넓이와 높이로 분리하여 변수로 지정
             this.width = width,
             this.height = height,
             this.health = health,
             this.damage = damage,
             this.speed = speed
             // "플레이어가 움직이는걸" 제어하기 위해 병합
             this.keys = {};
         }
     }
    
    • 가변성을 위해 변수로 승격한 상수들을 index.js에서 지정해주며 생성한다
     let player = new Player(ctx,20,20,3,3,300)
    
  2. 움직임 메서드

    • 움직임 감지 시 if 구문에 입력 key를 추가할 때 확장성이 낮아 보여 로직을 리팩토링하였다.
     // 키 맵핑
     keydown(key) {
         this.keys[key] = true
     }
     keyup(key) {
         this.keys[key] = false
     }
     // 플레이어 움직임 제어
     moving(deltaTime) {
         // 입력되는 값들을 배열로 저장
         const up = ["ArrowUp","W","w",""];
         const down = ["ArrowDown", "S", "s", ""];
         const left = ["ArrowLeft", "A", "a", ""];
         const right = ["ArrowRight", "D", "d", ""];
         // keys 에 있는 입력 키들을 확인하기 위해 배열생성
         const keys = Object.keys(this.keys)
         // array.some과 array.includes를 통해 입력된 key 확인
         if (keys.some((e) => up.includes(e) && this.keys[e]) && this.y > 0) 
             this.y -= this.speed * deltaTime;
         if (keys.some((e) => down.includes(e) && this.keys[e]) && this.y < this.canvas.height - this.height) 
             this.y += this.speed * deltaTime;
         if (keys.some((e) => left.includes(e) && this.keys[e]) && this.x > 0) 
             this.x -= this.speed * deltaTime;
         if (keys.some((e) => right.includes(e) && this.keys[e]) && this.x < this.canvas.width - this.width) 
             this.x += this.speed * deltaTime;
     }
    
    • player.keydown/up () 메서드를 이용해 index.js에서 키 할당
     window.addEventListener('keydown', (e) => {player.keydown(e.key)});
     window.addEventListener('keyup', (e) => {player.keyup(e.key)});
    
  3. update와 draw 메서드 구현

    • 원래 index.js의 update함수 내부에서 진행되던 동작들을 player의 메서드로 분리해줌
      // 확장성을 위해 핸들러처럼 사용
     update(deltaTime) {
         this.moving(deltaTime)
     }
    
     // 플레이어를 화면에 그리기
     draw() {
         //플레이어
         this.ctx.fillStyle = 'blue';
         this.ctx.fillRect(this.x, this.y, this.width, this.height);
    
         //체력바
         this.ctx.fillStyle = 'black';
         this.ctx.fillRect(this.x, this.y - 10, this.height, 5);
         // 체력 1칸당 비율
         const healthPerUnit = this.width / 3;
         // 체력바 그리기
         for (let i = 0; i < 3; i++) {
             // 현재 체력 
             if (i < this.health) {
                 this.ctx.fillStyle = 'green';
                 this.ctx.fillRect(this.x + i * healthPerUnit, this.y - 10, healthPerUnit, 5);
             }
             if (i < 2) {
                 // 구분 줄
                 this.ctx.fillStyle = 'black';
                 this.ctx.fillRect(this.x + (i + 1) * healthPerUnit, this.y - 10, 2, 5);
             }
         }
     }
    
    • index.js에 연결해주기
     /* update 함수 내부 */
      // 업데이트
     player.update(deltaTime);
     // 그려주기
     player.draw();
    
  4. 작동하지 않는 기능 확인하기

    오류확인

    • 충돌 기능이 정상작동 하지 않는 것 같다
     // 충돌 감지
     const colliedWith = (a, b) => {
         // a의 왼쪽이 b의 오른쪽보다 왼쪽에 있을 때
         return (a.x < b.x + b.size &&
             // a의 오른쪽이 b의 왼쪽보다 오른쪽에 있을 때
             a.x + a.size > b.x &&
             // a의 위가 b의 아래보다 위에 있을 때
             a.y < b.y + b.size &&
             // a의 아래가 b의 위보다 아래에 있을 때
             a.y + a.size > b.y)
     }
    
    • Player의 size를 height과 width로 변경해서 그런 것 같다!
      (item 과 monster도 바꿔줘야하기 때문에 차차 해결하면 될 것 같다)

공통 데이터 테이블 가져오기

  • 클라이언트 쪽에서도 이용할 데이터를 불러와야 한다!

서버의 fs 모듈 가져오기

  • 현재 서버쪽에서는 assets.js 를 통해 json 파일의 데이터 테이블 정보를 가져온다!

  • 이를 응용해서 클라이언트에 적용해보자!(복붙 ON)

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

//전역변수
let gameAssets = {};
//현재 파일의 절대경로 찾기
const __filename = fileURLToPath(import.meta.url);
//디렉토리 경로(현재 파일위치) 추출
const __dirname = path.dirname(__filename);
// 현재 파일위치 기준으로 assets 폴더 찾기(../../ => 최상위 폴더로 이동)
const basePath = path.join(__dirname, '../../assets')
//파일 읽기 함수
const readFileAsync = (filename) => {
    return new Promise ((resolve, reject) => {
        fs.readFile(path.join(basePath, filename), 'utf8', (err,data) => {
            // 에러의 경우 실패 처리 후 반환
            if (err) {
                reject(err);
                return;
            }
            // 성공 시 JSON 형태로 변환하여 반환
            resolve(JSON.parse(data))
        })
    })
};
//파일 로드!
export const loadGameAssets = async () => {
    try {
        // 파일들을 Promise.all() 을 이용해 병렬적으로 가져옴
        const [stages, unlock, item, monster] = await Promise.all([
            readFileAsync('stage.json'),
            readFileAsync('unlock.json'),
            readFileAsync('item.json'),
            readFileAsync('monster.json'),
        ]);
        gameAssets = { stages, unlock, item, monster }
        return gameAssets
    } catch(err) {
        throw new Error('Failed to load game assets: '+ err.message)
    }
}
//가져온 파일 데이터 읽기
export const getGameAssets = () => {
    return gameAssets;
};

fs오류

  • 아.. 브라우저에선 다른 방법을 써야 한다네요
    (fs 모듈이 Node.js에서만 호환되기에 브라우저에선 안됨)
fetch 사용하기
  1. 상위 구조에서 file을 참조할 수 어려우므로 public에 Data Table을 복사 해둔다!

  2. fetch를 이용하여 Data Table에서 정보를 가져와 JSON.parse를 통해 객체로 변환해준다!

     //전역변수
     let gameAssets = {};
    
     //파일 읽기 함수
     const readFileAsync = async (filename) => {
         // 성공 시 JSON 형태로 변환하여 반환
         return fetch(`assets/json/${filename}`)
             .then((response) => response.json())
             .catch((err) => {throw new Error(err)})
     };
     export const loadGameAssets = async () => {
         try {
             // 파일들을 Promise.all() 을 이용해 병렬적으로 가져옴
             const [stages, unlock, item, monster] = await Promise.all([
                 readFileAsync('stage.json'),
                 readFileAsync('unlock.json'),
                 readFileAsync('item.json'),
                 readFileAsync('monster.json'),
             ]);
             console.log(stages)
             gameAssets = { stages, unlock, item, monster }
             return gameAssets
         } catch (err) {
             throw new Error('Failed to load game assets: ' + err.message)
         }
     }
     export const getGameAssets = () => {
         return gameAssets;
     };
    
  3. 사용할 파일에서 참조를 통해 값들을 이용한다!

한줄 평 + 개선점

  • 파일들을 분리하고, 데이터 테이블을 가져오는 방법을 생각하는데만 대부분의 시간이 허비 되었다..

  • 시간 배분을 다시 생각해서 필요한 기능들만 최대한 구현을 마쳐야 겠다