해당 과제에 대하여 서버를 구축하고 테스트 하던 중...
문제점이 하나 발생했다. 나는 유니티를 모른다.. 허나 이번 문제를 풀기 위해서는 유니티를 어느정도 다룰 줄 알아야 마지막 좌표값을 토대로 유저가 스폰된다.
import { exitGameHandler } from '../handlers/game.handler.js';
import { removeUser } from '../session/user.sessions.js';
export const onEnd = (socket) => () => {
exitGameHandler(socket);
removeUser(socket);
console.log('Client Disconnected');
};
종료 이벤트 시 게임 세션에서 종료하고 유저 세션에서 삭제를 진행한다.
removeUser 실행 시 마지막 위치를 DB에 저장하는 과정을 거쳤다.
export const removeUser = (socket) => {
const user = getUserBySocket(socket);
saveUser(user.id, user.x, user.y);
const idx = userSessions.findIndex((user) => user.socket === socket);
if (idx != -1) return userSessions.splice(idx, 1)[0];
};
유저 정보를 세션에서 삭제하기 전 saveUser를 통해 유저의 아이디와 x,y값을 저장한다.
export const saveUser = async (id, x, y) => {
try {
const date = new Date();
const [result] = await pools.USER_DB.query(
`INSERT INTO user (id, device_id, last_login, created_at, x, y) VALUES (?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE x = VALUES(x), y = VALUES(y), last_login = VALUES(last_login)`,
[id, id, date, date, x, y],
);
console.log(result);
} catch (err) {
console.error(`${id} 저장 실패`);
}
};
테스트용으로 대충 만든 에러처리는 눈 감아주기로 하자..
해당 id가 없는 경우에는 새로 테이블에 추가가 되고, 있는 경우 x,y,최근 로그인만 갱신한다.
기존 클라이언트가 최초 초기화 패킷을 보내는 것을 처리하는 핸들러는 initHandler였다.
import { createResponse } from '../../utils/response/createResponse.js';
import { HANDLER_IDS, RESPONSE_SUCCESS_CODE } from '../constants/handlerIds.js';
import { addUser } from '../session/user.sessions.js';
import { joinGameHandler } from './game.handler.js';
export const initHandler = async (socket, userId, payload) => {
const { user, lastX, lastY } = await addUser(socket, userId, payload.latency);
joinGameHandler(user);
const initResponse = createResponse(
HANDLER_IDS.INITIAL,
RESPONSE_SUCCESS_CODE,
{ userId, lastX, lastY },
userId,
);
socket.write(initResponse);
};
기존 Response 패킷의 데이터는 userId만 반환했었지만,
addUser 과정을 통해 last 좌표값을 같이 넘겨주기로 하였다.
기존 유저는 그렇다쳐도 신규 유저는 저장된 값이 없는데,
undefined로 넘겨서 클라이언트측에서 처리하는 방법이 있고 아니면 서버측에서 0,0으로 넘기는 방식도 있을 것이라 생각이 드는데 테스트 겸 undefined로 넘겼다.
export const addUser = async (socket, uuid, latency) => {
if (!uuid) uuid = uuidV4();
const savedInfo = await loadUser(uuid);
const user = new User(socket, uuid, latency);
if (savedInfo.length !== 0) {
user.x = savedInfo.x;
user.y = savedInfo.y;
userSessions.push(user);
return { user, lastX: savedInfo.x, lastY: savedInfo.y };
}
userSessions.push(user);
return { user };
};
loadUser를 통하여 저장된 정보를 불러오고 넘어온 배열이 있을 경우 lastX, lastY를 같이 넘겨주나 없을 경우
user 인스턴스만 넘겨준다.
현재는 클라이언트측을 undefined 넘어올 것을 생각해 작성하여 위와 같이 코딩했지만 user만 넘기지 말고 0,0으로 넘겨줘도 좋을 것 같다.
export const loadUser = async (id) => {
try {
const [user] = await pools.USER_DB.query('SELECT x ,y FROM user WHERE id = ?', [id]);
if (user.length !== 0) return { x: user[0].x, y: user[0].y };
return [];
} catch (err) {
console.error(`${id} 로드 실패`);
}
};
id에 대한 저장된 데이터가 있다면 배열로 넘겨주고 없다면 빈 배열로 넘겨주고 있다.
스켈레톤 코드를 아무리 읽어보아도 최초 좌표에 대한걸 찾을 수 없어서 웹서핑을 좀 했다.
게임이 시작될 때 플레이어의 초기 위치는 일반적으로 스폰 포인트 또는 맵의 특정 위치로 설정되나 지정하지 않을 경우, (0, 0)이나 맵의 중앙 좌표와 같은 값으로 초기화할 수 있다
그러면 지정을 어떻게 하는지에 대해서도 찾아보았다.
player.transform.position = new Vector2(x, y);
위 코드는 플레이어의 현재 위치를 x와 y로 지정된 새로운 좌표로 설정하며 player.transform.position
은 Unity
에서 게임 오브젝트의 위치를 나타내며, new Vector2(x, y)
는 두 개의 부동 소수점 숫자(x,y)를 사용하여 2D 공간의 점을 나타낸다.
원래라면 protoBuf를 사용하여 페이로드에 담긴 바이트 버퍼를 처리하는게 맞았겠지만, 유니티 어린이(생후 1시간)으로서 그건 시간이 좀 걸릴 것 같아 바로 역직렬화 할 수 있는 방법을 찾았다.
if (response.data != null && response.data.Length > 0) {
var specificData = ProcessResponseData(response.data);
if (response.handlerId == 0 && specificData.lastX != 0 ) {
GameManager.instance.OnLastPositionReceived(specificData.lastX, specificData.lastY);
GameManager.instance.GameStart();
}
else if(response.handlerId == 0)
{
GameManager.instance.GameStart();
}
ProcessResponseData(response.data);
}
[System.Serializable]
public class SpecificDataType
{
public string deviceId;
public float lastX;
public float lastY;
}
public void OnLastPositionReceived(float x, float y)
{
player.transform.position = new Vector2(x, y);
}
이거 생김새가 비슷하긴 한데, 위 클래스는 Unity에서 사용하는 C#의 일반적인 Serializable 클래스로 Unity의 JSON 직렬화 및 Binary 직렬화와 같은 다양한 직렬화 방법을 지원하기 위해 만들어졌다고 한다.
성능적인 면에서는 떨어지지만, 최초 초기화 버퍼에만 사용하니 위와 같이 구성해도 문제는 없겠다고 생각했다.
종료 후 재 접속 시 마지막 위치로 가는 것을 확인할 수 있다.
근데 잘 보면, 처음 스폰될 때 중앙에 0.5초가량 있다가 마지막 저장위치로 스폰되는 것을 볼 수 있다.
처음에는 클라이언트가 문제겠거니 하고 유니티 고수에게 찾아가 자문을 구했더니 초기화 문제나 동기화가 제대로 반영되지 않는다, 필요한 값은 잘 들어오나 등의 중요한 정보를 얻었고 곰곰히 생각했다.
기존 유저의 경우 실시간으로 위치 정보를 받는데, 서버에서 x,y가 적용되지 않은 유저 인스턴스를 생성한 후 넣어주고 클라이언트에게 리스폰을 보내니 다른 유저 기준에서는 0,0에 있는 것으로 보였다가 해당 클라이언트가 다음 위치보고를 할 때 이동되는 현상이었던 것이다.
위에 적힌 코드들은 최종 코드이기 때문에 문제없이 가동되나, 차이는 밑에와 같다.
export const addUser = async (socket, uuid, latency) => {
if (!uuid) uuid = uuidV4();
const savedInfo = await loadUser(uuid);
userSessions.push(user);
const user = new User(socket, uuid, latency);
if (savedInfo.length !== 0) {
user.x = savedInfo.x;
user.y = savedInfo.y;
return { user, lastX: savedInfo.x, lastY: savedInfo.y };
}
return { user };
};
export const addUser = async (socket, uuid, latency) => {
if (!uuid) uuid = uuidV4();
const savedInfo = await loadUser(uuid);
const user = new User(socket, uuid, latency);
if (savedInfo.length !== 0) {
user.x = savedInfo.x;
user.y = savedInfo.y;
userSessions.push(user);
return { user, lastX: savedInfo.x, lastY: savedInfo.y };
}
userSessions.push(user);
return { user };
};
조아쓰~