기능 추가
랭킹 시스템 (최고점수 기록)
-
현재 클라이언트에서는 최고점수를 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을 저장하는 형식으로 만들었다
서버 연동
- 서버로 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
}
한줄 평 + 개선점
-
어제는 진도가 금방나가는 것 같으면서 많이 막혀서 답답했었다.
-
디테일도 좋지만 핵심기능을 먼저 구현하려고 계속해서 리마인딩을 해야겠다..