WSAAsyncSelect로 게임클라이언트 제작

2021. 11. 19. 21:48게임서버/namespace univ_dev

320x100
안녕하세요 대학생개발자입니다.

최근 어마어마한 과제와 어마어마한 일들로 인해서 포스팅을 하지 못했습니다.

드디어 과제가 끝나서 이 과제에 대한 포스팅을 해보려고 하는데요...

 

이번 글에서는

AsyncSelect 모델을 이용한 게임 클라이언트를 만들었고 그에대한 정리를 해볼겁니다.

이전에 말씀 드렸듯 AsyncSelect 모델은 최근 개발에서는 잘 사용하지 않는 모델입니다.

WinMain에 상당히 종속적이고 싱글스레드에서 사용하기 좋기 때문입니다.

뭐 이런 내용은 지금당장 크게 중요한 부분은 아니니 실제 개발부분으로 들어가겠습니다.

 

시작하겠습니다

먼저 게임에 대한 프로토콜인데요.

일단 프로토콜은 예전의 별 찍기를 만들었던 것과 거의 비슷하게 될 것 같습니다...

우선 프로토콜은 두 종류로 나눠서 사용하고있습니다.

서버에서 클라이언트로 그리고 클라이언트에서 서버로 보내는 종류의 두 메시지로 나눠서 만들어져있구요.

 

struct PacketHeader
{
	BYTE code;
	BYTE payloadSize;
	BYTE packetType;
};

헤더의 경우에는 전방 1바이트의 헤더 코드가 있구요.

(제가 만든 로직에 의한이아니라 사용하는 유저에 의해 데이터가 조작된다면 걸러내야 되기 때문입니다.)

그리고 payload에 대한 size가 1바이트 있습니다. 즉 모든 패킷은 255바이트를 넘지 않습니다.

그리고 마지막으로 packet type이 명시되어있습니다.

이렇게 헤더는 3바이트로 구성되어있구요,,, 추후에 check sum이라던가 그런걸 추가해보도록 하려고 합니다.

 

음 그리고 아까 말씀드렸듯이 서버가 클라이언트에게 보낼 수 있는 메시지와 클라이언트가 서버에 보낼 수 있는 메시지를 구분지어놨다고 말씀드렸는데..

뭐 단순합니다.

그냥 메시지 구조체를 몇개더 만들었다 입니다...

성향에 따라서 이것도 메모리 낭비인지라 싫어하시는 분이 있는가 하면 서버와 클라이언트의 메시지를 구분해서 더 확고하게 하려고 하시는 분들은 이걸 또 좋아하시더라구요...

 

대략 10개가 넘는 메시지를 다 여기 올리는것도 말도 안되는거고 메시지는 말그대로 구현자의 마음대로 만드는거라 서버와의 협의만 있다면 괜찮습니다.

저같은 경우에는 혼자서 클라와 서버를 다 만들고 있으니 협의따위 필요없고 제가 만들면 곧 그게 프로토콜입니다.

 

 

이 게임의 설계방식은 일반적인 구조는 아닙니다.

그것보다 서버와 클라이언트의 구조에 대해서 설명드리는게 나을것 같은데요.

이번 클라이언트의 경우에는 클라이언트에서 선 움직임을 취하고 그걸 서버에 전달하는 식입니다.

왜그러냐면.. 키보드조작이기 떄문인데요.

키보드 조작일 경우엔 마우스와 다르게 상대적으로 조작감이 떨어집니다.

왜냐하면 마우스 조작인 경우에는 대체적으로 클릭을 통해서 이동하게 되고 그순간 이동목적지가 정해지는 반면에 키보드의 경우에는 플레이어가 키보드에서 손을 때는 순간 그 곳이 목적지가 되기 떄문인데요.

 

그렇기 때문에 마우스 조작인 경우에는 목적지에 이펙트 혹은 뭐... 화살표든 뭐든 애니메이션 띄우고 서버의 컨펌을 기다리고 있더라도 플레이어 입장에서는 느리다는 느낌을 받지 않습니다.

왜냐면 이펙트 혹은 애니메이션 등으로 리액션을 받았기때문입니다.

 

하지만 키보드의 경우에는 목적지가 정해져있지 않다보니 일단 기다렸다가 서버에서 컨펌이 오면 가는 식으로 구현이 될겁니다.

그렇게 되면 당연히 RTT만큼의 시간동안 움직이지 않고 있을텐데... 사실 움직이고 안움직이고 보다 RTT동안 리액션이 없기 때문에 상대적으로 늦다고 느껴지는 부분이 생깁니다.

 

물론 마우스의 경우에도 반응성이 늦다고 판단되면 클라이언트에서 로직을 선처리하고 서버에 보낼 수 있습니다.

실제 게임의 사례로 클라이언트에서 액션을 하고 몬스터는 맞은것처럼 히트포인트가 나오게 하다가 서버에서 맞지 않았다고 판단하면 그때 데미지 대신 miss를 띄우는 방식으로 구현된 서버도 있다고합니다. (뭐... 이런건 서버의 로직이 얼마나 무겁냐 이런거에 따라서 충분히 달라질 사항이라고 생각합니다)

 

그렇기때문에 이번 클라이언트에서는 서버에서 인정하는 좌표에서 어느정도 거리 이하인 경우에는 클라이언트가 독단적으로 움직여도 허용을 해주고 그 이상 멀어진다면 올바르지 못한 사용자로 간주하고 그 플레이어와 연결을 끊는 방식을 취하고 있습니다.

 

뭐 말에대한 정리를 하고 넘어간다면

시작의 반응성 때문에 일단 클라이언트에서 먼저 출발을 하고 그뒤에 서버에서 그 좌표에 대한 컨펌을 하는 방식입니다.

그러기 위해서는 단 한치의 오차도 없어야 된다는 강박은 없어야겠죠...

그리고 서버에서는 컨펌된 클라이언트의 좌표를 기반으로 일정 범위의 원을 그려서 그 원안에서 클라이언트가 움직인다라는 메시지가 오게되면 그건 허용해 주는 것이죠.

 

일반적으로 서버에서는 클라이언트의 데이터를 믿지 않습니다.

왜냐면 클라이언트는 조작의 위험도가 많이 크니까요...

하지만 이번 케이스의 경우에는 클라이언트의 좌표를 믿는 방식을 채택하는겁니다.

물론 일정 좌표 안이라면 믿는 형식이죠.

 

클라이언트를 어떻게 만드냐에 따라 다르긴 하겠지만, 특정 클라이언트에서는 헤더에 들어가는 값을 고쳐서 서버가 허용하는 좌표안에서의 스피드 핵 같은 문제가 발생할 수 있겠죠.

 

그리고 또 얘기해봐야 할 부분이 스킬에 쿨타임이나 딜레이 부분입니다.

만약 공격이 300ms마다 사용할 수 있다면.. 서버에서 진짜 300ms를 재고있는다면 당연히 RTT때문에 300ms보다 훨씬더 늦게 도착할 것입니다. 그러면 클라이언트는 억울하잖아요..!?

그렇기 떄문에 예를 들어 서버에서 300ms - RTT의 평균수치로 두고 그 안에 들어온다면 오케이 하는 방식을 채택한다면 좋을 것 같습니다.

하지만 얘도 문제가있죠..

만약 이 헛점을 파악한 악의적인 플레이어가 고의적으로 250ms마다 공격을 하는 메시지를 보낸다면?? 이런 경우에는 당연히 정상적인 플레이어 보다 더 이득을 취할 수 있는 부분이 있을겁니다.

즉 오토에 대한 문제가 발생할 수 있습니다.

물론 한두번 이렇게 왔다고 해서 그 클라이언트가 오토나 핵 의심자로 판정되면 억울할겁니다.

그래서 우리는 데이터를 모아서 분석해야될거구요...

만약 일정 시간(혹은프레임)동안 보낼 수 있는 최대 패킷수 오버로 온다던지... 혹은 너무 일관적으로 250ms에 맞춰서 메시지가 도착한다던지... 그런 경우라면 핵 혹은 오토 사용 유저로 의심할 수 있겠죠... 그리고 gm이 개입을 하든... 그렇게 해야겠죠 나머지 판단은 사람이 해야죠 이걸 자동화 시켜버리면 억울하게 쫒겨나는 사람이 너무 많아질테니까요 ㅠㅠ..

 

일단은 이부분은 지금 당장은 서버에 반영이 되어있지 않습니다...

추후에 개선을 해야겠죠..!

일단은 서버의 방향성이 이렇게 될 것이다 라는것이구요 

 

 

그러면 다시 클라이언트로 넘어가보겠습니다. 

클라이언트의 돌아가는 방식을 먼저 보고 그다음에 구체적으로 보겠습니다.

Update()
{
    //Key입력;
	//Logic수행;
    //렌더링 스킵여부 판단;
    //렌더스킵 아니면 렌더;
}

이게 가장 큰 로직들입니다.

if (enqueueFlag)
    {
        PacketHeader header;
        int size;
        char* packetPointer = nullptr;
        switch (actionType)
        {
        case ACTION_ATTACK1:
        {
            if (actionType == oldActionType || ( oldActionType == ACTION_ATTACK2 || oldActionType == ACTION_ATTACK3))
            {
                if (!pPlayer->IsEndFrame()) break;
            }
            CS_PacketAttack1 packet;
            MakePacketAttack1(header, packet, direction, pPlayer->GetCurrentX(), pPlayer->GetCurrentY());
            packetPointer = (char*)&packet;
            size = sizeof(packet);
            break;
        }
        case ACTION_ATTACK2:
        {
            if (actionType == oldActionType || (oldActionType == ACTION_ATTACK1 || oldActionType == ACTION_ATTACK3))
            {
                if (!pPlayer->IsEndFrame()) break;
            }
            CS_PacketAttack2 packet;
            MakePacketAttack2(header, packet, direction, pPlayer->GetCurrentX(), pPlayer->GetCurrentY());
            packetPointer = (char*)&packet;
            size = sizeof(packet);
            break;
        }
        case ACTION_ATTACK3:
        {
            if (actionType == oldActionType || (oldActionType == ACTION_ATTACK1 || oldActionType == ACTION_ATTACK2))
            {
                if (!pPlayer->IsEndFrame()) break;
            }
            CS_PacketAttack3 packet;
            MakePacketAttack3(header, packet, direction, pPlayer->GetCurrentX(), pPlayer->GetCurrentY());
            packetPointer = (char*)&packet;
            size = sizeof(packet);
            break;
        }
        case ACTION_MOVE_LL:
        {
            if (action == pPlayer->GetOldAction()) break;
            if ((oldActionType == ACTION_ATTACK1 || oldActionType == ACTION_ATTACK2 || oldActionType == ACTION_ATTACK3))
            {
                if (!gp_Player->IsEndFrame()) break;
            }
            CS_PacketMoveStart packet;
            MakePacketStartMove(header, packet, action, pPlayer->GetCurrentX(), pPlayer->GetCurrentY());
            packetPointer = (char*)&packet;
            size = sizeof(packet);
            break;
        }
        case ACTION_MOVE_DD:
        {
            if (oldActionType == ACTION_MOVE_DD) break;
            if ((oldActionType == ACTION_ATTACK1 || oldActionType == ACTION_ATTACK2 || oldActionType == ACTION_ATTACK3))
            {
                {
                    if (!pPlayer->IsEndFrame()) break;
                }
            }
            CS_PacketMoveStop packet;
            MakePacketStopMove(header, packet, direction, pPlayer->GetCurrentX(), pPlayer->GetCurrentY());
            packetPointer = (char*)&packet;
            size = sizeof(packet);
            break;
        }
        default:
            packetPointer = nullptr;
        }
        if (packetPointer == nullptr) return;
        oldActionType = actionType;
        
        g_SendRingBuffer.Enqueue((const char*)&header, sizeof(PacketHeader));
        g_SendRingBuffer.Enqueue((const char*)packetPointer, size);
        WriteEvent();
    }

뭐... 클라이언트의 로직은 키 입력을 받으면 액션을 변경하고 그 액션이 변경되었다면 메시지를 SendRingBuffer에 Enqueue하는 방식으로 되어있습니다.

 

enqueueFlag의 경우에는 oldAction과 currentAction이 다른경우 true가 됩니다.

 

그리고 실제 Logic을 실행하는 부분에서 SendRingBuffer에 들어온 메시지가 있다면 보내고 있습니다.

WriteEvent에서는 네트워킹 그림그리기 보드에서 만들었던 것 처럼 Enqueue된 데이터가 있다면(즉 GetUseSize()가 packet.payloadSize + sizeof(PacketHeader)보다 작다면) 보내는 방식으로 되있습니다.

 

사실... 조건을 너무 심각하지 않냐 싶을정도로 많이 검사하고있습니다... 제 코드에 불신이 많다는 증거입니다ㅠㅠ...

검사를 많이하면 당연히 비교 횟수가 많아지니까 로직이 무거워집니다... 하지만 코드가 완벽하다고 검증되기 전까지는 저런식으로 검사한걸 또 검사하고 또 검사하는 로직을 넣을겁니다. 물론 이게 멀티쓰레드로 가게되어도 똑같습니다. 멀티스레드라면 더더욱 저렇게 해야되겠죠...

 

대신 ReadEvent와 WriteEvent부분에 이전과 달라진 부분이 하나있는데요.

음... 먼저 Key입력과 관련된 부분을 했으니 이어지는 send와 관련된 부분을 먼저 보고갈게요

bool NetWorkProc(DWORD lParam, DWORD wParam)
{
    switch (WSAGETSELECTEVENT(lParam))
    {
        case FD_CONNECT:return true;
        case FD_CLOSE:
        {
            MessageBox(g_hWnd, L"FD_CLOSE", L"끝났지롱~", MB_OK);
            PostMessage(g_hWnd, WM_DESTROY, 0, 0);
            return false;
        }
        case FD_WRITE:
        {
            bSendFlag = true;
            return WriteEvent();
        }
        case FD_READ:
        {
            return ReadEvent();
        }
    }
    return false;
}

WriteEvent의 경우에는 Window Message가 UM_NETWORK (WM_USER+1) 일 경우에 lParam이 FD_WRITE면 발생합니다.

FD_WRITE이 발생하는 경우는 저번에도 정리를 했었습니다.

먼저 최초 연결이 되었을때 인데요.

상대의 Syn bit에 의해서 ack와 seq가 공유되고 나면 FD_WRITE가 발생한다고 했습니다.

즉 서버가 accept를 하지 않아도 이미 FD_WRITE는 도착해있다 이말입니다.

그리고 다른 경우로 내 송신버퍼가 어떠한 이유로 다 찼을경우 뭐 가령 상대의 window size가 0일수도 있구요.

혹은 상대가 recv를 한건도 처리를 못하거나(안하거나) 하는 상황일 수도있습니다.

뭐 어떤 상황이던 간에 이런경우 send에서 SOCKET_ERROR이 반환되고 그 에러를 확인했을때 WSAEWOULDBLOCK이 나왔다면 송신버퍼는 가득찼단 의미고 그 상황이 종료되는 순간 단한번 저 메시지를 준다고 했었습니다.

그 외에는 우리가 직접 send를 호출하는 수 밖에없습니다...

 

즉 업데이트 로직에서 WriteEvent를 호출하는게 어쩔수 없이라는거죠...

그게 아니면 FD_WRITE메세지가 도착할때까지 기다려야되는데 올때까지 기다릴 수가없거든요.

그렇게 된다면 한프레임이라도 FD_WRITE를 놓쳐버린다면 다음번 send의 기회가 사라지니까요...

 

그 리 고

 

아직 링버퍼테스트 단계이고 완벽한지 파악이 되지 않았기 때문에... (원래 자기 코드는 믿게되는 경향이 큽니다)

이전에 만들었던 그림판과 이번 클라이언트의 초반에는 recv, send할 데이터를 바로 링버퍼에서 송수신 버퍼로 땡겨버리는 방식을 취하지 않고 임시 버퍼에 저장했다가 옮기는 방식으로 했었습니다.

 

하지만 이번코드부터는 그냥 바로 링버퍼의 포인터를 집어넣고 바로 밀어버리는 방식, 포인터를 얻어서 Enqueue Dequeue연산을 그냥 밖에서 두번의 copy없이 처리하는 방식으로 해버렸습니다.

이게 퍼포먼스도 더 좋을 뿐더러 공간도 낭비하지 않기떄문이죠...

심지어 코드도 줄어드는 효과가 있었습니다.

 

원래 코드의 경우에는

tempBuffer 1kb정도 잡은후 그 버퍼에 recv하고 그 tempBuffer중 recvRet 만큼 Enqueue해서 로직에서 사용할때 while문 돌리면서 헤더만큼 빼내고 패킷 크기만큼 빼내서 사용하는 방식으로 했었는데.

그중 tempBuffer와 연관된 부분이 사라지게 되므로 1kb만큼 줄어든거죠...

왜냐면 일단 게임로직이 완벽하지 않은 상태에서 완벽하지 않은 상태의 링버퍼를 붙여버리게 된다면...

어디서 문제가 났는지 찾기가 매우 어려워 지기 때문에 클라이언트가 완벽하게 구현될때 까지는 링버퍼를 저런식으로 사용했었습니다.

 

코드를 보면서 얘기를 좀해보죠.. 사실 할말은 별로없습니다만.

bool WriteEvent()
{
    if (!bSendFlag) return false;
    while (true)
    {
        int sendRet = 0;
        if (g_SendRingBuffer.GetUseSize() == 0) 
            break;
        sendRet = send(g_ClientSocket, g_SendRingBuffer.GetReadPtr(), g_SendRingBuffer.DirectDequeueSize(), 0);

        if (sendRet == SOCKET_ERROR)
        {
            int sendErr = WSAGetLastError();
            if (sendErr != WSAEWOULDBLOCK)
            {
                WCHAR buffer[10]{ 0 };
                _itow_s(sendErr, buffer, 10);
                MessageBox(g_hWnd, L"send()", buffer, MB_OK);
                PostMessage(g_hWnd, WM_DESTROY, 0, 0);
                return false;
            }
            bSendFlag = false;
            break;
        }
        g_SendRingBuffer.MoveFront(sendRet);
    }
    return true;
}

WrtieEvent에서는 이런 작업을 하고 있습니다.

먼저 SendFlag가 off상태라면 보내지 않습니다.

SendFlag가 off라는 의미는 WSAEWOULDBLOCK 즉 송신버퍼가 가득 찼다는 의미니까요.

(결국 로직이 똑같으니 매번 하는말이 똑같습니다...ㅋㅋ)

 

로직은 단순합니다.

SendRingBuffer에 사용중인 사이즈가(데이터가 들어가있는양) 0바이트 이상이라면 보내고 그 send된 값만큼 RingBuffer의 WritePointer를 앞으로 쭉 땡겨주는 코드입니다.

음... 여기서 달라진점은 예전에는 1024 -> 1kb만큼씩 뽑아서 루프를 뺑뻉뺑 돌았다면

지금의 경우에는 DirectDequeueSize()만큼 1번 혹은 2번안에 모든 데이터를 다뽑아낼 수 있게 만들어졌다는점입니다.

 

음... 만약 뭐 1kg짜리를 100번 옮기는거나 100kg짜리 물건을 1번옮기는거나 뭐가 다른거냐 라고 물으신다면 상당히 잘못 접근하신 부분입니다.

1kg짜리를 100번 1km를 옮긴다면 우리는 100km를 이동해야되지만 100kg짜리 한번을 옮긴다면 1km만 움직이면 되는거니까요 우리는 99km의 거리를 아끼게 되는겁니다.

즉 제가 하려는 말은 우리가 여기서 가장 주의해야되는건... recv와 send의 호출 횟입니다.

그걸 줄이려고 일부로 링버퍼 쓰고 하는거잖아요.

물론 운이 안좋아서 링버퍼의 direct enqueue size가 1이라면 좀 아쉽겠지만 이런것까지 다 커버를 하면서 쓸순 없습니다.

그리고 추후에 Overlapped IO부분에서 우리는 버퍼를 동시에 2개이상을 넣을 수 있기 때문에 거기서 해결할 수 있습니다.

bool ReadEvent()
{
    int recvRet;
    int recvErr;
    
    recvRet = recv(g_ClientSocket, g_RecvRingBuffer.GetWritePtr(), g_RecvRingBuffer.DirectEnqueueSize(), 0);
    if (recvRet == SOCKET_ERROR)
    {
        recvErr = WSAGetLastError();
        if (recvErr != WSAEWOULDBLOCK)
        {
            WCHAR buffer[10]{ 0 };
            _itow_s(recvRet, buffer, 10);
            MessageBox(g_hWnd, L"recv()", buffer, MB_OK);
            PostMessage(g_hWnd, WM_DESTROY, 0, 0);
            return false;
        }
        //WOULDBLOCK인 상황 받을게 더이상 없다고판단되면 로직수행하로가야됨
    }
    
    while (true)
    {
        PacketHeader header;
        int size = g_RecvRingBuffer.GetUseSize();
        if (g_RecvRingBuffer.GetUseSize() < sizeof(PacketHeader)) break;
        int headerPeekRet = g_RecvRingBuffer.Peek((char*)&header, sizeof(PacketHeader));
        if (header.code != 0x89)
        {
            WCHAR buffer[10]{ 0 };
            _itow_s(header.code, buffer, 10);
            MessageBox(g_hWnd, L"header.code", buffer, MB_OK);
            PostMessage(g_hWnd, WM_DESTROY, 0, 0);
            return false;
        }
        if (g_RecvRingBuffer.GetUseSize() < header.payloadSize + sizeof(header)) break;
        g_RecvRingBuffer.MoveFront(sizeof(PacketHeader));
        char packet[30];
        int pkRet = g_RecvRingBuffer.Peek(packet, header.payloadSize);
        if (pkRet != header.payloadSize) break;
        g_RecvRingBuffer.MoveFront(header.payloadSize);
        PacketProc(header.packetType, packet);
    }
    return true;
}

ReadEvent의 케이스도 동일합니다.

DirectEnqueueSize만큼 한번에 확 읽어서 그걸 처리하는 방식입니다.

WriteEvent와 다른점은 여기에서는 while문이 안붙는데요.

FD_READ의 경우에는 이번에 다 못읽으면 다음에 또 알려주기 때문에 문제가 없어서 입니다.

 

사실 이걸두고 객체지향 관점에서 본다면 제가 링버퍼를 사용하는 방식은 객체지향에 맞지 않는 방식이지만, 이게 더 퍼포먼스가 좋고 서버개발자라면 당연히 더 퍼포먼스가 좋은 방향으로 개발해야될겁니다.

사실 서버에 있어서 객체지향이라고 함은 그냥 유지 및 보수가 편해진다는 점에서 좋은거지 성능을 키우려고한다면 사실 C에서 개발하는 방식이 더 좋을때가 있으니까요.

 

네 다시 본론으로 돌아와서 Packet헤더와 Packet.payloadSize만큼 Dequeue를 해서 이제 그 패킷을 이용해 PacketProc라는 함수를 부를겁니다.

type과, packet을 전달하면 그 타입과 패킷에 의해서 분기를 타게되겠죠..

void PacketProc(BYTE packetType, char* packet)
{
    switch (packetType)
    {
    case dfPACKET_SC_CREATE_MY_CHARACTER:
    {
        PacketProcCreateMyPlayer(packet);
        break;
    }
    case dfPACKET_SC_CREATE_OTHER_CHARACTER:
    {
        PacketProcCreateOtherPlayer(packet);
        break;
    }
    case dfPACKET_SC_DELETE_CHARACTER:
    {
        PacketProcDeletePlayer(packet);
        break;
    }
    case dfPACKET_SC_MOVE_START:
    {
        PacketProcMoveStart(packet);
        break;
    }
    case dfPACKET_SC_MOVE_STOP:
    {
        PacketProcMoveStop(packet);
        break;
    }
    case dfPACKET_SC_ATTACK1:
    {
        PacketProcAttack1(packet);
        break;
    }
    case dfPACKET_SC_ATTACK2:
    {
        PacketProcAttack2(packet);
        break;
    }
    case dfPACKET_SC_ATTACK3:
    {
        PacketProcAttack3(packet);
        break;
    }
    case dfPACKET_SC_DAMAGE:
    {
        PacketProcDamage(packet);
        break;
    }
    default:
    {
        WCHAR buffer[50]{ 0 };
        _itow_s(packetType, buffer, 10);
        wcscat_s(buffer, L" : PACKET NUMBER IS WRONG");
        MessageBox(g_hWnd, L"MISS PACKET TYPE", buffer, MB_OK);
        break;
    }
    }
}

네 뭐 지금은 이렇게 분기를 나눠뒀습니다.

왜냐면 스위치의 케이스가 나름 깔끔하고 그 숫자도 255를 넘지 않기떄문입니다.

(255인 이유에 대해서는 예전에 말씀드렸었습니다. 점프 테이블 각 인덱스에 저장할 수있는게 1바이트 밖에 되지 않기떄문입니다.)

그래서 switch문안에 바로 기능이 들어가는게 아니라 함수를 하나더 래핑을 했습니다.

반환과 파라미터가 동일한함수들로만요.

이렇게 한 이유는 지금의 경우에는 당연히 점프테이블에서 add한번 jmp한번 하면 끝나겠지만 실제 라이브 되고있는 게임서버의 경우 그리고 게임 클라이언트의 경우는 저런 스위치문이 최소 몇백개 될겁니다

해시나 테이블을 이용하기 위함입니다.

그리고 값도 255보다 큰 값이 나올 수 밖에 없습니다.

그렇게 안하면 나중에 터졌을때 판단이 어려워 지니까요.

대역으로 나눠버립니다.

예를들면 A환경에서 나올 수 있는 패킷은 100~1000번대, B환경은 1000~2000대... 이런식으로 말이죠... 그러니까 switch문의 점프 테이블이 의미가 없어지는겁니다.

 

추가로 만약 2000개의 메시지가 있다면 이 2000번의 분기를 다타고 있을 멍청한 행동은 하셔서는 더더욱 안되는거구요... 당연히 모든 접근은 O(1)로 만들어버려야됩니다.

 

물론 함수포인터의 배열로 만들수도 있습니다만... 만약 메시지가  1000번에 있고 그다음 이어지는 메시지가 5000번에 있다면요? 1000~5000까지는 빈공간인데... 이런것도 사실 말이안되죠..?

 

제가 드리려는 말씀은 함수포인터를 해시로 두려는 겁니다.

해시로 둔다면 그냥 키를 이용해서 바로 값을 얻어낼 수 있으니까요...

그렇기 때문에 함수 모양을 다 맞춰버리고 있는 중입니다.(제가 이전에 정리했던 기본c부터 보셨다면... 아!! 하셨을겁니다... 이런 용도를 위해서 정리를했었으니까요...)

 

네 뭐 PacketProc~~~ 이하 함수들의 기능은 함수 이름이 곧 기능입니다.

CreateMyPlayer는 packet에 id, direction, xpos, ypos, hp 이런 정보가 들어오고... 그 정보를 이용해서 그저 캐릭터를 생성하고 BaseObject를 관리하는 list에 넣는것이 일입니다.

이런식으로 다들 뭐.. 이름에 걸맞는 행동을 하고있습니다.

 

그리고 실행영상입니다...

https://youtu.be/oIEsp2bUdms

안타깝게도 WM_ACTIVE체크는 꺼놓은 상태입니다.

거의 10개가까이 켰다껏다하는데 하나하나 컨트롤해서 다잡으려고 하니까...

감당이 안되서 사실 이렇게 했음에도 영상이 1분안에 안잘리더라구요...

그점은 양해부탁합니다...

 

음... 이번 글에서는 사실 위로갔다 아래로 갔다하면서 내용을 적었기때문에 중간에 내용이 이상하고 연결되지 않는 것 같은 느낌을 많이 받으셨을겁니다...

뭐 어째뜬 정리목적이니까 크게 중요하지 않다고 생각하구있습니다.

오늘도 긴글 읽어주신 여러분들께 감사드립니다.

그럼 다음에는 더 좋은 정보를 가지고 오도록 하겠습니다

그럼 안녕히계세요.

320x100

'게임서버 > namespace univ_dev' 카테고리의 다른 글

ObjectFreeList  (1) 2021.11.28
Serializing Buffer  (0) 2021.11.23
WSAAsyncSelect 그림그리기 클라이언트  (0) 2021.11.13
WSAAsyncSelect  (0) 2021.11.13
링 버퍼  (2) 2021.11.12