[ 2024.11.15 TIL ] 옵저버 패턴

박지영·2024년 11월 15일
0

Today I Learned

목록 보기
80/88

옵저버 패턴

상태 변화가 여러 객체에 영향을 미치거나 상태 변화가 발생할 때마다 여러 개체에 알림을 주어야하는 경우,

현재 프로젝트에서 적용할 수 있는 부분 -> 게임 세션 내의 유저들에게 모두 알림을 보내주어야 하는 경우.

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 게임 종료 조건이 충족되어 게임 종료 등

상태가 변화하면 상태에 따라 이벤트 전달 -> 유저 매니저에서 세션 내 여러 개체에게 알림.

현재 프로젝트에 맞는 부분만 간단하게 적용해보았다.

profile
신입 개발자

0개의 댓글