기능 추가

랭킹 시스템 (최고점수 기록)

  • 현재 클라이언트에서는 최고점수를 local.storage에 저장해 불러오는 형식으로 구현이 되어있다

  • 이를 서버에서 최고점수를 관리해 불러오도록 수정할 예정이다.
    (대신 uuid를 local.storage에 저장해 정보를 가져오자)

서버의 최고 점수 기록

  • 서버에서 유저마다 최고 점수들을 기록하기 위해 user.model에 함수를 추가 해준다.
// 최고점수 변경 함수
export const setHighSore = (uuid, highScore) => {
    const userIdx = users.findIndex((e) => e.uuid === uuid )
    const user = users[userIdx]
    if (user.highScore < highScore) users[userIdx].highScore = highScore 
}
  • user.model에 highScore 값을 지정해주기 위해 registerHandler의 입력값을 수정해준다
const registerHandler = (io) => {
    // 모든 유저가 '연결' 시 콜백함수 실행
    io.on('connection', (socket) => {
        //uuid 생성
        const userUUID = uuidv4();
        //유저 추가시 highScore를 0으로 생성
        addUser({ uuid: userUUID, socketId: socket.id, highScore: 0});
        // 추가 로직 생략
    })
}
  • game.handler의 gameEnd로 만든 함수를 이용해 user의 highScore를 설정해준다
import { setHighSore } from "../models/user.model.js";
export const gameEnd = (uuid, payload) => {
    const { timestamp: gameEndTime, score} = payload
    // 검증 로직 생략
    setHighSore(uuid, score)
    return { 
        status: "success",
        message: "Game ended",
        score
    }
}
  • 게임이 시작될 때마다 서버의 highScore를 불러와 플레이어에게 전달한다
import { getUser } from "../models/user.model.js";
export const gameStart = (uuid, payload) => {
    // 초기화 로직 생략
    const user = getUser().find((e) => e.uuid === uuid)

    // 최고 점수를 전달 
    return { status: "success", highScore: user.highScore}
}

클라이언트의 최고 점수 로직 변경

  • 게임 시작 및 서버 접속 시마다 sendEvent를 통해 highScore를 불러오도록 설정해야함

  • 그러기 전에 서버에 접속할 때 uuid를 local.storage에 저장해 재접속 시 이를 넘겨줄 수 있도록 재설계
    (보안 문제는 후에 생각해보자)

  • 서버와 연결 시 uuid를 탐색해서 보내준다.

const socket = io('http://localhost:3000', {
    query: {
        clientVersion: CLIENT_VERSION,
        // 로컬에 저장된 id 정보를 같이 보냄
        userId: localStorage.getItem(this.HIGH_SCORE_KEY) || null
    },
});
  • 그걸 읽은 서버는 userId의 유무로 유저를 생성하거나 확인한다
const registerHandler = (io) => {
    // 모든 유저가 '연결' 시 콜백함수 실행
    io.on('connection', (socket) => {
        // 첫 접속 값 가져오기
        const information = socket.handshake.query
        // 접속 시 클라이언트 버전 확인
        if (!CLIENT_VERSION.includes(information.clientVersion)) socket.emit('response', { status: "fail" })

        // 접속 시 userId 유무 확인 후 uuid 생성
        const userUUID = information.userId ? uuidv4() : socket.userId
        // 유저 확인
        let userInfo = getUser()[userUUID]
        // 서버에 유저가 없을 경우
        if (!userInfo) {
            userInfo = { uuid: userUUID, socketId: socket.id, highScore: 0 }
            // 생성
            addUser(userInfo);
        }
        // 만든 유저 정보를 클라이언트로 전달
        handleConnection(socket, userInfo)
        // 이벤트 맵핑 생략
    })
}
  • 서버 접속 후 값을 전달 받은 클라이언트는 값을 저장해 보내주는 함수를 생성한다.
let userId = null;
let highScore = null;

socket.on('response', (data) => {
    // 응답 중 score가 포함되어있으면 highScore로 전환
    if (data.score) {
        highScore = data.score
    }
})

export const getUser = async () => {
    if (!connection) {
        // 첫 접속 시
        await new Promise((resolve) => {
            socket.once('connection', (data) => {
                userId = data.uuid;
                highScore = data.highScore
                resolve()
            })
        })
        connection = true;
    }
    return {userId, highScore}
}
  • score class를 이용해 최고점수와 유저아이디를 저장할 수 있도록 해준다
// class 생성 (index.js)
const USER_INFO = await getUser()
const ctx = canvas.getContext('2d');
// 요소 생성(초기화)
function createSprites(scaleRatio) {
    //다른 요소 생략
    score = new Score(
        ctx, 
        USER_INFO,
        scaleRatio
    );
}
  • 이제 setUserId()는 요소 생성(createSprites) 맨 아래에 추가해준다(게임이 시작되거나 끝날 때마다 저장)
// score class (score.js)
constructor(ctx, userInfo, scaleRatio) {
    this.ctx = ctx;
    this.canvas = ctx.canvas;
    this.scaleRatio = scaleRatio;
    this.stageChange = true;
    this.scorePs = 1;
    this.time = 0;
    this.score = 0;
    this.highScore = userInfo.highScore
    this.userId = userInfo.userId
    this.stage = 0;
}
setUserId() {
    localStorage.setItem("userId", this.userId);
}

닉네임 시스템

  • 시작 페이지를 만들어 닉네임을 받고 서버연결을 통해 uuid와 nickname을 서버에 저장할 수 있도록 해야겠다!

  • 시작 페이지는 디자인적으로 ai에게 맡기고 localStorage에 nickname을 저장하는 형식으로 만들었다

image

서버 연동

  • 서버로 localStorage 값을 넘겨준다!
// localhost:3000 에 서버를 연결하여 값을 넘겨줌
const socket = io('http://localhost:3000', {
    query: {
        clientVersion: CLIENT_VERSION,
        // 로컬에 저장된 id 정보를 같이 보냄
        userId: localStorage.getItem("userId") || null,
        nickname: localStorage.getItem("nickname") || null
    },
});
  • 넘겨준 값을 통해 서버에 저장한 뒤 클라이언트에게 재송신해준다
const information = socket.handshake.query
const nickname = information.nickname || null

// 서버에 유저가 없을 경우
if (!userInfo) {
    userInfo = { uuid: userUUID, socketId: socket.id, nickname ,highScore: 0, itemScore: 0 }
    //유저 생성
    addUser(userInfo);
} else {
    // 유저 수정
    setUserSocket(userUUID, socket.id, nickname)
}

// 만든 유저 정보를 클라이언트로 전달
    handleConnection(socket, userInfo)
  • 유저는 이 정보를 가지고 닉네임을 표시한다

서버의 닉네임으로 채팅 사용

  • 서버의 chat.handler에서 닉네임 정보까지 같이보내 처리를 해준다
import { getUser } from "../models/user.model.js";

// 맵핑이 될 함수
export const handleChat = (uuid, payload) => {
    const user = getUser().find((e) => e.uuid === uuid)
    return { status: "success", id: uuid, nickname: user.nickname ,msg: payload, broadcast: true };
};
  • 클라이언트에서 닉네임을 받을 공간을 만들어 붙여준다
if (data.msg) {
    // 사용자 이름 확인
    const nicknameSpan = document.createElement('span');
    nicknameSpan.className = 'text-sm text-gray-600 mr-2 mb-1';  
    nicknameSpan.textContent = data?.nickname || "익명"

     if (data.msg) {
        // 사용자 이름 확인
        const nicknameSpan = document.createElement('span');
        nicknameSpan.className = 'text-sm text-gray-600 mr-2 mb-1';  
        nicknameSpan.textContent = data?.nickname || "익명"
        if (data.id === userInfo.uuid){
            // 사용자 메시지 추가  
            const userMessageDiv = document.createElement('div');
            userMessageDiv.className = 'flex flex-col items-end mb-2';

            const messageDiv = document.createElement('div');
            messageDiv.className = 'bg-blue-500 text-white p-2 rounded-lg max-w-[70%]';
            messageDiv.textContent = data.msg;  
 
            userMessageDiv.appendChild(nicknameSpan); 
            userMessageDiv.appendChild(messageDiv);
            chatMessages.appendChild(userMessageDiv);
            // 스크롤 맨 아래로  
            chatMessages.scrollTop = chatMessages.scrollHeight;
        } else {
            // 다른 사람 메시지
            const otherMessageDiv = document.createElement('div');
            otherMessageDiv.className = 'flex flex-col items-start mb-2';

            const messageDiv = document.createElement('div');
            messageDiv.className = "bg-gray-100 text-black p-2 rounded-lg max-w-[70%]";
            messageDiv.textContent = data.msg;  

            otherMessageDiv.appendChild(nicknameSpan);
            otherMessageDiv.appendChild(messageDiv);
            chatMessages.appendChild(otherMessageDiv);
            chatMessages.scrollTop = chatMessages.scrollHeight;
        }
    }
}

랭킹 기능 추가

  • 랭킹을 관리하는 model rank.model을 생성해준다
import { getUser } from "./user.model.js";

// 서버에 메모리형식으로 접속되어있는 ranking 저장
const rank = [];

export const loadRanking = () => {
    const rank = getUser().sort((a, b) => a.highScore - b.highScore).map((e) => {
        return [e.nickname, e.highScore]
    }).slice(0, 9)
    return rank
}

export const getRanking = () => {
    return rank
}
  • 이를 이용해 게임 시작과 이벤트 로딩 때마다 rank값을 넣어준다
// 연결될 시
export const handleConnection = (socket, userInfo) => {
    // rank 로딩
    loadRanking()
    //유저와 연결되면 uuid를 메세지로 전달
    socket.emit('connection', {...userInfo, rank: getRanking()} )
}
// 이벤트 발생 시
export const handlerEvent = (io, socket, data) => {
    loadRanking()
    // 검증 로직 생략
    io.emit('rank', getRanking())
}
  • 이제 이를 score에서 받아와 그려준다!
let rank = null;
// socket.js 에서 일괄 업데이트
socket.on('rank', (rank) => {
    rank = rank
})
export const getRank = () => {
    return rank
}

한줄 평 + 개선점

  • 어제는 진도가 금방나가는 것 같으면서 많이 막혀서 답답했었다.

  • 디테일도 좋지만 핵심기능을 먼저 구현하려고 계속해서 리마인딩을 해야겠다..