1. Client부분 Node.js에서 JSON 파일 읽어오기
Clinet가 JSON 파일을 읽어오기 위해
import itemJson from './assets/item.json'
위와 같이, 경로를 작성할 경우
Failed to load module script: The server responded with a non-JavaScript MIME type of “경로/json”. Strict MIME type checking is enforced for module scripts per HTML spec.
Web에서 위와 같은 오류 메시지가 출력 됐다.
위의 오류 메시지를 분석해보면,
Node.js는 javaScript 형식으로 import를 통해 JSON 파일을 읽어올 경우
JavaScript로 JSON 파일을 해석해 해석할 수 없다는 오류 메시지였다.
해결 방법으로는
1) Json 파일을 .js 파일로 export해 사용하기
exprot default
{
"fruit" : "Apple",
"size" : "Large",
"color" : "Red"
}
위와 같이 JSON 파일을 개발자가 .js 파일로 객체를 만들어 export 하는 방법
2) Fetch(URL 경로 사용하기)
fetch(‘http://www.domain.com/test.json')
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(JSON.stringify(myJson));
});
JSON 파일 자체가 Clinet 부분에 있을 경우 Fetch로 해당 JSON 파일의 경로 자체를
읽어와 사용하기
3) import 할 때 with { type : 'json'}
import stageJson from './assets/stage.json' with { type: 'json' };
위와 같이 import 할 때, with로 이 파일 자체를 json 타입으로 읽어오겠다는
내용을 추가하는 방법
총 3개가 존재했다.
나는 3번 방식으로 Clinet도 JSON을 읽어올 수 있게 만들어
Stage.Json, Item.Json, Item_Unlock.Json을 읽어올 수 있었다.
2. 시간 당 점수에서 아이템 점수 추가에 따른 스테이지 클리어와 서버 요청 문제
게임이 현재 시간 당 점수를 바탕으로 Stage 클리어 조건을 만족할 경우
다음 스테이지로 이동 및 서버에 요청을 보내는 상황에서
아이템 점수 추가를 한 결과 기존 코드
update(deltaTime) {
this.score += deltaTime * 0.01;
// 100의 배수이면서, this.stageChange가 true이면 실행
if (Math.floor(this.score) % 100 === 0 && Math.floor(this.score) !== 0 && this.stageChange) {
this.stageChange = false;
// currentStage와 targetStage 계산
const currentStage = 1000 + Math.floor(this.score / 100) - 1;
const targetStage = currentStage + 1;
// 이벤트 전송
sendEvent(11, { currentStage: currentStage, targetStage: targetStage });
// 1초 후 stageChange를 다시 true로 설정
setTimeout(() => {
this.stageChange = true;
}, 1000); // 1초 후에 실행
}
}
에서 문제가 발생했다.
ex)
// 아이템 먹을 경우 점수
getItem(itemId) {
this.score += itemJson.data[itemId - 1].score;
sendEvent(201, { stageId: this.currentStage.id, itemId });
}
위와 같이 아이템을 먹어 score가 올라갈 경우
유저가 현재 점수 95점에서 10점 짜리 아이템을 먹을 경우
105점과 같이 점수가 100으로 나눠 떨어지지 않아,
Client가 스테이지 이동을 Server에게 요청을 보내지 않는 문제가 존재했다.
해결 방법 :
import { sendEvent } from './Socket.js';
import stageJson from './assets/stage.json' with { type: 'json' };
import itemJson from './assets/item.json' with { type: 'json' };
import { unLockItem } from './index.js';
class Score {
score = 0;
HIGH_SCORE_KEY = 'highScore';
stageChange = true;
// 마지막 스테이지 점수 조건
stages = stageJson.data;
currentStage = this.stages[0];
targetStage = this.stages[this.currentStage.scorePerSecond];
update(deltaTime) {
this.score += deltaTime * 0.001 * this.currentStage.scorePerSecond;
// 1초당 scorePerSecond 만큼 점수 얻음
const currendScore = Math.floor(this.score);
//console.log('다음 스테이지 : ', this.targetStage.score, ', 현재 스테이지 : ', this.score);
if (this.targetStage) {
if (this.targetStage.score <= currendScore && this.stageChange) {
// 0 아닌 100 배수 단위 점수이면,
console.log('동작 유무 확인');
this.stageChange = false;
// 중복 실행 방지
// 다음 스테이지 점수 배율
unLockItem(this.targetStage.id);
sendEvent(11, { currentStage: this.currentStage, targetStage: this.targetStage });
sendEvent(101, {
currentStageId: this.currentStage.id,
targetStageId: this.targetStage.id,
});
this.currentStage = this.targetStage;
this.targetStage = this.stages[this.currentStage.scorePerSecond];
}
}
위의 코드와 같이 Client가 읽어 온 Stage 관련 JSON 파일을 바탕으로
if (this.targetStage.score <= currendScore && this.stageChange) {
스테이지 이동 조건 보다 점수가 높을 경우 스테이지를 이동할 수 있게 만들어줬다.
그 후,
this.currentStage = this.targetStage;
this.targetStage = this.stages[this.currentStage.scorePerSecond];
현재 스테이지를 다음 스테이지로 변경해주고
다음스테이지는 그 다음 스테이지 정보로 바꿔
여러 스테이지가 존재할 때 스테이지 이동 조건을 충족할 경우
계속 스테이지를 이동할 수 있게 만들어줬다.
3. 서버에서 아이템 점수를 추가해 Clinet 검증하기
const moveStageHandler = (userId, payload) => {
// 오름차순 -> 유저의 진행한 가장 큰 스테이지 ID를 확인
currentStages.sort((a, b) => a.id - b.id);
const playerStage = currentStages[currentStages.length - 1];
// 서버가 아는 현재 유저 스테이지 가져오기
// 서버(currentStage) VS 클라이언트(payload.currendStage) 비교
if (playerStage.id !== currentStage.id)
return { status: 'fail', message: 'Current Stage Mismatch' };
const serverTime = Date.now(); // 현재 타임스탬프
const elapsedTime = ((serverTime - playerStage.timestamp) / 1000) * playerStage.scorePerSecond;
// ((현재 시간 - 스테이지 시작 시간) / 1000) * 시간 당 점수
// 로 서버가 아는 유저 점수 구현
const userEatedItem = getUserItem(userId, currentStage.id).reduce((acc, cur) => {
return (acc += cur);
}, 0);
console.log('유저가 먹은 아이템 점수 합 : ', userEatedItem);
if (elapsedTime > 105 + userEatedItem) {
return { status: 'fail', message: 'Invalid elapsed time' };
}
// 오차 범위
if (!stages.data.some((stage) => stage.id === targetStage.id))
return { status: 'fail', message: 'Target Stage Not Found' };
// 스테이지 유무 확인
setStage(userId, targetStage, serverTime);
console.log('Stage : ', getStage(userId));
return { status: 'success' };
}
위와 같이 서버가 클라이언트가 플레이한 시간을 바탕으로 점수 구현 및 아이템 점수를 추가해줬을 경우
유저가 아이템을 먹지 않았을 때, getUserItem(userId, currentStage.id)가 undefined라
const userEatedItem = getUserItem(userId).reduce((acc, cur) => {
return (acc += cur);
}, 0);
reduce 함수가 동작하지 않는 문제로 서버가 다운되는 문제가 존재했다.
해결 방법 :
const moveStageHandler = (userId, payload) => {
const { stages } = getGameAssets();
const { currentStage, targetStage } = payload;
// 유저의 현재 스테이지 정보를 가져온다.
let currentStages = getStage(userId);
if (!currentStages.length) {
return { status: 'fail', message: 'No Stages Found For User' };
}
// 오름차순 -> 유저의 진행한 가장 큰 스테이지 ID를 확인
currentStages.sort((a, b) => a.id - b.id);
const playerStage = currentStages[currentStages.length - 1];
// 서버가 아는 현재 유저 스테이지 가져오기
// 서버(currentStage) VS 클라이언트(payload.currendStage) 비교
if (playerStage.id !== currentStage.id)
return { status: 'fail', message: 'Current Stage Mismatch' };
const serverTime = Date.now(); // 현재 타임스탬프
let elapsedTime = ((serverTime - playerStage.timestamp) / 1000) * playerStage.scorePerSecond;
// ((현재 시간 - 스테이지 시작 시간) / 1000) * 시간 당 점수
// 로 유저가 시간 당 얻은 점수 구하기
const playerEatedItem = getUserItem(userId, currentStage.id) || [];
console.log('플레이어 아이템 : ', playerEatedItem);
elapsedTime += playerEatedItem.reduce((acc, cur) => acc + cur, 0);
// 유저가 먹은 점수 더해주기
const lastItem = playerEatedItem[playerEatedItem.length - 1] || 0;
// 마지막에 먹은 점수로 유효 범위 정하기
if (elapsedTime > 105 + lastItem) {
return { status: 'fail', message: 'Invalid elapsed time' };
}
// 오차 범위
if (!stages.data.some((stage) => stage.id === targetStage.id))
return { status: 'fail', message: 'Target Stage Not Found' };
// 스테이지 유무 확인
setStage(userId, targetStage, serverTime);
return { status: 'success' };
};
getUserItem(userId, currentStage.id)를 가져왔을 때 바로 reduce함수를 실행하는 것이 아닌
const playerEatedItem = getUserItem(userId, currentStage.id) || [];
위와 같이 undefined, false, 0, Nan, Null일 경우 ||(기준으로) 오른쪽 구문이 실행되게 만들어
playerEatedItem에 [] 빈 배열이 들어갈 수 있게 만들어줬다. 그 다음,
elapsedTime += playerEatedItem.reduce((acc, cur) => acc + cur, 0);
서버가 측정하는 경과 시간에 reduce함수를 적용시켜
undefined와 같은 문제를 해결할 수 있었다.
위와 같은 경험을 통해,
const gameEndHandler = (uuid, payload) => {
// 클라이언트에서 받은 게임 종료 시 타임스탬프와 총 점수
const { timestamp, score } = payload;
console.log('timestamp : ', timestamp);
const stages = getStage(uuid);
if (!stages.length) {
return { status: 'fail', message: 'No stages found for user' };
}
//총 점수 계산
let totalScore = 0;
// 각 스테이지별 시간 당 점수 및 유저가 먹은 아티템 점수 합
stages.forEach((stage, index) => {
let stageEndTime;
if (index === stages.length - 1) {
// 마지막 스테이지의 경우 종료 시간이 게임의 종료 시간
stageEndTime = timestamp;
} else {
// 다음 스테이지의 시작 시간을 현재 스테이지의 종료 시간으로 사용
stageEndTime = stages[index + 1].timestamp;
}
const stageDuration = (stageEndTime - stage.timestamp) / 1000; // 스테이지 지속 시간 (초 단위)
totalScore += stageDuration * stage.scorePerSecond; // 시간 당 점수로 변경
const playerEatItem = getUserItem(uuid, stage.id) || [];
// 유저가 스테이지에 먹은 아이템
totalScore += playerEatItem.reduce((acc, cur) => acc + cur, 0);
});
console.log('클라이언트가 제공한 코드 : ', score, ' : 서버가 측정한 시간', totalScore);
// 점수와 타임스탬프 검증 (예: 클라이언트가 보낸 총점과 계산된 총점 비교)
// 오차범위 5
if (Math.abs(score - totalScore) > 5) {
return { status: 'fail', message: 'Score verification failed' };
}
return { status: 'success', message: 'Game ended successfully', score };
};
게임이 끝났을 경우, 유저의 점수를 검증하는 과정에서 비교적 쉽게 코드를 작성할 수 있었다.