상태 변화가 여러 객체에 영향을 미치거나 상태 변화가 발생할 때마다 여러 개체에 알림을 주어야하는 경우,
현재 프로젝트에서 적용할 수 있는 부분 -> 게임 세션 내의 유저들에게 모두 알림을 보내주어야 하는 경우.
e.g) 게임 시작 알림, 게임 끝, 상태 동기화 등
게임을 관리하는 게임 세션
import { EventEmitter } from 'events';
import messageType from '../../constants/header.js';
import IntervalManager from '../managers/interval.manager.js';
import GameStateManager from '../managers/game.manager.js';
import UserManager from '../managers/user.manager.js';
import Config from '../../config/config.js';
class GameSession extends EventEmitter {
constructor() {
super();
this.userManager = new UserManager();
this.intervalManager = new IntervalManager();
this.gameStateManager = new GameStateManager(this);
this.gameStartTime = null;
this.gameDuration = 120000;
this.gameEnded = false;
this.rank = [];
this.setupListeners();
}
setupListeners() {
this.on('gameStart', () => this.startGame());
this.on('gameEnd', () => this.endGame());
this.on('userDeath', (user) => this.rank.push(user));
}
addUser(user) {
this.userManager.addUser(user);
}
getUser(playerId) {
return this.userManager.getUser(playerId);
}
removeUser(user) {
this.userManager.removeUser(user);
}
getAllLocation() {
return {
syncPlayerDataNotification: {
players: this.userManager.getAllUsers().map((user) => ({
playerId: user.id,
x: user.x,
z: user.z,
hp: user.hp,
state: user.state,
})),
},
};
}
startGame() {
this.gameStartTime = Date.now();
this.gameEnded = false;
this.gameStateManager.startGame();
// 0.5초마다 상태 동기화
this.intervalManager.addPlayer(
'syncPlayerData',
() => {
const syncPacket = this.getAllLocation();
this.userManager.notifyUsers({
messageType: messageType.syncPlayerDataNotification,
payload: syncPacket,
});
if (!this.gameEnded) {
this.checkGameEnd();
}
},
500,
);
// 30초마다 맵 변경 2번만
this.intervalManager.addLimitedInterval(
'MapChange',
() => {
const packet = { syncMapChangeNotification: {} };
this.userManager.notifyUsers({
messageType: messageType.syncMapChangeNotification,
payload: packet,
});
},
30000,
2,
);
// 0.5초마다 HP 업데이트 및 사망 처리
this.intervalManager.addPlayer(
'updateHp',
() => {
this.userManager.getAllUsers().forEach((user) => {
if (Math.abs(user.x) > 15 || Math.abs(user.z) > 15) {
user.hp = Math.max(user.hp - 10, 0);
if (user.hp <= 0 && !(user.state == Config.STATE.DIE)) {
user.hp = 0;
user.state = Config.STATE.DIE;
this.gameStateManager.notifyDeath(user);
this.emit('userDeath', user);
}
}
});
},
500,
);
}
checkGameEnd() {
const elapsedTime = Date.now() - this.gameStartTime;
// 120초가 경과한 경우 게임 종료
if (elapsedTime >= this.gameDuration) {
this.emit('gameEnd');
}
// HP가 1 이상인 플레이어가 1명만 남은 경우
const alivePlayers = this.userManager.getAllUsers().filter((user) => user.hp > 0);
if (alivePlayers.length <= 1) {
this.emit('gameEnd');
}
}
// 게임 종료 처리
endGame() {
if (this.gameEnded) return;
this.gameEnded = true;
// 사망 순서에 따라 등수 매기기
const alivePlayers = this.userManager.getAllUsers().filter((user) => user.hp > 0);
const playerRanks = [];
// 살아남은 플레이어를 1등으로 처리
if (alivePlayers.length > 0) {
playerRanks.push({
playerId: alivePlayers[0].id,
rank: 1,
});
}
this.rank.forEach((user, index) => {
playerRanks.push({
playerId: user.id,
rank: alivePlayers.length > 0 ? index + 2 : index + 1,
});
});
// 아오 망할 repeated 페이로드
const endGamePacket = {
messageType: messageType.gameEndNotification,
payload: {
gameEndNotification: {
ranks: playerRanks,
endTime: Date.now(),
},
},
};
this.userManager.notifyUsers(endGamePacket);
this.intervalManager.clearAll();
}
}
export default GameSession;
이벤트 에미터를 이용해서 특정 이벤트(게임 시작 등)을 리스닝하고 있다가 이벤트를 전달받으면
지정된 메소드를 실행.
게임 세션에 등록된 유저 매니저를 통해 유저들에게 일제히 이벤트를 송신하는 방식.
세션 내 유저들에게만 알림을 보낼 수 있게 설계.
유저들을 관리할 유저 매니저
class UserManager {
constructor() {
this.users = [];
}
addUser(user) {
this.users.push(user);
console.log(`${user.id}님이 접속하셨습니다. 현재 유저: ${this.users.length}`);
}
getUser(playerId) {
return this.users.find((user) => user.id === playerId);
}
removeUser(user) {
const index = this.users.indexOf(user);
if (index !== -1) {
this.users.splice(index, 1);
console.log(`${user.id}님이 접속 해제하셨습니다. 현재 유저: ${this.users.length}`);
}
}
getAllUsers() {
return this.users;
}
notifyUsers(data) {
this.users.forEach((user) => user.emit('sendPacket', data));
}
}
export default UserManager;
notufyUsers를 통해서 세션 내 유저들에게 패킷 전송 전달.
import { EventEmitter } from 'events';
import { createResponse } from '../../utils/response/createResponse.js';
class User extends EventEmitter {
constructor(socket, id) {
super();
this.socket = socket;
this.id = id;
this.x = 0;
this.z = 0;
this.lastX = 0;
this.lastZ = 0;
this.hp = 100;
this.state = 0;
this.setupListeners();
}
setupListeners() {
this.on('sendPacket', this.sendPacket);
this.on('updatePosition', this.updatePosition);
}
sendPacket(data) {
const buffer = createResponse(data.messageType, '1.0.0', 0, data.payload);
this.socket.write(buffer);
}
updatePosition(x, z) {
this.lastX = this.x;
this.lastZ = this.z;
this.x = x;
this.z = z;
if (Math.abs(this.lastX - this.x) > 0 || Math.abs(this.lastZ - this.z) > 0) {
this.state = 1;
}
}
}
export default User;
유저 클래스는 패킷 전달 이벤트를 받으면 받은 데이터를 전송한다.
상태 변화 -> 특정 인원이 모여서 게임 시작, HP를 모두 소모해서 사망 처리, 시간 경과 or 게임 종료 조건이 충족되어 게임 종료 등
상태가 변화하면 상태에 따라 이벤트 전달 -> 유저 매니저에서 세션 내 여러 개체에게 알림.
현재 프로젝트에 맞는 부분만 간단하게 적용해보았다.