1. 최종프로젝트 1에서 일부러 class와 session 관리를 어떻게 하고 있는지 보여주지 않았습니다.
위의 이유에 대해서는 할말이 정말 많지만 이미 망가져버린 코드 구조의 흐름을 보여주고 싶지도 않았지만 일단, 누군가 진지하게 읽을 수 있으니 간단하게 설명하며 보여드리겠습니다.
2. user.class
유저 클래스에서는 로그인한 유저들을 관리하기 위해서 필요한 정보를 객체화 해서 관리하기 위해 만들었습니다.
const { TOWN_SPAWN_TRANSFORMS } = configs;
class User {
#pingQueue = [];
#intervalId = null;
constructor(socket) {
this.socket = socket;
this.id = socket.id;
this.transform = TOWN_SPAWN_TRANSFORMS;
this.latency = 0;
this.myClass = 0;
this.nickname = '';
this.dungeonId = '';
this.#intervalId = setInterval(this.ping.bind(this), 1000);
}
}
3. user.Session
유저 세션에서는 위의 user.class에 있는 정보들을 저장하여 해당 유저에 대한 정보를 쉽게 관리 하기
위해서 작성하였습니다.
import { userSessions } from './sessions.js';
const allUsersUUID = [];
const addUserSession = (socket) => {
if (userSessions.has(socket.id)) {
logger.error('이미 존재하는 유저 세션입니다.');
return userSessions.get(socket.id);
}
const user = new User(socket);
userSessions.set(socket.id, user);
allUsersUUID.push(socket.UUID);
return user;
};
const removeUserSession = (socket) => {
const user = userSessions.get(socket.id);
if (user) {
user.dispose();
userSessions.delete(socket.id);
const index = allUsersUUID.indexOf(socket.UUID);
if (index !== -1) {
allUsersUUID.splice(index, 1);
}
}
};
const getAllUserUUID = () => {
return allUsersUUID;
};
const getUserById = (userId) => {
return userSessions.get(userId);
};
export {
addUserSession,
removeUserSession,
getUserById,
getUserTransformById,
updateUserTransformById,
getAllUserUUID,
};
4. dungeon.class
저희의 프로젝트의 가장 중요한 Dungeon클래스인데 던전에서 관리하고 있는 것들은 게임 시작을 하면
던전 클래스에서 접근이 가능합니다.
하지만 너무 큰 의존성을 줄이기 위해서 이너클래스를 사용해서 관리하기 보다는 따로 작성하여
코드들 끼리의 의존성을 줄여 모듈화를 하려고 노력했습니다.
여기서 아쉬운 점은 addDungeonUser 부분인데 던전에 들어간 유저는 유저세션에 있는 정보와
스탯 정보 등 여러 정보가 추가 되기 때문에 원래 dungeonUser.class를 만들어 작성하려 하였지만
단 2주라는 시간이 주어졌고 이미 만들어진 코드에서 사용하는 방법 말고는 해당 기간에 마감할
방법이 없었습니다.
따라서, this.users = new Map();에 넣는 던전 유저는 addDungeonUser 메서드를 사용해 새로운 던전유저
객체를 만들어 const dungeonUser = { user, .... };추가하는 방식으로 사용하였습니다.
class Dungeon {
constructor(dungeonInfo) {
this.dungeonId = dungeonInfo.dungeonId;
this.name = dungeonInfo.name;
this.users = new Map();
this.usersUUID = [];
this.monsterLogic = new MonsterLogic(this);
this.nexusCurrentHp = 100;
this.nexusMaxHp = 100;
this.nexus = null;
this.respawnTimers = new Map();
this.droppedItems = {};
this.spawnTransforms = [
[2.5, 0.5, 112, 180],
[2.5, 0.5, -5.5, 0],
[42, 0.5, 52.5, 270],
[-38, 0.5, 52.5, 90],
];
this.startTime = Date.now(); // 던전 시작 시간 초기화
}
async addDungeonUser(user, statInfo) {
const userId = user.socket.id;
user.dungeonId = this.dungeonId;
await setSessionId(userId, this.dungeonId);
if (this.users.has(userId)) {
logger.info('이미 던전에 참여 중인 유저입니다.');
return null;
}
this.usersUUID.push(user.socket.UUID);
const dungeonUser = {
user,
_currentHp: statInfo.stats.maxHp,
get currentHp() {
return this._currentHp;
},
set currentHp(value) {
this._currentHp = Math.max(0, Math.min(value, statInfo.stats.maxHp));
},
userKillCount: 0,
monsterKillCount: 0,
statInfo,
skillList: {}, //getSkill가보면 dungeonUser.skillList[skillId] = { slot: slotIndex, ...skillData, lastUseTime: 0 }; 이렇게 등록함
};
this.users.set(userId, dungeonUser);
return user;
}
attackedNexus(damage, playerId) {
if (this.nexus) {
const isGameOver = this.nexus.hitNexus(damage, playerId, this.usersUUID);
if (isGameOver) {
logger.info(`Nexus destroyed in dungeon ${this.dungeonId}.`);
return true; // 게임 종료
}
}
return false;
}
handleGameEnd() {
const playerId = this.nexus.lastAttackerId;
logger.info(`Game ended in dungeonId ${this.dungeonId}. Winner is player: ${playerId}`);
createNotificationPacket(PACKET_ID.S_GameEnd, { playerId }, this.usersUUID);
}
spawnNexusNotification() {
this.nexus = new Nexus();
if (this.nexus) {
logger.info(
`Nexus spawned in dungeon: ${this.dungeonId}, Position: ${JSON.stringify(this.nexus.position)}`,
);
this.nexus.spawnNexusNotification(this.usersUUID);
this.nexus.updateNexusHpNotification(this.usersUUID);
return true;
}
return false;
}
/**
* 몬스터를 통해 드랍된 아이템 정보를 보관합니다.
*/
createDroppedObject(playerId, Id, itemInstanceId) {
if (!this.users.has(playerId)) {
return;
}
this.droppedItems[itemInstanceId] = { playerId, Id };
}
/**
* 몬스터를 통해 드랍되었던 아이템 정보를 가져오며 데이터를 제거합니다.
* @param {Number} playerId
* @param {Number} Id ItemID or SkillID
* @param {Number} itemInstanceId
* @returns
*/
getDroppedObject(playerId, Id, itemInstanceId) {
const droppedItem = this.droppedItems[itemInstanceId];
if (!droppedItem || droppedItem.playerId != playerId || droppedItem.Id != Id) {
logger.error(`getItemOrSkill. not matched droppedItemInfo playerID: ${playerId} or Id ${Id}`);
return;
}
delete this.droppedItems[itemInstanceId];
return droppedItem;
}
increaseMonsterKillCount(userId) {
const user = this.users.get(userId);
if (!user) {
logger.error(`해당 userId (${userId})를 가진 사용자가 던전에 없습니다.`);
return;
}
user.monsterKillCount += 1;
logger.info(
`플레이어 ${userId}의 몬스터 킬 수가 증가했습니다. 현재 몬스터 킬 수: ${user.monsterKillCount}`,
);
createNotificationPacket(
PACKET_ID.S_MonsterKillCount,
{ playerId: userId, monsterKillCount: user.monsterKillCount },
this.getDungeonUsersUUID(),
);
}
increaseUserKillCount(userId) {
const user = this.users.get(userId);
if (!user) {
logger.error(`해당 userId (${userId})를 가진 사용자가 던전에 없습니다.`);
return;
}
user.userKillCount += 1;
logger.info(
`플레이어 ${userId}의 유저 킬 수가 증가했습니다. 현재 유저 킬 수: ${user.userKillCount}`,
);
createNotificationPacket(
PACKET_ID.S_PlayerKillCount,
{ playerId: userId, playerKillCount: user.userKillCount },
this.getDungeonUsersUUID(),
);
}
async removeDungeonUser(userId) {
const dungeonUser = this.users.get(userId);
if (dungeonUser) {
const user = dungeonUser.user;
const userUUID = user.socket.UUID;
user.dungeonId = '';
const result = this.users.delete(userId);
await setSessionId(userId, '');
const index = this.usersUUID.indexOf((uuid) => uuid === userUUID);
if (index !== -1) {
this.usersUUID.splice(index, 1);
}
if (this.users.size == 0) {
this.Dispose();
}
return result;
}
}
getMaxLatency() {
let maxLatency = 0;
this.users.forEach((user) => {
const userLatency = user.userInfo.getLatency();
maxLatency = Math.max(maxLatency, userLatency);
});
return maxLatency;
}
callOnClose() {
if (this.users.size === 0) {
this.monsterLogic.pathServer.onClose();
}
}
removeDungeonSession() {
removeDungeonSession(this.dungeonId);
}
getDungeonUser(userId) {
return this.users.get(userId);
}
getDungeonUsersUUID() {
return this.usersUUID;
}
getSpawnPosition() {
return [...this.spawnTransforms];
}
getUserStats(userId) {
const user = this.getDungeonUser(userId);
return user.statInfo;
}
updateUserStats(userId, stats) {
const {
atk = 0,
def = 0,
maxHp = 0,
moveSpeed = 0,
criticalProbability = 0,
criticalDamageRate = 0,
} = stats;
const statInfo = this.getUserStats(userId);
statInfo.stats = {
atk: statInfo.stats.atk + atk,
def: statInfo.stats.def + def,
maxHp: statInfo.stats.maxHp + maxHp,
moveSpeed: statInfo.stats.moveSpeed + moveSpeed,
criticalProbability: statInfo.stats.criticalProbability + criticalProbability,
criticalDamageRate: statInfo.stats.criticalDamageRate + criticalDamageRate,
};
return statInfo.stats;
}
levelUpUserStats(user, nextLevel, maxExp) {
const { stats: currentStats, exp: currentExp, maxExp: currentMaxExp } = user.statInfo;
const newExp = currentExp - currentMaxExp;
const levelperStats = getGameAssets().levelperStats;
const userClassId = user.user.myClass;
const classLevelStats = levelperStats[userClassId]?.stats || {};
user.statInfo = {
level: nextLevel,
stats: {
maxHp: currentStats.maxHp + (classLevelStats.maxHp || 0),
atk: currentStats.atk + (classLevelStats.atk || 0),
def: currentStats.def + (classLevelStats.def || 0),
moveSpeed: currentStats.moveSpeed + (classLevelStats.speed || 0),
criticalProbability:
currentStats.criticalProbability + (classLevelStats.criticalProbability || 0),
criticalDamageRate:
currentStats.criticalDamageRate + (classLevelStats.criticalDamageRate || 0),
},
exp: newExp,
maxExp,
};
return user.statInfo;
}
addExp(userId, getExp) {
const user = this.getDungeonUser(userId);
// 레벨당 필요 경험치 불러오기
let maxExp = user.statInfo.maxExp;
const currentLevel = user.statInfo.level;
const nextLevel = currentLevel + 1;
const expAssets = getGameAssets().expInfo;
if (!maxExp) {
maxExp = expAssets[currentLevel].maxExp; // ID로 직접 접근
}
//에셋 정보가 없으면 테이블 문제 or 최대 레벨 도달
if (!expAssets[nextLevel]) {
return;
}
user.statInfo.exp += getExp;
const expResponse = createResponse(PACKET_ID.S_GetExp, {
playerId: userId,
expAmount: user.statInfo.exp,
});
enqueueSend(user.user.socket.UUID, expResponse);
if (user.statInfo.exp >= maxExp) {
const statInfo = this.levelUpUserStats(user, nextLevel, expAssets[nextLevel].maxExp);
this.levelUpNotification(userId, statInfo);
}
return user.statInfo.exp;
}
levelUpNotification(userId, statInfo) {
createNotificationPacket(
PACKET_ID.S_LevelUp,
{ playerId: userId, statInfo },
this.getDungeonUsersUUID(),
);
}
damagedUser(userId, damage) {
const user = this.users.get(userId);
const resultDamage = Math.max(1, Math.floor(damage * (100 - user.statInfo.stats.def) * 0.01)); // 방어력이 공격력보다 커도 최소뎀
createNotificationPacket(
PACKET_ID.S_HitPlayer,
{ playerId: userId, damage: resultDamage },
this.getDungeonUsersUUID(),
);
return resultDamage;
}
getAmountHpByKillUser(userId) {
const user = this.users.get(userId);
const userMaxHp = user.statInfo.stats.maxHp;
const healAmount = Math.floor(userMaxHp * 0.5);
user.currentHp = Math.min(user.currentHp + healAmount, userMaxHp);
createNotificationPacket(
PACKET_ID.S_UpdatePlayerHp,
{ playerId: userId, hp: user.currentHp },
this.getDungeonUsersUUID(),
);
}
updatePlayerHp(userId, amount) {
const user = this.users.get(userId);
// 스탯 불러오기 수정
const maxHp = user.statInfo.stats.maxHp;
const currentHp = user.currentHp;
const newHp = currentHp + amount;
user.currentHp = Math.max(0, Math.min(newHp, maxHp));
if (user.currentHp != currentHp) {
createNotificationPacket(
PACKET_ID.S_UpdatePlayerHp,
{ playerId: userId, hp: user.currentHp },
this.getDungeonUsersUUID(),
);
}
return user.currentHp;
}
increasePlayerAtk(userId, amount) {
const user = this.users.get(userId);
user.statInfo.stats.atk = Math.min(amount + user.statInfo.stats.atk, user.statInfo.stats.atk);
return user.statInfo.stats.atk;
}
increasePlayerDef(userId, amount) {
const user = this.users.get(userId);
user.statInfo.stats.def = Math.min(amount + user.statInfo.stats.def, user.statInfo.stats.def);
return user.statInfo.stats.def;
}
increasePlayerMaxHp(userId, amount) {
const user = this.users.get(userId);
user.statInfo.stats.maxHp = Math.min(
amount + user.statInfo.stats.maxHp,
user.statInfo.stats.maxHp,
);
return user.statInfo.stats.maxHp;
}
increasePlayerMoveSpeed(userId, amount) {
const user = this.users.get(userId);
user.statInfo.stats.moveSpeed = Math.min(
amount + user.statInfo.stats.moveSpeed,
user.statInfo.stats.moveSpeed,
);
return user.statInfo.stats.moveSpeed;
}
increasePlayerCriticalProbability(userId, amount) {
const user = this.users.get(userId);
user.statInfo.stats.criticalProbability = Math.min(
amount + user.statInfo.stats.criticalProbability,
user.statInfo.stats.criticalProbability,
);
return user.criticalProbability;
}
increasePlayerCriticalDamageRate(userId, amount) {
const user = this.users.get(userId);
user.statInfo.stats.criticalDamageRate = Math.min(
amount + user.statInfo.stats.criticalDamageRate,
user.statInfo.stats.criticalDamageRate,
);
return user.statInfo.stats.criticalDamageRate;
}
nexusDamaged(damage) {
this.nexusCurrentHp -= damage;
return this.nexusCurrentHp;
}
// int32 playerId = 1;
// TransformInfo transform = 2;
// StatInfo statInfo = 3;
onRespawn = (userId) => {
const user = this.users.get(userId);
const getSpawnPos =
this.spawnTransforms[Math.floor(Math.random() * this.spawnTransforms.length)];
const reviveResponse = {
playerId: userId,
transform: {
posX: getSpawnPos[0],
posY: getSpawnPos[1],
posZ: getSpawnPos[2],
rot: 0,
},
statInfo: user.statInfo,
};
createNotificationPacket(PACKET_ID.S_RevivePlayer, reviveResponse, this.getDungeonUsersUUID());
user.currentHp = user.statInfo.stats.maxHp;
logger.info(`userId: ${userId} 리스폰!`);
};
startRespawnTimer(userId, respawnTime) {
if (this.respawnTimers.has(userId)) {
logger.info(`respawnTimers에 userId: ${userId} 가 이미 존재합니다`);
return;
}
let remainingTime = respawnTime * 1000; // 초기 리스폰 시간 설정
const defaultIntervalTime = 1000; //기본값 1초
let intervalDuration = defaultIntervalTime;
let lastTime = Date.now();
const interval = setInterval(() => {
const currentTime = Date.now();
const timeDiff = currentTime - lastTime;
lastTime = currentTime;
remainingTime -= timeDiff; // 흐른 시간 만큼 감소
logger.info(`userId : ${userId} 리스폰 시간 ${remainingTime}ms`);
if (remainingTime <= 0) {
clearInterval(interval); // 타이머 종료
this.respawnTimers.delete(userId); // 관리 목록에서 제거
this.onRespawn(userId); // 내부 리스폰 처리
}
if (remainingTime < defaultIntervalTime) {
intervalDuration = remainingTime;
}
}, intervalDuration); // 1초 간격으로 실행
this.respawnTimers.set(userId, interval); // 타이머 등록
}
clearAllTimers() {
this.respawnTimers.forEach((interval) => clearInterval(interval));
this.respawnTimers.clear();
logger.info('모든 리스폰 타이머 클리어!');
}
Dispose() {
this.monsterLogic.Dispose();
this.monsterLogic = null;
this.clearAllTimers();
removeDungeonSession(this.dungeonId);
}
}
export default Dungeon;
5. dungeon.session
던전 세션은 유저세션과 비슷한데 던전에 들어간 유저들을 관리하는 던전 클래스로 만들어진 객체를
담아서 던전 자체를 관리하는 것 입니다.
저희는 서버가 1개 이기 때문에 여러 유저가 들어온다면 수많은 던전이 생성되기 때문에 그 던전을
서버 1개로 관리하기 위해서는 던전세션을 만들어 관리하는게 용이하기 때문에 만들었습니다.
import { dungeonSessions } from './sessions.js';
const addDungeonSession = (sessionId, dungeonLevel) => {
if (dungeonSessions.has(sessionId)) {
logger.error('세션 중복');
return null;
}
const dungeonCode = 1;
const dungeonAssets = getGameAssets().dungeonInfo; // 맵핑된 던전 데이터 가져오기
const dungeonInfo = dungeonAssets[dungeonCode]; // ID로 바로 접근
if (!dungeonInfo) {
logger.error(`던전 정보를 찾을 수 없습니다. dungeonCode: ${dungeonCode}`);
return null;
}
dungeonInfo.dungeonId = sessionId;
const dungeon = new Dungeon(dungeonInfo, dungeonLevel);
dungeonSessions.set(sessionId, dungeon);
return dungeon;
};
export const findDungeonByUserId = (userId) => {
let result = null;
for (const [_, dungeon] of dungeonSessions.entries()) {
if (dungeon.users.has(userId)) {
result = dungeon;
break;
}
}
return result;
};
const getDungeonSession = (dungeonId) => {
return dungeonSessions.get(dungeonId);
};
const getDungeonUsersUUID = (dungeonId) => {
const dungeon = getDungeonSession(dungeonId);
return dungeon.usersUUID;
};
const removeDungeonSession = (dungeonId) => {
const dungeon = dungeonSessions.get(dungeonId);
if (!dungeon) {
logger.error(`던전 세션이 존재하지 않습니다.`);
return;
}
return dungeonSessions.delete(dungeonId);
};
const getStatsByUserClass = (userClass) => {
const classAssets = getGameAssets().classInfo;
const classInfos = classAssets[userClass];
if (!classInfos) {
logger.error(`Class 정보를 찾을 수 없습니다. classId: ${userClass}`);
return null;
}
const expAssets = getGameAssets().expInfo;
const expInfos = expAssets[1];
if (!expInfos) {
logger.error('레벨 1의 경험치 정보를 찾을 수 없습니다.');
return null;
}
return {
level: 1,
stats: classInfos.stats,
exp: 0,
maxExp: expInfos.maxExp,
};
};
export {
addDungeonSession,
getDungeonSession,
removeDungeonSession,
getDungeonSessions,
getDungeonUsersUUID,
getStatsByUserClass,
};
6. 느낀점
작성하면서 여러 불편한 부분들이 많고 확장성은 전혀 신경쓰지 않는 부분을 고치지 않은 저의 잘못이 느껴지지만 그 부분을 고치려면 너무 많은 부분을 고쳐야해서 기일 시간을 맞추기 위해 일단 최대한 만들어진 코드의 스켈레톤 구조에서 추가하는 방식으로 하였습니다.
핸들러는 직접 다시 다 작성하고 있습니다.
가능한 SRP를 지키기 위해 노력중입니다.
'TIL' 카테고리의 다른 글
Node.js 서버 GetSkill.Handler 작성 - 최종프로젝트 1 (0) | 2024.12.17 |
---|---|
자원 전쟁 (서버) 프로젝트 - 용광로 만들기 2탄 (0) | 2024.12.05 |
자원 전쟁 (서버) 프로젝트 - 용광로 만들기 1탄 (0) | 2024.12.05 |
C# .csv파일 변환 방법과 예시 (0) | 2024.11.26 |
Class와 Structure의 차이점 및 사용 목적 (0) | 2024.11.26 |