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로 분리 과정을 설명한다!
-
생성자 생성
- 일단 기초 스탯과 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)
-
움직임 메서드
- 움직임 감지 시 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)});
-
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();
-
작동하지 않는 기능 확인하기
- 충돌 기능이 정상작동 하지 않는 것 같다
// 충돌 감지 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 모듈이 Node.js에서만 호환되기에 브라우저에선 안됨)
fetch 사용하기
-
상위 구조에서 file을 참조할 수 어려우므로 public에 Data Table을 복사 해둔다!
-
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; };
-
사용할 파일에서 참조를 통해 값들을 이용한다!
한줄 평 + 개선점
-
파일들을 분리하고, 데이터 테이블을 가져오는 방법을 생각하는데만 대부분의 시간이 허비 되었다..
-
시간 배분을 다시 생각해서 필요한 기능들만 최대한 구현을 마쳐야 겠다