본문 바로가기

부트캠프

webSocket_Project 마무리

Avoid_Planet

 

기존 공룡 점프 게임에서 날아다니는 슈팅게임을 만들고싶어서 우주를 배경으로 우주선이 날아다니는 게임을 만들었다.
  • 스테이지가 넘어가는 로직
//현재 점수와 게임에셋에 있는값과 비교하여 에셋에 있는값보다 크다면 stageIndex를 +1해주는 방식으로 구현

this.score += deltaTime  * 0.01;
    if (Math.floor(this.score) > stages.data[this.stageIndex].score) {
      this.stageChange = false;

      console.log("현재 스테이지:", stages.data[this.stageIndex].id);
      if (this.stageIndex + 1 < stages.data.length) {
        sendEvent(11, {
          currentStage: stages.data[this.stageIndex].id,
          targetStage: stages.data[this.stageIndex + 1].id,
          clientScore: this.score,
          stageIndex: this.stageIndex,
        });
        this.stageIndex += 1;

 

  • 스테이지 상승시 시간당 얻는 스코어 상승
//간단하게 게임 스테이지가 올라가면 현재 deltaTime에 게임에셋에 정의해둔 scorePerSecond를 더해줌
this.score += (deltaTime + stages.data[this.stageIndex].scorePerSecond) * 0.01;
  • 현재 스테이지 표시
    //현재 스테이지를 표시하는 로직 1.5초후 사라지는 로직
    this.stageMessageVisible = true;
    this.stageMessageTimer = 0; // 타이머 초기화
    
    
    //스테이지 메시지 타이머 업데이트
    if (this.stageMessageVisible && !stages.data.length - 1) {
      this.stageMessageTimer += deltaTime;
      if (this.stageMessageTimer > 1500) {
        // 1.5초 후에 메시지 사라짐
        this.stageMessageVisible = false;
      }
      
          // 스테이지 메시지 가운데 표시

    if (this.stageMessageVisible) {
      const fontSize = 100 * this.scaleRatio;
      this.ctx.font = `${fontSize}px Verdana`;
      this.ctx.fillStyle = "purple";
      const x = this.canvas.width / 3.1;
      const y = this.canvas.height / 1.9;
      this.ctx.fillText(`${this.stageIndex * 340}억 은하`, x, y);
    }
    
      // "은하" 텍스트를 맨 왼쪽에 배치
    const stageText = `${this.stageIndex * 340}억 은하`;
    const stageX = 20 * this.scaleRatio; // 왼쪽 여백
    this.ctx.fillText(stageText, stageX, y);

    this.ctx.fillText(scorePadded, scoreX, y);
    this.ctx.fillText(`HI ${highScorePadded}`, highScoreX, y);

 

  • 아이템 랜덤 생성
 //itemId를 받아와 게임에셋에 items의 index를 비교해 랜덤하게 생성하도록하고
 getItem(itemId) {
    for (let i = 0; i < items.data.length; i++) {
      if (itemId === items.data[i].id) {
        this.score += items.data[i].score;
        sendEvent(10, {
          currentItem: items.data[i].id,
          stageIndex: this.stageIndex,
        });
      }
    }
  }
  
  //서버에서 스테이지별 itemUnlock을 설정하여 특정 스테이지에 특정아이템만나오도록 수정
  //아이템 검증
  
  if (
    !itemUnlock.data[payload.stageIndex - 1].item_id.some(
      (item) => item === payload.currentItem - 1,
    )
  )
  {
   return { status: "fail", message: "아이템 없음" };
   }
  
  let currentItems = getItems(userId);
  if (!currentItems) {
    return { status: "fail", message: "스테이지에 있을수 없는 아이템" };
  }
  
  //에셋에 itemFrequency를 boolean 값으로 추가하여
  // 특정 시간이 지나기전 2개의 아이템을 먹는다면 어뷰징으로 간주
  
   if (items.itemFrequency) {
    items.itemFrequency = false;
    setTimeout(() => {
      items.itemFrequency = true;
    }, 750);
    // console.log(items.itemFrequency);
    setItems(userId, payload.currentItem);
  } else if (!items.itemFrequency) {
    return { status: "fail", message: "범법 행위를 자제해주세요." };
  }

//모두 만족할 경우
return { status: "seccess", message: `${payload.stageIndex}번 사람 구출` };

 

  • redis로 uuid와 score저장
//redisClient 정의 ioredis 사용


import Redis from "ioredis";
import dotenv from "dotenv";

dotenv.config();
export const redisClient = new Redis({
  host: process.env.REDIS_HOST, // Redis 클라우드 호스트
  port: process.env.REDIS_PORT, // Redis 클라우드 포트
  password: process.env.REDIS_PASSWORD, // Redis 비밀번호
});
redisClient.on("connect", () => {
  console.log("Redis connect");
});

redisClient.on("error", (err) => {
  console.error("Redis error: ", err);
});
//클라이언트로 보내기위한 api 구현

app.use("/api/gethighscore/:userId", async (req, res) => {
  try {
    const userId = req.params.userId;
    const readScore = await redisClient.smembers("user");
    const mapRead = readScore.map((x) => x.split(":"));
    // console.log(mapRead);
    const filterRead = mapRead.filter((userUUID) => userUUID[0] === userId);
    // console.log(filterRead);
    return res.json(filterRead);
  } catch (err) {
    console.error(err);
  }
});

 

  //redis 정보 저장 갱신


//현재 redis에 존재하는 user의 정보를 모두 조회한다.
  const readScore = await redisClient.smembers("user");
  
  // 조회한 정보를 :를 기준으로 나눠준다.
  const mapRead = readScore.map((x) => x.split(":"));
  
  //2개로 나누어진 값중 0번째 인덱스에값이 uuid와 일치하는지 확인
  const filterRead = mapRead.filter((userUUID) => userUUID[0] === uuid);
  
  if (filterRead.length === 0) {
     //해당 uuid가 존재하지 않는다면 새로 추가
    await redisClient.sadd("user", `${uuid}:${score}`);
    return { status: "success", message: "게임 종료" };
  } else {
    if (filterRead[0][1] < score) {
    
      //이미 존재하는 uuid의 score가 새로 달성항 scoer보다 작다면 
      //기존 데이터를 삭제하고 새로운 데이터를 재생성한다.
      await redisClient.srem("user", `${filterRead[0].join(":")}`);
      await redisClient.sadd("user", `${uuid}:${score}`);
      console.log("score:", score);
      return { status: "success", message: "점수 업데이트" };
    }
  }

 

  • 조작법

기본상태

 

방패

//X키를 누르면 방패를 든다

 if (event.code === "KeyX") {
      this.sitting = true;
      this.image = this.sittingImage;
    }
    
 //때면 방패를 내린다.
 
    if (event.code === "KeyX") {
      this.sitting = false;
      this.image = this.standingStillImage;
    }

방패

 

기모으기

    //기모으기 타이머
  ENERGY_TIMER = 10;
  energyAnimationTimer = this.ENERGY_TIMER;
  energyImages = [];
    
    //기 모으기 이미지

    const energyImage1 = new Image();
    energyImage1.src = "images/기모으기1.png";

    const energyImage2 = new Image();
    energyImage2.src = "images/기모으기2.png";

    this.energyImages.push(energyImage1);
    this.energyImages.push(energyImage2);


//C키를 누르면 기를 모음

if (event.code === "KeyC") {
      if (this.energyAnimationTimer <= 0 && !this.sitting) {
        // 에너지를 모으기 애니메이션 로직 수정
        this.energyAnimationTimer = this.ENERGY_TIMER; // 타이머 리셋
        if (this.image === this.energyImages[0]) {
          this.image = this.energyImages[1];
        } else {
          this.image = this.energyImages[0];
        }
      }
    }

기모으기

하늘 날기 

//기존 점프 로직 수정

//키 다운시 중력을 없애고 떨어지지 않도록함
  if (event.code === "Space") {
      this.jumpPressed = true;
      this.GRAVITY = 0;
      this.falling = false;
    }
    
//키 업시 중력을 추가하여 떨어지도록 변경
  
  if (event.code === "Space") {
      this.jumpPressed = false;
      this.GRAVITY = 1;
    }

비행


 

트러블슈팅
  • 스테이지 넘어가기

 

필수과제중 내가 구상한 스테이지를 바탕으로 플레이어의 Score가 100점이 넘어갈때마다 다음 스테이지로 넘어가게 하는 기능을 구현해야 한다.

 

기본적으로 

서버 코드

  // 점수 검증
  const serverTime = Date.now();
  const elapsedTime = (serverTime - currentStage.timestamp) / 1000; // 초 단위로 계산

  // 1초당 1점, 100점이상 다음스테이지 이동, 오차범위 5
  // 클라이언트와 서버 간의 통신 지연시간을 고려해서 오차범위 설정
  // elapsedTime 은 100 이상 105 이하 일 경우만 통과
  if (elapsedTime < 100 || elapsedTime > 105) {
    return { status: 'fail', message: 'Invalid elapsed time' };
  }

 

 

클라이언트 코드

 stageChange = true;
  
  ...
  
  update(deltaTime) {
    this.score += deltaTime * 0.001;
    // 점수가 100점 이상이 될 시 서버에 메세지 전송
    if (Math.floor(this.score) === 100 && this.stageChange) {
      this.stageChange = false;
      sendEvent(11, { currentStage: 1000, targetStage: 1001 });
    }
  }

 

플레이어가 게임을 시작한 시간과 게임이 끝난 시간을 구해서 (게임이 끝난시간 - 게임이 시작한 시간) 이 플레이어의 점수이고 그 점수가 100보다 크고 105보다 작을 경우에만 다음 로직으로 넘어갈 수 있도록 되어있다.

 

하지만 분명 이런 방식으로하자 deltaTime의 수치에 *0.001이 아닌 0.01 ▲이상의 수치를 입력하면 Invalid 에러가 뜨는 현상이 발생했다.

 

이런식으로는 내가 구상했던 방식으로는 구현이 될것 같지않았기에 다른 방법으로 검증을 하기로했다.

클라이언트에서 게임 종료시점의 score를 함께 서버로 보내주어 그 값을 기준으로 검증하도록 수정

  if (payload.clientTime > 100 || payload.clientTime < 105) {
    return { status: "fail", message: "시간 초과" };
  }

이런 방식으로 하자 스테이지 1000에서 스테이지 1001로 넘어가는건 문제없이 다음 로직으로 넘어가진다.

 

하지만 지금 작성된 코드는 score가 100점이 된 경우에만 다음 스테이지로 넘어가기때문에 점수가 200 300 400이 될 경우에는 아무런 반응을 하지않는다.

 

기존 구상은 모든 스테이지가 끝날때까지 유저가 특정 score을 달성한다면 다음 스테이지로 넘어가야한다.

그렇다면 어떻게 해줘야할까

 

1.currentStage의 값이 유저의 score가 특정 값에 도달할경우 1씩 올라가도록 해주어야한다.

2.유저의 score가 100에서 끝나는게 아닌 특정값에 도달할 때마다 실행되도록 해줘야한다.

3.유저의 점수의 검증또한 100~105만 검증하는것이아닌 특정 조건을 만족할때마다 검증이 가능해야한다.

 

일단 currentStage의 기본값을 Score클래스의 전역변수로 정의한다.

유저의 score가 100점일 경우 실행 하는것이아닌 유저가 100점에 도달할 때마다 실행해 줘야함으로 

score % 100 ===0 이 될 경우에 실행되도록 수정

이렇게 작성할경우 유저의 현재 점수가 100으로 나누어 떨어질 경우마다 실행된다.

아래의 코드는 현재 유저의 score가 100으로 나누어 떨어지고 stageChange가 true일 경우

stageChange의 값을 false로 바꾸고 sendEvent를 실행한다.

현재 스테이지의 값이 currentStage의 값으로 할당되고 다음스테이지 targetStage는 현재 스테이지의 +1 의 값으로 할당해주어 다음 스테이지 로 넘어간다.

그리고 점수 검증을 위해 score를 서버로 함께 보내준다.

 

클라이언트 코드

class Score {
  score = 0;
  HIGH_SCORE_KEY = "highScore";
  stageChange = true;
  currentStage = 1000;
  constructor(ctx, scaleRatio) {
    this.ctx = ctx;
    this.canvas = ctx.canvas;
    this.scaleRatio = scaleRatio;
  }

  update(deltaTime) {
    this.score += deltaTime * 0.01;
    if (Math.floor(this.score) % 100 === 0 && this.stageChange) {
      this.stageChange = false;

      console.log("현재 스테이지:", this.currentStage);
      sendEvent(11, {
        currentStage: this.currentStage,
        targetStage: this.currentStage + 1,
        clientTime: this.score,
      });

 하지만 이렇게 작성한다면 stageChange가 false로 고정되어 한번만 다음스테이지로 넘어가며 

다음 스테이지로 넘어갔을 경우 그 다음 스테이지의 값이 들어오지않는다.

이를 해결하기위해 조건을 추가해주었다.

     this.currentStage += 1;
    }
    if (Math.floor(this.score) % 100 !== 0) {
      this.stageChange = true;
    }

score % 100 ===0 이며 stageChange가 false라면 sendEvent를 실행하고 currentStage에 1을 더한다.

이후 score % 100 !==0 라면 stageCjange의 값을 true로 변경하여 다시 처음으로 돌아간다. 

이제 서버에서도 클라이언트에서 보내준 score를 사용해 점수 검증을 해준다.

 if (payload.clientTime % 100 === 0) {
    return { status: "fail", message: "시간 초과" };
  }

이렇게 검증까지 통과한다면

 

 

위 사진처럼 score가 100점이 넘어갈때마다 다음 스테이지로 넘어가고 currentStage의 값이 게임에셋에 있는 데이터와 일치하지않는다면 fail을 반환한다.

하지만 이럴경우 아이템을 획득하여 에셋에있는 분기를 넘어버릴 경우 ex)90 에서 20점짜리 아이템획득 => 110점으로 변환되지만 스테이지는 1스테이지에 머무는 문제가 발생했다.

근본적으로 스테이지를 넘어가는 방법에 문제가 있다는걸 확인하고 스테이지를 넘어가는 방식을 변경

  • 스테이지 넘어가는 조건
if (Math.floor(this.score) > stages.data[i].score){

    this.stageChange = false;
    

      if (stages.data[i].id + 1 < stages.data.length) {
        sendEvent(11, {
          currentStage: stages.data[i].id,
          targetStage: stages.data[i+1].id,
        });
   
    }

현재 score가 게임에셋의 스테이지와 score보다 크다면 stageChange = false로 바꿔주고 moveStageHandler를 sendEvent로 호출해주고 data의 값을 반복문을통해 바꿔주면 될꺼라 생각했지만 이미 전체적인 코드가 재귀함수로 무한 반복되어서 그런건지 알수없는 에러가 발생했다.

 

결국 지금 해결방법은 i에 해당하는 부분에 현재 스테이지의 index를 넣어주면 된다.

전역변수로 stageIndex를 생성하고 그값을 스테이지가 넘어갈때마다 + 해주는 방식으로 수정

 

 if (Math.floor(this.score) > stages.data[this.stageIndex].score) {
      this.stageChange = false;

      console.log("현재 스테이지:", stages.data[this.stageIndex].id);
      if (this.stageIndex + 1 < stages.data.length) {
        sendEvent(11, {
          currentStage: stages.data[this.stageIndex].id,
          targetStage: stages.data[this.stageIndex + 1].id,
          clientScore: this.score,
          stageIndex: this.stageIndex,
        });
        this.stageIndex += 1;

if (Math.floor(this.score) < stages.data[this.stageIndex].score) {
      this.stageChange = true;
    }

현재 스테이지가 게임에셋의 데이터 보다 크다면 stageChange를 false로 변경하여 아래 로직이 끝날때까지 스테이지를 넘어가지 못하게하고 현재 스테이지의 index+1보다 게임에셋의 총스테이지의 길이가 작다면 다음스테이지로 넘어가는 로직이다.

마무리로 현재 스코어가 게임에셋에 정의된 스코어보다 작다면 다시 stageChange를 true로 변경하여 스코어가 특정분기에 도달하면 다음스테이지로 넘어가는 로직이 실행된다.

결과화면

위 사진처럼 문제없이 실행된다.

 

  • redis연결후 게임종료가 나타나지않고 계속 빈 객체만 반환하는 에러 발생

처음에는 redis를 연결한다고 gameEnd로직을 건드려서 그런줄알고 하나하나 콘솔을 찍어봤다.

하지만 return 직전까지 콘솔이 문제없이 찍힘

콘솔 로그

여기저기 찾아보던중 비동기 실행의 에러라는 말을 보고 await를 하나씩 건드려보니 하나의 결과를 도출해냈다.

현재 gameEnd로직은 핸들러 맵핑을 통해 handlerEvent에  response로 클라이언트로 전달되는데 

gameEnd를 비동기로 작성했기때문에 gameEnd와 관련된 모든 함수를 비동기로 만들어줘야한다는거다.

export const handlerEvent = async(비동기로 수정) (io, socket, data) => {
  if (!CLIENT_VERSION.includes(data.clientVersion)) {
    socket.emit("response", { status: "fail", message: "클라이언트 버전이 잘못됨" });
    return;
  }
  //핸들러 찾기
  const handler = handlerMapping[data.handlerId];
  if (!handler) {
    socket.emit("response", { status: "fail", message: "핸들러 없음" });
    return;
  }
  //핸들러 실행
  const response = await(비동기로 수정) handler(data.userId, data.payload);
  if (response.broadcast) {
    io.emit("response", "broadcast");
  }

  socket.emit("response", response);

아래와같이 handlerEvent를 비동기로 바꿔주고실행하면

멀쩡하게 게임종료를 반환한다.

'부트캠프' 카테고리의 다른 글

변수 표기법  (0) 2024.10.08
CPU  (2) 2024.10.08
webSocket_Project_2  (0) 2024.10.02
webSocket_Project_1  (0) 2024.10.01
Nalgangdoo_Project 5일차  (1) 2024.09.24