쉐이더 파일의 경우, 동일한 쉐이더가 여러번 컴파일될 필요가 없기 때문에 EngineBase에 쉐이더 파일을 따로 저장하도록 하였다.

 

그리고 쉐이더를 생성할 때, EngineBase에서 해당 쉐이더가 이미 존재하는지를 판단한 뒤 없을때에만 새로 생성하고 컴파일하도록 하였다.

 

struct VertexShaderData
{
    Microsoft::WRL::ComPtr<ID3D11VertexShader> VertexShader;
    Microsoft::WRL::ComPtr<ID3D11InputLayout> InputLayout;
};

 

이렇게, 버텍스쉐이더와 인풋 레이아웃을 함께 담는 구조체를 하나 선언하였다.

std::unordered_map<const std::wstring, VertexShaderData> VertexShaders;

 

EngineBase내부엔 위와 같은 자료구조를 선언하여,  버텍스 쉐이더 데이터를 저장하도록 하였다.

key는 쉐이더 파일의 이름이다.

 

BOOL EngineBase::CreateVertexShader(const std::wstring& _ShaderFileName, std::vector<D3D11_INPUT_ELEMENT_DESC> _InputElement)
{
    if (VertexShaders.find(_ShaderFileName) != VertexShaders.end())
    {
        std::cout << "Don't try to Create existed VertexShader" << std::endl;
        return TRUE;
    }

    Microsoft::WRL::ComPtr<ID3DBlob> ShaderBlob;
    Microsoft::WRL::ComPtr<ID3DBlob> ErrorBlob;

    HRESULT Result =
        D3DCompileFromFile(_ShaderFileName.c_str(), 0, 0, "main", "vs_5_0", 0, 0, &ShaderBlob, &ErrorBlob);

    if (Result != S_OK) 
    {
        if ((Result & D3D11_ERROR_FILE_NOT_FOUND) != 0) 
        {
            std::cout << "File not found." << std::endl;
        }

        if (ErrorBlob)
        {
            std::cout << "Shader compile error\n" << (char*)ErrorBlob->GetBufferPointer() << std::endl;
        }

        return FALSE;
    }

    Microsoft::WRL::ComPtr<ID3D11VertexShader> NewVertextShader;

    Result = 
        EngineBase::GetInstance().GetDevice()->CreateVertexShader(ShaderBlob->GetBufferPointer(), ShaderBlob->GetBufferSize(), NULL,
        &NewVertextShader);

    if (Result != S_OK)
    {
        std::cout << "CreateVertexShader() failed" << std::endl;
        return FALSE;
    }

    Microsoft::WRL::ComPtr<ID3D11InputLayout> _InputLayOut;
    if (!CreateInputLayOut(_InputElement, _InputLayOut, ShaderBlob))
    {
        return FALSE;
    }

    VertexShaders.insert({ _ShaderFileName, {NewVertextShader, _InputLayOut} });

    return TRUE;
}

 

먼저, 지금 만들고자 하는 쉐이더가 이미 생성되어 있다면 따로 만들지 않도록 해주었다.

이후, 입력된 파일 이름을 기반으로 쉐이더 컴파일을 실행하였고 파일이 있는지 없는지, 혹은 다른 오류로 컴파일이 실패하였는지 확인 과정을 거쳐주었다.

 

다음은 컴파일된 쉐이더를 통해 버텍스 쉐이더를 생성해주었다.

버텍스 쉐이더를 만들었으면, 인풋 레이아웃도 만들어야 하기 때문에, 인풋 레이아웃도 만들어주었다.

 

마지막으로, VertexShaders 자료구조에 데이터를 삽입해주었다.

BOOL EngineBase::CreateInputLayOut(std::vector<D3D11_INPUT_ELEMENT_DESC> _InputElement, Microsoft::WRL::ComPtr<ID3D11InputLayout> _InputLayOut, Microsoft::WRL::ComPtr<ID3DBlob> _ShaderBlob)
{
    HRESULT Result = 
    EngineBase::GetInstance().GetDevice()->CreateInputLayout(_InputElement.data(), UINT(_InputElement.size()),
        _ShaderBlob->GetBufferPointer(), _ShaderBlob->GetBufferSize(),
        &_InputLayOut);

    if (Result != S_OK)
    {
        std::cout << "CreateInputLayOut() failed" << std::endl;
        return FALSE;
    }

    return TRUE;
}

인풋 레이아웃 생성 함수는 위와 같다. 간단하다.

 

std::unordered_map<const std::wstring, Microsoft::WRL::ComPtr<ID3D11PixelShader>> PixelShaders;

픽셀 쉐이더에 대해서도 자료구조를 생성해주었다.

 

BOOL EngineBase::CreatePixelShader(const std::wstring& _ShaderFileName)
{
    Microsoft::WRL::ComPtr<ID3DBlob> ShaderBlob;
    Microsoft::WRL::ComPtr<ID3DBlob> ErrorBlob;

    HRESULT Result =
        D3DCompileFromFile(_ShaderFileName.c_str(), 0, 0, "main", "ps_5_0", 0, 0, &ShaderBlob, &ErrorBlob);
    
    if (Result != S_OK)
    {
        if ((Result & D3D11_ERROR_FILE_NOT_FOUND) != 0)
        {
            std::cout << "File not found." << std::endl;
        }

        if (ErrorBlob)
        {
            std::cout << "Shader compile error\n" << (char*)ErrorBlob->GetBufferPointer() << std::endl;
        }

        return FALSE;
    }

    Microsoft::WRL::ComPtr<ID3D11PixelShader> NewPixelShader;

    Result = 
    EngineBase::GetInstance().GetDevice()->CreatePixelShader(ShaderBlob->GetBufferPointer(), ShaderBlob->GetBufferSize(), NULL,
        &NewPixelShader);

    if (Result != S_OK)
    {
        std::cout << "CreatePixelShader() failed" << std::endl;
        return FALSE;
    }

    PixelShaders.insert({ _ShaderFileName, NewPixelShader });

    return TRUE;
}

 

픽셀 쉐이더도 버텍스 쉐이더와 동일하게 만들어주었다.

이제, 조금만 더 하면 화면에 정육면체를 띄울 수 있을 것 같다...!

버텍스 버퍼, 인덱스 버퍼와 동일한 과성으로 상수버퍼도 추가해주었다.

글이 나뉘는게 좀 불편하긴 한데, 글 제목이 너무 길어지는 것도 거슬려서 따로 작성하였다.

 

template <typename DataType>
void CreateConstantBuffer(DataType& _Data)
{
    D3D11_BUFFER_DESC CBDesc;
    CBDesc.ByteWidth = sizeof(DataType);
    CBDesc.Usage = D3D11_USAGE_DYNAMIC;
    CBDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    CBDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    CBDesc.MiscFlags = 0;
    CBDesc.StructureByteStride = 0;

    D3D11_SUBRESOURCE_DATA InitData;
    InitData.pSysMem = &_Data;
    InitData.SysMemPitch = 0;
    InitData.SysMemSlicePitch = 0;

    HRESULT Result =
        EngineBase::GetInstance().GetDevice()->CreateBuffer(&CBDesc, &InitData, ConstantBuffer.GetAddressOf());

    if (Result != S_OK)
    {
        std::cout << Name << " :" << "CreateConstantBuffer() failed." << std::hex << "\nResult : " << Result << std::endl;
    };
}

 

동일한 방식에 옵션만 다르게 하여 상수버퍼도 만들어주었다.

상수버퍼는 일단 Transform에 대해서만 사용할 예정이지만, 추후 어떤 타입이 추가될지 알 수 없기 때문에

템플릿을 이용하여 여러 자료형에 대한 가능성을 열어놓은 채로 정의하였다.

 

일단 실행은 잘 되는데, 화면에 보이지를 않으니 제대로 되고 있는건지 잘 모르겠다.

빨리 쉐이더까지 추가를 해보아야 할 듯 하다.

저번에, EngineBase를 싱글톤 패턴으로 바꾸고 나니, 메모리 누수가 감지되었다.

아무래도 소멸자의 호출 시기가 애매해서 그런 것 같았다.

 

실제로, 메모리 해제 함수를 만든다음 프로그램이 종료되기 전에 명시적으로 메모리를 해제해주니 메모리 누수가 사라졌음을 확인할 수 있었다.

 

하지만, 이렇게 메모리 해제를 직접 호출해야 하는 상황이 그렇게 좋은 방식으로 보이지는 않았고, 그냥 마이어스 싱글톤 형식으로 수정하였다.

public:
    static EngineBase& GetInstance()
    {
        static EngineBase Instance;
        return Instance;
    }

이렇게, 지역 스태틱을 선언하고 참조자를 반환하는 형식으로 구현하였고, 동적으로 메모리를 할당하지 않았기 때문에 당연히 누수도 사라졌음을 확인할 수 있었다.

렌더링 파이프라인을 설정하기 전에, 렌더러의 간단한 구조를 먼저 잡고 가는게 좋을 것 같아서 렌더러를 일단 구성해보았다.

#pragma once
#include <memory>

class RenderBase
{

public:
    RenderBase();
    ~RenderBase();

    RenderBase(const RenderBase& _Other) = delete;
    RenderBase(RenderBase&& _Other) noexcept = delete;
    RenderBase& operator=(const RenderBase& _Other) = delete;
    RenderBase& operator=(RenderBase&& _Other) noexcept = delete;

public:
    virtual void Init() = 0;
    virtual void Render() = 0;

protected:
    std::vector<Vertex> Vertices;
    std::vector<uint16_t> Indices;

    Microsoft::WRL::ComPtr<ID3D11Buffer> VertexBuffer;
    Microsoft::WRL::ComPtr<ID3D11Buffer> IndexBuffer;
    Microsoft::WRL::ComPtr<ID3D11Buffer> ConstantBuffer;
private:

};

 

일단, 모든 렌더러들은 RenderBase를 상속받도록 설정하였다.

EngineBase에서는 RenderBase으로 렌더러들을 저장한 뒤, 렌더링 할 때 Render함수를 실행할 것이기 때문에, Render함수를 순수가상함수로 만들어 하위 클래스에서 반드시 구현하도록 설정하였다.

 

Init도 마찬가지로, 렌더러가 처음 생성될 때 설정해야 하는 것들을 반드시 정의하도록 순수가상함수로 만들어두었다.

그 외에도, 렌더링에 사용될 버텍스 관련 변수들을 선언해주었다.

 

public:
    template <typename T>
    static std::shared_ptr<T> CreateRenderer()
    {
        std::shared_ptr<RenderBase> NewRenderer = std::make_shared<T>();
        NewRenderer->Init();
        Renderers.insert(NewRenderer);

        return std::dynamic_pointer_cast<T>(NewRenderer);
    }

 

렌더러의 생성은 EngineBase 클래스에 static함수로 선언해두었다.

템플릿 함수로 선언하여, RenderBase를 상속받은 클래스라면 어떤 클래스든 생성할 수 있도록 구성하였고

EngineBase가 소유하고 있는 RenderBase를 담는 자료구조인 Renderers에 생성된 Renderer를 insert해주었다.

private:
    std::list<std::shared_ptr<RenderBase>> Renderers;

 

렌더러가 몇 개가 될 지 예측이 불가능하기 때문에, 수시로 Reserve를 해주어야 하는 vector대신 list를 사용하였다.

이후, 렌더링 기능이 추가되면 위의 list의 원소를 순회하면서 Render함수를 호출해줄 것이다.

 

이제, 버텍스 버퍼와 인덱스 버퍼 등 렌더링에 필요한 것들을 추가해보자.

테스트용으로 박스 렌더러 하나를 추가해볼것이다.

#pragma once
#include "RenderBase.h"

class BoxRenderer : public RenderBase
{

public:   
    BoxRenderer();
    ~BoxRenderer();

    BoxRenderer(const BoxRenderer& _Other) = delete;
    BoxRenderer(BoxRenderer&& _Other) noexcept = delete;
    BoxRenderer& operator=(const BoxRenderer& _Other) = delete;
    BoxRenderer& operator=(BoxRenderer&& _Other) noexcept = delete;

public:
    virtual void Render() override{}
    virtual void Init() override{}

protected:

private:
    void CreateVertexAndIndex();
};

 

이렇게 RenderBase 클래스를 상속받았으며, Render함수와 Init함수를 오버라이딩 해주었다.

멤버함수로 Vertex와 Index를 만드는 함수도 만들어주었다.

 

함수 내부는 대강 아래와 같은데, 그냥 노가다이다.

더보기
void BoxRenderer::CreateVertexAndIndex()
{
    std::vector<DirectX::SimpleMath::Vector3> Positions;
    Positions.reserve(24);

    std::vector<DirectX::SimpleMath::Vector3> Colors;
    Colors.reserve(24);

    std::vector<DirectX::SimpleMath::Vector3> Normals;
    Normals.reserve(24);

    //윗면
    Positions.push_back({ -1.0f, 1.0f, -1.0f });
    Positions.push_back({ -1.0f, 1.0f, 1.0f });
    Positions.push_back({ 1.0f, 1.0f, 1.0f });
    Positions.push_back({ 1.0f, 1.0f, -1.0f });
    
    Colors.push_back({1.0f, 0.0f, 0.0f});
    Colors.push_back({1.0f, 0.0f, 0.0f});
    Colors.push_back({1.0f, 0.0f, 0.0f});
    Colors.push_back({1.0f, 0.0f, 0.0f});
   
    Normals.push_back({0.0f, 1.0f, 0.0f});
    Normals.push_back({0.0f, 1.0f, 0.0f});
    Normals.push_back({0.0f, 1.0f, 0.0f});
    Normals.push_back({ 0.0f, 1.0f, 0.0f});

    //아랫면
    Positions.push_back({ 1.0f, -1.0f, 1.0f });
    Positions.push_back({ 1.0f, -1.0f, -1.0f });
    Positions.push_back({ -1.0f, -1.0f, -1.0f });
    Positions.push_back({ -1.0f, -1.0f, 1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ 0.0f, -1.0f, 0.0f });
    Normals.push_back({ 0.0f, -1.0f, 0.0f });
    Normals.push_back({ 0.0f, -1.0f, 0.0f });
    Normals.push_back({ 0.0f, -1.0f, 0.0f });

    //왼쪽
    Positions.push_back({ -1.0f, -1.0f, -1.0f });
    Positions.push_back({ -1.0f, -1.0f, 1.0f });
    Positions.push_back({ -1.0f, 1.0f, 1.0f });
    Positions.push_back({ -1.0f, 1.0f, -1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ -1.0f, 0.0f, 0.0f });
    Normals.push_back({ -1.0f, 0.0f, 0.0f });
    Normals.push_back({ -1.0f, 0.0f, 0.0f });
    Normals.push_back({ -1.0f, 0.0f, 0.0f });

    //오른쪽
    Positions.push_back({ 1.0f, 1.0f, -1.0f });
    Positions.push_back({ 1.0f, 1.0f, 1.0f });
    Positions.push_back({ 1.0f, -1.0f, 1.0f });
    Positions.push_back({ 1.0f, -1.0f, -1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ 1.0f, 0.0f, 0.0f });
    Normals.push_back({ 1.0f, 0.0f, 0.0f });
    Normals.push_back({ 1.0f, 0.0f, 0.0f });
    Normals.push_back({ 1.0f, 0.0f, 0.0f });

    //앞쪽
    Positions.push_back({ -1.0f, 1.0f, 1.0f });
    Positions.push_back({ -1.0f, -1.0f, 1.0f });
    Positions.push_back({ 1.0f, -1.0f, 1.0f });
    Positions.push_back({ 1.0f, 1.0f, 1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ 0.0f, 0.0f, 1.0f });
    Normals.push_back({ 0.0f, 0.0f, 1.0f });
    Normals.push_back({ 0.0f, 0.0f, 1.0f });
    Normals.push_back({ 0.0f, 0.0f, 1.0f });

    //뒷쪽
    Positions.push_back({ -1.0f, -1.0f, -1.0f });
    Positions.push_back({ -1.0f, 1.0f, -1.0f });
    Positions.push_back({ 1.0f, 1.0f, -1.0f });
    Positions.push_back({ 1.0f, -1.0f, -1.0f });

    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });
    Colors.push_back({ 1.0f, 0.0f, 0.0f });

    Normals.push_back({ 0.0f, 0.0f, -1.0f });
    Normals.push_back({ 0.0f, 0.0f, -1.0f });
    Normals.push_back({ 0.0f, 0.0f, -1.0f });
    Normals.push_back({ 0.0f, 0.0f, -1.0f });


    for (size_t i = 0; i < Positions.size(); i++) 
    {
        Vertex NewVertex;
        NewVertex.Position = Positions[i];
        NewVertex.Color = Colors[i];
        NewVertex.Normal = Normals[i];

        Vertices.push_back(NewVertex);
    }

    Indices = { 0, 1, 2,
                0, 2, 3,
                4, 6, 5,
                4, 7, 6,
                8, 9, 10,
                8, 10, 11,
                12, 13, 14,
                12, 14, 15,
                16, 17, 18,
                16, 18, 19,
                20, 21, 22,
                20, 22, 23 };
}

 

이렇게, 버텍스에 관한 정보를 모두 Vertices와 Indices에 담아주었다면, 이 정보를 기반으로 버퍼를 만들어야 한다.

버퍼를 만드는 함수는 RenderBase에 정의해두었다.

 

그런데, RenderBase에서 버텍스 버퍼를 만들려면 Device가 필요했는데, EngineBase의 Device를 어떻게 참조해야하나 고민하다가 그냥 EngineBase 클래스를 전역변수로 바꿔서 생성해주었다.

 

private:
    EngineBase();

    ~EngineBase()
    {
    	if (Instance != nullptr)
    	{
    	    delete (Instance);
    	}
    }

    static EngineBase* Instance;

public:
    static EngineBase* GetInstance()
    {
        if (Instance == nullptr)
        {
            Instance = new EngineBase();
        }

        return Instance;
    }

 

이렇게, Enginebase를 싱글톤 방식으로 구성한 뒤, 전역변수로 선언해주었다.

이후, EngineBase내부에 GetDevice함수를 추가해주었다.

void RenderBase::CreateVertexBuffer()
{
    D3D11_BUFFER_DESC bufferDesc;
    ZeroMemory(&bufferDesc, sizeof(bufferDesc));

    bufferDesc.Usage = D3D11_USAGE_IMMUTABLE; 
    bufferDesc.ByteWidth = UINT(sizeof(Vertex) * Vertices.size());
    bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    bufferDesc.CPUAccessFlags = 0; 
    bufferDesc.StructureByteStride = sizeof(Vertex);

    D3D11_SUBRESOURCE_DATA vertexBufferData = { 0, }; 
    vertexBufferData.pSysMem = Vertices.data();
    vertexBufferData.SysMemPitch = 0;
    vertexBufferData.SysMemSlicePitch = 0;

    const HRESULT Result =
        EngineBase::GetInstance()->GetDevice()->CreateBuffer(&bufferDesc, &vertexBufferData, VertexBuffer.GetAddressOf());

    if (Result != S_OK) 
    {
        std::cout << "CreateVertexBuffer() failed. " << std::hex << Result << std::endl;
    };
}

 

그리고 규칙에 맞게 버텍스 버퍼를 생성해주었다.

이렇게 작성하고 보니, 어떤 렌더러에서 버퍼 생성이 실패했는지 가시적으로 확인하기 위해 이름을 부여해주는 것이 좋겠다 싶었고, RenderBase에 이름을 추가해주었다.

BoxRenderer::BoxRenderer()
{
    Name = "BOX";
}

 

그리고 이렇게 생성자에서 이름을 설정해주고, 위의 버텍스 버퍼 생성 코드의 마지막 중에 있는 출력을 아래와 같이 바꿔주었다.

if (Result != S_OK) 
{
    std::cout << Name << " :" << "CreateVertexBuffer() failed." << std::hex << "\nResult : " << Result  << std::endl;
};

 

void RenderBase::CreateIndexBuffer()
{
    D3D11_BUFFER_DESC bufferDesc = {0, };
    bufferDesc.Usage = D3D11_USAGE_IMMUTABLE;
    bufferDesc.ByteWidth = UINT(sizeof(uint16_t) * Indices.size());
    bufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
    bufferDesc.CPUAccessFlags = 0; 
    bufferDesc.StructureByteStride = sizeof(uint16_t);

    D3D11_SUBRESOURCE_DATA indexBufferData = { 0 };
    indexBufferData.pSysMem = Indices.data();
    indexBufferData.SysMemPitch = 0;
    indexBufferData.SysMemSlicePitch = 0;

    HRESULT Result = 
        EngineBase::GetInstance()->GetDevice()->CreateBuffer(&bufferDesc, &indexBufferData, IndexBuffer.GetAddressOf());

    if (Result != S_OK)
    {
        std::cout << Name << " :" << "CreateIndexBuffer() failed." << std::hex << "\nResult : " << Result << std::endl;
    };
}

 

인덱스 버퍼도 만들어주었다.

 

이제, 상수버퍼도 만들고 쉐이더도 연결해야 하는데, 일단 밥먹고 나서 진행해야겠다.

코드를 보다보니, 윈도우 함수를 초기화하면서 윈도우 크기를 설정을 안해놓은 것을 확인했다..

그래서 먼저 윈도우 크기 설정부터 추가하였다.

 

Init함수는 윈도우 크기 인자를 추가로 받도록 해주었다.

 

Init함수 내부에선 이 크기를 멤버변수에 저장해주었다.

 

이후, CreateWindow할 때, 윈도우의 크기를 세팅해주었다.

 

이제, DirectX를 초기화해보자.

 

먼저, Init함수 내부를 조금 수정해주었다.

BOOL EngineBase::Init(HINSTANCE _hInstance, int _Width, int _Height)
{
    WindowWidth = _Width;
    WindowHeight = _Height;

    if (!WindowInit(_hInstance))
    {
        return FALSE;
    }

    if (!DirectXInit())
    {
        return FALSE;
    }

    return TRUE;
}

 

이렇게, Window초기화와 DirectX초기화를 나누어주었다.

 

BOOL EngineBase::DirectXInit()
{
    if (!CreateDevice())
    {
        std::cout << "CreateDevice() is Failed!" << std::endl;
        return FALSE;
    }

    if (!CreateSwapChain())
    {
        std::cout << "CreateSwapChain() is Failed!" << std::endl;
        return FALSE;
    }

    if (!CreateRasterizerState())
    {
        std::cout << "CreateRasterizerState() is Failed!" << std::endl;
        return FALSE;
    }

    if (!CreateDepthStencil())
    {
        std::cout << "CreateDepthStencil() is Failed!" << std::endl;
        return FALSE;
    }

    SetViewport();

    return TRUE;
}

 

DirectXInit함수 내부에서도 만들어야 하는 것들을 구분해서 만들도록 하였다.

 

BOOL EngineBase::CreateDevice()
{
    //하드웨어 드라이버를 사용할건가 소프트웨어 드라이버를 사용할건가
    const D3D_DRIVER_TYPE DriverType = D3D_DRIVER_TYPE_HARDWARE;

    //디버그 기능을 사용할 것인가
    UINT CreateDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    CreateDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

    //다이렉트X의 버전 목록 (만약 컴퓨터에 버전이 없다면, 더 낮은 버전으로 Device 생성을 시도
    const D3D_FEATURE_LEVEL featureLevels[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_9_3 };
    
    //생성된 Device의 다이렉트X 버전을 저장
    D3D_FEATURE_LEVEL featureLevel;

    HRESULT Result = D3D11CreateDevice(
        nullptr,                  // nullptr이면 기본 어댑터를 사용
        DriverType,               // 어떤 드라이버를 사용하여 Device를 만들 것인가
        0,                        // 소프트웨어 드라이버를 사용할 것이라면, 어떤걸 사용할지 선택하는 옵션인듯
        CreateDeviceFlags,        // 플래그
        featureLevels,            // 다이렉트X 버전 목록 배열
        ARRAYSIZE(featureLevels), // 위의 배열의 사이즈
        D3D11_SDK_VERSION,        // 무조건 D3D11_SDK_VERSION 쓰라고 써있네
        &Device,                  // 생성된 디바이스를 저장
        &featureLevel,            // 생성된 디바이스의 DirectX 버전을 저장
        &Context                  // 생성된 디바이스의 Context를 저장
    );

    if (Result != S_OK)
    {
        return FALSE;
    }

    return TRUE;
}

CreateDevice함수는 위와 같다.

Device가 무사히 생성되었다면, TRUE를 리턴할 것이다.

 

다음은 SwapChain을 만들어보자.

BOOL EngineBase::CreateSwapChain()
{
    HRESULT Result = S_OK;

    Microsoft::WRL::ComPtr<IDXGIDevice> DXGIDevice;
    Result = Device.As(&DXGIDevice);

    Microsoft::WRL::ComPtr<IDXGIAdapter> DXGIAdapter;
    Result = DXGIDevice->GetAdapter(&DXGIAdapter);

    Microsoft::WRL::ComPtr<IDXGIFactory> DXGIFactory;
    Result = DXGIAdapter->GetParent(IID_PPV_ARGS(&DXGIFactory));

    SwapChain.Reset();

    //멀티샘플링 안티에일리어싱 (MSAA)
    UINT numQualityLevels;
    Device->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM, 4, &numQualityLevels);
    if (numQualityLevels <= 0) 
    {
        std::cout << "MSAA not supported!" << std::endl;
    }

    DXGI_SWAP_CHAIN_DESC SD;
    ZeroMemory(&SD, sizeof(SD));
    SD.BufferDesc.Width = (UINT)WindowWidth;                 // 백버퍼 사이즈 (너비)
    SD.BufferDesc.Height = (UINT)WindowHeight;               // 백버퍼 사이즈 (높이)
    SD.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 색상 포맷
    SD.BufferCount = 2;                                // 백버퍼 개수
    SD.BufferDesc.RefreshRate.Numerator = 60;          // 갱신률 (분자)
    SD.BufferDesc.RefreshRate.Denominator = 1;         // 갱신률 (분모)
    SD.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;  // 스왑체인을 어떻게 쓸 것인가
    SD.OutputWindow = hWnd;                            // 스왑체인이 사용될 윈도우
    SD.Windowed = TRUE;                                // 창모드, 전체모드 
    SD.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // 창모드, 전체모드 전환을 허용할 것인가
    SD.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;  

    SD.SampleDesc.Count = 4;
    SD.SampleDesc.Quality = numQualityLevels - 1;

    Result = DXGIFactory->CreateSwapChain(Device.Get(), &SD, SwapChain.GetAddressOf());

    if (Result != S_OK)
    {
        return FALSE;
    }

    //백버퍼의 렌더타겟 뷰 생성
    ID3D11Texture2D* BackBuffer;
    SwapChain->GetBuffer(0, IID_PPV_ARGS(&BackBuffer));

    if (BackBuffer) 
    {
        Device->CreateRenderTargetView(BackBuffer, NULL, &RenderTargetView);
        BackBuffer->Release();
    }
    else 
    {
        std::cout << "CreateRenderTargetView() failed!" << std::endl;
        return FALSE;
    }

    return TRUE;
}

 

스왑체인을 만들면서, 멀티샘플링 설정도 해주었다.

뭔가 길고 복잡해보이지만, 그냥 원하는대로 설정하면 된다.

 

안에 보면, BackBuffer의 경우엔 Release를 호출해주고 있는데, BackBuffer는 Comptr을 사용해서 선언한 것이 아니라 직접 Release를 호출해주어야 한다.

 

Comptr로 선언한 애들은 프로세스가 종료될 때 알아서 해제해준다.

BOOL EngineBase::CreateRasterizerState()
{
    D3D11_RASTERIZER_DESC RD;
    ZeroMemory(&RD, sizeof(D3D11_RASTERIZER_DESC));
    RD.FillMode = D3D11_FILL_MODE::D3D11_FILL_SOLID;
    RD.CullMode = D3D11_CULL_MODE::D3D11_CULL_BACK;
    RD.FrontCounterClockwise = false;

    HRESULT Result = Device->CreateRasterizerState(&RD, &RasterizerState);
    if (Result != S_OK)
    {
        return FALSE;
    }
    
    return TRUE;
}

 

RasterizerState도 만들어주었다.

후면 컬링도 설정해주었다.

BOOL EngineBase::CreateDepthStencil()
{
    D3D11_TEXTURE2D_DESC DepthStencilBufferDesc;

    DepthStencilBufferDesc.Width = WindowWidth;
    DepthStencilBufferDesc.Height = WindowHeight;
    DepthStencilBufferDesc.MipLevels = 1;
    DepthStencilBufferDesc.ArraySize = 1;

    DepthStencilBufferDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
   
    if (NumQualityLevels > 0) 
    {
        DepthStencilBufferDesc.SampleDesc.Count = 4;
        DepthStencilBufferDesc.SampleDesc.Quality = NumQualityLevels - 1;
    }
    else 
    {
        DepthStencilBufferDesc.SampleDesc.Count = 1; 
        DepthStencilBufferDesc.SampleDesc.Quality = 0;
    }

    DepthStencilBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    DepthStencilBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
    DepthStencilBufferDesc.CPUAccessFlags = 0;
    DepthStencilBufferDesc.MiscFlags = 0;

    HRESULT Result = Device->CreateTexture2D(&DepthStencilBufferDesc, 0, DepthStencilBuffer.GetAddressOf());

    if (Result != S_OK)
    {
        return FALSE;
    }

    Result = Device->CreateDepthStencilView(DepthStencilBuffer.Get(), 0, &DepthStencilView);
    
    if (Result != S_OK)
    {
        return FALSE;
    }

    D3D11_DEPTH_STENCIL_DESC DepthStencilDesc;
    ZeroMemory(&DepthStencilDesc, sizeof(D3D11_DEPTH_STENCIL_DESC));
    DepthStencilDesc.DepthEnable = true;
    DepthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK::D3D11_DEPTH_WRITE_MASK_ALL;
    DepthStencilDesc.DepthFunc = D3D11_COMPARISON_FUNC::D3D11_COMPARISON_LESS_EQUAL;

    Result = Device->CreateDepthStencilState(&DepthStencilDesc, DepthStencilState.GetAddressOf());

    if (Result != S_OK)
    {
        return FALSE;
    }

    return TRUE;
}

 

뎁스스텐실 버퍼와 뎁스스텐실 뷰도 생성해보았다.

void EngineBase::SetViewport()
{
    ZeroMemory(&ScreenViewPort, sizeof(D3D11_VIEWPORT));
    ScreenViewPort.TopLeftX = 0;
    ScreenViewPort.TopLeftY = 0;
    ScreenViewPort.Width = float(WindowWidth);
    ScreenViewPort.Height = float(WindowHeight);
    ScreenViewPort.MinDepth = 0.0f;
    ScreenViewPort.MaxDepth = 1.0f;
    
    Context->RSSetViewports(1, &ScreenViewPort);
}

 

마지막으로, 뷰포트 세팅까지 해주면서, 기본적인 초기화는 얼추 끝났다..

 

 

홍정모 강사님의 그래픽스 강의를 듣다가 갑자기 프로젝트를 하나 만들고 싶었다.

 

해당 강의는 아무래도 다이렉트 X 자체보다는 그래픽스에 더 집중하는 부분이 있었다. 물론 이 부분은 교육에 정말 도움이 되고 긍정적으로 생각하고 있다. 어느 그래픽스 라이브러리를 사용하더라도, 쉽게 적응할 수 있도록 이론적인 부분에 더 초점을 두신 것 같다.

 

그렇게, 큰 틀은 대부분 직접 구현하신 상태로 그래픽스에 관한 부분만 비워서 직접 작성해보며 테스트 할 수 있게 예제 프로젝트를 제공해주시는데, 갑자기 왠지 모를 욕심이 생겨서 나만의 프로젝트를 만들어서 초기화하며 교육 내용을 직접 적용해보고 싶다는 생각이 들었다.

 

그런 이유로, DirectX를 이용한 그래픽스 프로젝트를 시작하기로 했다...

프로젝트라고 하지만 거창한 무언가를 할 계획은 아니고, 강의를 들으며 배운 내용을 나의 프로젝트에 적용해보며 구현해볼 생각이다. 

 

먼저 Windows API기반으로 프로젝트를 생성한 뒤, DirectXTK를 프로젝트에 추가해주었다.

(홍정모 강사님의 강의에서 SimpleMath를 사용하셔서, 추가하게 되었다.)

 

그리고, Main함수에 있는 윈도우 초기화 함수들을 정리해주었다.

#include "framework.h"
#include "GraphicsProject.h"
#include "EngineBase.h"

#include <iostream>
#include <d3d11.h>

#pragma comment (lib, "d3d11.lib")
#pragma comment(linker, "/entry:wWinMainCRTStartup /subsystem:console")

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    EngineBase NewEngineBase;

    if (!NewEngineBase.Init(hInstance))
    {
        std::cout << "Init Failed!" << std::endl;
        return -1;
    }

    NewEngineBase.Loop();

    WPARAM EndParam = NewEngineBase.End();

    return (int)EndParam;
}

 

먼저, EngineBase라는 클래스를 하나 정의하였고, 해당 함수에 Init, Loop, End 함수를 만들었다.

Init에선 여러 초기화를 실행하도록 정리하였고, Loop에선 메세지 루프가 실행되도록 하였다.

End는 프로그램이 종료될 때 처리해야 할 것들을 구현하기 위해서 정의하였다.

 

EngineBase에는 기존에 작성되어 있던 윈도우 초기화 함수들을 정리해서 넣어주었다.

#pragma once
#include "Windows.h"

class EngineBase
{

public:

    EngineBase();
    ~EngineBase();

    EngineBase(const EngineBase& _Other) = delete;
    EngineBase(EngineBase&& _Other) noexcept = delete;
    EngineBase& operator=(const EngineBase& _Other) = delete;
    EngineBase& operator=(EngineBase&& _Other) noexcept = delete;

    ATOM MyRegisterClass(HINSTANCE hInstance);
    static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
    BOOL InitInstance(HINSTANCE hInstance, int nCmdShow);
    
    BOOL Init(HINSTANCE _hInstance);
    void Loop();
    WPARAM End();

protected:

private:
   HINSTANCE hInstance;
   WNDCLASSEXW wcex;
   MSG msg;
};

 

먼저, HINSTANCE나 MSG같은 중요한 변수들은 멤버변수에 저장해주었다.

 

멤버함수들의 구현부를 보면 아래와 같다.

BOOL EngineBase::Init(HINSTANCE _hInstance)
{
    hInstance = _hInstance;

    MyRegisterClass(hInstance);

    if (!InitInstance(hInstance, SW_SHOW))
    {
        return FALSE;
    }

    return TRUE;
}

 

Init함수에선 MyRegisterClass함수를 먼저 실행해주었다.

해당 함수는 윈도우를 생성하기 전에, 어떻게 생성할지에 관한 여러 정보를 세팅하는 함수이다.

 

InitInstance함수는 윈도우를 생성하는 함수이다. 만약 윈도우 생성이 실패했다면, init함수는 FALSE를 반환하도록 하였다.

main함수에서 Init이 FALSE를 반환할 경우, 콘솔창에 메세지를 출력하도록 함으로써 디버깅을 좀 더 수월하게 하였다.

ATOM EngineBase::MyRegisterClass(HINSTANCE _hInstance)
{
    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = &EngineBase::WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = _hInstance;
    wcex.hIcon = nullptr; //LoadIcon(_hInstance, MAKEINTRESOURCE(IDI_GRAPHICSPROJECT));
    wcex.hCursor = nullptr; //LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = nullptr; //MAKEINTRESOURCEW(IDC_GRAPHICSPROJECT);
    wcex.lpszClassName = L"WindowDefault";
    wcex.hIconSm = nullptr; //LoadIcon(wcex._hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&wcex);
}

 

myRegisterClass함수 내부이다.

윈도우의 모양이나 색상, 이름 등을 정의할 수 있는데, 지금 당장 필요하다고 생각하는 것들이 딱히 없어서 대부분 nullptr로 설정해주었다.

 

wcex.lpfnwndproc를 보면, 메세지 함수를 콜백 형식으로 저장할 수 있는데 본인은 멤버함수를 사용하기 위해, WndProc함수를 static으로 선언해주었다.

 

BOOL EngineBase::InitInstance(HINSTANCE _hInstance, int nCmdShow)
{
    hInstance = _hInstance;

    HWND hWnd = CreateWindowW(L"WindowDefault", L"GraphicsProject", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

    if (!hWnd)
    {
        return FALSE;
    }

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    return TRUE;
}

 

InitInstance함수 내부도 심플하다. 윈도우를 생성해주었고, 실패시 FALSE를 반환하도록 하였다.

 

다음은 Loop함수를 보자.

void EngineBase::Loop()
{
    while(WM_QUIT != msg.message)
    {
        if ((PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
}

 

기본적인 메세지 루프만 담고있다.

While문이나 if문의 조건문을 조금 수정해주었다.

 

WM_QUIT 메세지가 전달되기 전까지 반복문을 계속 돌도록 하였으며, 메세지를 전달받으면 해당 메세지를 처리하도록 해주었다.

 

 

다음은 End이다.

WPARAM EngineBase::End()
{
    return msg.wParam;
}

 

아직은 뭐 추가한 기능이 하나도 없어서, End에서 처리할 것이 따로 없다.

기존에 메인함수에서 msg.wparam을 return하도록 되어있길래, 동일하게 이 것을 리턴해주며 main함수에서는 반환된 값을 저장하여 return하도록 해주었다.

 

WindowAPI의 기초적인 함수만 정리하는데도 생각보다 시간이 좀 소요되었다.

쓸모없는 기능은 쳐내고, 필요한 기능은 남겨두면서 정리를 하려고 했는데, 뭐가 쓸모없는 기능인지 찾는 것도 쉽지 않았다.

 

일단, 간단한 틀은 완성되었고 이 틀에 계속 살을 붙혀서 원하고자 하는 것을 렌더링 할 수 있을 때까지 가보도록 하겠다.

 

winsock2 라이브러리를 활용하여 간단하게 채팅서버를 구현해 보았다.

 

https://youtu.be/fOAxBkvQnZc?si=SgOIYln8jq_Sh4oa

 

위의 영상은 결과물을 동영상으로 찍은 것이다.

 

먼저, 간단하게 구조만 정리해보자.

 

접속 과정을 그림으로 정리해보았다.

서버에선 listen을 통해 연결 대기열을 만들고 접속을 대기한다.

이후, 클라이언트가 connect함수를 이용해 연결을 시도하면 연결 대기열에 클라이언트가 추가되며,

서버에선 이를 감지하면 accept를 함수를 통해 해당 클라이언트의 접속을 허가해준다.

 

 

메세지 송수신 과정을 위 그림과 같다.

특정 클라이언트에서 메세지를 입력하게 되면, 해당 메세지는 서버로 송신된다.

서버는 수신을 대기하고 있다가 메세지를 수신하게 되면, 동일한 메세지를 다른 클라이언트에게 모두 뿌려준다.

다른 클라언트들은 서버로부터 데이터 수신을 대기하고 있다가 메세지를 수신하게 되면 해당 메세지를 채팅기록 자료구조에 저장하여 화면에 렌더링해주면 된다.

 

정말 간단하면서도 알아야 하는게 많은 프로젝트였다.

네트워크에 대한 기초를 공부하기에 간단한 서버 하나 만들어 본게 꽤나 도움이 많이 된 것 같다.

저번 게시글에선 서버를 만들어보았으니 이번엔 클라이언트를 만들어보도록 하겠다.

전체적인 틀은 거의 똑같다.

 

DirectX와 IMGUI를 사용하였고, Winsock2 라이브러리를 활용하여 소켓통신을 구현할 것이다.

 

다만, 서버에서 클라이언트의 연결을 허가하고 받은 데이터를 다른 클라이언트에게 뿌려주는 기능을 만들었다면

클라이언트에선 서버에 연결을 요청하고 메세지를 전송하는 기능을 만들어 볼 것이다

 

 

클라이언트 구현


char IP[PACKET_SIZE] = { 0, };
char Name[PACKET_SIZE] = { 0, };

 

먼저, IP와 이름을 담을 변수를 선언했다.

IMGUI를 통해 입력을 받게 되면, 해당 변수에 저장될 것이다.

 

bool isSetName = false;
bool isSetIP = false;

 

또한, 이름이 입력되었는지 IP가 입력되었는지 true/false를 저장하는 bool 값을 하나 만들었다.

입력에 순서가 있기 때문에, 이를 구분하기 위해 분기를 나누었다.

분기는 아래의 그림과 같다.

 

 

 

이후, 분기에 맞게 이름을 먼저 입력받도록 코드를 작성하였다.

if(isSetName == false)
{
    ImGui::Text("Please Input Name");
    
    if (ImGui::InputText(" ", Name, IM_ARRAYSIZE(Name), ImGuiInputTextFlags_EnterReturnsTrue) == true)
    {
        isSetName = true;
    }
}

 

이름을 입력받으면, Name에 입력된 이름을 저장한 뒤 isSetName을 true로 만들어준다.

다음 프레임엔 isSetName이 true가 되었기 때문에 해당 분기에 들어오지 않고 다음 분기로 넘어가게 된다.

 

else if (isSetName == true && isSetIP == false)
{
    ImGui::Text("Please Input IP");

    if (ImGui::InputText(" ", IP, IM_ARRAYSIZE(IP), ImGuiInputTextFlags_EnterReturnsTrue) == true)
    {
        WSAInit();
        ConnectToServer();
        
        send(Server, Name, sizeof(Name), 0);
        isSetIP = true;
    }
}

 

 isSetName이 true지만 isSetIP 가 false이므로 IP를 입력받도록 하였다.

IP를 입력받은 뒤엔 WSA 초기화를 하였고, 서버에 연결까지 하였다.

연결을 마친 귀에는 이름을 서버에 전송하였다. 

 

서버를 구현할 때, 클라이언트와 연결한 뒤 처음으로 받는 데이터는 반드시 클라이언트의 이름으로 저장하도록 하였기 때문에, 클라이언트에선 연결하자마자 이름을 보내주었다. 

 

이후, isSetIP를 true로 만들어 해당 분기에 다음 프레임부터 들어오지 않도록 하였다.

 

WSAInit() 과 ConnectToServer함수 내부를 보자.

int WSAInit()
{
    WSADATA Wsa;

    int WsaStartResult = WSAStartup(MAKEWORD(2, 2), &Wsa);
    if (WsaStartResult != 0)
    {
        std::cerr << "WSAStartup failed with error code: " << WsaStartResult << std::endl;
        return 1;
    }

    return 0;
}

WSAInit함수에선 서버와 동일하게 윈속 라이브러리를 초기화 해주었다

 

void ConnectToServer()
{
    Server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    SOCKADDR_IN Addr = { 0, };
    Addr.sin_addr.s_addr = inet_addr(IP);
    Addr.sin_port = PORT;
    Addr.sin_family = AF_INET;

    while (connect(Server, reinterpret_cast<SOCKADDR*>(&Addr), sizeof(Addr)));

    std::thread(RecvData, std::ref(Server)).detach();
}

 

ConnectToServer 함수 내부에선 서버와 통신하기 위한 소켓을 만들었다.

이후, IMGUI를 통해 입력받은 IP와 포트를 이용하여 서버에 연결을 시도하였다.

 

connect는 연결이 되는는 순간 1을 반환하고, 연결이 되지 않으면 0을 반환한다.

이 특성을 이용해 while문으로 연결이 될 때까지 대기하였다.

 

(connect함수는 서버에서 사용했던 accept함수나 recv함수와 동일하게 다음 코드로 진행하지 않도록 블로킹해주는 기능이 있다고 한다. 그러니 굳이 while문을 사용하지 않고 if문으로 접속 여부만 확인하여도 된다.)

 

이후, 서버로부터 데이터를 전송받는 RecvData 함수를 백그라운드에서 실행하였다.

 

이제 채팅 코드를 보자.

아래는 Name과 IP가 모두 세팅된 이후에 실행되는 분기이다.

else
{
    char ChatText[1024] = { 0, };

    ImGui::Text("Chat Start");
    std::string NameText = "User Name : ";
    NameText += Name;

    ImGui::TextColored({0.0f, 1.0f, 0.0f, 1.0f}, NameText.c_str());

    if (ImGui::InputText(" ", ChatText, IM_ARRAYSIZE(ChatText), ImGuiInputTextFlags_EnterReturnsTrue) == true)
    {
        std::string ChatStr = Name;
        ChatStr += " : ";
        ChatStr += ChatText;

        AddChat(ChatStr);

        send(Server, ChatText, sizeof(ChatText), 0);
    }

    if (Chats.size() > 20)
    {
        EraseChat(Chats.begin());
    }

    for (int i = 0; i < Chats.size(); i++)
    {
        ImGui::Text(Chats[i].c_str());
    }
}

 

먼저, IMGUI의 기능을 활용하여 아래 사진과 같이 본인의 이름을 초록색으로 화면에 렌더링 해준다.

 

IMGUI를 통해 데이터를 입력받게 되면 해당 데이터를 "이름 : 메세지"의 형식으로 가공하여 서버에 송신하였다.

또한, 채팅기록을 담고 있는 자료구조에 저장도 해주었다.

 

그 이후, 매 프레임마다 채팅기록을 최대 20개까지 렌더링 해주었다.

 

이것을 매 프레임마다 반복하면 끝이다.

shutdown(Server, SD_BOTH);
closesocket(Server);

WSACleanup();

 

 

코드 마지막줄에 위 3개를 추가하여, 프로세스가 종료될 때 서버와 연결을 완전히 끊어주기만 하면 클라이언트는 완성이다.

 

다음 게시글에서 완성된 채팅서버와 함께 간단한 구조 정리를 해보도록 하겠다. 

 

 

네트워크에 대한 이해가 너무 부족하다고 생각했기 때문에, 간단하게 나마 네트워크의 기초를 이해해볼 수 있는 프로젝트를 하나 진행해보고 싶었다.

 

아무래도 채팅서버가 가장 간단하면서 서버의 개념을 잡기 좋은 프로젝트라고 생각하였고, 채팅서버를 하나 만들어보기로하였다.

 

메시지 입력, 출력, 렌더링 등의 기능을 편하게 구현하기 위해 IMGUI의 기능을 가져와서 활용하였다.

 

네트워크를 구축하는데 사용되는 라이브러리는 여러가지가 있겠지만, 본인은 Winsock2를 사용하였다.

윈도우에서 기본으로 지원해주는 라이브러리이기 때문에 추가적인 설치나 세팅이 필요하지 않았고 윈도우, 비주얼 스튜디오 환경에서 문제없이 돌아갈 것이라 생각하였기 때문이다.

 

네트워크 프로그램이기 때문에, 서버와 클라이언트를 분리해서 2개의 프로젝트를 생성해주었다.

 

 

서버 구현


 

먼저, Winsock2 헤더 파일을 추가해주었다.

#include <WinSock2.h>
#include <windows.h>

 

두 가지를 추가해주면 된다.

 

또한, Winsock2을 사용하기 위해선, ws2_32.lib 파일을 링크해주어야 한다.

#pragma comment(lib, "ws2_32.lib")

추가로 선언해주었다.

 

그런데, 여기서 주의해야 할 점이 하나 있다.

#include <windows.h>
#include <winsock2.h>

헤더 파일을 위 순서로 include하면 오류난다...

웃기지만, 사실이다. windows.h를 먼저 include하면 두 헤더파일이 충돌하게 된다.

원인은 무엇일까?

 

winsock2는 이름 그대로, winsock의 2번째 버전이다.

첫번째 버전인 그냥 winsock도 있다는 뜻이다.

 

이 winsock.h가 windows.h에 포함되어 있다고 한다.

구버전의 winsock을 추가한 상태에서 신버전의 winsock2를 추가하게 되면 서로 재정의를 하면서 충돌이 나는 것이다.

 

winsock2.h에는 winsock.h를 막는 코드가 포함되어 있기 때문에, winsock2.h를 먼저 include하게 되면

windows.h안에 있는 winsock.h이 무시된다고 한다. 그래서 순서에 따라 오류가 나는 것이라고 한다.

 

물론 순서를 계속 맞추는 것이 귀찮기 때문에, 이를 해결할 수 있는 방법이 있다.

#define _WINSOCKAPI_

#include <windows.h>
#include <WinSock2.h>

 

이렇게 상단에 매크로를 하나 추가하게 되면, 순서에 상관 없이 구버전의 winsock.h를 무시해준다고 한다.

 

헤더 파일도 추가했고, lib파일도 링크해주었으니 코드를 작성해보았다.

WSADATA Wsa;

int WsaStartResult = WSAStartup(MAKEWORD(2, 2), &Wsa);

if (WsaStartResult != 0)
{
    std::cerr << "WSAStartup failed with error code: " << WsaStartResult << std::endl;
    return 1;
}

 

먼저, WSAStartUp을 실행하여 초기화해주었다.

 

Winsock2의 경우 ws2_32.dll이라는 동적 라이브러리도 사용한다고 한다.

이 동적 라이브러리의 초기값을 설정해주는 역할이 WSAStartUp이라고 한다.

 

첫 번째 인자는 winsock의 어떤 버전을 사용할 지를 입력하는 것이라는데, 사실 버전의 차이를 구별할 정도의 지식은 아니라서 가장 최신버전인 2,2를 입력해주었다.

버전의 정수부와 실수부를 입력하면 내부에서 버전을 표기하는 방식으로 변환해준다고 한다.

 

두 번째 인자의 경우, 라이브러리 초기화를 한 뒤 WSADATA 구조체에 몇 가지 유용한 정보를 담아준다고 한다.

소켓은 최대 몇 개까지 열 수 있는가, winsock의 최신 버전이 몇인가 등의 정보가 있다는데 어디다가 써야하는 지는 잘 모르겠다. 

 

WsaStartUp이 0이 아닌 다른 값을 반환했다면, 뭔가 문제가 있는 것이다.

당장 값을 확인해서 구글링을 해보자. 에러코드별 내용이 상세하게 구분되어있다.

 

SOCKET Server;

Server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

 

이후, SOCKET 구조체를 하나 선언해준 뒤, socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 함수의 반환값을 저장하였다.

 

첫번쨰 인자는 IPv4를 기반으로 한 인터넷 통신을 하겠다는 의미이며, 두번째 인자는 연결 지향형 통신을 하겠다는 의미이고, 세번째는 TCP 프로토콜을 사용하겠다는 것이다.

 

근데 생각해보면 IPv4를 사용하며 연결 지향형 통신을 하는게 TCP인데 왜 또 세번째인자에선 TCP냐 UDP냐를 받는 것일까?

 

이를 찾아보니, 첫번째 인자를 일반적으로 IPv4를 사용하기 때문에 두번째 인자가 SOCK_STREAM이 되면 "반드시 TCP다" 라는 결론이 나오지만 첫번째 인자로 다른 프로토콜 체계를 선택한 경우, 연결 지향형 통신임에도 TCP로 단정할 수 없는 경우가 있다고 한다. 그래서 이를 구체화하기 위해 세번째 인자를 사용하는 것이라고 한다.

 

SOCKADDR_IN Addr = { 0, };

Addr.sin_addr.s_addr = htonl(INADDR_ANY);
Addr.sin_port = PORT;
Addr.sin_family = AF_INET;

int BindResult = bind(Server, reinterpret_cast<SOCKADDR*>(&Addr), sizeof(Addr));

if (BindResult != 0)
{
    std::cerr << "Bind failed. error code : " << BindResult << std::endl;
    return 1;
}

 

아무튼 소켓을 다 만들고, IP 주소와 포트번호를 소켓에 바인딩해주었다.

htonl이 뭔가 찾아봤더니, 호스트에선 리틀앤디안 방식을 사용하지만 네트워크에선 빅앤디안 방식을 사용하기 때문에 이를 변환해주는 함수라고 한다. hton은 host to network 의 준말이며, 마지막 l은 자료형이라고 한다. ( unsigned long )

 

INADDR_ANY은 하나의 PC에 여러개의 IP가 존재하는 있는 경우, 어떤 IP로 데이터를 수신받아도 처리할 수 있도록 해주는 것이라고 한다.

 

예를 들어, 컴퓨터 하나에 랜카드가 A, B 2개가 부착되어 있다고 할 때, 만약 소켓에 A의 IP를 정확히 박아버리면 B를 통해

들어오는 데이터는 처리할 수가 없게 된다. 

 

반면, INADDR_ANY를 사용하게 되면 어떤 IP로 데이터가 들어오든 처리할 수 있도록 해준다고 한다.

 

또한, IP를 정확히 명시하게 되면 다른 컴퓨터에선 코드의 수정 없이는 사용할 수 없게 되는데, INADDR_ANY 매크로를 사용하게 되면 다른 컴퓨터에서도 수정없이 사용할 수 있어서 이식성이 좋아진다고 한다.

 

마지막으로 bind 결과에 대한 예외처리를 해주자.

소켓에 주소를 묶었으니 이제 연결을 기다릴 차례이다.

 

int ListenResult = listen(Server, SOMAXCONN);

if (ListenResult != 0) 
{
    std::cerr << "Listen failed. Error code : " << ListenResult << std::endl;
    return 1;
}

std::thread(AcceptClient, std::ref(Server)).detach();

 

먼저, 연결 대기열을 만들어주자.

Listen 함수는 연결 대기열을 생성해주는 함수라고 한다.

서버에 100명이 연결을 요청한다고 하면, 요청 순서대로 listen이 만들어 놓은 queue에 쌓이게 되는 것이다.

 

listen의 두번째 인자는 연결 대기열의 길이를 설정할 수 있는 곳인데, SOMAXCONN을 입력하면 알아서 길이를 세팅해준다고 한다.

 

이후, 연결을 처리하기 위한 AcceptClient 함수를 백그라운드에서 돌려주었다.

해당 함수의 설명은 뒤쪽에서 하도록 하겠다.

 

이제, 클라이언트의 연결 요청을 처리하는 함수를 만들어야 한다.

먼저, 클라이언트가 서버에 접속 성공했을 때, 클라이언트의 정보를 보관하기 위한 클래스를 하나 만들어보았다.

class Client
{
public:
	SOCKET ClientSock = { 0, };
	SOCKADDR_IN Addr = { 0, };

	int ClientSize = sizeof(Addr);
	int Number = -1;

	bool bIsDeath = false;

	Client(){}
};

 

먼저, 소켓을 하나 선언하였다.

해당 클라이언트와 통신하기 위한 소켓이다.

 

클라이언트 별로 다른 소켓이 생성되기 때문에, 클라이언트 별로 소켓을 저장해놓을 필요가 있다.

SOCKADDR_IN은 클라이언트의 인터넷 정보를 저장하는 구조체이다.

서버에서 클라이언트에 대한 요청을 처리한 뒤, 해당 구조체에 정보를 넣어준다.

 

ClientSize는 서버에서 연결 요청을 처리한 뒤, 정보를 넣어준 구조체의 크기 또한 반환해주는데 이걸 저장해둘 변수이다.

Number는 클라이언트의 인덱스이다. 클라이언트를 편하게 관리하기 위해 클라이언트마다 인덱스를 부여해 줄 것이다.

 

bIsDeath는 이 클라이언트가 현재 연결이 끊겼는지를 알려주는 변수이다.

이 변수는 채팅서버 구현 과정에서 발견된 문제 하나를 해결하기 위한 임시방편이다.

문제에 대해선 뒤쪽에서 얘기해보도록 하겠다.

 

이제, 연결을 처리하는 함수를 만들어 보겠다.

std::vector<std::pair<Client, std::string>> Clients;

void AcceptClient(SOCKET& _Socket)
{
    int Order = 0;

    while (true)
    {
        Clients.push_back(std::pair<Client, std::string>{Client(), ""});

        Clients[Order].first.ClientSock = accept(_Socket, reinterpret_cast<SOCKADDR*>(&Clients[Order].first.Addr), &Clients[Order].first.ClientSize);
        Clients[Order].first.Number = Order;
        Clients[Order].first.bIsDeath = false;

        std::thread(RecvData, Clients[Order].first.ClientSock, Order).detach();

        Order++;
    }
}

 

클라이언트 구조체와 이름을 담는 벡터를 하나 선언하였다.

 

이후, 연결 요청이 들어오면 구조체에 소켓, 인터넷 정보, 인덱스 등을 모두 저장하였다.

그리고 해당 클라이언트와 1:1 통신을 하기 위해 만들어놓은 RecvData라는 함수를 백그라운드에서 돌려주었다.

 

처음엔 while문을 저렇게 계속 돌리는데 뭐가 제대로 되려나? 싶었는데 accept 함수의 경우 연결 요청이 없으면 그 자리에서 코드 진행을 막아준다고 한다. 

 

위에서 bIsDeath라는 변수를 만들었던 이유가 여기서 나온다.

만약, 클라이언트가 연결이 끊긴다면 그 데이터를 자료구조에서 지우는 것이 일반적이다.

 

하지만, 벡터의 경우 자료구조 중간의 데이터를 지워버리면 그 뒤에 있는 원소들의 인덱스가 1씩 감소하게 된다.

위에서 멀티스레드 함수를 보면, 인자로 인덱스를 보내고 있다.

 

RecvData 함수가 최초에 입력받은 인덱스를 기반으로 실행되고 있는데, 중간에 인덱스가 바뀌어 버리면 문제가 발생하는 것이다.. 이를 임시방편으로 해결하기 위해 bIsDeath 변수를 추가하여 연결이 끊겼는지를 확인하여 예외처리를 두었다.

 

(하지만, 연결이 끊긴 클라이언트의 정보를 계속 가지고 있는 것이 메모리적으로 굉장한 손해이기 때문에 ,추후에는 새로운 접속이 들어오면 벡터를 앞에서부터 탐색하여 bIsDeath가 true인 인덱스의 데이터를 덮어씌우는 식으로 구조를 바꿔 인덱스의 변경 없이 메모리 낭비를 최소화해보려고 한다.)

 


std::vector<std::string> RecvChats;
std::mutex ChatsMutex;

void AddRecvChat(const std::string& _Chat)
{
    std::lock_guard<std::mutex> lock(ChatsMutex);
    RecvChats.push_back(_Chat);
}

void SendMsgToAllCLient(int _IgnoreIndex, std::string_view _Msg)
{
    for (int i = 0; i < Clients.size(); i++)
    {
        if (i == _IgnoreIndex || Clients[i].first.bIsDeath == true)
        {
            continue;
        }
    
        send(Clients[i].first.ClientSock, _Msg.data(), sizeof(char) * PACKET_SIZE, 0);
    }
}

void RecvData(SOCKET _Socket, int Num)
{
    char Buffer[PACKET_SIZE] = { 0, };
    recv(_Socket, Buffer, sizeof(Buffer), 0);
    Clients[Num].second = Buffer;
    
    AddRecvChat(Clients[Num].second + " join.");
    
    std::string JoinMsg = Clients[Num].second;
    JoinMsg += " join.";
    
    SendMsgToAllCLient(Num, JoinMsg);
    
    while (true)
    {
        ZeroMemory(Buffer, sizeof(Buffer));
    
        int RecvReturn = recv(_Socket, Buffer, sizeof(Buffer), 0);
    
        if (RecvReturn == 0)
        {
            Clients[Num].first.bIsDeath = true;
            AddRecvChat(Clients[Num].second + " leave. \n");
    
            std::string LeaveMsg = Clients[Num].second;
            LeaveMsg += " Leave.";
    
            SendMsgToAllCLient(Num, LeaveMsg);
    
            shutdown(Clients[Num].first.ClientSock, SD_BOTH);
            closesocket(Clients[Num].first.ClientSock);
            
            break;
        }
    
        AddRecvChat(Clients[Num].second + " : " + Buffer + "\n");
    
        std::string SendMsg = Clients[Num].second;
        SendMsg += " : ";
        SendMsg += Buffer;
    
        SendMsgToAllCLient(Num, SendMsg);
    }
}

 

이는 RecvData 함수의 코드이다.

 

 

먼저, 수신한 채팅을 로그에 저장하기 위해 문자열을 담는 벡터 RecvChats를 선언하였다.

멀티스레딩을 사용하는 만큼 RecvChats에 대한 동기화를 달성하기 위해 ChatsMutex라는 이름으로 std::mutex도 하나 선언해주었다.

 

채팅 메세지를 송신받으면 lock_guard를 이용하여 메모리 영역을 동기화한 뒤, 메세지를 삽입하도록 했다.

 

SendMsgToAllClient는 수신한 채팅을 그대로 다른 클라이언트들에게 보내주어야 하기 위해 만든 기능이다.

메세지를 보낸 사람을 제외한 나머지 클라이언트에게 메세지를 송신한다.

 

(채팅은 서버를 통해 이루어지기 때문에, 내가 보낸 채팅을 서버에서 송신해주지 않으면 상대방은 메세지를 받을 수 없다.)

 

RecvData에선 맨 처음으로 recv를 한다.

recv는 소켓을 통해 클라이언트에게 데이터를 받는 함수이다.

클라이언트 쪽에선 연결되면 처음으로 이름을 송신하도록 하였기 때문에 처음 recv에선 설정된 이름을 받게 된다.

 

이후, 해당 이름을 자료구조에 넣어주었다.

그리고 해당 이름의 유저가 채팅서버에 접속되었다는 알림을 모든 클라이언트에게 송신해주었다.

 

그 다음은 반복문을 통해 계속 Recv를 해주었다.

메세지가 들어온다면, "이름 : 메세지" 의 형태로 모든 클라이언트에게 송신해주었다.

  

그런데 위에 보면 예외처리가 하나 있는데, RecvReturn이 0일때 이다.

Recv함수는 기본적으로 받은 메세지의 길이를 반환해준다. 아무것도 없는 메세지여도 \0 (널) 하나는 저장되어 있기 때문에 최소 1의 길이는 갖게 된다.

Recv함수가 0을 반환할 때는 클라이언트가 접속을 끊었다는 얘기이다.

 

Recv가 0을 반환하면 해당 유저가 떠났음을 모든 클라이언트에게 공지한 뒤, 반복문을 탈출하여 해당 함수를 종료하고 스레드를 파괴해주었다.

 

참고로 통신을 끊을 때 shutdown함수와 closesocket함수를 반드시 호출해주어야 한다.

몇 가지 시행착오가 있었는데, 해당 함수를 제대로 호출해주지 않으면 실제론 연결이 끊겼는데 계속 허공에다가 recv를 하는 상황이 발생할 수 있다. 이 떄문에 로그에 빈 문자열이 프레임마다 출력되는 참사가 발생하기도 하였었다. 

 

void PrintLog()
{
    ImGui::Begin("Server");

    ImGui::Text("Log");

    if (RecvChats.size() > 20)
    {
        EraseRecvChat(RecvChats.begin());
    }

    for (int i = 0; i < RecvChats.size(); i++)
    {
        ImGui::Text(RecvChats[i].c_str());
    }

    ImGui::End();
}

 

이후, 매 프레임마다 자료구조에 저장된 메세지를 IMGUI윈도우에 출력해주면 로그 출력까지 마무리된다.

본인은 로그가 너무 길어지는 것을 막기 위해 최대 20개까지만 저장하고 출력하도록 하였다.

 

이로서 서버 구현은 완료되었다.

아래는 코드 전문이다.

 

 

헤더파일

더보기

#pragma once
#pragma comment(lib, "ws2_32.lib")

//Windows.h 에는 WinSock.h(WinSock2.h의 구버전)이 포함되어 있어 중복 정의가 발생. 
//구버전을 무시하겠다는 의미.
#define _WINSOCKAPI_

//이렇게 그냥 WinSock2.h 를 먼저 include 해도 됨. (WinSock2.h 내부에는 WinSock.h의 포함을 막는 코드가 있다고 함) 
#include <WinSock2.h>
#include <windows.h>

#include <iostream>
#include <thread>
#include <vector>
#include <string>


class Client
{
public:
SOCKET ClientSock = { 0, };
SOCKADDR_IN Addr = { 0, };

int ClientSize = sizeof(Addr);
int Number = -1;

bool bIsDeath = false;

Client(){}
};

bool CreateDeviceD3D(HWND hWnd);
void CleanupDeviceD3D();
void CreateRenderTarget();
void CleanupRenderTarget();

void PrintLog();

int WindowInit(HINSTANCE& _hInstance);
int ServerInit();

void RecvData(SOCKET _Socket, int Num);
void AcceptClient(SOCKET& _Socket);

 

void AddRecvChat(const std::string& _Chat);
void EraseRecvChat(const std::vector<std::string>::iterator& _ChatIter);


std::vector<std::pair<Client, std::string>> Clients;

CPP파일

더보기

// WindowsProject1.cpp : 애플리케이션에 대한 진입점을 정의합니다.

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "ws2_32.lib")

#include "framework.h"
#include "resource.h"

#include "imgui.h"
#include "imgui_impl_win32.h"
#include "imgui_impl_dx11.h"

#include "ServerHeader.h"

#include <d3d11.h>
#include <tchar.h>

#include <filesystem>
#include <vector>
#include <string>
#include <string_view>
#include <iostream>
#include <thread>
#include <mutex>

#include <WinSock2.h>

#pragma comment(lib, "d3d11.lib")

#define MAX_LOADSTRING 100
#define PACKET_SIZE 1024
#define PORT 9090

static ID3D11Device* g_pd3dDevice = nullptr;
static ID3D11DeviceContext* g_pd3dDeviceContext = nullptr;
static IDXGISwapChain* g_pSwapChain = nullptr;
static UINT                     g_ResizeWidth = 0, g_ResizeHeight = 0;
static ID3D11RenderTargetView* g_mainRenderTargetView = nullptr;

// 전역 변수:
HINSTANCE hInst;                                
WCHAR szTitle[MAX_LOADSTRING];                 
WCHAR szWindowClass[MAX_LOADSTRING];            

WNDCLASSEXW wc;
HWND hwnd;
ImVec4 clear_color;

LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);

SOCKET Server;

std::vector<std::string> RecvChats;
std::mutex ChatsMutex;
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    if (WindowInit(hInstance) == 1)
    {
        return 1;
    }

    if (ServerInit() == 1)
    {
        return 1;
    }

    char Name[PACKET_SIZE] = { 0, };
    char Message[PACKET_SIZE] = { 0, };

    // Main loop
    bool done = false;
    while (!done)
    {
        // Poll and handle messages (inputs, window resize, etc.)
        // See the WndProc() function below for our to dispatch events to the Win32 backend.
        MSG msg;
        while (::PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE))
        {
            ::TranslateMessage(&msg);
            ::DispatchMessage(&msg);
            if (msg.message == WM_QUIT)
                done = true;
        }
        if (done)
            break;

        // Handle window resize (we don't resize directly in the WM_SIZE handler)
        if (g_ResizeWidth != 0 && g_ResizeHeight != 0)
        {
            CleanupRenderTarget();
            g_pSwapChain->ResizeBuffers(0, g_ResizeWidth, g_ResizeHeight, DXGI_FORMAT_UNKNOWN, 0);
            g_ResizeWidth = g_ResizeHeight = 0;
            CreateRenderTarget();
        }

        // Start the Dear ImGui frame
        ImGui_ImplDX11_NewFrame();
        ImGui_ImplWin32_NewFrame();

        ImGui::NewFrame();

        PrintLog();

        // Rendering
        ImGui::Render();
        const float clear_color_with_alpha[4] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w };
        g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, nullptr);
        g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color_with_alpha);
        ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());

        g_pSwapChain->Present(1, 0);
    }

    ImGui_ImplDX11_Shutdown();
    ImGui_ImplWin32_Shutdown();
    ImGui::DestroyContext();

    CleanupDeviceD3D();
    ::DestroyWindow(hwnd);
    ::UnregisterClassW(wc.lpszClassName, wc.hInstance);

    WSACleanup();

    return 0;
}

bool CreateDeviceD3D(HWND hWnd)
{
    // Setup swap chain
    DXGI_SWAP_CHAIN_DESC sd;
    ZeroMemory(&sd, sizeof(sd));
    sd.BufferCount = 2;
    sd.BufferDesc.Width = 0;
    sd.BufferDesc.Height = 0;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = hWnd;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.Windowed = TRUE;
    sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;

    UINT createDeviceFlags = 0;
    //createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
    D3D_FEATURE_LEVEL featureLevel;
    const D3D_FEATURE_LEVEL featureLevelArray[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0, };
    HRESULT res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
    if (res == DXGI_ERROR_UNSUPPORTED) // Try high-performance WARP software driver if hardware is not available.
        res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
    if (res != S_OK)
        return false;

    CreateRenderTarget();
    return true;
}

void CleanupDeviceD3D()
{
    CleanupRenderTarget();
    if (g_pSwapChain) { g_pSwapChain->Release(); g_pSwapChain = nullptr; }
    if (g_pd3dDeviceContext) { g_pd3dDeviceContext->Release(); g_pd3dDeviceContext = nullptr; }
    if (g_pd3dDevice) { g_pd3dDevice->Release(); g_pd3dDevice = nullptr; }
}

void CreateRenderTarget()
{
    ID3D11Texture2D* pBackBuffer = nullptr;
    g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
    g_pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &g_mainRenderTargetView);
    pBackBuffer->Release();
}

void CleanupRenderTarget()
{
    if (g_mainRenderTargetView) { g_mainRenderTargetView->Release(); g_mainRenderTargetView = nullptr; }
}

extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam))
        return true;

    switch (msg)
    {
    case WM_SIZE:
        if (wParam == SIZE_MINIMIZED)
            return 0;
        g_ResizeWidth = (UINT)LOWORD(lParam); // Queue resize
        g_ResizeHeight = (UINT)HIWORD(lParam);
        return 0;
    case WM_SYSCOMMAND:
        if ((wParam & 0xfff0) == SC_KEYMENU) // Disable ALT application menu
            return 0;
        break;
    case WM_DESTROY:
        ::PostQuitMessage(0);
        return 0;
    }
    return ::DefWindowProcW(hWnd, msg, wParam, lParam);
}

int WindowInit(HINSTANCE& _hInstance)
{
    // 전역 문자열을 초기화합니다.
    LoadStringW(_hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(_hInstance, IDC_IMGUISERVER, szWindowClass, MAX_LOADSTRING);

    //IMGUI
    wc = { sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, GetModuleHandle(nullptr), nullptr, nullptr, nullptr, nullptr, L"ImGui Example", nullptr };
    ::RegisterClassExW(&wc);

    hwnd = ::CreateWindowW(wc.lpszClassName, L"Dear ImGui DirectX11 Example", WS_OVERLAPPEDWINDOW, 100, 100, 300, 400, nullptr, nullptr, wc.hInstance, nullptr);

    // Initialize Direct3D
    if (!CreateDeviceD3D(hwnd))
    {
        CleanupDeviceD3D();
        ::UnregisterClassW(wc.lpszClassName, wc.hInstance);
        return 1;
    }

    // Show the window
    ::ShowWindow(hwnd, SW_SHOWDEFAULT);
    ::UpdateWindow(hwnd);

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO(); (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;

    std::filesystem::path MyPath = std::filesystem::current_path();
    MyPath += "\\malgun.ttf";

    io.Fonts->AddFontFromFileTTF(MyPath.string().c_str(), 17.0f, NULL, io.Fonts->GetGlyphRangesKorean());

    ImGui::StyleColorsDark();

    ImGui_ImplWin32_Init(hwnd);
    ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);

    bool show_demo_window = true;
    bool show_another_window = false;
    clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);


    return 0;
}

int ServerInit()
{
    //서버
    WSADATA Wsa;

    int WsaStartResult = WSAStartup(MAKEWORD(2, 2), &Wsa);

    if (WsaStartResult != 0)
    {
        std::cerr << "WSAStartup failed with error code: " << WsaStartResult << std::endl;
        return 1;
    }

    Server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    SOCKADDR_IN Addr = { 0, };

    Addr.sin_addr.s_addr = htonl(INADDR_ANY);
    Addr.sin_port = PORT;
    Addr.sin_family = AF_INET;

    int BindResult = bind(Server, reinterpret_cast<SOCKADDR*>(&Addr), sizeof(Addr));

    if (BindResult != 0)
    {
        std::cerr << "Bind failed. error code : " << BindResult << std::endl;
        return 1;
    }

    int ListenResult = listen(Server, SOMAXCONN);

    if (ListenResult != 0) 
    {
        std::cerr << "Listen failed. Error code : " << ListenResult << std::endl;
        return 1;
    }

    std::thread(AcceptClient, std::ref(Server)).detach();

    return 0;
}

void AcceptClient(SOCKET& _Socket)
{
    int Order = 0;

    while (true)
    {
        Clients.push_back(std::pair<Client, std::string>{Client(), ""});

        Clients[Order].first.ClientSock = accept(_Socket, reinterpret_cast<SOCKADDR*>(&Clients[Order].first.Addr), &Clients[Order].first.ClientSize);
        Clients[Order].first.Number = Order;
        Clients[Order].first.bIsDeath = false;

        std::thread(RecvData, Clients[Order].first.ClientSock, Order).detach();

        Order++;
    }
}

void SendMsgToAllCLient(int _IgnoreIndex, std::string_view _Msg)
{
    for (int i = 0; i < Clients.size(); i++)
    {
        if (i == _IgnoreIndex || Clients[i].first.bIsDeath == true)
        {
            continue;
        }

        send(Clients[i].first.ClientSock, _Msg.data(), sizeof(char) * PACKET_SIZE, 0);
    }
}

void RecvData(SOCKET _Socket, int Num)
{
    char Buffer[PACKET_SIZE] = { 0, };
    recv(_Socket, Buffer, sizeof(Buffer), 0);
    Clients[Num].second = Buffer;

    AddRecvChat(Clients[Num].second + " join.");
    
    std::string JoinMsg = Clients[Num].second;
    JoinMsg += " join.";

    SendMsgToAllCLient(Num, JoinMsg);

    while (true)
    {
        ZeroMemory(Buffer, sizeof(Buffer));

        int RecvReturn = recv(_Socket, Buffer, sizeof(Buffer), 0);

        if (RecvReturn == 0)
        {
            Clients[Num].first.bIsDeath = true;
            AddRecvChat(Clients[Num].second + " leave. \n");

            std::string LeaveMsg = Clients[Num].second;
            LeaveMsg += " Leave.";

            SendMsgToAllCLient(Num, LeaveMsg);

            shutdown(Clients[Num].first.ClientSock, SD_BOTH);
            closesocket(Clients[Num].first.ClientSock);

            break;
        }

        AddRecvChat(Clients[Num].second + " : " + Buffer + "\n");

        std::string SendMsg = Clients[Num].second;
        SendMsg += " : ";
        SendMsg += Buffer;

        SendMsgToAllCLient(Num, SendMsg);
    }
}

void PrintLog()
{
    ImGui::Begin("Server");

    ImGui::Text("Log");

    if (RecvChats.size() > 20)
    {
        EraseRecvChat(RecvChats.begin());
    }

    for (int i = 0; i < RecvChats.size(); i++)
    {
        ImGui::Text(RecvChats[i].c_str());
    }

    ImGui::End();
}

 


void AddRecvChat(const std::string& _Chat)
{
    std::lock_guard<std::mutex> lock(ChatsMutex);
    RecvChats.push_back(_Chat);
}

void EraseRecvChat(const std::vector<std::string>::iterator& _ChatIter)
{
    std::lock_guard<std::mutex> lock(ChatsMutex);
    RecvChats.erase(_ChatIter);
}

 

간단한 프로그램 하나 만드는데도 배워야 할 지식이 많고, 고민이 많다는 것은 아직 나아가야 할 길이 한참 멀다는 의미인 것 같다. 서버에 대한 기초 지식이 늘었음에 뿌듯하기도 하지만, 앞으로 더 많은 것을 배워야 한다. 멈추지 말고 끝없이 정진하자.

 

다음 게시글에선 클라이언트 구현을 해보겠다.

+ Recent posts