게임엔진프로그래밍 - 수학

이미지
1. 브레젠험 직선 알고리즘 컴퓨터에서 계산이 느린 실수 연산을 사용하지 않고 직선을 그리기 위해 만들이진 알고리즘입니다. 평면을 아래와 같이 8분 면으로 나누어 직선을 그립니다. 링크:  https://sulinep.blogspot.com/2020/05/bresenhams-line-algorithm.html 2. 엔진 기초 수학 여기서는 본격적으로 구현에 들어가기 전 기초 수학 지식을 쌓았습니다.  수학에서의 체는 대수적 구조의 하나로 덧셈, 뺄셈, 곱셈, 나눗셈의 사칙연산을 집합 안에서 소화할 수 있는 집합을 의미합니다. 체를 이루기 위한 조건과 체라는 개념을 알아보았습니다. 처음에 체 라는 개념이 뭔가 머릿속에서 애매했는데 이후 갈로이스 체에 대해 배운 후 조금 더 명확해졌습니다. 스칼라 는 벡터를 정의하기 위한 필수 요소이고 크기만 있고 방향이 없는 성분이다. 벡터는 크기와 방향을 포함하는 표현 도구이다. 겨기서 벡터의 기본 연산자들을 알아보았습니다. 선형성 은 직선처럼 똑바른 도형 또는 그와 비슷한 성질을 가진 대상이라는 뜻으로 함수의 경우 함수가 진행하는 모양이 직선이라는 의미로 사용된다. 선형성을 만족하려면 두 가지 조건을 만족해야 하는데 균질성과 첨가성이다. homogeneity (균질성):   additivity (첨가성): 선형이라고 부르는 수식들은 중첩의 원리 가 적용된다는 특징이 있다. 이때 행렬과 선형 변환의 관계에 대하여도 알아보았었는데 선형 변환과 행렬은 1:1 대응된다. 기저 란 어떤 벡터 공간을 선형 생성하는 선형 독립인 벡터들이다. 각각의 원소들이 다시 벡터 공간을 생성할 수 있어야하고 일차 독립이어야 한다. 표준 기저 는 많은 기저들 중 성분 1개만이 1이고 나머지 성분이 모두 0인 표준 적인 벡터이다. 여기서 벡터 공간 R의 기저를 구성하는 원소의 개수가 해당 공간의 차원 이다. 행렬 은 열기반 행렬과 행기반 행렬 중 어떤 걸 사용하느냐에 따라 계산 방식이 달라진다. 여기서는 벡터의 크기, 회전, ...

[네트워크] 졸작 게임 서버 제작기 1

처음에는 쓸까 말까 고민했지만
졸업작품에 사용하는 게임 서버 제작기를 간단하게 쓰기로 결정했습니다.

아직 배울 것이 한참 많아 틀린 부분이나 좋지 않은 부분이 많겠지만 양해 부탁드립니다.
혹시 틀린 점이나 고치면 좋은 부분이 있다면 언제든지 알려주세요!



서버 및 클라이언트 개발 환경

서버: C++, windows
클라이언트: C#, Unity

서버와 클라이언트 둘 다 기본적으로 비동기 방식을 이용했습니다.

첫 번째는 서버와 측의 기본 네트워크 코드에 대해서 작성해 보려합나다.
(이미 연동되고 게임 로직을 짜고 있긴 하지만... 처음부터 한다는 느낌으로...)


처음에는 저희 졸업작품 팀에서 클라이언트가 C#이라서 서버도 C#으로 만드는게 편할까?
라는 생각을 했습니다. 라이브러리 공유도 힘들고 C#, C++ 왔다갔다 하면서 비슷한 코드를 두 번 만들어야하고... (물론 CLR나 P/Ivoke 방식도 고민했습니다.)

차근차근 진행해보겠습니다!

일단 서버 객체입니다.
크게 보면 IOCPManager, SessionManager, LogicManager 이렇게 세가지로 이루어져 있습니다.

IOCPManager는 기본적으로 IO작업의 처리를 담당합니다.
SessionManager는 SessionPool을 가지고 있고 Accept 처리와 각 세션에 대한 총괄 관리를 합니다.
LogicManager는 분류된 패킷들을 처리합니다.

 








먼저 IOCPManager 를 작성하겠습니다.

1. 윈도우 소켓을 초기화 한 후 IOCP를 만들어줍니다.

 // Winsock Init
 WSADATA wsa;
 if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) 
   return false;

 // Create IOCP
 m_CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
 NULLCHEACK_B(m_CompletionPort, "Create CompletiontPort Error...");

 SYSTEM_INFO si;
 GetSystemInfo(&si);
 m_iocpThreadCount = si.dwNumberOfProcessors * 2;
 // Make Thread
 HANDLE hThread;
 for (int i = 0; i < m_iocpThreadCount; i++)
 {
   hThread = CreateThread(NULL, 0, IoCompletionThread, m_CompletionPort, 0, NULL);
   NULLCHEACKB(hThread);
   CloseHandle(hThread);
 }

2. 그다음 소캣 모드에 따라 소켓을 만들어 줍니다.

 switch (param.sockMode)
 {
  case SocketMode::TCP:
   CreateTCPSocket(param.port);
   break;

  case SocketMode::UDP:
   CreateUDPSocket();
   break;

  case SocketMode::DUAL:
   CreateTCPSocket(param.port);
   CreateUDPSocket();
   break;

  default:
   ERRQUIT("[IOCP Initialize]Wrong Socket Mode");
   return false;
 }

소켓을 IOCP와 연결하고 소켓을 바인드하고.. 서버에서 리슨 준비를 해줍니다.

3. 다음은 worker-thread에서 사용할 함수를 만듭니다.

뭔가 길지만 차근차근 이야기해보겠습니다.

DWORD __stdcall LunaNet::IOCPManager::IoCompletionThread(LPVOID arg)
{
 int retVal;
 HANDLE hcp = (HANDLE)arg; // Completion Port Handle

 while (true)
 {
  DWORD cbTransferred;
  SOCKET client_sock;
  ClientToken* token;

                // IO
  retVal = GetQueuedCompletionStatus(hcp, &cbTransferred,
   (PULONG_PTR)&client_sock, (LPOVERLAPPED*)&token, INFINITE);

  token->byteTransperred = cbTransferred;

  if (retVal == 0 || cbTransferred == 0)...


  NULLCHEACK_B(token, "Token Null");
  
  switch (token->m_lastOperation)
  {
   case SocketOperation::SO_ZeroByteRecv: // page-locking 방지
    token->m_session->Receive();
    token->m_session->ReturnToken(token);
    break;

  case SocketOperation::SO_Accept: 
   token->m_session->AcceptComplete(token);
   break;

  case SocketOperation::SO_Connect:
   break;

  case SocketOperation::SO_Disconnect:
   token->m_session->DisconnectComplete(token);
   break;

  case SocketOperation::SO_Send:
   token->m_session->SendComplete(token);
   break;

  case SocketOperation::SO_Receive:
   token->m_session->ReceiveComplete(token);
   break;

  case SocketOperation::SO_SendTo:
   token->m_session->SendToComplete(token);
   break;

  case SocketOperation::SO_ReceiveFrom:
   token->m_session->ReceiveFromComplete(token);
   break;

  default:
   ERRQUIT("Invalid Operlation");
   break;
  }
 }

 return 0;
}

우선 ClientToken은 OVERLAPPED 구조체 입니다.
이 구조체에는 OVERLAPPED구조체, WSABUF, 해당 세션 정보, 바이트 길이 등으로 이루어져 있습니다.

GetQueuedCompletionStatus에 작업이 있으면 해당 IO작업 때 사용된 토큰을 가지고 옵니다.
그럼 해당하는 명령에따라 분기되어 동작을 실행해주게 됩니다.
Switch 문에 있는 enum 값들은 아래와 같은 구성으로 이루어져 있습니다.

enum class SocketOperation
{
 SO_None = 0,
 SO_Accept, // 서버일때 연결 수락시
 SO_Connect, // 연결 성공시
 SO_Disconnect, // 연결 해제시
 SO_Send, // 전송 (TCP)
 SO_SendTo, // 전송 (UDP)
 SO_Receive, // 받기 (TCP)
 SO_ReceiveFrom, // 받기 (UDP)
 SO_ZeroByteRecv, // 페이지 락을 방지하기 위한 Zero-byte-Recv
};

작업에 따라 해당 세션별로 완료 처리를 해줍니다.

이번 글은 여기까지입니다. 감사합니다


다음은 서버에서 세션별로 어떤식으로 처리하고 구성했는지 적어보겠습니다.

댓글

이 블로그의 인기 게시물

[알고리즘] 브레젠험 직선 알고리즘 (Bresenham's line algorithm)

오일러 각, 로드리게스 회전 공식, 평면의 방정식

[알고리즘] 코헨 서더랜드 알고리즘(Cohen–Sutherland algorithm)