우선 Unity에서 새로운 2D 프로젝트를 생성해주자. 나는 여기서 unity 버전 2022.3.16f1을 사용했다.

만들어주고 탑뷰에 사용할 간단한 에셋을 Asset Store에서 다운 받아주자. 나는 다음 두 에셋을 사용하였다.

https://assetstore.unity.com/packages/2d/environments/tiny-rpg-town-environment-88293

 

Tiny RPG Town Environment | 2D 주변환경 | Unity Asset Store

Elevate your workflow with the Tiny RPG Town Environment asset from Ansimuz. Find this & more 주변환경 on the Unity Asset Store.

assetstore.unity.com

https://assetstore.unity.com/packages/2d/environments/2d-farm-game-grasslands-4-seasons-tileset-244042

 

2D Farm Game Grasslands 4 Seasons Tileset | 2D 주변환경 | Unity Asset Store

Elevate your workflow with the 2D Farm Game Grasslands 4 Seasons Tileset asset from ElvGames. Find this & more 주변환경 on the Unity Asset Store.

assetstore.unity.com

 

1.이동 및 애니메이션

우선 캐릭터를 하나를 sprite를 가져와서 만들어주자.

 

여기서 Inspector에서 RigidBody 2D Component를 추가해주자. 그리고 우선 조작키를 받을 수 있도록 Unity Registry에서 Input System을 추가해주자

 

이렇게 추가해준다음 새로운 Input Action을 생성해주자. 

그 다음 원하는 기능에 따라 Maps로 대분류를 해주고 키를 받아서 실제 기능을 하게될 Actions를 통해 키를 mapping 해주자.

 

그리고  Save Asset을 해주게 되면 그에 대응되는 C# Script가 생기는데 이를 Player Scrip를 만들어준 다음 여기서 가져

와서 사용하도록 하자.

이때 Actions의 이름에 맞게 함수를 만들고 바인딩 시켜주자. 

Player.cs

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.InputSystem;

public class Player : MonoBehaviour
{
    [SerializeField] private Rigidbody2D rb;
    [SerializeField] private float speed = 5f;
    [SerializeField] private Vector2 movementInput;

    private PlayerInputActions playerInputActions;
    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        playerInputActions = new PlayerInputActions();
    }

    private void FixedUpdate()
    {
        onMove();
    }

    public void Move(InputAction.CallbackContext context)
    {
        // 움직임 입력 받기
        movementInput = context.ReadValue<Vector2>();
    }

    private void onMove()
    {
        // 움직임 처리
        rb.velocity = movementInput * speed;
    }


    private void OnEnable()
    {
        // Move 액션이 수행될 때 호출될 함수 연결
        playerInputActions.PlayerAction.Move.performed += Move;
        playerInputActions.PlayerAction.Move.canceled += Move;

        // 액션 활성화
        playerInputActions.Enable();
    }

    private void OnDisable()
    {
        // 액션 비활성화
        playerInputActions.Disable();
    }
}

 

이렇게 해준 방향에 맞는 Sprite를 출력해주도록 Animator와 Animation Controller를 만들어주자. 

받아온 에셋에 보면 up down left right에 맞는 Sprite들이 나눠져 있는 경우가 있다. 이렇게 나눠져있으면 그에 해당하는 Sprite를 바탕으로 Animator를 만들어주자. 그리고 이러한 Animator 흐름을 설정해줄 수 있는 Animation Controller를 생성해주자.

Animation Controller는 기본은 Idle 상태로 두고 이를 움직이는 Parameters는 Speed, MoveX MoveY로 해서 어느방향으로 얼마나 움직이는지에 따라 다른 애니메이션이 실행되도록할 것이다. 그리고 up down left right의 애니메이션은 Blend Tree를 통해 흐름이 제어되도록 할 것인데 기존 애니메이션의 흐름은 저 화살표인 Transition을 통해 이루어지는데 4방향간의 모든 흐름을 제어하려면 화살표가 너무 복잡해진다. 그렇기 때문에 값에 따라 애니메이션을 분배해주는 Blend Tree가 적합하다.

 

BlendTree을 생성하고 Blend Type에서 2D Simple Directional로 설정하는 것으로 2개  x,y 값을 통해 Blend가 이루어지도록 설정하자. 그리고 4방향의 motion을 추가해주고 해당하는 값도 추가해주자.

 

이렇게 해주고 캐릭터의 움직임에 따라 Parameters값이 Update 되도록 Player 코드를 수정해주면 된다.

Player.cs

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.InputSystem;

public class Player : MonoBehaviour
{
    [SerializeField] private Rigidbody2D rb;
    [SerializeField] private float speed = 5f;
    [SerializeField] private Vector2 movementInput;

    private PlayerInputActions playerInputActions;
    private Animator animator;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        playerInputActions = new PlayerInputActions();
        animator = GetComponent<Animator>();
    }

    private void Update()
    {
        // Animator 파라미터 업데이트
        animator.SetFloat("MoveX", movementInput.x);
        animator.SetFloat("MoveY", movementInput.y);
        animator.SetFloat("speed", movementInput.sqrMagnitude); // 이동 중인지 여부 판단
    }

    private void FixedUpdate()
    {
        onMove();
    }

    public void Move(InputAction.CallbackContext context)
    {
        // 움직임 입력 받기
        movementInput = context.ReadValue<Vector2>();
    }

    private void onMove()
    {
        // 움직임 처리
        rb.velocity = movementInput * speed;
    }

    // 나무와 충돌 감지
    void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Tree")
        {
            // 충돌하면 움직임을 멈추거나 처리할 로직
            Debug.Log("나무에 부딪혔습니다.");
        }
    }


    private void OnEnable()
    {
        // Move 액션이 수행될 때 호출될 함수 연결
        playerInputActions.PlayerAction.Move.performed += Move;
        playerInputActions.PlayerAction.Move.canceled += Move;

        // 액션 활성화
        playerInputActions.Enable();
    }

    private void OnDisable()
    {
        // 액션 비활성화
        playerInputActions.Disable();
    }
}

 

이렇게 해주면 Player가 잘 움직이는 것을 볼 수 있다.

 

2.Tile Map

게임 배경에 해당하는 Tile Map은 2D Object / TileMap / Rectangular을 선택해주면 된다. 그리고 에셋에 보면 배경에 넣을 수 있도록 미리 Tile Pallete가 설정되어 있는 경우가 있다. 이런 경우에는 그냥 사용을 해주면 된다. 사용법은 일단 

Window / 2D / Tile Palette를 선택해서 팔레트를 켜주고 원하는 Map을 선택한다음 맵에 그려주고 싶은 부분을 선택해주고 붓모양을 클릭한다음 그려주면 된다.

 

만약 나무와 같이 다른 오브젝를 만들때는 겹치게 새로운 TileMap을 만들고 그 위에 그려주면 된다.

https://www.acmicpc.net/problem/28279

 

문제의 요구사항대로 deque를 사용한 다음 switch문을 통해 입력에 따라 동작을 수행해주면 된다. 이때 시간초과가 뜰 수 도 있는데 이때 입출력에서 최적화해주기 위해 다음 코드를 입출력 앞에서 선언해주자.

  ios::sync_with_stdio(false);  // C++ 표준 입출력과 C 표준 입출력의 동기화를 비활성화
  cin.tie(NULL);  // cin과 cout의 묶음을 해제하여 성능 향상

 

정답코드

#include <iostream>
#include <deque>

using namespace std;

int main() {
    ios::sync_with_stdio(false);  // C++ 표준 입출력과 C 표준 입출력의 동기화를 비활성화
    cin.tie(NULL);  // cin과 cout의 묶음을 해제하여 성능 향상

    deque<int> dq;
    int n, x;

    cin >> n;

    for (int i = 0; i < n; i++) {
        int order;
        cin >> order;

        switch (order) {
        case 1:
            cin >> x;
            dq.push_front(x);
            break;
        case 2:
            cin >> x;
            dq.push_back(x);
            break;
        case 3:
            if (!dq.empty()) {
                cout << dq.front() << '\n';  // '\n' 사용
                dq.pop_front();
            }
            else {
                cout << "-1\n";
            }
            break;
        case 4:
            if (!dq.empty()) {
                cout << dq.back() << '\n';  // '\n' 사용
                dq.pop_back();
            }
            else {
                cout << "-1\n";
            }
            break;
        case 5:
            cout << dq.size() << '\n';
            break;
        case 6:
            cout << (dq.empty() ? 1 : 0) << '\n';
            break;
        case 7:
            if (!dq.empty()) {
                cout << dq.front() << '\n';
            }
            else {
                cout << "-1\n";
            }
            break;
        case 8:
            if (!dq.empty()) {
                cout << dq.back() << '\n';
            }
            else {
                cout << "-1\n";
            }
            break;
        }
    }

    return 0;
}

https://www.acmicpc.net/problem/11866

 

queue 자료형와 1부터 시작하는 cnt라는 횟수를 세는 변수를 통해

만약 cnt가 k보다 작다면 queue의 front에서 제일 뒤로 push 해준다음 pop하면서 cnt를 1늘려준다.

만약 cnt가 k가 된다면 그 값을 vector에 삽입하고 cnt를 1로 초기화해준다. 

이과정을 vector의 크기가 n이 될 때까지 반복해주고 그 vector를 출력형식에 맞게 출력해주면 된다.

 

 

정답코드(queue사용)

#include <iostream>
#include <queue>
#include <vector>

using namespace std;

vector<int> v;
queue<int> q;
int main()
{
	int n, k, cnt = 1;

	cin >> n >> k;

	for (int i = 1; i <= n; i++)		//큐 초기화
	{
		q.push(i);
	}


	while (v.size() != n)
	{
		if (cnt < k)
		{
			cnt++;
			q.push(q.front());
			q.pop();
		}
		else
		{
			v.push_back(q.front());
			q.pop();
			cnt = 1;
		}
	}

	cout << "<";
	for (int i = 0; i < n; i++)
	{
		if (i < n - 1)
		{
			cout << v[i] << ", ";
		}
		else
		{
			cout << v[i] << ">" << endl;
		}
	}


	return 0;
}

 

 

네트워크 수업을 듣다가 과제로 나온 서버 - 클라이언트 방식의 간단한 게임을 만들어보자 

과제의 요구사항을 먼저 살펴보자 

 

클라이언트 요구사항

1. 클라이언트는 매번 아래 3가지 행동 중 1개를 선택할 수 있다.
서버 연결 요청(최초 1번만 가능)
내 캐릭터 이동 명령
다른 캐릭터 공격 명령
2. 클라이언트는 플레이어의 입력(: 이동, 공격)을 받아 이를 적절한 메시지 유형으로 캡슐화하여 서버로 전송한다. 예를 들어, 플레이어가 캐릭터를 움직인다면 위치 업데이트 메시지를 생성하여, 캐릭터의 새로운 좌표와 함께 서버로 전송한다.
 

3. 클라이언트는 서버로부터 주기적으로 게임 상태 업데이트 메시지를 수신한다. 해당 메시지에는 내 캐릭터를 포함한 연결된 모든 사용자들의 캐릭터 상태 정보가 담겨있으며, 메시지를 수신할 때마다 해당 정보들을 화면에 출력한다

 

 

서버 요구사항

1. 서버 애플리케이션은 게임 참여 요청을 한 모든 클라이언트 애플리케이션(사용자)의 캐릭터 상태 정보를 생성하고, 이벤트가 발생하면 정보를 갱신한다.
2. 서버 애플리케이션은 클라이언트 애플리케이션(사용자)으로부터 명령을 전달받고 캐릭터 상태 정보를 갱신한다.
3. 서버 애플리케이션은 20ms 주기로 연결된 모든 클라이언트 애플리케이션(사용자)에게 모든 사용자들의 캐릭터 상태 정보를 전달한다.
 

사용자 캐릭터의 상태 정보는 아래 4가지 필드로 구성된다.

캐릭터 이름(영문 10자 이내)

캐릭터 위치 좌표(x, y)

캐릭터 체력(초기 값 500으로 고정)

캐릭터 공격력(초기 값 10으로 고정)

 

애플리케이션 계층 프로토콜 개발 요구사항

헤더 정보

Message type + Data length + Data

 

메시지 타입은 게임에서 사용하는 다양한 메시지 종류를 나타낸다. 주요 메시지 유형은 다음과 같다.

0x00: 연결 요청 – Data 필드에 내 캐릭터의 이름이 저장되어 있음을 나타냄

0x01: 위치 업데이트 – Data 필드에 내 캐릭터의 현재 좌표가 저장되어 있음을 나타냄

0x02: 행동 명령 – Data 필드에 내 캐릭터의 공격 명령이 저장되어 있음을 나타냄

0x03: 상태 업데이트 – Data 필드에 모든 플레이어들의 상태 정보가 저장되어 있음을 나타냄

 

통신은 소캣을 사용할 것이다.

 

일단 설계를 해보자면

서버에서는 각 캐릭터 정보를 동기화 하기 위하여 캐릭터를 구조체로 선언해주면 될 것 같다. 물론 클래스로 선언해도 되겠지만 생성자가 굳이 필요하진 않기때문에 구조체를 사용해주자. 그리고 각 플레이어의 핑을 수집한 다음 가장 긴 핑으로 

정보 동기화를 실행해주자.

클라이언트에서는 쓰레드를 활용하여 서버에서 보내주는 메세지를 처리해주고 입력도 받을 수 있도록 했다. 

서버에서도 쓰레드를 활용하여 여러 클라이언트의 메세지를 동시에 처리할 수 있도록 했다.

Server.cpp

#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <thread>  // 스레드 사용
#include <chrono>  // 핑 계산을 위한 타이머
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>

// 캐릭터 구조체 정의
struct Character {
    std::string name;
    int x = 0, y = 0;
    int health = 500;
    int attackPower = 10;
};

// 메시지 타입 정의
enum MessageType {
    CONNECTION_REQUEST = 0x00,
    POSITION_UPDATE = 0x01,
    ACTION_COMMAND = 0x02,
    STATUS_UPDATE = 0x03,
    PING_REQUEST = 0x04,
    PING_RESPONSE = 0x05
};

// 메시지 구조체 정의
struct Message {
    MessageType type;
    uint32_t data_length;
    char data[256];
};

// sockaddr_in 구조체를 비교하기 위한 함수 (IP와 포트 번호를 함께 비교)
bool compareSockaddr(const sockaddr_in &a, const sockaddr_in &b) {
    return a.sin_addr.s_addr == b.sin_addr.s_addr && a.sin_port == b.sin_port;
}

// 서버 클래스 정의
class GameServer {
public:
    std::map<int, Character> players;
    std::vector<sockaddr_in> client_addresses;
    std::map<int, int> client_ping_times;  // 클라이언트 별 핑 값을 저장
    int client_counter = 0;

    // 클라이언트 ID 할당
    int getClientID(sockaddr_in client_addr) {
        for (size_t i = 0; i < client_addresses.size(); ++i) {
            if (compareSockaddr(client_addresses[i], client_addr)) {
                return i;
            }
        }
        client_addresses.push_back(client_addr);
        return client_counter++;
    }

    // 핑 요청 및 응답 처리
    void handlePing(SOCKET sockfd, sockaddr_in client_addr, int addr_len, int clientID) {
        Message pingRequest;
        pingRequest.type = PING_REQUEST;
        pingRequest.data_length = 0;

        // PING_REQUEST 전송
        sendto(sockfd, (char*)&pingRequest, sizeof(pingRequest), 0, (struct sockaddr*)&client_addr, addr_len);

        // PING 응답 처리
        Message pingResponse;
        auto start = std::chrono::high_resolution_clock::now();
        recvfrom(sockfd, (char*)&pingResponse, sizeof(pingResponse), 0, (struct sockaddr*)&client_addr, &addr_len);
        if (pingResponse.type == PING_RESPONSE) {
            auto end = std::chrono::high_resolution_clock::now();
            int pingTime = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
            client_ping_times[clientID] = pingTime;
            std::cout << "Ping time for client " << clientID << ": " << pingTime << " ms" << std::endl;
        }
    }

    // 메시지 처리 함수 (스레드 내에서 실행)
    void handleClient(SOCKET sockfd, sockaddr_in client_addr, Message message, int addr_len) {
        int clientID = getClientID(client_addr);
        processMessage(clientID, message);

        // 핑 요청 및 처리
        handlePing(sockfd, client_addr, addr_len, clientID);

        // 가장 큰 핑을 기준으로 상태 브로드캐스트
        int max_ping = getMaxPingTime();
        Sleep(max_ping);  // 가장 높은 핑만큼 대기
        broadcastState(sockfd, addr_len);
    }

    // 메시지 처리
    void processMessage(int clientID, Message message) {
        switch (message.type) {
            case CONNECTION_REQUEST:
                players[clientID].name = std::string(message.data);
                std::cout << "Player connected: " << players[clientID].name << std::endl;
                break;
            case POSITION_UPDATE:
                int x, y;
                sscanf(message.data, "%d %d", &x, &y);
                players[clientID].x = x;
                players[clientID].y = y;
                std::cout << "Player " << clientID << " moved to (" << x << ", " << y << ")\n";
                break;
            case ACTION_COMMAND:
                int targetID;
                sscanf(message.data, "%d", &targetID);
                if (players.find(targetID) != players.end()) {
                    players[targetID].health -= players[clientID].attackPower;
                    std::cout << "Player " << clientID << " attacked Player " << targetID << std::endl;
                }
                break;
            default:
                break;
        }
    }

    // 모든 클라이언트에 상태 전송
    void broadcastState(SOCKET sockfd, int addr_len) {
        Message message;
        message.type = STATUS_UPDATE;
        std::string status;
        for (const auto &player : players) {
            status += player.second.name + ": (" + std::to_string(player.second.x) + ", " +
                      std::to_string(player.second.y) + ") HP: " + std::to_string(player.second.health) + "\n";
        }
        strncpy(message.data, status.c_str(), sizeof(message.data) - 1);
        message.data[sizeof(message.data) - 1] = '\0';

        // 각 클라이언트에 상태 정보 전송
        for (const auto &client_addr : client_addresses) {
            sendto(sockfd, (char*)&message, sizeof(message), 0, (struct sockaddr *)&client_addr, addr_len);
        }
    }

    // 가장 높은 핑 값을 반환
    int getMaxPingTime() {
        int max_ping = 0;
        for (const auto &entry : client_ping_times) {
            if (entry.second > max_ping) {
                max_ping = entry.second;
            }
        }
        return max_ping;
    }
};

// 서버 메인 코드
int main() {
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        std::cerr << "WSAStartup failed" << std::endl;
        return 1;
    }

    SOCKET sockfd;
    sockaddr_in server_addr, client_addr;
    int addr_len = sizeof(client_addr);

    // 소켓 생성
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == INVALID_SOCKET) {
        std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;
        WSACleanup();
        return 1;
    }

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);

    // 바인딩
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
        std::cerr << "Bind failed: " << WSAGetLastError() << std::endl;
        closesocket(sockfd);
        WSACleanup();
        return 1;
    }

    GameServer gameServer;

    // 메인 루프
    while (true) {
        Message message;
        recvfrom(sockfd, (char*)&message, sizeof(message), 0, (struct sockaddr *)&client_addr, &addr_len);

        // 각 클라이언트 메시지를 별도의 스레드에서 처리
        std::thread clientThread(&GameServer::handleClient, &gameServer, sockfd, client_addr, message, addr_len);
        clientThread.detach();  // 스레드가 종료되면 자동으로 자원 반환
    }

    closesocket(sockfd);
    WSACleanup();
    return 0;
}

 

Client.cpp

#include <iostream>
#include <string>
#include <thread>  // 비동기 처리를 위한 스레드 사용
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>  // Windows API 타이머 및 Sleep 함수 사용

// 메시지 타입 정의
enum MessageType {
    CONNECTION_REQUEST = 0x00,
    POSITION_UPDATE = 0x01,
    ACTION_COMMAND = 0x02,
    STATUS_UPDATE = 0x03
};

// 메시지 구조체 정의
struct Message {
    MessageType type;
    uint32_t data_length;
    char data[256];
};

// 클라이언트 함수
void sendMessage(SOCKET sockfd, sockaddr_in server_addr, Message message) {
    int result = sendto(sockfd, (char*)&message, sizeof(message), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (result == SOCKET_ERROR) {
        std::cerr << "Error sending message: " << WSAGetLastError() << std::endl;
    }
}

// 서버에서 메시지 수신 (비동기)
void receiveMessage(SOCKET sockfd) {
    Message message;
    sockaddr_in server_addr;
    int addr_len = sizeof(server_addr);
    
    while (true) {
        int result = recvfrom(sockfd, (char*)&message, sizeof(message), 0, (struct sockaddr*)&server_addr, &addr_len);
        if (result == SOCKET_ERROR) {
            std::cerr << "Error receiving message: " << WSAGetLastError() << std::endl;
        } else {
            if (message.type == STATUS_UPDATE) {
                std::cout << "\nGame State:\n" << message.data << std::endl;
            }
        }
    }
}

int main() {
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);  // Winsock 초기화
    if (result != 0) {
        std::cerr << "WSAStartup failed: " << result << std::endl;
        return 1;
    }

    SOCKET sockfd;
    sockaddr_in server_addr;

    // 소켓 생성
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == INVALID_SOCKET) {
        std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;
        WSACleanup();
        return 1;
    }

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(8080);

    // 캐릭터 이름 설정 및 서버 연결 요청
    std::string playerName;
    std::cout << "Enter your character name: ";
    std::cin >> playerName;

    Message connectMessage;
    connectMessage.type = CONNECTION_REQUEST;
    
    // strncpy로 버퍼에 복사 및 널 문자 처리
    strncpy(connectMessage.data, playerName.c_str(), sizeof(connectMessage.data) - 1);
    connectMessage.data[sizeof(connectMessage.data) - 1] = '\0';  // 널 문자 추가
    connectMessage.data_length = playerName.size();
    
    sendMessage(sockfd, server_addr, connectMessage);

    // 비동기적으로 서버에서 메시지를 수신하는 스레드 시작
    std::thread receiveThread(receiveMessage, sockfd);
    receiveThread.detach();  // 비동기 수신

    // 메인 루프: 사용자 입력 처리
    while (true) {
        std::cout << "1. Move\n2. Attack\nChoose an action: ";
        int choice;
        std::cin >> choice;

        if (choice == 1) {
            // 위치 업데이트
            int x, y;
            std::cout << "Enter new coordinates (x y): ";
            std::cin >> x >> y;
            Message moveMessage;
            moveMessage.type = POSITION_UPDATE;
            snprintf(moveMessage.data, sizeof(moveMessage.data), "%d %d", x, y);
            moveMessage.data_length = strlen(moveMessage.data);
            sendMessage(sockfd, server_addr, moveMessage);
        } else if (choice == 2) {
            // 공격 명령
            int targetID;
            std::cout << "Enter target player ID: ";
            std::cin >> targetID;
            Message attackMessage;
            attackMessage.type = ACTION_COMMAND;
            snprintf(attackMessage.data, sizeof(attackMessage.data), "%d", targetID);
            attackMessage.data_length = strlen(attackMessage.data);
            sendMessage(sockfd, server_addr, attackMessage);
        }
    }

    closesocket(sockfd);  // 소켓 닫기
    WSACleanup();  // Winsock 종료
    return 0;
}

 

실행은 VS code의 Terminal을 통해 해주자 일단 컴파일해주기 위하여 MinGW를 설치해주자 다운로드는 밑의 페이지에서 제일 최선버전 zip을 받아준다음 원하는 위치에 압축해제해주고 환경변수 설정만 해주면된다.

https://winlibs.com/#download-release

 

WinLibs - GCC+MinGW-w64 compiler for Windows

WinLibs standalone build of GCC and MinGW-w64 for Windows Jump to:   Download | How to use from Windows Command Prompt | How to use from Code::Blocks | Philosophy | Donate What is it? In short: it's a free C and C++ compiler for Microsoft Windows. GCC (GN

winlibs.com

 

환경 변수는 Path에서 밑에 그림같이 추가해주면 된다.

 

 

cmd창에서 ggc --version했을때 다음과 같은 화면이 나오면 올바르게 설치된 것이다.

 

이렇게 해준다음 컴파일 해주고 exe파일이 문제없이 생긴다면 

g++ server.cpp -o server.exe -lws2_32 -Wall

g++ client.cpp -o client.exe -lws2_32 -Wall

 

다음과 같이 실행해주고 테스트해주면 된다.

./server.exe      

 ./client.exe

 

 

실행화면

 

이렇게 해주면 간단한 온라인게임이 완성된다.

 

강의

https://inf.run/PCcgN

 

학습 페이지

 

www.inflearn.com

 

 

 

뷰포트변환

물체가 월드 ~ 투영 변환을 거친 다음에 뷰포트 변환을 해주는 것으로 실제 화면에 표시되는 영역을 정의한다.

 

우리가 만약 3D게임에서 물체를 클릭한다고 했을 때 물체가 선택되는 것은 3D세계가 뷰포트인 2D로 변환되어 보여지는데 이 2D화면에서 우리가 선택한 물체인 3D가 선택되는 것으로 그냥 이루어지는 것이 아니라는 것을 알 수 있다. 

클릭할 때 생각해보면 우리가 누른 좌표를 3D 좌표로 바꿀 수 있어야 한다. 만약 800 600의 정중앙인 400 300의 좌표를 3D 좌로 바꿔보면 깊이값을 더 해줘야하는데 이 깊이값을 어떤 값으로 정해줄 지가 문제이다.

UI도 3D세상에 2D인 UI를 넣는 것이다. 구현하는 방법에는 3D물체를 직교투영 카메라로 찍는 방법과 후처리를 해서 스크린 영역에 붙여주는 방법등 여러 방법이 있다.

 

결국에는 2D에서 3D로 넘어가는 것이 필수적이다 라는 것이다.

우리가 만든 DX코드에서는 카메라에 넣는것도 방법이 될수 있겠지만 지금은 최종적인 상태인 뷰포트라는 것을 관리해서 기능을 추가해주자. 3D에서 2D 변환을 하기 위해서 프로젝션을 하기 위해 중요한 역활을 하는 것이 뷰포트이기 때문이다.

 

이를 위해서 ViewPort라는 별도의 클래스를 만들어주자. 이때 클래스 변수 중에 D3D11_VIEWPORT라는 구조체에 이미 필요한 정보가 다 있기 때문에 이를 활용해주자. 프로젝션 공식은 D3D11Math 라이브러리를 활용해주자.

현재 위치가 주어졌을 때, world, View, Projection 행렬을 적절히 활용하여 2D -> 3D , World까지 가는 행렬또는 View로 넘어가는 것을 만들어주면 된다. 연산은 밑의 M 공식대로 변수값을 적용시켜주면 된다.

 

Viewport.h

#pragma once

class Viewport
{
public:
	Viewport();
	Viewport(float width, float height, float x = 0, float y = 0, float minDepth = 0, float maxDepth = 1);
	~Viewport();

	void RSSetViewport();
	void Set(float width, float height, float x = 0, float y = 0, float minDepth = 0, float maxDepth = 1);

	float GetWidth() { return _vp.Width; }
	float GetHeight() { return _vp.Height; }

	//2D-> 3D
	Vec3 Project(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P);
	//3D-> 2D
	Vec3 Unproject(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P);

private:
	D3D11_VIEWPORT _vp;
};

 

Viewport.cpp

#include "pch.h"
#include "Viewport.h"

Viewport::Viewport()
{
	Set(800, 600);
}

Viewport::Viewport(float width, float height, float x, float y, float minDepth, float maxDepth)
{
	Set(width, height, x, y, minDepth, maxDepth);
}

Viewport::~Viewport()
{

}

void Viewport::RSSetViewport()
{
	DC->RSSetViewports(1, &_vp);
}

void Viewport::Set(float width, float height, float x, float y, float minDepth, float maxDepth)
{
	_vp.TopLeftX = x;
	_vp.TopLeftY = y;
	_vp.Width = width;
	_vp.Height = height;
	_vp.MinDepth = minDepth;
	_vp.MaxDepth = maxDepth;
}

//3D -> 2D
Vec3 Viewport::Project(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P)
{
	Matrix wvp = W * V * P;
	Vec3 p = Vec3::Transform(pos, wvp);

	//행렬연산공식
	p.x = (p.x + 1.0f) * (_vp.Width / 2) + _vp.TopLeftX;
	p.y = (-p.y + 1.0f) * (_vp.Height / 2) + _vp.TopLeftY;
	p.z = p.z * (_vp.MaxDepth - _vp.MinDepth) + _vp.MinDepth;

	return p;
}

//2D->3D  깊이값은 있음
Vec3 Viewport::Unproject(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P)
{
	Vec3 p = pos;

	p.x = 2.f * (p.x - _vp.TopLeftX) / _vp.Width - 1.f;
	p.y = -2.f * (p.y - _vp.TopLeftY) / _vp.Height + 1.f;
	p.z = ((p.z - _vp.MinDepth) / (_vp.MaxDepth - _vp.MinDepth));

	Matrix wvp = W * V * P;
	Matrix wvpInv = wvp.Invert();

	p = Vec3::Transform(p, wvpInv);
	return p;
}

 

이렇게 만들어주고 Graphics에서 ViewPort에 해당하는 부분을 수정해주자.

Graphics.h

#pragma once
#include "Viewport.h"

//그려지는 것에 필요한 것들
class Graphics
{
	DECLARE_SINGLE(Graphics);

public:
	void SetViewport(float width, float height, float x = 0, float y = 0, float minDepth = 0, float maxDepth = 1);
	Viewport& GetViewport() { return _vp; }

private:
	// Misc
	//D3D11_VIEWPORT _viewport = { 0 };
	Viewport _vp;
};

Graphics.cpp

#include "pch.h"
#include "Graphics.h"

void Graphics::Init(HWND hwnd)
{
	_hwnd = hwnd;

	CreateDeviceAndSwapChain();
	CreateRenderTargetView();
	CreateDepthStencilView();
	SetViewport(GAME->GetGameDesc().width, GAME->GetGameDesc().height);
}

void Graphics::RenderBegin()
{
	_deviceContext->OMSetRenderTargets(1, _renderTargetView.GetAddressOf(), _depthStencilView.Get());
	_deviceContext->ClearRenderTargetView(_renderTargetView.Get(), (float*)(&GAME->GetGameDesc().clearColor));
	_deviceContext->ClearDepthStencilView(_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1, 0);
	_vp.RSSetViewport();
}

void Graphics::SetViewport(float width, float height, float x /*= 0*/, float y /*= 0*/, float minDepth /*= 0*/, float maxDepth /*= 1*/)
{
	_vp.Set(width, height, x, y, minDepth, maxDepth);
}

이걸 테스트할 수 있도록 메인코드를 만들어주자.

ViewportDemo.cpp

#include "pch.h"
#include "ViewportDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "SceneDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"

void ViewportDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		CUR_SCENE->Add(camera);
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}

	// Mesh
	for (int32 i = 0; i < 10; i++)
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(rand() % 10, 0, rand() % 10));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Cube");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}

		CUR_SCENE->Add(obj);
	}
}

void ViewportDemo::Update()
{
	static float width = 800.f;
	static float height = 600.f;
	static float x = 0.f;
	static float y = 0.f;

	ImGui::InputFloat("Width", &width, 10.f);
	ImGui::InputFloat("Height", &height, 10.f);
	ImGui::InputFloat("X", &x, 10.f);
	ImGui::InputFloat("Y", &y, 10.f);

	GRAPHICS->SetViewport(width, height, x, y);
}

void ViewportDemo::Render()
{

}

실행시켜보았을 때 만약 뷰표트의 크기를 줄이면 시야각도 줄어드는 효과가 난다. 결국 뷰포트는 -1 ~ 1로 넘어온 뷰를

얼마만큼의 뷰로 렌더링해줄 것인지를 이야기하는 것이다. 

 

이제 뷰포트변화를 보았으니 물체가 프로젝션을 했을 때 어떻게 동작하는지 코드를 통해 알아보자. 이를 위해 물체는 하나로 설정하고 0,0,0에 배치해주자. 

프로젝션은 임의의 점(ImGui사용)을 연산해주자.

 

중요한 것은 2D->3D로 갔다가 다시 3D->2D로 가면 원래좌표가 뜬다는 것이다. 그리고 보는 방향에 따라 중앙 Pos2D값이 달라지는 것을 볼 수 있다. 깊이값은 0~1까지 나타난다.


오늘은 기존에 사용하던 RenderManager 코드를 수정해주자.

지금은 쉐이더가 달라지면 기존에 있던 부분이 날라가는 문제점이 있다. 이 문제를 해결해주기 위하여 코드를 공용으로

모아두고 필요한 부분만 사용하지만 그것을 물체마다 할 수 있게 만들어주면 된다.

그렇게 하기위하여 쉐이더 클래스에 이 부분 코드를 넣어주고 데이터를 밀어넣는 부분에 만약에 버퍼가 없다면 버퍼를 

생성하고초기화하는 부분도 같이 넣어주자.

Shader.h

#pragma once
#include "Pass.h"
#include "Technique.h"
#include "BindShaderDesc.h"

class Shader
{

public:

	void PushGlobalData(const Matrix& view, const Matrix& projection);
	void PushTransformData(const TransformDesc& desc);
	void PushLightData(const LightDesc& desc);
	void PushMaterialData(const MaterialDesc& desc);
	void PushBoneData(const BoneDesc& desc);
	void PushKeyframeData(const KeyframeDesc& desc);
	void PushTweenData(const InstancedTweenDesc& desc);

private:
	//프레임마다 한번만 세팅
	GlobalDesc _globalDesc;
	shared_ptr<ConstantBuffer<GlobalDesc>> _globalBuffer;
	//정보넘겨주기
	ComPtr<ID3DX11EffectConstantBuffer> _globalEffectBuffer;


	TransformDesc _transformDesc;
	shared_ptr<ConstantBuffer<TransformDesc>> _transformBuffer;
	//정보넘겨주기
	ComPtr<ID3DX11EffectConstantBuffer> _transformEffectBuffer;

	LightDesc _lightDesc;
	shared_ptr<ConstantBuffer<LightDesc>> _lightBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _lightEffectBuffer;

	MaterialDesc _materialDesc;
	shared_ptr<ConstantBuffer<MaterialDesc>> _materialBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _materialEffectBuffer;

	BoneDesc _boneDesc;
	shared_ptr<ConstantBuffer<BoneDesc>> _boneBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _boneEffectBuffer;

	KeyframeDesc _keyframeDesc;
	shared_ptr<ConstantBuffer<KeyframeDesc>> _keyframeBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _keyframeEffectBuffer;

	InstancedTweenDesc _tweenDesc;
	shared_ptr<ConstantBuffer<InstancedTweenDesc>> _tweenBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _tweenEffectBuffer;

};

Shader.cpp

void Shader::PushGlobalData(const Matrix& view, const Matrix& projection)
{
	if (_globalEffectBuffer == nullptr)
	{
		_globalBuffer = make_shared<ConstantBuffer<GlobalDesc>>();
		_globalBuffer->Create();
		_globalEffectBuffer = GetConstantBuffer("GlobalBuffer");
	}

	_globalDesc.V = view;
	_globalDesc.P = projection;
	_globalDesc.VP = view * projection;
	_globalDesc.VInv = view.Invert();
	_globalBuffer->CopyData(_globalDesc);
	_globalEffectBuffer->SetConstantBuffer(_globalBuffer->GetComPtr().Get());
}

void Shader::PushTransformData(const TransformDesc& desc)
{
	if (_transformEffectBuffer == nullptr)
	{
		_transformBuffer = make_shared<ConstantBuffer<TransformDesc>>();
		_transformBuffer->Create();
		_transformEffectBuffer = GetConstantBuffer("TransformBuffer");
	}

	_transformDesc = desc;
	_transformBuffer->CopyData(_transformDesc);
	_transformEffectBuffer->SetConstantBuffer(_transformBuffer->GetComPtr().Get());
}

void Shader::PushLightData(const LightDesc& desc)
{
	if (_lightEffectBuffer == nullptr)
	{
		_lightBuffer = make_shared<ConstantBuffer<LightDesc>>();
		_lightBuffer->Create();
		_lightEffectBuffer = GetConstantBuffer("LightBuffer");
	}

	_lightDesc = desc;
	_lightBuffer->CopyData(_lightDesc);
	_lightEffectBuffer->SetConstantBuffer(_lightBuffer->GetComPtr().Get());
}

void Shader::PushMaterialData(const MaterialDesc& desc)
{
	if (_materialEffectBuffer == nullptr)
	{
		_materialBuffer = make_shared<ConstantBuffer<MaterialDesc>>();
		_materialBuffer->Create();
		_materialEffectBuffer = GetConstantBuffer("MaterialBuffer");
	}

	_materialDesc = desc;
	_materialBuffer->CopyData(_materialDesc);
	_materialEffectBuffer->SetConstantBuffer(_materialBuffer->GetComPtr().Get());
}

void Shader::PushBoneData(const BoneDesc& desc)
{
	if (_boneEffectBuffer == nullptr)
	{
		_boneBuffer = make_shared<ConstantBuffer<BoneDesc>>();
		_boneBuffer->Create();
		_boneEffectBuffer = GetConstantBuffer("BoneBuffer");
	}

	_boneDesc = desc;
	_boneBuffer->CopyData(_boneDesc);
	_boneEffectBuffer->SetConstantBuffer(_boneBuffer->GetComPtr().Get());
}

void Shader::PushKeyframeData(const KeyframeDesc& desc)
{
	if (_keyframeEffectBuffer == nullptr)
	{
		_keyframeBuffer = make_shared<ConstantBuffer<KeyframeDesc>>();
		_keyframeBuffer->Create();
		_keyframeEffectBuffer = GetConstantBuffer("KeyframeBuffer");
	}

	_keyframeDesc = desc;
	_keyframeBuffer->CopyData(_keyframeDesc);
	_keyframeEffectBuffer->SetConstantBuffer(_keyframeBuffer->GetComPtr().Get());
}

void Shader::PushTweenData(const InstancedTweenDesc& desc)
{
	if (_transformEffectBuffer == nullptr)
	{
		_tweenBuffer = make_shared<ConstantBuffer<InstancedTweenDesc>>();
		_tweenBuffer->Create();
		_tweenEffectBuffer = GetConstantBuffer("TweenBuffer");
	}

	_tweenDesc = desc;
	_tweenBuffer->CopyData(_tweenDesc);
	_tweenEffectBuffer->SetConstantBuffer(_tweenBuffer->GetComPtr().Get());
}

이렇게 수정해주고 RenderManager는 필요한 구조체만 남기고 없애주자. 그리고 이름을 BindShaderDesc로 바꿔주자. 그리고 RenderManager가 없어진부분을 이제 shader쪽에서 가져오거나 주석처리를 해주도록하자. 

BindShaderDesc.h

#pragma once
#include "ConstantBuffer.h"

class Shader;

struct GlobalDesc
{
	Matrix V = Matrix::Identity;
	Matrix P = Matrix::Identity;
	Matrix VP = Matrix::Identity;
	Matrix VInv = Matrix::Identity;
};

struct TransformDesc
{
	Matrix W = Matrix::Identity;
};

//Light
struct LightDesc
{
	Color ambient = Color(1.f, 1.f, 1.f, 1.f);
	Color diffuse = Color(1.f, 1.f, 1.f, 1.f);
	Color specular = Color(1.f, 1.f, 1.f, 1.f);
	Color emissive = Color(1.f, 1.f, 1.f, 1.f);

	Vec3 direction;
	float padding0;
};

struct MaterialDesc
{
	Color ambient = Color(0.f, 0.f, 0.f, 1.f);
	Color diffuse = Color(1.f, 1.f, 1.f, 1.f);
	Color specular = Color(0.f, 0.f, 0.f, 1.f);
	Color emissive = Color(0.f, 0.f, 0.f, 1.f);
};


//Bone 개수
#define MAX_MODEL_TRANSFORMS 250
#define MAX_MODEL_KEYFRAMES 500
#define MAX_MODEL_INSTANCE 500

struct BoneDesc
{
	Matrix transforms[MAX_MODEL_TRANSFORMS];
};

//Animation
struct KeyframeDesc
{
	int32 animIndex = 0;
	uint32 currFrame = 0;
	uint32 nextFrame = 0;
	float ratio = 0.f;
	float sumTime = 0.f;
	float speed = 1.f;
	Vec2 padding;
};

struct TweenDesc
{
	TweenDesc()
	{
		curr.animIndex = 0;
		next.animIndex = -1;
	}

	void ClearNextAnim()
	{
		next.animIndex = -1;
		next.currFrame = 0;
		next.nextFrame = 0;
		next.sumTime = 0;
		tweenSumTime = 0;
		tweenRatio = 0;
	}

	float tweenDuration = 1.0f;
	float tweenRatio = 0.f;
	float tweenSumTime = 0.f;
	float padding = 0.f;
	KeyframeDesc curr;
	KeyframeDesc next;
};

struct InstancedTweenDesc
{
	TweenDesc tweens[MAX_MODEL_INSTANCE];
};

이렇게 바꿔주고 주석처리해준 Light와 Camera부분은 따로 관리주어야하는데 이 부분은 Scene쪽에서 관리해주도록 

하자. RenderInstancing 부분이 있는 매쉬, 모델, 모델애니메이터에서  빛과 카메라 부분을 업데이트해주도록 하자. 일단 카메라와 빛의 개수 하나라고 가정하자. 이것은 나중에 직교투영하는 UI카메라를 구현할 때는 고쳐주어야 한다. 

Scene.h

#pragma once


class Scene
{
public:
	virtual void Start();
	virtual void Update();
	virtual void LateUpdate();
	
	//추가
	virtual void Add(shared_ptr<GameObject> object);
	//제거
	virtual void Remove(shared_ptr<GameObject> object);

	
	unordered_set<shared_ptr<GameObject>> GetObjects() { return _objects; }
	//일단 하나라고 가정하고 제일 처음꺼 가져오기
	shared_ptr<GameObject> GetCamera() { return _cameras.empty() ? nullptr : *_cameras.begin(); }
	shared_ptr<GameObject> GetLight() { return _lights.empty() ? nullptr : *_lights.begin(); }

private:
	//물체를 가지고있는 추가 삭제 편하지만 순회에는 안좋다 검색활용
	unordered_set<shared_ptr<GameObject>> _objects;
	//카메라
	unordered_set<shared_ptr<GameObject>> _cameras;
	//빛
	unordered_set<shared_ptr<GameObject>> _lights;
};

ModelRenderer.cpp

void ModelRenderer::RenderInstancing(shared_ptr<class InstancingBuffer>& buffer)
{
	if (_model == nullptr)
		return;

	// GlobalData
	_shader->PushGlobalData(Camera::S_MatView, Camera::S_MatProjection);

	// Light
	auto lightObj = SCENE->GetCurrentScene()->GetLight();
	if (lightObj)
		_shader->PushLightData(lightObj->GetLight()->GetLightDesc());

	//Bones -> shader
	BoneDesc boneDesc;

	const uint32 boneCount = _model->GetBoneCount();

	for (uint32 i = 0; i < boneCount; i++)
	{
		shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i);
		boneDesc.transforms[i] = bone->transform;
	}
	_shader->PushBoneData(boneDesc);

	//부품
	const auto& meshes = _model->GetMeshes();
	for (auto& mesh : meshes)
	{
		if (mesh->material)
			mesh->material->Update();

		//BoneIndex
		_shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);

		//IA
		mesh->vertexBuffer->PushData();
		mesh->indexBuffer->PushData();

		//World position 넣어주기
		buffer->PushData();

		_shader->DrawIndexedInstanced(0, _pass, mesh->indexBuffer->GetCount(), buffer->GetCount());

	}
}

고쳐준 다음 컴파일해주고 Client부분에 오류가 나는 부분을 수정해주자.

이렇게해주면 이제 다시 정상적으로 작동하는 것을 볼 수 있다. 

 

이제 이렇게 해주는 것으로 우리가 여러가지 쉐이더를 사용할 수 있게 되었다.

https://www.acmicpc.net/problem/2164

 

문제에서 설명하는대로 제일 앞에 수를 버리고 제일 위에 수를 제일 뒤로 보내는 과정을 queue의 크기가 1일때 까지 반복해주면 된다.

 

정답코드

#include <iostream>
#include <queue>

using namespace std;

int main()
{
	int n;

	cin >> n;

	queue<int> q;

	for (int i = 1; i <= n; i++)
	{
		q.push(i);
	}

	while (q.size() != 1)
	{
		int x;
		q.pop();
		x = q.front();
		q.pop();
		q.push(x);
	}

	cout << q.front() << endl;

	return 0;
}

 

https://www.acmicpc.net/problem/12789

 

우선 모든 입력을 queue를 통해 받아둔 다음에 stack자료형을 통해 만약 순서대로 처리가 가능하다면 순서대로 보내주고 아니라면 queue에 있는 값을 stack에 받고 순서대로 일경우 stack을 pop해주면 된다.

 

 

정답코드

#include <iostream>
#include <stack>
#include <queue>

using namespace std;

int main() {
    int n;
    cin >> n;

    queue<int> line;
    stack<int> side;
    int next = 1;

    // 대기열 입력받기
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        line.push(x);
    }

    // 대기열 처리
    while (!line.empty()) {
        if (line.front() == next) {
            // 대기열에서 바로 나갈 수 있을 때
            line.pop();
            next++;
        }
        else { // 대기열에서 바로 나갈 수 없을 때
            
            if (!side.empty() && side.top() == next) {
                // 스택에서 나갈 수 있는 경우
                side.pop();
                next++;
            }
            else {
                // 스택에 push
                side.push(line.front());
                line.pop();
            }
        }
    }

    // 스택에 남아있는 사람들 처리
    while (!side.empty() && side.top() == next) {
        side.pop();
        next++;
    }

    // 모두 순서대로 나갔는지 확인
    if (side.empty()) {
        cout << "Nice" << endl;
    }
    else {
        cout << "Sad" << endl;
    }

    return 0;
}



이제 Structure 버퍼를 만들어서 사용해보자 

Structure Buffer는 정해진 구조체를 통해 배열을 만들어서 관리하는 버퍼를 통해 더 쉽게 이 작업을 해줄 수 있다. 

구현과 작동 자체는 RawBuffer와 유사하다. 

우선 Buffer 클래스를 만들어주자.

StructuredBuffer.h

#pragma once


class StructuredBuffer
{
public:
	StructuredBuffer(void* inputData, uint32 inputStride, uint32 inputCount, uint32 outputStride = 0, uint32 outputCount = 0);
	~StructuredBuffer();

public:
	void CreateBuffer();

private:
	void CreateInput();
	void CreateSRV();
	void CreateOutput();
	void CreateUAV();
	void CreateResult();

public:
	//데이터개수 * 데이터크기
	uint32 GetInputByteWidth() { return _inputStride * _inputCount; }
	//데이터개수 * 데이터크기
	uint32 GetOutputByteWidth() { return _outputStride * _outputCount; }

	void CopyToInput(void* data);
	void CopyFromOutput(void* data);

public:
	ComPtr<ID3D11ShaderResourceView> GetSRV() { return _srv; }
	ComPtr<ID3D11UnorderedAccessView> GetUAV() { return _uav; }

private:
	ComPtr<ID3D11Buffer> _input;
	ComPtr<ID3D11ShaderResourceView> _srv; // Input
	ComPtr<ID3D11Buffer> _output;
	ComPtr<ID3D11UnorderedAccessView> _uav; // Output
	ComPtr<ID3D11Buffer> _result;

private:
	void* _inputData;
	uint32 _inputStride = 0;
	uint32 _inputCount = 0;
	uint32 _outputStride = 0;
	uint32 _outputCount = 0;
};

StructuredBuffer.cpp

#include "pch.h"
#include "StructuredBuffer.h"

StructuredBuffer::StructuredBuffer(void* inputData, uint32 inputStride, uint32 inputCount, uint32 outputStride, uint32 outputCount)
	: _inputData(inputData), _inputStride(inputStride), _inputCount(inputCount), _outputStride(outputStride), _outputCount(outputCount)
{
	if (outputStride == 0 || outputCount == 0)
	{
		_outputStride = inputStride;
		_outputCount = inputCount;
	}

	CreateBuffer();
}

StructuredBuffer::~StructuredBuffer()
{

}

void StructuredBuffer::CreateBuffer()
{
	CreateInput();
	CreateSRV();
	CreateOutput();
	CreateUAV();
	CreateResult();
}

void StructuredBuffer::CreateInput()
{
	D3D11_BUFFER_DESC desc;
	ZeroMemory(&desc, sizeof(desc));

	desc.ByteWidth = GetInputByteWidth();
	desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;		//구조체에 맞게
	desc.StructureByteStride = _inputStride;
	desc.Usage = D3D11_USAGE_DYNAMIC;
	desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

	D3D11_SUBRESOURCE_DATA subResource = { 0 };
	subResource.pSysMem = _inputData;

	if (_inputData != nullptr)
		CHECK(DEVICE->CreateBuffer(&desc, &subResource, _input.GetAddressOf()));
	else
		CHECK(DEVICE->CreateBuffer(&desc, nullptr, _input.GetAddressOf()));
}

void StructuredBuffer::CreateSRV()
{
	D3D11_BUFFER_DESC desc;
	_input->GetDesc(&desc);

	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
	ZeroMemory(&srvDesc, sizeof(srvDesc));
	srvDesc.Format = DXGI_FORMAT_UNKNOWN;
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
	srvDesc.BufferEx.NumElements = _inputCount;

	CHECK(DEVICE->CreateShaderResourceView(_input.Get(), &srvDesc, _srv.GetAddressOf()));
}

void StructuredBuffer::CreateOutput()
{
	D3D11_BUFFER_DESC desc;
	ZeroMemory(&desc, sizeof(desc));

	desc.ByteWidth = GetOutputByteWidth();
	desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
	desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
	desc.StructureByteStride = _outputStride;

	CHECK(DEVICE->CreateBuffer(&desc, nullptr, _output.GetAddressOf()));
}

void StructuredBuffer::CreateUAV()
{
	D3D11_BUFFER_DESC desc;
	_output->GetDesc(&desc);

	D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
	ZeroMemory(&uavDesc, sizeof(uavDesc));
	uavDesc.Format = DXGI_FORMAT_UNKNOWN;
	uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
	uavDesc.Buffer.NumElements = _outputCount;

	CHECK(DEVICE->CreateUnorderedAccessView(_output.Get(), &uavDesc, _uav.GetAddressOf()));
}


void StructuredBuffer::CreateResult()
{
	D3D11_BUFFER_DESC desc;
	_output->GetDesc(&desc);

	desc.Usage = D3D11_USAGE_STAGING;
	desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
	desc.BindFlags = 0;
	desc.MiscFlags = 0;

	CHECK(DEVICE->CreateBuffer(&desc, NULL, _result.GetAddressOf()));
}


void StructuredBuffer::CopyToInput(void* data)
{
	D3D11_MAPPED_SUBRESOURCE subResource;
	DC->Map(_input.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
	{
		memcpy(subResource.pData, data, GetInputByteWidth());
	}
	DC->Unmap(_input.Get(), 0);
}

void StructuredBuffer::CopyFromOutput(void* data)
{
	DC->CopyResource(_result.Get(), _output.Get());

	D3D11_MAPPED_SUBRESOURCE subResource;
	DC->Map(_result.Get(), 0, D3D11_MAP_READ, 0, &subResource);
	{
		memcpy(data, subResource.pData, GetOutputByteWidth());
	}
	DC->Unmap(_result.Get(), 0);
}

 

쉐이더쪽 코드는 구조체를 통해 입출력 구조를 정해주고 이를 버퍼 변수로 지정해주고 출력에는 RW를 붙여서 Read + Write가 가능하도록 해주자. 그리고 지금은 입력값에 행렬을 받는데 이 행렬에 간단한 연산만 해서 반환하는 식으로 동작하게 해주자

StructuredBufferDemo.fx

struct InputDesc
{
    matrix input;
};

struct OutputDesc
{
    matrix result;
};

//하나의 배열로 보면된다
StructuredBuffer<InputDesc> Input;
RWStructuredBuffer<OutputDesc> Output;

//thread 개수 x*y*z 총합 
[numthreads(500,1,1)]       //x만 500개 행렬값
void CS(uint id : SV_GroupIndex)
{
    matrix result = Input[id].input * 2;
    
    Output[id].result = result;
}


technique11 T0
{
    Pass P0
    {
        SetVertexShader(NULL);
        SetPixelShader(NULL);
        //컴퓨트 쉐이더 세팅
        SetComputeShader(CompileShader(cs_5_0, CS()));
    }
};

 

이에 맞게 항등 행렬인 Matrix vector를 선언하고 이를 버퍼를 통해 작업할 수 있도록 메인 코드를 만들어주자

StructuredBufferDemo.fx

#include "pch.h"
#include "StructuredBufferDemo.h"
#include "StructuredBuffer.h"

void StructuredBufferDemo::Init()
{
	_shader = make_shared<Shader>(L"27. StructuredBufferDemo.fx");

	vector<Matrix> inputs(500, Matrix::Identity);

	auto buffer = make_shared<StructuredBuffer>(inputs.data(), sizeof(Matrix), 500, sizeof(Matrix), 500);

	_shader->GetSRV("Input")->SetResource(buffer->GetSRV().Get());
	_shader->GetUAV("Output")->SetUnorderedAccessView(buffer->GetUAV().Get());

	_shader->Dispatch(0, 0, 1, 1, 1);

	vector<Matrix> outputs(500);
	buffer->CopyFromOutput(outputs.data());
}

void StructuredBufferDemo::Update()
{

}

void StructuredBufferDemo::Render()
{

}

 

이렇게 해주고 코드끝부분에 Break Point를 잡고 실행시켜보면 

이렇게 행렬 x부분에 2가 곱해져서 반환되는 것을 볼 수 있다. 이런식으로 RawBuffer를 활용하는 방법보다

인스턴싱에서 매트릭스나 본 행렬 정보와 같은 정해진 구조를 여러개 넣어주는 경우가 많기 때문에 Sturctured Buffer

를 활용하는 방법이 좋을 것이다.


StructureBuffer를 살펴보기전에 텍스처를 주고받는 형태의 TextureBuffer를 알아보자. 
이 버퍼를 가지고 우리가 사용한 텍스처를 간단하게 조작해서 다른 색상을 입힌 결과물을 리턴받는 방식으로 한번 만들어보자. 

이 텍스처이미지는 2D로 픽셀 단위이기 때문에 정보가 많다. 이것을 우리가 여러개의 쓰레드가 처리할 수 있게 분산해주자. 일단 RawBuffer를 사용할 때와 마찬가지로 대칭성있게 입력을 받고 입력을 묘사하는 SRV를 만들고 결과를 받고 결과를 묘사하는 UAV를 받고 이를 복사해서 결과를 취합해주는 Result를 가져와주면 된다.

TextureBuffer.h

#pragma once


class TextureBuffer
{
public:
	TextureBuffer(ComPtr<ID3D11Texture2D> src);
	~TextureBuffer();

public:
	void CreateBuffer();

private:
	void CreateInput(ComPtr<ID3D11Texture2D> src);
	void CreateSRV();
	void CreateOutput();
	void CreateUAV();
	void CreateResult();

public:
	uint32 GetWidth() { return _width; }
	uint32 GetHeight() { return _height; }
	uint32 GetArraySize() { return _arraySize; }

	ComPtr<ID3D11Texture2D> GetOutput() { return (ID3D11Texture2D*)_output.Get(); }
	ComPtr<ID3D11ShaderResourceView> GetOutputSRV() { return _outputSRV; }

public:
	ComPtr<ID3D11ShaderResourceView> GetSRV() { return _srv; }
	ComPtr<ID3D11UnorderedAccessView> GetUAV() { return _uav; }

private:
	ComPtr<ID3D11Texture2D> _input;
	ComPtr<ID3D11ShaderResourceView> _srv; // Input
	ComPtr<ID3D11Texture2D> _output;
	ComPtr<ID3D11UnorderedAccessView> _uav; // Output

private:
	uint32 _width = 0;
	uint32 _height = 0;
	uint32 _arraySize = 0;
	DXGI_FORMAT _format;
	ComPtr<ID3D11ShaderResourceView> _outputSRV;
};

TextureBuffer.cpp

#include "pch.h"
#include "TextureBuffer.h"

TextureBuffer::TextureBuffer(ComPtr<ID3D11Texture2D> src)
{
	CreateInput(src);
	CreateBuffer();
}

TextureBuffer::~TextureBuffer()
{

}

void TextureBuffer::CreateBuffer()
{
	CreateSRV();
	CreateOutput();
	CreateUAV();
	CreateResult();
}

void TextureBuffer::CreateInput(ComPtr<ID3D11Texture2D> src)
{
	D3D11_TEXTURE2D_DESC srcDesc;
	src->GetDesc(&srcDesc);

	_width = srcDesc.Width;
	_height = srcDesc.Height;
	_arraySize = srcDesc.ArraySize;
	_format = srcDesc.Format;

	D3D11_TEXTURE2D_DESC desc;
	ZeroMemory(&desc, sizeof(desc));
	desc.Width = _width;
	desc.Height = _height;
	desc.ArraySize = _arraySize;
	desc.Format = _format;
	desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	desc.MipLevels = 1;
	desc.SampleDesc.Count = 1;

	CHECK(DEVICE->CreateTexture2D(&desc, NULL, _input.GetAddressOf()));

	DC->CopyResource(_input.Get(), src.Get());
}

void TextureBuffer::CreateSRV()
{
	D3D11_TEXTURE2D_DESC desc;
	_input->GetDesc(&desc);

	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
	ZeroMemory(&srvDesc, sizeof(D3D11_SHADER_RESOURCE_VIEW_DESC));
	srvDesc.Format = desc.Format;
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
	srvDesc.Texture2DArray.MipLevels = 1;
	srvDesc.Texture2DArray.ArraySize = _arraySize;

	CHECK(DEVICE->CreateShaderResourceView(_input.Get(), &srvDesc, _srv.GetAddressOf()));
}

void TextureBuffer::CreateOutput()
{
	D3D11_TEXTURE2D_DESC desc;
	ZeroMemory(&desc, sizeof(D3D11_TEXTURE2D_DESC));
	desc.Width = _width;
	desc.Height = _height;
	desc.ArraySize = _arraySize;
	desc.Format = _format;
	desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE;		//바인딩되도록
	desc.MipLevels = 1;
	desc.SampleDesc.Count = 1;

	CHECK(DEVICE->CreateTexture2D(&desc, nullptr, _output.GetAddressOf()));
}

void TextureBuffer::CreateUAV()
{
	D3D11_TEXTURE2D_DESC desc;
	_output->GetDesc(&desc);

	D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
	ZeroMemory(&uavDesc, sizeof(D3D11_UNORDERED_ACCESS_VIEW_DESC));
	uavDesc.Format = DXGI_FORMAT_UNKNOWN;
	uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2DARRAY;
	uavDesc.Texture2DArray.ArraySize = _arraySize;

	CHECK(DEVICE->CreateUnorderedAccessView(_output.Get(), &uavDesc, _uav.GetAddressOf()));
}

void TextureBuffer::CreateResult()
{
	D3D11_TEXTURE2D_DESC desc;
	_output->GetDesc(&desc);

	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
	ZeroMemory(&srvDesc, sizeof(D3D11_SHADER_RESOURCE_VIEW_DESC));
	srvDesc.Format = desc.Format;
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
	srvDesc.Texture2DArray.MipLevels = 1;
	srvDesc.Texture2DArray.ArraySize = _arraySize;

	CHECK(DEVICE->CreateShaderResourceView(_output.Get(), &srvDesc, _outputSRV.GetAddressOf()));
}

 

이렇게 버퍼를 만들어주고 우리가 텍스처를 조작할 때 사용할 수 있도록 텍스처 클래스에 헬퍼함수와 SRV에서 Texture2D를 가져오는 함수를 추가해주자.

Texture.h

#pragma once
#include "ResourceBase.h"

class Texture : public ResourceBase
{
	using Super = ResourceBase;
public:
	ComPtr<ID3D11Texture2D> GetTexture2D();
	void SetSRV(ComPtr<ID3D11ShaderResourceView> srv) { _shaderResourveView = srv; }
};

Texture.cpp

ComPtr<ID3D11Texture2D> Texture::GetTexture2D()
{
	//자기 SRV-> TExture2D
	ComPtr<ID3D11Texture2D> texture;
	_shaderResourveView->GetResource((ID3D11Resource**)texture.GetAddressOf());
	return texture;
}

 

 

2D Texture를 가지고 조작해볼 것이기 때문에 쓰레드를 2차원으로 설정해주고 고유한 아이디에 해당하는 정보를 가져와서 조작할 수 있도록 쉐이더코드를 만들어주자.

TextureBufferDemo.fx

//RGBA
Texture2DArray<float4> Input;
RWTexture2DArray<float4> Output;

//thread 개수 a*b*c 총합 
[numthreads(32,32,1)]       //2차원
void CS(uint3 id : SV_DispatchThreadID)
{
    float4 color = Input.Load(int4(id, 0));     //쓰레드 넘버링값에 해당하는 정보 추출
    
    //Output[id] = color;
    Output[id] = 1.0f - color;      //반전 
    //Output[id] = (color.r + color.g + color.b) / 3.0f;        //회색풍
}


technique11 T0
{
    Pass P0
    {
        SetVertexShader(NULL);
        SetPixelShader(NULL);
        //컴퓨트 쉐이더 세팅
        SetComputeShader(CompileShader(cs_5_0, CS()));
    }
};

 

쓰레드를 설정해두고 이에 맞는 픽셀을 가공하는데 이미지 파일은 쓰레드 개수로 딱 떨어지지 않고 훨씬 더 클 것이다. 

그래서 여러 단위로 만들어서 작업을 하는데 이렇게 작업을 하다보면 남는 데이터가 있을텐데 이 데이터도 하나로 인식해서 처리해야하기 때문에 만약에 나누었을 때 작게 나오는 것보다는 크게 그룹을 설정해주는게 좋기 때문에 쓰레드 개수 -1 

/ 쓰레드 개수 이런식으로 처리해주면 크게 그룹이 설정된다.

TextureBufferDemo.h

#pragma once
#include "IExecute.h"

class TextureBufferDemo : public IExecute
{
public:
	void Init() override;
	void Update() override;
	void Render() override;

private:
	shared_ptr<Shader> _shader;

private:
	ComPtr<ID3D11ShaderResourceView> MakeComputeShaderTexture();
};

TextureBufferDemo.cpp

#include "pch.h"
#include "TextureBufferDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "TextureBuffer.h"

void TextureBufferDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");
	auto newSrv = MakeComputeShaderTexture();

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		CUR_SCENE->Add(camera);
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}
	// Mesh
	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = make_shared<Texture>();
		texture->SetSRV(newSrv);
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}

	for (int32 i = 0; i < 100; i++)
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(rand() % 100, 0, rand() % 100));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}

		CUR_SCENE->Add(obj);
	}

	RENDER->Init(_shader);
}

void TextureBufferDemo::Update()
{

}

void TextureBufferDemo::Render()
{

}

ComPtr<ID3D11ShaderResourceView> TextureBufferDemo::MakeComputeShaderTexture()
{
	auto shader = make_shared<Shader>(L"26. TextureBufferDemo.fx");

	auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
	shared_ptr<TextureBuffer> textureBuffer = make_shared<TextureBuffer>(texture->GetTexture2D());
	
	shader->GetSRV("Input")->SetResource(textureBuffer->GetSRV().Get());
	shader->GetUAV("Output")->SetUnorderedAccessView(textureBuffer->GetUAV().Get());

	uint32 width = textureBuffer->GetWidth();
	uint32 height = textureBuffer->GetHeight();
	uint32 arraySize = textureBuffer->GetArraySize();

	uint32 x = max(1, (width + 31) / 32);
	uint32 y = max(1, (height + 31) / 32);
	shader->Dispatch(0, 0, x, y, arraySize);
	return textureBuffer->GetOutputSRV();
}

 

이렇게 해준 다음 실행해주면 색이 반전된 구들이 나오게 된다.

 

+ Recent posts