DB 연동
마이그레이션
-
DB에 저장할 정보들을 지정해두어 DB가 바뀌어도 테이블의 기본 형식을 설정해주어 사용가능하도록 해준다!
-
DB 마이그레이션에 사용할 sql 문법
CREATE TABLE IF NOT EXISTS users
(
device_id VARCHAR(255) PRIMARY KEY,
location_x FLOAT DEFAULT 0,
location_y FLOAT DEFAULT 0,
last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- 이를 불러와 사용해주는 js 함수
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import pools from '../database.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename)
const executeFile = async (pool, filePath) => {
const sql = fs.readFileSync(filePath, 'utf-8');
const queries = sql.split(";")
.map((query) => query.trim())
.filter((q) => q.length > 0);
for (const query of queries) await pool.query(query);
}
const createSchemas = async () => {
const filePath = path.join(__dirname, 'database.sql')
try {
await executeFile(pools.USER_DB, filePath);
} catch(e) {
console.error("DB 생성 오류",e);
}
}
createSchemas().then(()=> {
console.log("DB 생성완료");
process.exit(0);
}).catch((e) => {
console.error(e);
process.exit(1);
})
쿼리 정리
- DB에 사용할 쿼리와 실행시켜주는 함수를 만들어준다!
// 사용할 쿼리들만 모아두기
export const USERS_QUERIES = {
CREATE_USER: 'INSERT INTO users (device_id) VALUES (?)',
FIND_USER: 'SELECT device_id, location_x, location_y FROM users WHERE device_id = ?',
SAVE_LOCATION: 'UPDATE users SET location_x = ?, location_y = ? WHERE device_id = ?',
UPDATE_USER_LOGIN: 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE device_id = ?',
}
- 이를 실행시켜주는 함수를 만들어준다.
import pools from "../database.js"
import { USERS_QUERIES } from "../queries.js"
export const createUser = async (id) => {
await pools.USER_DB.query(USERS_QUERIES.CREATE_USER, [id])
}
export const updateLogin = async (id) => {
await pools.USER_DB.query(USERS_QUERIES.UPDATE_USER_LOGIN, [id])
}
export const saveLocation = async (id, x, y) => {
await pools.USER_DB.query(USERS_QUERIES.SAVE_LOCATION, [x, y, id])
}
export const findUser = async (id) => {
const [rows] = await pools.USER_DB.query(USERS_QUERIES.FIND_USER, [id])
console.log(rows[0])
return rows[0]
}
기능 연결
- 이제 이 함수들을 로그인 / 접속종료 에 연결해주면 된다!
/* 첫 연결 핸들러 내부 */
// 세션에 유저 존재 확인
let user = users.getUser({userId})
// 존재 시 socket 을 바꿔줌
if(user) {
user.updateSocket(socket);
// DB에 최근 로그인 시간 업데이트
await updateLogin(userId);
// 서버에 없을 시
} else {
// DB에 유저 존재 확인
const dbUser = await findUser(userId);
user = users.addUser(userId, socket, latency)
// 존재 시 db에 저장된 위치 불러옴
if (dbUser) {
user.updatePosition(dbUser.x, dbUser.y)
await updateLogin(userId);
// 없을 시 db에 유저 생성
} else await createUser(userId)
}
- 또한 연결이 끊어질 때 사용되는 메서드에도 saveLocation 함수를 매핑해준다!
/* users 세션 관리 */
removeUser = async ({ userId, socket }) => {
if (socket) {
userId = this.socketToUser.get(socket)
this.socketToUser.delete(socket)
}
// 참여한 게임이 있을 시 확인해서 삭제
const user = this.users.get(userId)
// user를 찾을 수 없으면 종료
if (!user) return
if (user.gameId) games.games.get(user.gameId).removeUser(userId)
// 마지막 위치 DB에 저장
await saveLocation(userId, user.x, user.y)
this.users.delete(userId)
}
추측항법
-
현재는 이러한 과정으로 위치 동기화가 일어나고 있다!
-
클라이언트가 프레임마다 위치를 보낸다.
-
서버는 일정주기(0.2초) 마다 받아온 위치들을 종합하여 유저들에게 보내준다
-
-
이로인해 클라이언트는 계속 움직였음에도 서버에서 받아온 값을 적용하는 과정에서 뒤로 돌아간다..
-
이를 방지하기 위해 클라이언트에게 주는 위치 정보를 지연시간을 계산한 값으로 보내줘야한다! (=추측항법)
계산
-
새로운 x 좌표 = 현재 x 좌표 + (속도 방향의 x 성분 시간)
-
새로운 y 좌표 = 현재 y 좌표 + (속도 방향의 y 성분 시간)
-
위와 같은 공식을 이용해 기존 좌표와 받은 좌표를 분석해 새로운 좌표를 계산해야한다!
속도
-
일단 서버에서 계산 할 때 방향과 시간은 주고받는 latency와 (x,y) 좌표들을 활용해 계산할 수 있다!
-
속도 또한 받은 (x,y)를 통해 계산할 순 있지만!… 서버의 존재 의의 “검증”이 안됀다..
-
그렇기에 유저가 게임에 참여할 때 속도를 주고,
속도 관련 이벤트 발생 시 서버에서 업데이트하는 형식으로 구현하는 것이 좋을 것 같다!
-
클라이언트는 접속 시 초기 speed를 보내도록 설정
( 아마 추후 프로젝트에선 assets 같은 자료구조에서 상수로 관리할 것 같음)void SendInitialPacket() { InitialPayload initialPayload = new InitialPayload { deviceId = GameManager.instance.deviceId, playerId = GameManager.instance.playerId, latency = GameManager.instance.latency, speed = GameManager.instance.player.speed, }; // handlerId는 0으로 가정 SendPacket(initialPayload, (uint)Packets.HandlerIds.Init); }
-
서버에선 이를 이용해 speed 초기값을 지정
import { users } from "../../session.js" export const initialHandler = async ({socket, payload}) => { const { deviceId: userId, latency, speed} = payload // users 세션에 유저 추가에 speed 값 포함 users.addUser(userId, socket, latency, speed) }
방향 계산
- 좌푯값을 업데이트할 때마다 방향을 구해준다!
updatePosition(x, y) {
//아크 탄젠트로 방향을 얻음
this.direct = Math.atan2(y - this.y,x - this.x);
this.x = x;
this.y = y;
this.lastUpdateTime = Date.now();
}
- cos()과 sin()을 이용해 방향(라디안)에서 x와 y의 벡터 좌표(방향성) 를 얻는다!
// 추측항법 시 사용
calculatePosition(latency) {
// 가장 긴 지연시간 기준 (초 단위)
const timeDiff = latency / 1000;
const nextX = this.x + this.speed * Math.cos(this.direct) * timeDiff
const nextY = this.y + this.speed * Math.sin(this.direct) * timeDiff
return {x: nextX, y:nextY }
}
- 현재 위치 동기화의 intervalTime을 고려하지 못한 점과 방향이 서버->클라->서버 에 의해 양의방향으로 올라가는 문제도 해결해야한다!
한줄 평 + 개선점
-
추측항법을 구현하는 과정을 좀더 구체화시키며 개념을 확립시켜야겠다.
-
식 및 입력값을 개선하는 방법을 잘 찾아보자!