응애맘마조
221124 강의 본문
어제에 이어서 ImGui에 대해 써보겠습니다.
사실 오늘 이 부분이 끝날 줄 알았는데 써야 되는 코드가 많다 보니 설명은 내일 해주신다고 했습니다.
오늘은 코드가 들어갑니다.
//Framework
#pragma once
#include <Windows.h>
#include <iostream>
#include <time.h>
#include <stdlib.h>
#include <string>
#include <vector>
#include <unordered_map>
#include <assert.h>
using namespace std;
//순서 중요
//DirectX
#include <d3dcompiler.h>
#include <d3d11.h>
#include <D3DX10math.h>
#include <D3DX11async.h>
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "d3dx11.lib")
#pragma comment(lib, "d3dx10.lib")
#pragma comment(lib, "d3dcompiler.lib")
/*---------------------------------------------------*/
//ImGui
#include "ImGui/imgui.h"
#include "ImGui/imgui_internal.h"
#include "ImGui/imgui_impl_dx11.h"
#include "ImGui/imgui_impl_win32.h"
#pragma comment(lib, "ImGui/ImGui.lib")
/*---------------------------------------------------*/
//Framework
#include "Systems/Window.h"
#include "Utilities/SingletonBase.h"
#include "Systems/Graphics.h"
#include "Systems/Keyboard.h"
#include "Systems/Mouse.h"
#include "Systems/Timer.h"
#include "Utilities/Gui.h"
/*---------------------------------------------------*/
//typedef
typedef D3DXVECTOR3 Vector3;
typedef D3DXVECTOR2 Vector2;
typedef D3DXMATRIX Matrix;
typedef D3DXCOLOR Color;
typedef UINT uint;
/*---------------------------------------------------*/
//define
#define DEVICE Graphics::Get()->GetDevice
#define DC Graphics::Get()->GetDC
#define CHECK(p) assert(SUCCEEDED(p))
#define SAFE_DELETE(p) { if(p) {delete(p); (p) = nullptr;} }
#define SAFE_DELETE_ARRAY(p) { if(p) {delete[](p); (p) = nullptr;} }
#define SAFE_RELEASE(p) { if(p) {(p)->Release(); (p) = nullptr;} }
#define WinMaxWidth 1280
#define WinMaxHeight 720
/*---------------------------------------------------*/
//Variable
extern HWND handle;
어제에 이어서 추가된 부분이 있습니다.
어제부터 Framework.h였습니다. 더 이상 stdafx.h가 아닙니다.
//Gui.h
#pragma once
class Gui : public SingletonBase<Gui>
{
public:
friend class SingletonBase<Gui>;
LRESULT MsgProc(HWND handle, UINT message, WPARAM wParam, LPARAM lParam);
void Resize();
void Update();
void Render();
private:
Gui();
~Gui();
private:
void ApplyStyle();
};
새로 만든 Gui.h입니다.
사실 어제 만들었지만 완성이 안되었기 때문에 완성된 오늘 올립니다.
//Gui.cpp
#include "Framework.h"
#include "Gui.h"
LRESULT Gui::MsgProc(HWND handle, UINT message, WPARAM wParam, LPARAM lParam)
{
return ImGui_ImplWin32_Proc(handle, message, wParam, lParam);
}
void Gui::Resize()
{
ImGui_ImplDX11_InvalidateDeviceObjects();
ImGui_ImplDX11_CreateDeviceObjects();
}
void Gui::Update()
{
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
}
void Gui::Render()
{
ImGui::Render();
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
}
Gui::Gui()
{
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
ImGui_ImplWin32_Init(handle);
//ImGui_ImplDX11_Init(DEVICE, DC);
ApplyStyle();
}
Gui::~Gui()
{
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
}
void Gui::ApplyStyle()
{
ImGui::GetIO().ConfigWindowsResizeFromEdges = true;
ImGui::StyleColorsDark();
ImGuiStyle& style = ImGui::GetStyle();
float fontSize = 15.0f;
float roundness = 2.0f;
ImVec4 white = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
ImVec4 text = ImVec4(0.76f, 0.77f, 0.8f, 1.0f);
ImVec4 black = ImVec4(0.0f, 0.0f, 0.0f, 1.0f);
ImVec4 backgroundVeryDark = ImVec4(0.08f, 0.086f, 0.094f, 1.00f);
ImVec4 backgroundDark = ImVec4(0.117f, 0.121f, 0.145f, 1.00f);
ImVec4 backgroundMedium = ImVec4(0.26f, 0.26f, 0.27f, 1.0f);
ImVec4 backgroundLight = ImVec4(0.37f, 0.38f, 0.39f, 1.0f);
ImVec4 highlightBlue = ImVec4(0.172f, 0.239f, 0.341f, 1.0f);
ImVec4 highlightBlueHovered = ImVec4(0.202f, 0.269f, 0.391f, 1.0f);
ImVec4 highlightBlueActive = ImVec4(0.382f, 0.449f, 0.561f, 1.0f);
ImVec4 barBackground = ImVec4(0.078f, 0.082f, 0.09f, 1.0f);
ImVec4 bar = ImVec4(0.164f, 0.180f, 0.231f, 1.0f);
ImVec4 barHovered = ImVec4(0.411f, 0.411f, 0.411f, 1.0f);
ImVec4 barActive = ImVec4(0.337f, 0.337f, 0.368f, 1.0f);
// Spatial
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
style.FramePadding = ImVec2(5, 5);
style.ItemSpacing = ImVec2(6, 5);
style.Alpha = 1.0f;
style.WindowRounding = roundness;
style.FrameRounding = roundness;
style.PopupRounding = roundness;
style.GrabRounding = roundness;
style.ScrollbarSize = 20.0f;
style.ScrollbarRounding = roundness;
// Colors
style.Colors[ImGuiCol_Text] = text;
style.Colors[ImGuiCol_WindowBg] = backgroundDark;
style.Colors[ImGuiCol_Border] = black;
style.Colors[ImGuiCol_FrameBg] = bar;
style.Colors[ImGuiCol_FrameBgHovered] = highlightBlue;
style.Colors[ImGuiCol_FrameBgActive] = highlightBlueHovered;
style.Colors[ImGuiCol_TitleBg] = backgroundVeryDark;
style.Colors[ImGuiCol_TitleBgActive] = bar;
style.Colors[ImGuiCol_MenuBarBg] = backgroundVeryDark;
style.Colors[ImGuiCol_ScrollbarBg] = barBackground;
style.Colors[ImGuiCol_ScrollbarGrab] = bar;
style.Colors[ImGuiCol_ScrollbarGrabHovered] = barHovered;
style.Colors[ImGuiCol_ScrollbarGrabActive] = barActive;
style.Colors[ImGuiCol_CheckMark] = white;
style.Colors[ImGuiCol_SliderGrab] = bar;
style.Colors[ImGuiCol_SliderGrabActive] = barActive;
style.Colors[ImGuiCol_Button] = barActive;
style.Colors[ImGuiCol_ButtonHovered] = highlightBlue;
style.Colors[ImGuiCol_ButtonActive] = highlightBlueActive;
style.Colors[ImGuiCol_Header] = highlightBlue;
style.Colors[ImGuiCol_HeaderHovered] = highlightBlueHovered;
style.Colors[ImGuiCol_HeaderActive] = highlightBlueActive;
style.Colors[ImGuiCol_Separator] = backgroundLight;
style.Colors[ImGuiCol_ResizeGrip] = backgroundMedium;
style.Colors[ImGuiCol_ResizeGripHovered] = highlightBlue;
style.Colors[ImGuiCol_ResizeGripActive] = highlightBlueHovered;
style.Colors[ImGuiCol_PlotLines] = ImVec4(0.0f, 0.7f, 0.77f, 1.0f);
style.Colors[ImGuiCol_PlotHistogram] = highlightBlue;
style.Colors[ImGuiCol_PlotHistogramHovered] = highlightBlueHovered;
style.Colors[ImGuiCol_TextSelectedBg] = highlightBlue;
style.Colors[ImGuiCol_PopupBg] = backgroundVeryDark;
style.Colors[ImGuiCol_DragDropTarget] = backgroundLight;
}
Gui.cpp입니다. ApplyStyle에 내용이 많습니다. 이 부분도 작성한 게 아니라 이 부분만 복사 붙여넣기 했습니다.
47번째 줄에 주석이 된 ImGui_ImplDX11_Init(DEVICE, DC); 이 부분은 아직 설정이 안 되었기 때문에 작성하고 주석 처리했습니다. 아마 내일 사용할 것 같습니다.
//IObject.h
#pragma once
class IObject
{
public:
virtual void Init() = 0;
virtual void Destroy() = 0;
virtual void Update() = 0;
virtual void Render() = 0;
virtual void PostRender() = 0;
virtual void GUI() = 0;
};
새로 작성한 IObject.h입니다. (헤더 파일만 있습니다.)
함수 뒤에 0을 붙인 이유는 순수가상함수이기 때문입니다.
virtual void Destroy() = 0은 소멸자를 대체합니다.
virtual void PostRender() = 0은 후처리가 필요한 코드입니다.
virtual void GUI() = 0은 외부 라이브러리입니다.
//Graphics.h
#pragma once
class D3DEnumAdapterInfo;
class D3DEnumOutputInfo;
class Graphics : public SingletonBase<Graphics>
{
public:
friend class SingletonBase<Graphics>;
ID3D11Device* GetDevice() { return device; }
ID3D11DeviceContext* GetDC() { return deviceContext; }
void Resize(const UINT& width, const UINT& height);
void SetViewport(const UINT& width, const UINT& height);
void Begin();
void End();
void GUI();
private:
void CreateSwapchain();
void CreateRenderTargetView();
void DeleteSurface();
private:
Graphics();
~Graphics();
private:
void EnumrateAdapters();
bool EnumerateAdapterOutput(D3DEnumAdapterInfo* adapterInfo);
private:
ID3D11Device* device = nullptr;
ID3D11DeviceContext* deviceContext = nullptr;
IDXGISwapChain* swapChain = nullptr;
ID3D11RenderTargetView* rtv = nullptr;
D3D11_VIEWPORT viewport;
D3DXCOLOR clearColor = 0xff555566;
UINT numerator = 0;
UINT denominator = 1;
UINT gpuMemorySize;
wstring gpuName;
vector<D3DEnumAdapterInfo*> adapterInfos;
int selectedAdapterIndex = 0;
bool bVsync = true;
};
class D3DEnumAdapterInfo
{
public:
~D3DEnumAdapterInfo();
UINT adapterOrdinal = 0;
IDXGIAdapter1* adapter = nullptr;
DXGI_ADAPTER_DESC1 adapterDesc = { 0 };
D3DEnumOutputInfo* outputInfo = nullptr;
};
class D3DEnumOutputInfo
{
public:
~D3DEnumOutputInfo();
IDXGIOutput* output = nullptr;
DXGI_OUTPUT_DESC outputDesc = { 0 };
UINT numerator = 0;
UINT denominator = 1;
};
여기부터 오늘 작성한 Graphics.h입니다.
37번째 줄부터 작성된 struct 부분에 첫 글자 I는 인터페이스입니다. 그래서 붙게 되었습니다.
ID3D11Device* device = nullptr는 기능 지원 점검과 자원 할당에 쓰입니다.
ID3D11DeviceContext* deviceContext = nullptr는 렌더 대상 설정, 자원을 그래픽 파이프라인에 묶고, GPU가 렌더링 명령들을 지시하는 데 사용합니다.
IDXGISwapChain* swapChain = nullptr은 스왑 체인입니다. 스왑 체인에 관한 내용은 22일에 관련 내용을 작성하였습니다.
ID3D11RenderTargetView* rtv = nullptr는 쓰기 전용(입력받기)입니다.
D3D11_VIEWPORT viewport는 스크린의 어떤 위치에 작업한 내용을 표시할 것인지 구역을 정하는 구조체(화면에 보일 영역)입니다.
잠깐 생소한 단어일 수도 아닐 수도 있지만
numerator은 분모
denominator은 분자입니다.
vector<D3DEnumAdapterInfo*> adapterInfos에서 D3DEnumAdapterInfo는 디스플레이가 쓰고 있는 그래픽 카드의 정보를 가져오는 역할을 합니다.
//Graphics.cpp
#include "Framework.h"
#include "Graphics.h"
void Graphics::Resize(const UINT& width, const UINT& height)
{
DeleteSurface();
{
HRESULT hr = swapChain->ResizeBuffers
(
0,
width,
height,
DXGI_FORMAT_UNKNOWN,
0
);
assert(SUCCEEDED(hr));
}
CreateRenderTargetView();
SetViewport(width, height);
}
void Graphics::SetViewport(const UINT& width, const UINT& height)
{
viewport.TopLeftX = 0.0f;
viewport.TopLeftY = 0.0f;
viewport.Width = (float)width;
viewport.Height = (float)height;
viewport.MinDepth = 0.0f;
viewport.MaxDepth = 1.0f;
}
void Graphics::Begin()
{
deviceContext->OMSetRenderTargets(1, &rtv, nullptr);
deviceContext->RSSetViewports(1, &viewport);
deviceContext->ClearRenderTargetView(rtv, clearColor);
}
void Graphics::End()
{
HRESULT hr = swapChain->Present(bVsync == true ? 1 : 0, 0);
assert(SUCCEEDED(hr));
}
void Graphics::GUI()
{
static bool bOpen = true;
ImGui::SetNextWindowPos({ 0, 15 });
ImGui::SetNextWindowSize(ImVec2(200, 30));
ImGui::Begin
(
"Vsync",
&bOpen,
ImGuiWindowFlags_NoBackground |
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar
);
ImGui::Checkbox("##Vsync", &bVsync);
ImGui::End();
}
void Graphics::CreateSwapchain()
{
SAFE_RELEASE(device);
SAFE_RELEASE(deviceContext);
SAFE_RELEASE(swapChain);
DXGI_SWAP_CHAIN_DESC desc;
ZeroMemory(&desc, sizeof(DXGI_ADAPTER_DESC));
desc.BufferDesc.Width = 0;
desc.BufferDesc.Height = 0;
if (bVsync)
{
desc.BufferDesc.RefreshRate.Numerator = adapterInfos[0]->outputInfo->numerator;
desc.BufferDesc.RefreshRate.Denominator = adapterInfos[0]->outputInfo->denominator;
}
else
{
desc.BufferDesc.RefreshRate.Numerator = 0;
desc.BufferDesc.RefreshRate.Denominator = 1;
}
desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
desc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
desc.BufferCount = 1;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
desc.OutputWindow = handle;
desc.Windowed = true;
desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
vector<D3D_FEATURE_LEVEL> featureLevel
{
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
};
UINT maxVRam = 0;
for (UINT i = 0; i < adapterInfos.size(); i++)
{
if (adapterInfos[i]->adapterDesc.DedicatedVideoMemory > maxVRam)
{
selectedAdapterIndex = i;
maxVRam = adapterInfos[i]->adapterDesc.DedicatedVideoMemory;
}
}
for (int i = 0; i < adapterInfos.size(); i++)
{
gpuName = adapterInfos[i]->adapterDesc.Description;
wcout << "GPU Name : " << adapterInfos[i]->adapterDesc.Description << endl;
cout << "VRam : " << adapterInfos[i]->adapterDesc.DedicatedVideoMemory << endl;
cout << endl;
}
wcout << "Selected GPU Name : "
<< adapterInfos[selectedAdapterIndex]->adapterDesc.Description << endl;
HRESULT hr = D3D11CreateDeviceAndSwapChain
(
adapterInfos[selectedAdapterIndex]->adapter,
D3D_DRIVER_TYPE_UNKNOWN,
nullptr,
0,
featureLevel.data(),
featureLevel.size(),
D3D11_SDK_VERSION,
&desc,
&swapChain,
&device,
nullptr,
&deviceContext
);
assert(SUCCEEDED(hr));
Resize(WinMaxWidth, WinMaxHeight);
}
void Graphics::CreateRenderTargetView()
{
ID3D11Texture2D* backbuffer = nullptr;
HRESULT hr = swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backbuffer);
assert(SUCCEEDED(hr));
hr = device->CreateRenderTargetView(backbuffer, nullptr, &rtv);
assert(SUCCEEDED(hr));
SAFE_RELEASE(backbuffer);
}
void Graphics::DeleteSurface()
{
SAFE_RELEASE(rtv);
}
Graphics::Graphics()
{
EnumrateAdapters();
CreateSwapchain();
}
Graphics::~Graphics()
{
SAFE_RELEASE(rtv);
SAFE_RELEASE(swapChain);
SAFE_RELEASE(deviceContext);
SAFE_RELEASE(device);
}
void Graphics::EnumrateAdapters()
{
IDXGIFactory1* factory;
if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory)))
return;
UINT index = 0;
while (true)
{
IDXGIAdapter1* adapter = nullptr;
HRESULT hr = factory->EnumAdapters1(index, &adapter);
if (hr == DXGI_ERROR_NOT_FOUND)
break;
assert(SUCCEEDED(hr));
D3DEnumAdapterInfo* adapterInfo = new D3DEnumAdapterInfo();
ZeroMemory(adapterInfo, sizeof(D3DEnumAdapterInfo));
adapterInfo->adapter = adapter;
adapter->GetDesc1(&adapterInfo->adapterDesc);
adapterInfo->adapter = adapter;
EnumerateAdapterOutput(adapterInfo);
adapterInfos.push_back(adapterInfo);
index++;
}
SAFE_RELEASE(factory);
}
bool Graphics::EnumerateAdapterOutput(D3DEnumAdapterInfo* adapterInfo)
{
return false;
}
D3DEnumAdapterInfo::~D3DEnumAdapterInfo()
{
}
D3DEnumOutputInfo::~D3DEnumOutputInfo()
{
}
Graphics.cpp입니다. 여기에서 시간을 많이 사용하였지만 간간히 코드에 대해 설명도 해주셨습니다.
(몇몇 코드는 직접 검색해보라고 하셨습니다. 그 부분은 따로 표기하겠습니다.)
ImGui::SetNextWindowSize(ImVec2(200, 30))은 위젯 창 크기 설정입니다. (X가 200, Y가 30)
(검색) DXGI_SWAP_CHAIN_DESC desc에서 desc는 description입니다.
DXGI_SWAP_CHAIN_DESC는 구조체로 되어있어서 들어가 보면
typedef struct DXGI_SWAP_CHAIN_DESC
{
DXGI_MODE_DESC BufferDesc;
DXGI_SAMPLE_DESC SampleDesc;
DXGI_USAGE BufferUsage;
UINT BufferCount;
HWND OutputWindow;
BOOL Windowed;
DXGI_SWAP_EFFECT SwapEffect;
UINT Flags;
} DXGI_SWAP_CHAIN_DESC;
이렇게 나옵니다.
특히 오늘 DXGI가 많았는데
DXGI는 DirectX Graphic Interface라고 합니다. 컴퓨터에서 그래픽 작업을 수행하기 위해 운영체제에서 지원을 해주는데 그 지원이 DXGI입니다.
또 이 구조체 안을 보면 DXGI_MODE_DESC라고 있는데 이 부분을 또 타고 들어가 보면
typedef struct DXGI_MODE_DESC
{
UINT Width;
UINT Height;
DXGI_RATIONAL RefreshRate;
DXGI_FORMAT Format;
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;
DXGI_MODE_SCALING Scaling;
} DXGI_MODE_DESC;
라고 또 다른 구조체가 나오게 됩니다.
UINT Width, height는 백 버퍼의 폭과 높이니다.
DXGI_RATIONAL RefreshRate는 디스플레이 모드 갱신율입니다.
DXGI_FORMAT Format은 백 버퍼의 픽셀 형식입니다.
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering과 DXGI_MODE_SCALING Scaling에서는 MODE가 들어가는데
이는 말 그대로 모드라서 스캔라인 모드하고 이미지를 모니터 화면에 맞게 확대 / 축소한다고 보면 됩니다.
자세한 내용은 https://learn.microsoft.com/en-us/windows/win32/api/dxgi/ns-dxgi-dxgi_swap_chain_desc
DXGI_SWAP_CHAIN_DESC (dxgi.h) - Win32 apps
Describes a swap chain. (DXGI_SWAP_CHAIN_DESC)
learn.microsoft.com
여기에서 확인하시면 됩니다.
(검색) desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM에서는 8이 많이 들어가는데 이건 8비트(R8 G8 B8 A8 - RGB에 ALPHA 채널이 추가된 것)입니다.
UNORM은 어떤 것인지 알려주는 부분입니다. 부호가 없는 정규화된 정수 n비트를 의미합니다.
https://learn.microsoft.com/ko-kr/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-data-conversion
데이터 변환 규칙 - Win32 apps
다음 섹션에서는 Direct3D에서 데이터 형식 간 변환을 처리하는 방법을 설명합니다.
learn.microsoft.com
UNORM은 여기에서 가져왔습니다.
(검색) DXGI_MODE_SCALING_UNSPECIFIED와 DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED 또한 앞에서 말한 DXGI_MODE_SCANLINE_ORDER 안에 들어 있습니다.
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT은 현재 작동을 알려줍니다.
desc.SampleDesc.Count = 1은 버퍼 카운터 수입니다.
desc.SampleDesc.Quality = 0은 샘플의 퀄리티 설정입니다.
desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD은 백 버퍼를 처리할 때 처리효과 서술합니다.
(DISCARD를 쓰는 이유는 백 버퍼 교환 후 교환 후의 백 버퍼를 버리기 위해서 씁니다.)
vector<D3D_FEATURE_LEVEL> featureLevel은 현재 DirectX의 기능 수준을 의미합니다.
D3D_FEATURE_LEVEL_11_1은 다이렉트 11 버전은 11.1
D3D_FEATURE_LEVEL_11_0은 다이렉트 11 버전은 11.0
D3D_FEATURE_LEVEL_10_1은 다이렉트 10 버전은 10.1
D3D_FEATURE_LEVEL_10_0은 다이렉트 10 버전은 10.0
이렇게 보시면 됩니다.
UINT maxVRam = 0은 비디오 카드 수 검색입니다.
HRESULT hr = D3D11CreateDeviceAndSwapChain이 부분에서 제대로 작동되면 예외 발생이 생기지 않습니다.
(개발자들 사이에서는 코드가 터졌다라고도 말합니다.)
마지막에 있는 소멸자인포는 아직 적지 않았습니다. 내용이 없거나 내일 코드를 작성할 것 같습니다.
※여담으로 내일 "더 샌드박스 메타버스에서의 NFT크리에이터 게임 생태계"라는 주제로 강의를 진행해서 따로 들으러 갑니다. 딱히 샌드박스나 메타버스에 관해서는 별로 관심은 없지만 들어둬서 나쁠 건 없을 것 같기에 신청해둬서 오후에 들으러 갑니다. 이 강의 내용에 대해서도 적을지 적지 않을지는 모르겠지만 유익하다면 적어보도록 하겠습니다.
읽어주셔서 감사합니다.