멀티플레이어 게임 프로그래밍 - 4장 연습문제

jh Seo·2023년 4월 11일
0
  1. 원시자료형들은 memcpy를 사용해 보내도 잘 수신이 되지만,
    클래스를 보내게 되면 해당 클래스 내부의 함수들도 함수 포인터를 통해
    보내지는데 수신자의 가상함수테이블이 송신자의 가상함수테이블과 같다는 보장이 없다.
    따라서 메모리접근예외가 발생하거나 엉뚱한 위치의 코드를 실행하게될수도 있어 안전하지 않다.

  2. 여러 바이트로 이뤄진 숫자를 저장할 때, 플랫폼마다 그 방식이 다르다.
    해당 플랫폼이 바이트를 어떤 순서로 저장하는지 그 방식을 플랫폼의 엔디언이라고 하고,
    빅 엔디언, 리틀 엔디언으로 구별된다.

    • 리틀 엔디언 플랫폼에선 여러 바이트로 구성된 숫자를 가장 작은 자리의 숫자부터 먼저 기재한다.
      0x12345678을 0x01000000번지에 저장하게 되면,
      0x78이 0x01000000, 0x56이 0x01000001, 0x34이 0x01000002, 0x12이 0x01000003
      이런 순서로 저장된다.
    • 반대로 빅 엔디언 플랫 폼에선
      0x12345678을 0x01000000번지에 저장하게 되면,
      0x12가 0x01000000, 0x34이 0x01000001, 0x56이 0x01000002, 0x78이 0x01000003
      이런 순서로 저장된다.
  3. 드문드문 채워져 있거나, 완전히 안 채워진 자료구조를 최적화 하며 압축을 하는 방식이다.
    이 책의 예제같은 경우에는 mName을 128바이트 배열로 설정해두었는데 string값으로 설정후 strlen을 이용하는 식으로 메모리를 줄일 수 있다.

  4. 포인터 포함한 객체를 직렬화하는 경우는 두 가지로,
    임베딩(인라이닝) 또는 링킹이 그것이다.

    • 임베딩은 자신이 참조하는 데이터를 다른 객체와 공유하지 않을 때 사용하는 방식이다.
      예를 들어 vector같은 컨테이너나 배열의 주소를 스트림에 복사해서 넣어버리면 원격 호스트의 해당 주소에 똑같은 컨테이너가 있다고 기대하기 힘들다.
      따라서 벡터의 크기를 먼저 기록 후 해당 벡터의 내용물을 전부 기록하는 식으로 직렬화를 한다.
    • 링킹은 하나 이상의 포인터로 여러곳에서 참조되는 경우 사용하는 방식이다.
      책의 예제로 보면 Robocat변수 중에는 기지 오브젝트인
      GameObject* mHomeBase가 포함되어 있다.
      Robocat이 직렬화되어 전송될때 마다 원격 호스트는
      mHomeBase를 생성할것이고 기지가 계속 복사되고 말것이다.
      또 다른 예제로는 두 클래스가 서로를 참조하는 상황일 때,
      복사하는 과정에서 무한재귀를 통해 스택오버플로우가 벌어질것이다.
      따라서 각 객체에 고유 ID를 부여하였다가 이들 객체를 직렬화할 때 오로지 식별자값만 직렬화하는 방식으로 구현 후,
      원격 호스트는 모든 객체 데이터를 구현 후, 각 식별자에 대응하는 참조 객체를 찾아 적절히 끼워넣는 과정을 링킹이라 한다.
  5. 엔트로피 인코딩이란 출현 데이터의 예측 가능성 정도가 얼마나 높고 낮나에 따라 압축률이 달라지는 이론이다.
    예를 들어 고양이가 대다수의 시간을 땅에서 보내는데
    고양이의 높이값으로 계속 4바이트 값을 전송하는 건 메모리 낭비다.
    따라서 cpu의 연산을 좀 사용하더라도 고양이 높이를 비교연산을 통해 0이 아닐때만 높이를 전송하는 식으로 구현하면 절약이 가능하다.

  6. 소수점의 자리를 요구한다고 전부 float값을 할당해서 전송해버리면 낭비되는 비트가 생길것이다. 따라서 게임의 수치의 가능 범위 및 요구 정밀도를 파악한 후, 해당 정밀도에 맞게 고정소수점 방식으로 변환하면 절약할 수 있다.
    이 책의 예처럼 월드 크기가 4천x4천이고, 월드 중심이 0,0,0이며, 클라이언트 좌표이동은 0.1단위의 정밀도면 충분하다고 했을 때, x축이 가질수있는 값의 최대는 (2000-(-2000))/0.1 +1 으로
    40001개의 값만 있으면 표현할 수 있다.

  7. WriteBits()함수는 리틀 엔디언에서만 작동을 하는데,
    이유는 바이트를 0번부터 기록하기 때문이다.
    빅 엔디언환경에서는 최상위 바이트부터 기록하므로 반대가 되어야한다.
    빅엔디언 환경에서도 작동하려면 해당 플랫폼의 엔디언 환경을 체크한 후 빅엔디언이라면
    바이트 스왑함수를 이용해 스왑해준 후 writeBits함수를 사용하는식으로 해야한다.

  1. MemoryOutputStream::Write(const unordered_map<int,int>&) 멤버함수를 구현해보았다.
    void MemoryOutputStream::Write(const unordered_map<int,int>& inMap)
    {
    	//inMap사이즈만큼 할당해준 후,
    	size_t elemCnt = inMap.size();
       //사이즈 Write
    	Write(elemCnt);
       //Map안의 pair에 대해 write연산 진행
    	for (const auto& iter : inMap) {
    		Write(iter.first);
    		Write(iter.second);
    	}
    }
  2. 대응하는 Read함수도 구현해보았다.
    void MemoryOutputStream::Read(unordered_map<int, int>& outMap) {
    	
    	size_t elemCnt;
       //사이즈 할당 후,
    	Read(elemCnt);
       //사이즈만큼 크기 조정해주고
    	outMap.reserve(elemCnt);
       //각 first값과 second값을 Read연산을 통해 저장
    	for (const auto& iter : outMap) {
    		Read(iter.first);
    		Read(iter.second);
    	}
    }
  3. tKey, tValue를 매핑하는 Read함수를 구현해보았다.
    template<typename tKey, typename tValue >
    void MemoryOutputStream::Read(unordered_map<tKey, tValue>& outMap) {
    	size_t elemCnt;
    	Read(elemCnt);
    	outMap.reserve(elemCnt);
    	for (const auto& iter : outMap) {
    		Read(iter.first);
    		Read(iter.second);
    	}
    }
profile
코딩 창고!

0개의 댓글