오늘의 목표

더보기

✔️ 서버 전용 더미 클라 


⏱️ 오늘의 일정

서버 전용 더미 클라


📜 서버 전용 더미 클라

 

서버 스트레스 테스트를 하기 위해 더미 클라를 작성하고 있다.

class Client {
    private clientSocket: any;

    constructor() {
        this.clientSocket = new net.Socket();
    }

    connect() {
        this.clientSocket.connect(5555, "127.0.0.1", async () => {
            console.log(`"127.0.0.1" : 5555 서버와 연결`);

            setTimeout(() => {
                const registerPacket = CreatePacket(config.packetType.REGISTER_REQUEST, "1.0.0", 1, {
                    id: "123",
                    password: '123',
                    email: '123'
                });

                this.clientSocket.write(registerPacket);
            }, 2000);

            this.clientSocket.on('data', (data: Buffer) => {
                let offset = 0;

                const packetType = data.readUInt16BE(offset);
                offset += 2;

                const versionLength = data.readUInt8(offset);
                offset += 1;

                const version = data.subarray(offset, offset + versionLength).toString('utf-8');
                offset += versionLength;

                const sequence = data.readUInt32BE(offset);
                offset += 4;

                const payloadLength = data.readUInt32BE(offset);
                offset += 4;

                const payload = data.subarray(offset, offset + payloadLength);
                const packet = protoMessages.packet.GamePacket;

                const payloadData = packet.decode(payload);

                switch (packetType) {
                    case config.packetType.REGISTER_RESPONSE:
                        console.log("payloadData", payloadData);
                        break;
                }

            });
        });
    }
}

const dummyClient = new Client();
dummyClient.connect();

 

우선 간단하게 서버로 접속하고 서버 한테 응답 패킷을 받는지 확인하는 것까지만 했다.

내일 부터 본격적으로 채팅서버와 게임서버에 접속하고 게임 진행 및 채팅 전송까지 시나리오 대로

더미 클라를 조정하는 코드를 작성해 스트레스 테스트를 진행해야겠다.

 

 

오늘의 목표

더보기

✔️ 채팅 서버 완성


⏱️ 오늘의 일정

채팅 서버 완성


📜 채팅 서버 완성

 

채팅 서버를 완성했다. 

완성이라는 말이 거창하긴 하지만 ...

유저가 게임방을 생성하면 채팅방을 생성하고, 해당 채팅방으로 입장한다.

게임이 시작되면 엔터키를 이용해 채팅을 전송하고,

채팅을 전송한 유저가 속해 있는 방 유저들에게 채팅을 모두 전송한다.

 

ChattingServer Update

update() {
        const now = performance.now();
        
        const elapsed = now - this.lastExecutionTime;

        if (elapsed >= this.updateTime) {
            // JobQue에서 Job을 꺼내 패킷 처리
            while (this.chattingServerJobQue.length > 0) {
                const job = this.chattingServerJobQue.shift();
                if (!job) {
                    console.log("job이 undefine");
                    break;
                }

                const jobHandler = getChattingServerJobHandlerByJobType(job.jobType);
                jobHandler?.(job);
            }

            // 방 업데이트
            this.rooms.forEach((room) => {
                room.update();
            });      

            
            this.lastExecutionTime = now - (elapsed % this.updateTime);
        }
                          
        const delay = this.updateTime - (now - this.lastExecutionTime); 
        
        setTimeout(() => this.update(), delay);
    }

 

update() 문을 돌면서 chattingServerJobQue에 Job이 들어오면,

Job을 꺼내고 jobHandler에게 전달해 Job을 처리한다.

 

 

chattingLoginJobHandler

// 채팅 서버 로그인
export const chattingLoginJobHandler = async (job: Job): Promise<void> => {
    const userEmail = job.payload[0] as string;
    const userSocket = job.payload[1] as CustomSocket;

    const user: any = await DatabaseManager.getInstance().findUserByEmail(userEmail);
    if (!user) {
        return;
    }

    const loginUserNickName = user.nickname as string;

    const newChattingUser = new ChattingUser(userSocket, userEmail, loginUserNickName);
    ChattingServer.getInstance().getUsers().push(newChattingUser);
}

 

클라가 보낸 Email, 그리고 socket을 가져오고

DB에서 User에 대한 정보를 가져와 새로운 User를 생성해 저장한다.

 

 

chattingChatSendJobHandler

export const chattingChatSendJobHandler = (job: Job): void => {
    const chattingRoom = job.payload[0] as ChattingRoom;
    const chatMessageSendUser = job.payload[1] as ChattingUser;
    const chatMessage = job.payload[2] as string;

    const isFindUser = chattingRoom.userFind(chatMessageSendUser.getId());
    if (isFindUser === undefined) {
        console.log("chattingChatSendJobHandler User 없음");
        return;
    }

    const chattingChatSendJob = new Job(config.jobType.CHATTING_CHAT_SEND_REQUEST_JOB, chatMessageSendUser, chatMessage);
    chattingRoom.roomJobQue.push(chattingChatSendJob);
}

 

유저가 보낸 채팅 메세지를 재조립해 유저가 속한 Room의 JobQue에 넣는다.

 

 

Room Update

 update() {
        while (this.roomJobQue.length > 0) {
            const job = this.roomJobQue.shift();
            if (!job) {
                console.log("job이 없음");
                break;
            }

            switch (job.jobType) {
                case config.jobType.CHATTING_CHAT_SEND_REQUEST_JOB:
                    const chattingSendUser = job.payload[0] as ChattingUser;
                    const chatMessage = job.payload[1] as string;

                    const chatSendResponse = {
                        nickName: chattingSendUser.getNickName(),
                        chatMessage
                    }

                    this.users.forEach((user: ChattingUser) => {
                        sendChattingPacket(user.getUserSocket(), config.chattingPacketType.CHATTING_CHAT_SEND_RESPONSE, chatSendResponse);
                    });
                    break;
            }
        }
    }

 

update가 호출되면 roomJobQue에서 job을 꺼내 처리한다.

jobType이 CHATTING_CHAT_SEND_REQUEST_JOB라면

 

채팅 응답 메세지를 작성해 방에 있는 모든 유저들에게 전송한다.

 

 

chattingLeaveRoomJobHandler

export const chattingLeaveRoomJobHandler = (job: Job): void => {
    const chattingLeaveRoomUser = job.payload[0] as ChattingUser;

    const leaveChattingRoomId = chattingLeaveRoomUser.getJoinRoomId();

    const leaveChattingRoom = ChattingServer.getInstance().getRoomByRoomId(leaveChattingRoomId);
    if (leaveChattingRoom === undefined) {
        console.log("퇴장하려는 채팅방이 없음");
        return;
    }

    leaveChattingRoom.roomUserDelete(chattingLeaveRoomUser.getId());
}

 

유저가 게임에서 나가거나, 접속을 종료하면 채팅방에서 나간다.

기본적으로 Node.js는 단일 스레드에서 실행되고, 이벤트 루프는 한번에 하나의 프로세스만 발생한다.

따라서 CPU 집약적인 작업 ( 대규모 데이터 처리, 이미지 변환 등 )이 메인 스레드를 차단 할 수 있다.

 

Worker Thread

CPU 집약적인 작업을 실행할 때 메인 스레드의 성능과 응답성을 유지하기 위해 사용하는 백그라운드 스레드다.

Wokrer는 비동기적으로 실행되고, JavaScript의 싱글 스레드 모델을 보완하여 멀티스레드 환경을 제공한다.

 

1. 독립된 스레드

 Woker는 메인 스레드와 별도의 실행 컨텍스트에서 동작하며, 독립적인 global scope를 가진다.

 

2. 메세지 기반 통신

 메인 스레드와 Worker는 postMessage()와 onMessage를 통해 데이터를 교환한다.

 

woker.js

// Worker 내부 코드
onmessage = function(event) {
  const data = event.data; // 메인 스레드에서 받은 데이터
  const result = data * 2; // 작업 처리
  postMessage(result); // 결과 반환
};

 

main.js

// 메인 스레드 코드
const worker = new Worker('worker.js');

// 메시지 보내기
worker.postMessage(10);

// 메시지 받기
worker.onmessage = function(event) {
  console.log('Worker로부터 받은 결과:', event.data); // 20
};

// Worker 종료
worker.terminate();

 

오늘의 목표

더보기

✔️ 최종 프로젝트 진행


⏱️ 오늘의 일정

최종 프로젝트 진행


📜 최종 프로젝트 진행 ( 채팅 서버 구현 시작 )

 

프로젝트가 게임 종료 처리만 구현하면 기본적인 게임 진행이 완성되어서,

채팅서버 구현을 맡게 되었다.

 

채팅서버는 기존에 배워놨었던 c++에서 구현한 서버를 기반으로 해서 구현하려고 한다.

 update() {
        const now = performance.now();
        
        const elapsed = now - this.lastExecutionTime;

        if (elapsed >= this.updateTime) {

            // 로직 처리
            
            this.lastExecutionTime = now - (elapsed % this.updateTime);
        }
                          
        const delay = this.updateTime - (now - this.lastExecutionTime);
        
        setTimeout(() => this.update(), delay);
    }

 

메인 서버와는 다르게 채팅서버는 update() 함수가 있다.

update() 함수는 60 프레임을 기준으로 루프를 돌면서 로직을 처리한다.

 

메인 서버에서는 핸들러에서 로직을 처리해줬지만,

export class Job {
    public jobType: number;
    public payload: Buffer;
    constructor(jobType: number, payload: Buffer) {
        this.jobType = jobType;
        this.payload = payload;
    }
}

 

채팅 서버에서는 핸들러에서 job을 생성하고, 생성한 job을 채팅 서버에게 전달하면,

채팅 서버에서 로직을 처리하는 방식으로 구현할 예정이다.

 

클라에서 온 패킷을 빠르게 빠르게 조립해서 큐잉만 하고, 채팅 서버에서 직접 처리하는 방식

오늘의 목표

더보기
최종 프로젝트 진행 ( 상점 기능 )

⏱️ 오늘의 일정

최종 프로젝트 진행 ( 상점 기능 )


📜 최종 프로젝트 진행 ( 상점 기능 )

 

최종 프로젝트를 진행했다.

게임에 맞게 상점 기능을 구현했다.

 

export const fleaMarketCardCreate = async (level: number, roomId: number, cardCount: number): Promise<void> => {
    const cards: CardType[] = [];

    let fleaMarketCards = await getRedisData('fleaMarketCards');
    if (!fleaMarketCards) {
        fleaMarketCards = { [roomId]: [] };        
    } else if (!fleaMarketCards[roomId]) {
        fleaMarketCards[roomId] = [];        
    }

    for (let i = 0; i < cardCount; i++) {
        const randomCardType = randomNumber(1, 23) as CardType;
        cards.push(randomCardType);
    }

    fleaMarketCards[roomId][level] = cards;

    await setRedisData('fleaMarketCards', fleaMarketCards);
}

 

게임을 시작 하면 위 함수를 호출한다.

level에 따라 카드를 생성해 redis에 roomId를 key값으로 해서 저장한다.

 

레디스에 저장되어 있는 카드 목록

 

 

    let redisFleaMarketCards = await getRedisData('fleaMarketCards');
    if (!redisFleaMarketCards) {
        console.error("fleaMarketOpen 레디스에 플리 마켓 카드 없음");
        return;
    }

    const fleaMarketCards = redisFleaMarketCards[room.id];
    if (!fleaMarketCards) {
        console.error("fleaMarketOpen 방에 생성된 플리 마켓 카드 없음");
        return;
    }

    sendPacket(socket, config.packetType.FLEA_MARKET_PICK_RESPONSE, {
        fleaMarketCardTypes: fleaMarketCards
    });

 

상점 오픈 요청이 서버로 오면 레디스에서 카드목록을 읽어와 클라에게 전달한다.

 

생성된 카드 목록

 

 

 let redisFleaMarketCards = await getRedisData('fleaMarketCards');
    if (!redisFleaMarketCards) {
        console.error("fleaMarketItemSelect 레디스에 상점 카드가 없음")
        return;
    }

    const cards = redisFleaMarketCards[room.id];
    if (!cards || cards.length === 0 || cards.length < fleaMarketItemSelectPayload.pickIndex) {
        console.error("fleaMarketItemSelect 레디스 상점 카드에 선택한 카드가 없음");
        return;
    }

    const fleMarketPickCard = cards[fleaMarketItemSelectPayload.pickIndex];
    if (!fleMarketPickCard) {
        console.error("fleaMarketItemSelect PickCard empty");
        return;
    }

    const newCard: Card = {
        type: fleMarketPickCard,
        count: 1
    }

    cardPickUser.character.handCards.push(newCard);

    redisFleaMarketCards[room.id] = cards.filter((card: number) => card !== fleMarketPickCard);

    await setRedisData('fleaMarketCards', redisFleaMarketCards);

    sendPacket(socket, config.packetType.FLEA_MARKET_CARD_PICK_RESPONSE,
        {
            userId: cardPickUser.id,
            handCards: cardPickUser.character.handCards
        }
    );

 

클라가 카드를 선택하면 레디스에 저장되어 있는 카드중에서 해당 카드를 가져와

카드를 생성하고 저장한 후, 클라에게 결과를 전달한다.

 

선택한 카드를 레디스에서 제거해서, 다른 유저들은 해당 카드를 선택하지 못하도록 한다.

지금은 똑같은 카드를 선택하게 되면 막아주는 부분이 없는데,

클라에서 추가로 카드의 타입을 전달해 검증하는 로직을 추가할 예정!

 

 

오늘의 목표

더보기

✔️ 최종프로젝트 진행

 


⏱️ 오늘의 일정

최종 프로젝트 진행


📜 최종프로젝트 진행

 

 

몬스터 사망 보상과 유저 상태 업데이트를 구현했다.

 

몬스터 사망 보상

export const monsterDeathReward = async (killer: User, monster: User) => {
    if (!monster || !killer) {
        console.log(`monsterDeathReward monster : ${monster} user : ${killer}가 없음`);
        return;
    }

    const monsterHP = monster.character.hp;
    if (monsterHP > 0) {
        console.log(`monsterDeathReward 몬스터가 죽지 않음 hp : ${monsterHP}`);
        return;
    }

    const rooms: Room[] | null = await getRooms();
    if (!rooms) {
        console.log('monsterDeathReward 방이 없음');
        return;
    }

    let killerRoom: Room | null | undefined = null;
    let monsterRoom: Room | null | undefined = null;

    killerRoom = rooms.find(room =>
        room.users.some(user => user.id === killer.id)
    );

    monsterRoom = rooms.find(room =>
        room.users.some(user => user.id === monster.id)
    );

    if (!killerRoom) {
        console.log('monsterDeathReward killer가 속한 방이 없음');
        return;
    }

    if (!monsterRoom) {
        console.log('monsterDeathReward monster가 속한 방이 없음');
        return;
    }

    if (killerRoom.id !== monsterRoom.id) {
        console.log('monsterDeathReward killerRoom과 monsterRoom이 다름');
        return;
    }

    if (!killer.character) {
        console.log('monsterDeathReward killerCharacter가 없음');
        return;
    }

    if (!killer.character.handCards) {
        console.log('monsterDeathReward killerCharacter handCards가 없음');
        return;
    }

    const monsterDeathRewardCardType = randomNumber(1, 23);
    let isExistCard = false;

    for (let i = 0; i < killer.character.handCards.length; i++) {
        if (killer.character.handCards[i].type === monsterDeathRewardCardType) {
            killer.character.handCards[i].count++;
            isExistCard = true;
            break;
        }
    }

    if (isExistCard === false) {
        const newCard: Card = {
            type: monsterDeathRewardCardType,
            count: 1
        }

        killer.character.handCards.push(newCard);
    }

    for (let i = 0; i < rooms.length; i++) {
        if (rooms[i].id = killerRoom.id) {
            rooms[i] = killerRoom;
            break;
        }
    }

    await setRedisData('roomData', rooms);

    userUpdateNotification(killerRoom);
}

 

몬스터를 죽인 대상과 몬스터를 매개변수로 받아 각종 검사를 진행한다.

검사를 통과하면 카드를 생성해 캐릭터에게 보상으로 건네주고 업데이트 해준다.

 

유저 상태 업데이트

export const userUpdateNotification = (room: Room | null) => {
    if (!room) {
        console.log('userUpdateNoti userSocket 방이 없음');
        return;
    }

    room.users.forEach(async (user) => {
        const userSocket = await getSocketByUser(user);

        if (userSocket) {
            sendPacket(userSocket, config.packetType.USER_UPDATE_NOTIFICATION,
                {
                    user: room.users
                }
            );
        }
        // else {
        //     console.log('`userUpdateNoti userSocket이 없음');
        //     return;
        // }
    });

    return;
}

 

방안에 있는 유저들에게 방안에 있는 유저들의 정보를 전달한다.

오늘의 목표

더보기

✔️ 최종프로젝트 진행

 


⏱️ 오늘의 일정

최종프로젝트 진행


📜 최종프로젝트 진행

 

최종프로젝트를 진행했다.저번에 이어 게임 준비와 게임 시작 까지 완성해 게임의 기본적인 틀을 완성했다.

 

게임 준비 요청 처리

import net from 'net';
import { GlobalFailCode } from '../enumTyps.js';
import {
  CustomSocket,
  RedisUserData,
  Room,
  User,
} from '../../interface/interface.js';
import { config } from '../../config/config.js';
import { sendPacket } from '../../packet/createPacket.js';
import {
  getRoomByUserId,
  getRooms,
  getSocketByUser,
  getUserBySocket,
  setCharacterInfoInit,
  setRedisData,
} from '../handlerMethod.js';

export const gamePrepareHandler = async (
  socket: CustomSocket,
  payload: Object,
) => {
  try {
    // requset 보낸 유저
    const user: RedisUserData = await getUserBySocket(socket);

    // 유저가 있는 방 찾기
    if (user !== undefined) {
      const room: Room | null = await getRoomByUserId(user.id);
      if (room === null) {
        return;
      }
      if (room.users.length <= 1) {
        console.error('게임을 시작 할 수 없습니다(인원 부족).');
        const responseData = {
          success: false,
          failCode: GlobalFailCode.INVALID_REQUEST,
        };
        sendPacket(
          socket,
          config.packetType.GAME_PREPARE_RESPONSE,
          responseData,
        );
      } else {
        // 게임준비 시작 요건 충족
        const responseData = {
          success: true,
          failCode: GlobalFailCode.NONE,
        };
        sendPacket(
          socket,
          config.packetType.GAME_PREPARE_RESPONSE,
          responseData,
        );

        // 방에있는 유저들 캐릭터 랜덤 배정하기
        room.users = setCharacterInfoInit(room.users);
        const rooms: Room[] | null = await getRooms();
        if (!rooms) {
          return;
        }
        // 변경한 정보 덮어쓰기
        for (let i = 0; i < rooms.length; i++) {
          if (rooms[i].id === room.id) {
            rooms[i] = room;
            break;
          }
        }
        // 레디스에 있는 룸 배열에서 user가 속해 있는 방을 수정하고,
        // 위에서 수정한 방이 포함되어 있는 전체 배열을 넣음
        await setRedisData('roomData', rooms);

        // 방에있는 유저들에게 notifi 보내기
        for (let i = 0; i < room.users.length; i++) {
          const userSocket = await getSocketByUser(room.users[i]);
          if (!userSocket) {
            console.error('gamePrepareHandler: socket not found');
            return;
          }
          //console.dir(room, { depth: null });
          sendPacket(userSocket, config.packetType.GAME_PREPARE_NOTIFICATION, {
            room,
          });
        }
      }
    } else {
      console.error('위치: gamePrepareHandler, 유저를 찾을 수 없습니다.');
      const responseData = {
        success: false,
        failCode: GlobalFailCode.INVALID_REQUEST,
      };
      sendPacket(socket, config.packetType.GAME_PREPARE_RESPONSE, responseData);
    }
  } catch (err) {
    const responseData = {
      success: false,
      failCode: GlobalFailCode.INVALID_REQUEST,
    };
    sendPacket(socket, config.packetType.GAME_PREPARE_RESPONSE, responseData);
    console.error('gameStartHandler 오류', err);
  }
};

 

위 코드는 게임 준비 요청을 클라가 전송하면 서버에서 처리를 해주는 코드로,

우선 클라가 속한 방을 찾은 후, 방이 게임 준비 요건에 맞는지 검사한다.

 

방이 게임 준비 요건에 맞으면, 각 캐릭터들의 역할을 랜덤하게 서버가 정해주고 방 정보를 저장한 후,

방 안에 있는 유저들에게 noti를 날려 게임 준비가 완료되었다고 알려준다.

 

게임 준비가 완료되어 캐릭터와 역할이 정해진 모습


게임시작 요청 처리

import net from 'net';
import {
  CharacterPositionData,
  CustomSocket,
  RedisUserData,
} from '../../interface/interface.js';
import { GlobalFailCode, PhaseType } from '../enumTyps.js';
import { sendPacket } from '../../packet/createPacket.js';
import { config } from '../../config/config.js';
import {
  getRedisData,
  getRoomByUserId,
  getSocketByUser,
  getUserBySocket,
  setRedisData,
} from '../handlerMethod.js';

export const gameStartHandler = async (
  socket: CustomSocket,
  payload: Object,
) => {  
  try {
    // requset 보낸 유저
    const user: RedisUserData = await getUserBySocket(socket);
    // 유저가 있는 방 찾기
    if (user !== undefined) {
      const room = await getRoomByUserId(user.id);
      if (room === null) {
        const responseData = {
          success: false,
          failCode: GlobalFailCode.INVALID_REQUEST,
        };
        sendPacket(socket, config.packetType.GAME_START_RESPONSE, responseData);

        return;
      }

      if (room.users.length <= 1) {
        console.error('게임을 시작 할 수 없습니다.(인원 부족)');
      }

      const responseData = {
        success: true,
        failCode: GlobalFailCode.NONE,
      };
      sendPacket(socket, config.packetType.GAME_START_RESPONSE, responseData);

      let characterPositionDatas = await getRedisData('characterPositionDatas');
      if (!characterPositionDatas) {
        characterPositionDatas = { [room.id]: [] };
      } else {
        characterPositionDatas[room.id] = [];
      }

      for (let i = 0; i < room.users.length; i++) {
        // 위치 데이터 로직 필요
        const characterPositionData: CharacterPositionData = {
          id: room.users[i].id,
          x: 0 + 1 * i,
          y: 0 - 1 * i,
        };
        characterPositionDatas[room.id].push(characterPositionData);
      }

      await setRedisData('characterPositionDatas', characterPositionDatas);

      // 방에있는 유저들에게 notifi 보내기
      for (let i = 0; i < room.users.length; i++) {
        const userSocket = await getSocketByUser(room.users[i]);
        if (!userSocket) {
          console.error('gameStartHandler: socket not found');
          const responseData = {
            success: false,
            failCode: GlobalFailCode.INVALID_REQUEST,
          };
          sendPacket(
            socket,
            config.packetType.GAME_START_RESPONSE,
            responseData,
          );
          return;
        }
        // noti 데이터
        const now = Date.now() + 300000;
        const gameStateData = { phaseType: PhaseType.DAY, nextPhaseAt: now };
        const notifiData = {
          gameState: gameStateData,
          users: room.users,
          characterPositions: characterPositionDatas[room.id],
        };

        sendPacket(
          userSocket,
          config.packetType.GAME_START_NOTIFICATION,
          notifiData,
        );
      }
    } else {
      console.error('위치: gameStartHandler, 유저를 찾을 수 없습니다.');

      const responseData = {
        success: false,
        failCode: GlobalFailCode.INVALID_REQUEST,
      };
      sendPacket(socket, config.packetType.GAME_START_RESPONSE, responseData);
    }
  } catch (err) {
    const responseData = {
      success: false,
      failCode: GlobalFailCode.INVALID_REQUEST,
    };
    sendPacket(socket, config.packetType.GAME_START_RESPONSE, responseData);
    console.log('gameStartHandler 오류', err);
  }
};

 

위 코드는 게임 시작 요청을 클라가 전송하면 서버에서 처리를 해주는 코드로,

우선 클라가 속한 방을 찾은 후, 방이 게임 준비 요건에 맞는지 검사한다.

 

방이 게임 시작 요건에 맞으면, 각 캐릭터들의 좌표를 설정해주고, 레디스에 좌표를 저장 한 후

게임 시작에 필요한 정보를 설정하고, 방안에 있는 유저들에게 noti를 날려 게임시작이 완료되었다고 알려준다.

 

 

게임시작이 완료되어 캐릭터가 생성된 모습

 

 

게임 시작에서 캐릭터의 시작 위치를 랜덤하게 설정해줬는데,

추후에 미리 스폰 위치를 여러개 설정하고, 스폰 위치 중에서 랜덤하게 선택해 유저가 스폰될 수 있도록 고쳐야 겠다.

오늘의 목표

더보기

✔️ 최종프로젝트 진행

 


⏱️ 오늘의 일정

최종프로젝트 진행


📜 최종프로젝트 진행

 

 

 

게임에서 사용할 데이터들을 ERD를 활용해 작성했다.기본적으로 서버에서 사용하는 모든 데이터들은 redis를 통해 관리한다.

다만, 유저에 대한 소켓을 저장해야하는 부분만큼은 서버에서

export const socketSessions: Record<number, CustomSocket> = {};

 

위와 같이 따로 관리해주도록 했다. 

 

 

 

프로젝트가 위처럼 어느정도 기본틀은 갖추게 되어서,

 

캠프에서 제공해주는 클라기능은 위 그림에 남아있는

게임 준비완료, 게임 시작, 위치 업데이트까지 구현하고,

 

우리 팀만의 프로젝트를 진행하기로 결정했다.

 

 

 

 

여기까지 완성!

 

오늘의 목표

더보기

✔️ 최종프로젝트 진행


⏱️ 오늘의 일정

최종 프로젝트 진행


📜 최종 프로젝트 진행

 

아무래도 이번 프로젝트는 최종프로젝트이니까 모든 팀원들이 서버에 작성한 모든 코드를 이해해야한다는 것을 팀원 모두 공유 했기 때문에 Live Share를 통해 같이 코드를 구현하도록 했다.

 

스켈레톤 코드의 내용은 회원가입, 로그인, 패킷을 주고 받는 과정, DB 연결 사용까지 구현하고,이후 지정되어 있는 패킷을 팀원들 각자 진행하기로 했다.

 

기존 강의로 배웠던 서버는 기본적으로 함수로 listen을 하고, connect 연결을 담당했는데나의 주장으로 클래스로 구성하기로 했다.

 

class Server {
  private static gInstance : Server | null = null;
  private protoMessages: { [key: string]: any } = {};  
  private server: net.Server;

  private constructor() {
    this.server = net.createServer(this.clientConnection);
  }

  static getInstance(){
    if(Server.gInstance === null)
    {
      Server.gInstance = new Server();
    }

    return Server.gInstance;
  }    
  
  clientConnection(socket : net.Socket)
  {
    const customSocket = socket as CustomSocket;

    console.log(`Client connected from: ${socket.remoteAddress}:${socket.remotePort}`);    
      
    customSocket.buffer = Buffer.alloc(0);

    customSocket.on('data', onData(customSocket));
    customSocket.on('end', onEnd(customSocket));
    customSocket.on('error', onError(customSocket));
  }

  listen () {
    this.server.listen(config.server.port, config.server.host, () => {
      console.log(`서버가 ${config.server.host}:${config.server.port}에서 실행 중입니다.`);
      console.log(this.server.address());
    });
  }

  start()
  {   
    this.listen();
  }
}

export default Server;

 

server라는 클래스를 생성해 start() 함수를 호출함으로써 서버를 시작한다.

 

import Server from './class/server.js';

function main() {
  Server.getInstance().start();  
}
main();

 

main.ts에서 server.start()를 호출하는 모습

 

import net from "net";

class Client {
    private clientSocket: any;    

    constructor() {
        this.clientSocket = new net.Socket();        
    }

    connect(){
        this.clientSocket.connect(5555, "127.0.0.1", async () => {
            console.log(`"127.0.0.1" : 5555 서버와 연결`);
            
            this.clientSocket.on('data', (data:Buffer) => {

            });           
        });
    }
}

const dummyClient = new Client();
dummyClient.connect();

 

간단하게 서버에 접속할 수 있는 client를 만들어 테스트

오늘의 목표

더보기

✔️ 최종프로젝트 진행


⏱️ 오늘의 일정

최종프로젝트 진행


📜 최종프로젝트 진행

 

최종 프로젝트 회의를 진행했다.

캠프에서 제공해주는 클라는 총 2가지 인데, 다음과 같다. 

 

 

 

3d 캐릭터로 구성되어 있는 턴제 기반의 클라

 


 

 

 

2D 캐릭터로 구성되어 있는 클라를 제공해줬다.

 


 

게임 플로우 정하기

 

팀원분들과 회의를 거쳐서 우리 팀이 만들고자 하는 게임을 정했다.

내용은 다음과 같다.

 

5명의 유저 중 1명이 보스가 되고 4명이 일반 유저가 되어 서로 싸우는 게임

  •  5명의 유저가 매칭이 되면 랜덤하게 1명이 보스가 되고 나머지 4명의 유저는 4개의 직업을 1개씩 나눠 가지고 게임이 시작된다.보스는 4명의 유저가 성장하기 전에 쫒아다니면서 그들을 죽인다.
  • 성장의 시간이 끝나면 탈출로가 열리고 해당 탈출로를 통해 탈출한 유저는 승리 그렇지 못한 유저는 패배가 된다.
  • 4명의 유저는 스폰되는 몬스터를 잡아서 보스에 대적할 수 있도록 실시간으로 성장하며 보스는 4명의 유저가 성장하기 전에 쫒아다니면서 그들을 죽인다.
  • 성장의 시간이 끝나면 탈출로가 열리고 해당 탈출로를 통해 탈출한 유저는 승리 그렇지 못한 유저는 패배가 된다.

 

위와 같은 게임 진행과정을 생각했을때, 개인적으로 2번 클라가 잘 어울린다고 생각을 해 2번을 추천했다.

높은 비율로 2번이 추천되어서 2번 클라를 선택했다.

 


ERD 그리기

 

 

User 테이블과 Match 테이블을 준비했다.

 

Match 테이블은 User가 게임 진행한 상황을 기록하는 테이블이다.

+ Recent posts