w99hyunw99hyun
메인프로필
프로젝트
전체 프로젝트 보기 →
FunnikulusUnity · XR / AI · 2026EmbersUnity · MMORPG · 2024 ㅡ 2025OrbitUnity · FPS · 2024리썰딜리버리 VRUnity · VR · 2024미로의 숲Unity · 교육 · 2024
블로그연락처
메인
프로필
프로젝트
블로그
연락처
메인프로필프로젝트블로그연락처

© 2026 w99hyun. All Rights Reserved.

본 사이트는 개인 포트폴리오 게시용으로 제작되었습니다.

블로그

기술의 흐름을 따라가며 작성한 블로그 포스트입니다.

블로그에서 더 보기
보기 방식

2025.11.15.

글 보기

유니티 UI 최상위 렌더링 (Render Objects)

UI 최상위 렌더링? 유니티에서 World Space UI를 사용하다보면, 다른 게임 오브젝트에 가려져서 UI가 보이지 않을 때가 있다. 예를 들면 사진처럼 체력, 지역정보, 무기 정보 등을 표시하는 HUD 등이 있을 것이다. 사진(Orbit 게임)의 HUD도 World Space UI로 제작되었다. (몰입감을 위한 기울기를 표시하기 위해) 또한, World Space UI는 VR 게임을 제작할 때 필수로 사용된다. VR에서는 Screen Space로 렌더링되는 UI는 보이지 않기 때문이다. 아래의 사진을 보자. 원본 이미지는 이런 이미지이다. 이 이미지를 유니티에서 World Space UI에 넣고, 게임 오브젝트와 겹쳐놓으면 아래처럼 UI가 짤려 보인다. 만약 이 UI가 체력 등을 표시하는 HUD 였다면, 플레이어는 굉장한 불편함을 느낄 것이다. 하지만 UI를 최상위로 렌더링 할 수 있는 기능이 존재한다. 해당 설정을 하면, 아래와 같이 표시된다. 위치는 전혀 조정하지 않았다. 본 정보는 Unity6 URP를 기준으로 합니다. 다른 유니티 버전을 사용하거나 HDRP를 사용할 경우 메뉴의 위치가 다를 수 있습니다. 적용 방법 우선 Edit → Project Settings 에서 Quality (품질) 탭을 들어간다. 현재 사용중인 Levels(초록색 네모박스로 체크되어 있는 것)를 누르면 렌더 파이프라인 에셋 이 있다. 이걸 누르면 현재 사용중인 Universal Render Pipeline Asset을 찾을 수 있고, 여기서 렌더러 목록에 있는 Univesal Renderer Data를 눌러준다. 해당 Data를 찾았다면, Add Renderer Feature → Render Objects 로 추가한다. 그리고 아래와 같이 설정한다. Event AfterRenderingTransparents Filters → Queue 투명(Transparent) Filters → Layer Mask 항상 최상위에 렌더링 하고 싶은 Layer 이름(새로 정의) Depth Check Depth Test 항상(Always) Event 에는 다양한 렌더링 순서가 있으므로, 각자의 프로젝트 상황에 맞게 조절해도 된다. 모든 설정을 하고, 최상위에 두고 싶은 UI가 있는 Canvas의 Layer를 Render Objects에서 설정해준 Layer와 동일하게 설정해주면 끝. 적용 모습 오브젝트와 겹쳐있어도 UI가 최상위로 렌더링된다.

2025.11.12.

글 보기

QT - C++ ↔ QML connect

QML에서 C++ 객체를 사용하려면? C++ 클래스를 QML 타입으로 등록한다. QML_ELEMENT 매크로 등록을 통해 QML 타입으로 등록하면, QML에서 C++의 함수나 변수에 접근이 가능하다. 헤더에서 함수 원형을 선언할 때, Q_INVOKABLE 매크로를 앞에 붙여주면 QML에서 직접 해당 함수를 호출할 수 있다. - QT5 에서는 main.cpp의 main 함수에서 qmlRegister가 필요하다 ... qmlRegisterType<cpp 클래스명>("프로젝트명", 0, 1, "{QML에서 사용할 네임}"); qmlRegisterType<MessageProcessor>("Course", 0, 1, "MessageProcessor"); ... 이후, 해당 cpp 파일을 쓸 qml파일에서 import {프로젝트명} {버전}을 통해 QML에서 cpp클래스에 정의된 함수를 사용할 수 있다. import Course 0.1 Q_PROPERTY C++ 클래스의 속성을 QML이나 다른 C++ 클래스에서 사용할 수 있게 노출 QML에서 해당 속성의 값을 읽고 쓸 수 있게 되며, 속성 값이 변경될 때 알림을 받을 수 있음 Q_PROPERTY(Type name READ readFunction WRITE writeFunction NOTIFY notifySignal) //Type: 속성(프로퍼티)의 데이터타입 //name: 속성의 이름 //READ: 속성 값을 읽는 함수 //WRITE: 속성 값을 설정하는 함수 (선택) //NOTIFY: 속성 값이 변경될 때 발생하는 시그널 (선택) CONST 키워드 해당 속성이 상수임을 나타냄. Q_PROPERTY(QString version READ version CONSTANT) MEMBER 키워드 별도의 읽기/쓰기 함수를 정의하지 않아도 자동으로 읽기/쓰기 제공 Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged) NOTIFY 시그널 속성 값이 변경될 때 발생하는 시그널을 지정 속성 값의 변경을 모니터링 할 수 있음 데이터 바인딩이나 모델-뷰 아키텍처에서 매우 유용 Q_INVOKABLE C++ 클래스의 멤버 함수를 QML에서 호출할 수 있는 메소드로 노출 비즈니스 로직은 C++ 에서 처리하고, QML에서는 View만 제공하도록 함 QObject QObject는 부모-자식 관계를 통해 객체들을 트리 형태로 관리함 부모 객체가 소멸되면 자식 객체도 모두 소멸되어 메모리 누수를 방지함 스레드를 사용할 때는 객체가 생성된 스레드와 동일한 스레드에서 접근해야 함 QAbstractItemModel 프록시 모델, 리스트 모델, 테이블 모델 등을 제공하는 모델 원형 이 클래스를 상속받아 만들어진 여러 모델이나 커스텀 모델을 통해 C++에서 데이터를 관리하고, QML의 UI에서 표현할 수 있음 QAbstractListModel Custom Model을 통한 ListView 생성 예제 /* MyModel.h */ #pragma once #include <QAbstractListModel> class MyItem { public: QString name; QString description; }; class MyModel : public QAbstractListModel { Q_OBJECT public: enum ItemRoles { NameRole = Qt::UserRole + 1, DestiptionRole, }; explicit MyModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; //데이터 추가 함수 Q_INVOKABLE void addItem(const QString &name, const QString &description); protected: QHash<int, QByteArray> roleNames() const override; private: QList<MyItem> m_items; }; /* MyModel.cpp */ #include "mymodel.h" MyModel::MyModel(QObject *parent) : QAbstractListModel{parent} { } QVariant MyModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_items.count()) { return QVariant(); } const MyItem &item = m_items[index.row()]; switch(role) { case NameRole: return item.name; case DestiptionRole: return item.description; default: return QVariant(); } } int MyModel::rowCount(const QModelIndex &parent) const { return m_items.count(); } void MyModel::addItem(const QString &name, const QString &description) { beginInsertRows(QModelIndex(), rowCount(), rowCount()); MyItem item; item.name = name; item.description = description; m_items << item; endInsertRows(); } // beginInsertRows(), endInsertRows() 사이에 있는 아이템에 변화가 생길 경우 UI가 업데이트됨 //RoleEnum 값과 매칭되는 name(qml에서 접근할) 매핑 QHash<int, QByteArray> MyModel::roleNames() const { QHash<int, QByteArray> roles; roles[NameRole] = "name"; roles[DestiptionRole] = "description"; return roles; } /* main.qml */ MyModel { id: myModel } ListView { id: listView anchors.fill: parent model: myModel ScrollBar.vertical: ScrollBar { id: verticalScrollBar width: 14 policy: ScrollBar.AlwaysOn } //List Item Preset delegate: Item { height: column.height + 10 Column { id: column Text { text:model.name font.bold: true } Text { text:model.description color: "lightpink" } } } Component.onCompleted: { // 어플리케이션이 시작될 때 항목 추가 myModel.addItem("Item1", "This is the first item.") for (var i = 0; i < 20; i++) { myModel.addItem("item" + i, "This is " +i+ " item.") } } }

2025.11.10.

글 보기

QT - QML summary

Item 계열 컴포넌트 Item 가장 기본적인 UI 컴포넌트 위치/크기 등 기본 property Rectangle Item을 상속받아 만들어짐 간단한 직사각형 border, radius, color 등 지원 Text text: property 사용 폰트, 크기, 색상, wrapMode(NoWrap, WordWrap, WrapAnywhere 등) 지원 Image source를 통해 이미지 표시 State 값을 통해 이미지 로딩 수준 표시 가능 Button onclicked: 를 통해 클릭 이벤트 발생 (vanila JS 문법) ListView 목록 형태 데이터 표현 model과 delegate 설정이 핵심 방향 설정 property(Qt.Horizontal, Qt.Vertical / Qt.LeftToRight, Qt.RightToLeft 등) 제공 Slider 범위 내 값 선택이 가능한 Slider Snap 기능 지원 (눈금별 Snap) Switch 토글 스위치 사용 onCheckedChanged: 이벤트 지원 signal ↔ slot 개념. signal에서 slot으로만 보낼 수 있음 (이벤트 / 핸들러) TextField placeholderText: property 제공 ProgressBar value 값을 통해 진행률 표시 가능 진행의 끝을 알 수 없으면 inderterminate: true; 프로퍼티 사용 ScrollView 내용이 많을 때 스크롤뷰에 내용을 넣으면 스크롤하며 내용을 확인할 수 있음 QT QML의 스크롤뷰에 기본적으로 bouncing 효과(스크롤 끝에 도달하면 overscroll되며 튕기는 효과)가 적용되어 있는데, 이를 비활성화시키는 방법은 다음과 같다. ScrollView { id: root ... Component.onCompleted: { root.contentItem.boundsBehavior = Flickable.StopAtBounds } } 버전에 따라 지원되지 않는 경우도 있다는데, 그 때는 아래 방법을 사용해본다. ScrollView { ... Binding { target: scrollView.contentItem property: "boundsBehaviour" value: Flickable.StopAtBounds } } > Window 계열 컴포넌트 Window 독립적인 윈도우 자체적인 렌더링 컨텍스트 소유 modal : 다른 창 조작 금지 유무 flags : 윈도우 flag (https://doc.qt.io/archives/qt-5.15/qt.html#WindowType-enum) Window { visible: true width: 300 height: 2200 title: "Custom Window" flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint Text { text: "This window has custom flags!" anchors.centerIn: parent } } `ApplicationWindow` - 메뉴 바, footer, header 등의 윈도우 구성 요소 제공 - Window를 상속받아 만들어짐 ApplicationWindow { visible: true width: 640 height: 480 title: "My Application" Text { text: "Hello, World!" anchors.centerIn: parent } } `Tooltip` - Button 등에서 지원 Button { text: "Hover me" // 마우스 커서가 버튼 위에 올라와 있는 상태일 때 Tooltip 표시 ToolTip.visible: hovered ToolTip.text: "This is a tooltip!" } `Popup` - 사용자에게 추가 입력을 요청할 때 등에 임시 UI로 사용 Popup { id: myPopup width: 200 height: 200 modal: true focus: true Rectangle { color: "white" border.color: "black" anchors.fill: parent Text { text: "This is a popup" anchors.centerIn: parent } } } Button { text: "Show Popup" onClicked: myPopup.open() } > Position Manual Position x, y, z를 통해 부모 컴포넌트의 좌표계에 어디에 위치할지 결정 (좌상단 기준) 같은 z값(depth)에 있는 컴포넌트를 sibling으로 명칭함 수동으로 위치를 조정하는 것이기 때문에 반응형과 같은 디자인에서 적합하지 않고 디버깅이 어려움. Anchor 한 요소의 한 측면(상/하/좌/우)을 다른 요소의 특정 측면에 고정함 <img src="https://velog.velcdn.com/images/usfree/post/90449eb8-59d1-4b0b-9277-2fd4468eb044/image.png" style="width: 60%"> anchors.centerIn을 통해 요소의 정가운데에 위치 가능 anchors { horizontalCenter : parent.horizontalCenter verticalCenter: parent.verticalCenter } anchors.centerIn : parent //정 가운데 anchors.fill: parent //부모의 요소를 가득 채움 - 서로 충돌하는 앵커를 설정하면 안됨. ex) anchors.left와 anchors.right를 동시에 설정 - col, row, grid layout 등에서는 anchor가 설정이 되지 않음. Column / Row Row는 자식 요소를 수평 방향으로 일렬로 정렬하고, 내부적으로 크기와 위치를 유지함. Column은 수직 방향으로 일렬 정렬, Row와 마찬가지로 자식들의 크기와 위치를 유지함. spacing을 통해 자식 요소간 간격 설정이 가능함. Flow 자식 요소들을 수평으로 배치하고, 공간이 부족해지면 (부모 요소의 크기가 작아지면) 다음 줄로 넘어가는 방식으로 정렬 컨테이너 크기에 따라 자식 요소를 동적으로 재배치해주는 것 anchor의 지정이 필요함 Flow { anchors.fill: parent spacing: 10 width: 300 } `GridLayout` - 자식을 그리드 형태로 정렬해줌 - Flow와 다르게 컨테이너의 크기를 줄어들어도 개행이 일어나지는 않음. GridLayout { anchors.fill: parent columns: 3</p> //default Spacing값 = 5 rowSpacing: 10 columnSpacing: 10 //첫 번째 행 Rectangle { color: "red" width: 100 height: 100 Layout.row: 0 Layout.column: 0 } Rectangle { color: "green" width: 100 height: 100 Layout.row: 0 Layout.column: 1 } Rectangle { color: "blue" width: 100 height: 100 Layout.row: 0 Layout.column: 2 } //두 번째 행 Rectangle { color: "black" width: 100 height: 100 Layout.row: 1 Layout.column: 0 } Rectangle { color: "lightpink" width: 100 height: 100 Layout.row: 1 Layout.column: 1 } Rectangle { color: "lightblue" width: 100 height: 100 Layout.row: 1 Layout.column: 2 } } `StackLayout` - 여러 컴포넌트를 쌓아 놓고, 각 시점에 하나의 컴포넌트만 보여줌. (사용자가 여러 페이지나 뷰를 전환할 수 있음) - currentIndex 속성을 통해 현재 활성화된 자식 컴포넌트를 제어 StackLayout { id: stackLayout width: 300 height: 300</p> Rectangle { color: "lightblue" width: parent.width height: parent.height } Rectangle { color: "yellow" width: parent.width height: parent.height } } Button { text: "Newt Page" onClicked: { stackLayout.currentIndex = (stackLayout.currentIndex + 1) % stackLayout.children.length } } ### Mouse Event `MouseArea` - 마우스 Area에 클릭/커서 위치 변경/드래그 등 다양한 마우스 이벤트를 캡쳐해서 행동 생성 가능 각종 이벤트 MouseArea { anchors.fill: parent onClicked: { console.log("Mouse Clicked at: ", mouse.x, ".", mouse.y) } onDoubleClicked: { } onEntered: { } onExited: { } onPositionChanged: { } } - Drag & Drop Rectangle { id: draggableRect width: 100 height: 100 color: "lightpink" x: 10 y: 10 z: 1</p> MouseArea { id: dragArea anchors.fill: parent drag.target: draggableRect onReleased: { if (dropRect.contains(mapToItem(dropRect, dragArea.mouseX, dragArea.mouseY))) { dropRect.color = "black" dropRect.name = "Dropped Here" } } } } Rectangle { id: dropRect width: 200 height: 200 color: "lightgray" anchors.centerIn: parent property string name: "Drop Here" Text { id: dropText text: dropRect.name color: "green" anchors.centerIn: parent } } `TapHandler` 터치도 함께 인식함 Rectangle { width: 100 height: 100 color: "lightpink" TapHandler { onTapped: console.log("Rect Tappedd"); onDoubleTapped: console.log("Rect double Tapped"); onLongPressed: console.log("Rect long Pressed"); } } `HoverHandler` - 마우스 호버 이벤트 처리 Rectangle { width: 100 height: 100 color: "lightpink"</p> Text { id: rectText text: "Hover me" anchors.fill: parent font.pointSize: 14 } HoverHandler { id: hoverHandler acceptedDevices: PointerDevice.Mouse cursorShape: Qt.PointingHandCursor } } `QML Function` - function 키워드를 통해 함수 작성 가능 - 함수는 컴포넌트 내에서 로컬 범위로 접근 - 슬롯이나 속성 바인딩에서 호출될 수 있음 Rectangle { id: rectangle //width: 200 //height: 200 anchors.fill: parent</p> //vanila js 문법 사용 function calculateArea(width, height) { return width * height } Text { id: areaText //property에 함수 바인딩 text: "Area: " + rectangle.calculateArea(rectangle.width, rectangle.height) } } - *.js 파일에 함수 별도 정의 /* mathFunctions.js */ .pragma library function add(x, y) { return x+y; } function multiply(x, y) { return x*y; } /* main.qml */ import "mathFunctions.js" as MathFunc //as 별칭은 대문자로 시작 Window { width: 640 height: 480 visible: true title: qsTr("Example") Item { Component.onCompleted: { // 해당하는 UI 컴포넌트가 생성되었을 때, 수행할 액션 정의 console.log("3+5 =", MathFunc.add(3, 5)) console.log("3*5 =", MathFunc.multiply(3, 5)) } } } `Signal ↔ Slot` Signal: 어떤 이벤트가 발생했을 때 발송되는 메세지 Slot: Signal을 수신하여 특정 동작을 수행하는 함수 1Depth 예시 Item { anchors.fill: parent Rectangle { width: 200; height: 100; color: "lightblue" signal buttonClicked(string msg) // msg : string onButtonClicked: (msg) => { console.log(msg) } Button { text: "Click me" anchors.centerIn: parent onClicked: { parent.buttonClicked("Button was clicked") } } } } - 2Depth 예시 Item { anchors.fill: parent</p> Rectangle { id: parentRect width: 300; height: 200; color: "lightblue" signal parentSignal(string msg) onParentSignal: (msg) => { console.log("Received in parent:", msg) // 4) 부모 시그널 수신 } Rectangle { id: childRect width: 200; height: 100 color: "lightpink" anchors.centerIn: parent signal childSignal(string msg) onChildSignal: (msg) => { console.log("Received in child:", msg) // 2) 자식 시그널 수신 parentRect.parentSignal("Parent received child's signal") // 3) 부모 시그널 발생 } MouseArea { anchors.fill: parent onClicked: childRect.childSignal("Child was clicked") // 1) 자식 시그널 발생 } } } } `Custom QML Component` - Custom Button 예시 /* CustomButton.qml */ import QtQuick 2.15 Rectangle { id: root width: 100 height: 40 color: "lightpink" radius: 10 signal clicked() //onClicked 동작 순서 (2) Text { text: "Click Button" anchors.centerIn: parent } MouseArea { anchors.fill: parent onClicked: { //onClicked 동작 순서 (1) root.clicked() root.color = "cyan" } } } /* main.qml */ CustomButton { anchors.centerIn: parent onClicked: { //onClicked 동작 순서 (3) console.log("Button Clicked") } } - State 관리를 가진 Advanced Button 예시 /* AdvancedButton.qml */ Rectangle { id: root width: 100; height: 40 color: "lightpink" radius: 4 border.color: "black" property string label: "Clicked Me" Text { text: root.label anchors.centerIn: parent } MouseArea { anchors.fill: parent onClicked: root.state = ((root.state === "pressed") ? "default" : "pressed") } state: "default" states: [ State { name: "default" PropertyChanges { target: root color: "lightpink" border.color: "black" } }, State { name: "pressed" PropertyChanges { target: root color: "black" border.color: "yellow" } } ] } //Animation Transition transitions: [ Transition { from: "default" to: "pressed" ColorAnimation { target: root property: "color" duration: 200 } }, Transition { from: "pressed" to: "default" ColorAnimation { target: root property: "color" duration: 100 } } ]

2025.11.02.

글 보기

유니티 맵 청크(Chunk) 시스템 (feat. 잉걸불)

Embers 게임은 오픈 월드를 기반으로 하는 MMORPG 게임이다. 이 때문에 맵을 어디까지 로딩할지, 어떻게 로딩할지는 메모리 관리 등의 성능적인 측면에서 매우 중요한 문제이다. 이 게임에서는 청크 시스템 을 사용하기로 했다. 청크 시스템은 플레이어의 위치(사진상 중앙)를 기준으로 주변 1칸에 해당하는 맵만 로딩하여 메모리 효율성을 높이는 것이다. 각 맵은 별도의 씬(Scene)들로 저장되어, 플레이어의 위치가 이동할 때마다 필요없는 씬은 Unload시키고, 필요한 씬은 load시키는 것이 핵심이다. Embers에서는 이런식으로 구현되어 동작한다. 현재는 정사각형 형태로만 구현했지만 지형 특성이나 맵 디자인에 따라 지형을 다르게하고, 씬만 분리해주면 된다. 영상과 같이, 씬의 경계를 넘어갈 때(현재 맵의 이름이 중앙에 표시될 때) 새로운 씬이 멀리서 로드된 것이 보일 것이다. 청크 로딩은 다음과 같이 구현했다. public void RequireUpdateChunk(Vector3 playerPosition) { UpdateChunks(playerPosition).Forget(); } private async Awaitable UpdateChunks(Vector3 playerPosition) { Vector2Int currentChunkCoord = GetChunkCoord(playerPosition); // playerPosition값 기준으로 필요한 청크 계산 List<Vector2Int> chunksToLoad = new List<Vector2Int>(); for (int x = -(Singleton.Game.LoadRange); x <= Singleton.Game.LoadRange; x++) { for (int z = -(Singleton.Game.LoadRange); z <= Singleton.Game.LoadRange; z++) { Vector2Int coord = new Vector2Int(currentChunkCoord.x + x, currentChunkCoord.y + z); chunksToLoad.Add(coord); } } // 1) 필요한 청크 로드 foreach (var coord in chunksToLoad) { if (!chunkStates.ContainsKey(coord) || chunkStates[coord] == ChunkLoadState.NONE) { string sceneName = $"Chunk_{coord.x}_{coord.y}"; // **여기서 chunkList의 ChunkInfo에 sceneName이 있는지 검사** bool existsInList = chunkList.chunkSceneNames.Any(ci => ci.sceneName == sceneName); if (!existsInList) { chunkStates[coord] = ChunkLoadState.LOADED; continue; } await LoadChunk(sceneName, coord); } } // 2) 필요 없는 청크 언로드 var loadedCoords = new List<Vector2Int>(chunkStates.Keys); foreach (var coord in loadedCoords) { if (!chunksToLoad.Contains(coord)) { if (chunkStates[coord] == ChunkLoadState.LOADED) { string sceneName = $"Chunk_{coord.x}_{coord.y}"; await UnloadChunk(sceneName, coord); } } } //플레이어를 InGame씬으로 이동 if (NetworkClient.localPlayer != null) { Scene inGameScene = SceneManager.GetSceneByName("InGame"); if (inGameScene.isLoaded) { SceneManager.MoveGameObjectToScene(NetworkClient.localPlayer.gameObject, inGameScene); } } // 현재 활성화된 씬 설정 string activeSceneName = $"Chunk_{currentChunkCoord.x}_{currentChunkCoord.y}"; SceneManager.SetActiveScene(SceneManager.GetSceneByName(activeSceneName)); var chunkInfo = chunkList.GetChunkInfo(activeSceneName); if (chunkInfo != null && chunkInfo.bgm != null) { PlayBGM(chunkInfo.bgm).Forget(); } } private async Awaitable LoadChunk(string sceneName, Vector2Int coord) { chunkStates[coord] = ChunkLoadState.LOADING; var loadOp = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); while (!loadOp.isDone) { await Awaitable.NextFrameAsync(); } chunkStates[coord] = ChunkLoadState.LOADED; } private async Awaitable UnloadChunk(string sceneName, Vector2Int coord) { chunkStates[coord] = ChunkLoadState.UNLOADING; if (true == SceneManager.GetSceneByName(sceneName).isLoaded) { var unloadOp = SceneManager.UnloadSceneAsync(sceneName); while (unloadOp != null && !unloadOp.isDone) { await Awaitable.NextFrameAsync(); } } chunkStates[coord] = ChunkLoadState.NONE; } public Vector2Int GetChunkCoord(Vector3 position) { int x = Mathf.FloorToInt(position.x / Singleton.Game.ChunkSize); int z = Mathf.FloorToInt(position.z / Singleton.Game.ChunkSize); return new Vector2Int(x, z); } 이 게임은 멀티플레이 게임이기 때문에 네트워킹에 관련된 처리도 필요했다. 플레이어는 씬의 경계를 넘어갈 때마다 RequireUpdateChunk 를 요청하고, 현재 플레이어의 위치에 따라 순차적으로 필요한 씬 로드 → 필요 없는 씬 언로드 → 플레이어 Object의 하이어라키상 위치를 현재 위치한 씬으로 이동 의 프로세스를 거치게 된다. 부가적으로, BGM 관리 부분도 추가했다. 현재 재생중인 clip을 저장하고, 다른 씬이어도 BGM은 같을 수 있기 때문에(같은 대단원 지역이라면) BGM의 정보가 변경될 때만 재생 clip이 변경되도록 하였다. public async Awaitable PlayBGM(AudioClip bgmClip, float fadeDuration = 1.0f) { if (bgmClip == null) return; //현재 재생중인 BGM과 동일하면 계속 재생(같은 노래 새롭게 시작하는 문제 방지) if (_currentBGMName == bgmClip.name) return; _currentBGMName = bgmClip.name; if (_audioSource.isPlaying) { await FadeOut(fadeDuration); } _audioSource.clip = bgmClip; _audioSource.Play(); await FadeIn(fadeDuration); } public async Awaitable StopBGM(float fadeDuration = 1.0f) { if (_audioSource.isPlaying) { await FadeOut(fadeDuration); _audioSource.Stop(); _currentBGMName = null; } } private async Awaitable FadeOut(float duration) { float startVolume = _audioSource.volume; for (float t = 0; t < duration; t += Time.deltaTime) { _audioSource.volume = Mathf.Lerp(startVolume, 0, t / duration); await Awaitable.NextFrameAsync(); } _audioSource.volume = 0; } private async Awaitable FadeIn(float duration) { float startVolume = _audioSource.volume; _audioSource.volume = 0; for (float t = 0; t < duration; t += Time.deltaTime) { _audioSource.volume = Mathf.Lerp(0, 1, t / duration); await Awaitable.NextFrameAsync(); } _audioSource.volume = 1; } 이렇게 동적으로 씬을 로딩하게 될 때, 새롭게 로드되는 씬이 조금 더 부드럽게 로드할 수 있는 방안도 있을 것 같다. 가령 새로 로딩되는 씬의 모든 Object를 순차적으로 로드해주거나, Visible 속성을 사용하는 등 말이다. 네트워킹이 지원되는 오픈월드 게임인만큼, 앞으로도 최적화에 특히 신경을 쓰면서 개발할 예정이다.

2025.09.09.

글 보기

Modern C++ Summary (vs. C#)

개인적으로 공부하면서, C#과 같이 사용하면서 헷갈릴 수 있는 부분(C++과 다른 점)과 C++에서 특정 버전 이상에서만 지원하는 문법을 정리한다. C++ 버전별 주요 특징 Modern C++은 C++11 이후 버전을 의미한다. typedef typedef double my_type_t; == using my_type_t = double; (C++11 이상) enum enum { }; enum class { }; (C++11 이상) //c#의 enum과 같이 사용하려면 enum class를 사용한다. //enum은 스코프를 사용하지 않지만 enum class에서는 C#과 마찬가지로 스코프를 사용한다. string //c++의 string은 std 라이브러리에 포함된다. //cin으로 문자열을 온전히 받기 위해선 std::getline(std::cin, {변수명}) //이 사용된다. //cin으로 입력받은 버퍼를 비우기 위해 std::cin.ignore(std::numeric_limitsstd::streamsize::max(), '\n'); == std::cin.ignore(32767, '\n'); //사용이 필요하다. 코드에서 긴급한 탈출 (halt) HALT → exit(0); 랜덤 난수 생성 #include <cstdlib> std::srand(5323); //5323 = seed → 시드 넘버 지정 std::srand(static_cast<unsigned int>(std::time(0)); // 시간과 연동하여 시드가 계속 변경됨 std::rand(); #include <random> (C++11 이상) std::random_device rd; std::mt19937_64 mesenne(rd()); // or mt19937 std::uniform_int_distribution<> dice(1 ,6); // 1~6까지 같은 확률 cin 활용 # 버퍼 지우기 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); == std::cin.ignore(32767, '\n'); 입력 범위 문제 확인하기 (둘이 같이 사용됨.) std::cin.fail(); // return true / false std::cin.clear(); // 내부 상태 플래그 초기화 배열과 포인터 배열을 정의하면 배열의 이름 자체가 주소가 된다. 다만 배열을 함수의 인자로 넘겨, 매개변수로 배열을 받는다면 이 매개변수는 '포인터'로 취급된다. 때문에 매개변수로 받은 배열의 이름(포인터)은 배열의 주소를 저장하는 다른 주소가 된다. → 함수에서 이 (배열로 보이지만 포인터 변수인)배열의 sizeof를 찍으면 4바이트(32비트, 64비트에서는 8바이트)로 출력된다. 포인터를 그냥 선언해서는 변수값을 담을 수 없다.<blockquote> char *name = "nolda"; (X) 변수값을 담기 위해선 const 선언을 넣으면 사용할 수 있다. const char *name = "nolda"; (O) </blockquote> //참고 (*this).memberValue; == this->memberValue; foreach C#의 foreach와 사용법은 비슷하지만 문법의 차이가 존재한다. for (int value : array) { } 메모리 할당 정적으로 할당된 메모리는 stack에 저장되며, stack은 용량이 작고 컴파일 타임에 크기가 결정된다. 동적으로 할당된 메모리는 Heap에 할당된다. Heap 영역은 런타임에 결정된다. Stack / Heap Memory의 Segment 1. Code - Program 2. BSS - uninitialized data 3. Data - initialized data 4. Stack - 로컬 변수, 메인 함수, 메소드 등이 스택으로 쌓임 - 사이즈가 작기 때문에 Stack Overflow 발생 가능 5. Heap - 동적 할당일 경우 Heap 영역에 저장됨 const와 포인터 int value = 5; const int *ptr1 = &value; //ptr1에 저장돼있는 주소를 바꾸는건 가능하지만, 주소가 가리키는 value 값을 바꾸는건 안됨 int *const ptr2 = &value; //ptr2에 저장돼있는 주소 값 바꾸는게 안됨 const int *const ptr3 = &value; //주소 값도 바꿀 수 없고 de-referencing으로 값을 바꿀 수도 없음 (다 안됨) std::vector #include <vector> std::vector<int> array_value; 동적할당 배열에 유용하게 쓰이고 널리 쓰임. C#의 List와 비슷하다. //size : 사용하는 용량 -> .resize() //capacity : 총 용량(size에서 가려진 총 용량) -> .reserve //vector를 stack처럼 사용하기 .push_back();, .pop_back() std::tuple 여러 개의 반환 값을 return 시킬 수 있다. #include <tuple> std::tuple<int, double> getTuple() { return std::make_tuple(5, 3.14); } int main() { std::tuple<int, double> tp = getTuple(); std::get<0>(tp); //int 값 std::get<1>(tp); //double 값 //C++17 이상에서는 아래가 가능하다. auto[a, b] = getTuple(); } 함수포인터 int func() { return 5; } int func2() { return 9; } //함수포인터 변수 선언 int(*fcnptr)() = func; -> int(*변수이름)(매개변수) //다른 함수 할당 fcnptr = func2; //functional Library (C++11 이상) #include <functional> std::functional<리턴타입(매개변수)> fcnptr = func; 일립시스 (Ellipsis) //매개변수의 갯수제한을 두지 않고 받는 방법 double findAverage(int count, ...) //count : 매개변수 갯수 { double sum = 0; va_list list; var_start(list, count); for (int arg = 0; arg < count; ++arg) { sum += va_arg(list, int); } var_end(list); return sum / count; } 연쇄호출(Chaining) class 내부 함수의 리턴을 class 자신의 reference 값으로 설정하면, 연쇄적으로 호출이 가능하다. class Calc { int m_value; Calc& Add(int value) { m_value += value; return *this; } Calc& Sub(int value) { m_value -= value; return *this; } }; int main() { Calc cal(10); cal.Add(10).Sub(20).Add(10); } friends keyword class에서 다른 함수의 선언부를 가져와 friend 선언을 해주면 해당 함수에서는 선언된 class의 private 멤버에 접근할 수 있다. 각자 다른 class에서 공통 함수에 friend를 선언할 경우 전방선언이 필요할 수 있음. 순서에 의해 friend 함수에서 멤버 변수 등을 인식하지 못하면, class에는 선언부만 남겨두고, 인식하지 못한 class의 하단에 구현부를 넣어준다. 익명 변수 class A { void print() { cout << "A Print" << endl; } }; int main() { A().print(); A().print(); //위의 객체와 다르기 때문에 생성자와 소멸자가 각각 호출됨. } 연산자 오버로딩 //산술 연산자 class Cents { private: int m_cents; public: int& getCents() { return m_cents; } Cents operator + (const Cents &c2) //멤버 함수 { return Cents(this->m_cents + c2.m_cents); } friend Cents operator + (const Cents &c1, const Cents &c2) //friend 함수 { return Cents(c1.getCents() + c2.getCents()); } }; //입출력 연산자 class Point { private: double m_x, m_y, m_z; public: Point(double x = 0.0, double y = 0.0, double z = 0.0) : m_x(x), m_y(y), m_z(z) { } //outstream friend std::ostream& operator << (std::ostream &out, const Point &point) { out <<<< point.m_x << ", " << point.m_y << ", " << point.m_z; return out; } //instream friend std::istream& operator >> (std::istream& in, Point& point) { in >> point.m_x >> point.m_y >> point.m_z; return in; } }; int main() { Point p1(0.0, 0.1, 0.2), p2(3.4, 1.5, 2.0); Point p3, p4; cout << p1 << " " << p2 << endl; cin >> p3 >> p4; } //단항 연산자 Cents operator - () const { return Cents(-m_cents); } bool operator ! () const { return (m_cents == 0 ? true : false); } //비교 연산자 friend bool operator == (const Cents& c1, const Cents& c2) { return c1.m_cents == c2.m_cents; } friend bool operator < (const Cents& c1, const Cents& c2) { return c1.m_cents < c2.m_cents; } //증감 연산자 Cents& operator ++ () //prefix { ++m_cents; return *this; } Cents& operator ++ (int) //postfix { Cents temp(m_cents); ++(*this); return *temp; } //첨자 연산자 [] class IntList { private: int m_list[10]; public: int& operator [] (const int index) { return m_list[index]; } const int& operator [] (const int index) const { return m_list[index]; } }; int main() { IntList list; list[3] = 10; //if IntList *list = new IntList; list[3] = 10; //X (*list)[3] = 10; //O } //형변환 operator int() { return m_cents; } explicit / delete class Fraction { int m_numerator; int m_denominator; Fraction(char) = delete; //사용 못하게 막음 (explicit) Fraction(int num = 0, int den = 1) : m_numerator(num), m_denominator(den) { } }; void doSomething(Fraction frac) { cout << frac << endl; } int main() { doSomething(7); //원래는 컴파일러에서 Fraction(7)처럼 변환해줌 //만약 Fraction 생성자에 explicit 키워드가 앞에 붙는다면 불가능해짐 } class 깊은 복사 주의점 //class의 멤버변수로 char *m_data = nullptr;가 정의되어 있을 때, //이 class를 기본 복사 생성자를 통해 복사하면 새로운 class instance에도 같은 포인터 주소를 가리킨다. //이 때, 새로운 class instance가 삭제되면, 소멸자에서 해당 포인터 변수가 가리키는 값을 지워버리게 되고, //원본의 데이터까지 지워버리는 상황이 발생한다. (얕은 복사) //이 때문에, 깊은 복사를 위해선 별도의 복사 생성자를 정의해줘야 한다. MyString(const MyString &source) //복사 생성자 (깊은 복사) { m_length = source.m_length; if (source.m_data != nullptr) { m_data = new char[m_length]; for (int i = 0; i < m_length; ++i) m_data[i] = source.m_data[i]; } else m_data = nullptr; } } //operator = (대입 연산자) 의 경우에도 비슷하게 정의해줄 수 있음. MyString& operator = (const MyString &source) { if (this == &source) return *this; delete[] m_data; //기존에 갖고있던 메모리 할당 해제 m_length = source.m_length; if (source.m_data != nullptr) { m_data = new char[m_length]; for (int i = 0; i < m_length; ++i) m_data[i] = source.m_data[i]; } else m_data = nullptr; } } //std::string을 사용하면 필요 없는 일이다. Initializer List 생성자 IntArray(unsigned length) : m_length(length) { m_data = new int[length]; } IntArray(const std::initializer_list<int> &list) : IntArray(list.size()) { int count = 0; for (auto & element : list) { m_data[count] = element; ++count; } } 상속 관련 Keyword //virtual 상속 구조에서 부모 클래스의 메소드에 virtual 키워드를 사용시 자식 클래스의 객체를 부모 클래스의 포인터에 넣어서 호출해도 자식클래스의 메소드가 호출됨. 부모 클래스의 함수를 자식 클래스에서 오버라이딩한 것으로 인식 가상 소멸자 부모 클래스 객체에 자식 클래스를 넣을 경우, 자식 클래스의 동적 할당된 메모리를 지우기 위해 소멸자에도 virtual 키워드를 붙여주면 자식 클래스의 소멸자도 실행됨. //override 상속 구조에서 자식 클래스의 함수 매개변수 끝에 override 키워드 작성시 오버로딩이 아닌 오버라이드를 의도한것이라고 인식하게 하는 키워드 //final final 키워드 사용시 자식 클래스에서 더 이상 오버라이딩할 수 없음 다이아몬드 상속 문제 A라는 부모클래스를 통해 B와 C클래스를 상속받아 생성할 때, 상속 접근제한자에 virtual을 붙여주지 않으면 B와 C클래스 각각 다른 A클래스를 상속받는 문제가 발생할 수 있음. class B : virtual public A 와 같이 상속받아야 함. Object Slicing 부모 클래스로부터 상속받아 생성된 자식클래스에 새로운 변수가 있는데, 부모 클래스에 자식 클래스 인스턴스를 넣어버린 경우 데이터 슬라이싱 발생 std::vector를 사용하는 경우 std::vector<std::reference_wrapper<부모 클래스>> vec; 을 사용할 수 있다. dynamic cast Derived d1; Base *base = &d1; auto *base_to_d1 = dynamic_cast<Derived1*>(base); Base로 형변환 됐던 변수를 다시 Derived로 형변환 dynamic_cast의 경우 에러 체크를 통해 에러일 경우 nullptr 반환함. (안전한 형변환) static_cast는 에러 체크를 하지 않음. Template //함수 템플릿 template<typename T> T getMax(T x, T y) { return (x > y) ? x : y; } //클래스 템플릿 <*.h> template<typename T> class MyArray { private: int m_length; T *m_data; public: MyArray(int length) { m_lenth = length; m_data = new T [length]; } void print(); } ... <*.cpp> template<typename T> void MyArray<T>::print() { ... } template void MyArray<char>; //템플릿 클래스의 멤버함수의 구현부를 cpp 파일로 옮길 경우 explicit instantiation 필요 smart pointer: auto_ptr (Regacy) std::auto_ptr<int> //c++98 ~ c++11까지 존재 c++17부터 제거 //auto_ptr의 구조 template<typename T> class AutoPtr { public: AutoPtr(T* ptr = nullptr) : m_ptr(ptr) { } ~AutoPtr() { if (m_ptr != nullptr) delete m_ptr; } }; 이동(move semantics)의 의미 → auto_ptr의 변수는 복사하면 '이동'이 된다. A변수와 B변수가 존재할 때, B에 A를 대입하면 A는 nullptr을 갖게된다. auto_ptr은 배열에 사용할 경우 배열 이름 즉, 배열[0]요소에 대해서만 delete를 하는 치명적인 단점을 갖고있다. R-value Reference int x = 5; const int cx = 5; //int &&rr1 = x; (X) //int &&rr2 = cx; (X) int &&rr3 = 5; L Value Reference와 다르게 메모리가 할당되지 않은 값을 할당 가능 (곧 사라질 값들만 할당 가능) std::move AutoPtr<Resource> res2 = std::move(res1); //res1이 R-value임을 인식시켜줌 //이렇게 처리한 경우 res1을 사용하지 않는다는 의미 template<class T> void MySwap(T &a, T &b) { //Copy constructor T tmp = a; a = b; b = tmp; //Move Semantics T tmp { std::move(a) }; a = std::move(b); b = std::move(tmp); } //AutoPtr class에 Move Semantics를 정의했음 example) vector<string> v; string str = "Hello"; v.push_back(str); //L-value cout << std << endl; //Hello cout << v[0] << endl; //Hello v.push_back(std::move(str)); //R-value cout << str << endl; // 공백 cout << v[0] << " " << v[1] << endl; // Hello Hello std::unique_ptr / std::make_unique (주로 사용되는 smart pointer) #include <memory> std::unique_ptr<Resource> res(new Resource(10000)); 영역 밖을 벗어나 사용되지 않으면 자동으로 소멸됨 auto res1 = std::make_unique<Resource>(5); 권장되는 초기화 방식 res2 = res1; (x) res2 = std::move(res1); (O) unique pointer는 L-value 복사가 되지 않음 void doSomething(std::unique_ptr<Resource> res) { } doSomething(std::unique_ptr<Resource>(new Resource(1000))); (X) doSomething(std::make_unique<Resource>(1000)); (O) 위의 방식을 쓰면 컴파일러에 따라 문제가 발생할 수 있음 (parameter에서 new X) std::shared_ptr / std::make_shared std::shared_ptr<Resource> ptr1(res); ... { std::shared_ptr<Resource> ptr2(ptr1); std::shared_ptr<Resource> ptr2(res); //이렇게 사용하면 ptr1이 res의 소유권이 다른 데에도 있다는 것을 알 수가 없음 } ptr2의 블럭을 벗어나도, ptr1은 존재한다. auto ptr1 = std::make_shared<Resource>(3); std::weak_ptr class 내부에서 shared_ptr을 통해 서로를 참조시키면, memory leak이 남아있는 채로 종료됨 (순환 참조 문제) weak_ptr은 단독으로 사용할 수 없고, lock()을 해서 shared_ptr로 return해줘야 한다. const std::shared_ptr<Person> getPartner() const { return m_partner.lock(); //std::weak_ptr<Person> m_partner; } STL(Standard Template Library) CPP Reference https://cppreference.com/index.html Algorithms, Containers, Functions, Iterators를 포함 - std::set<T> //집합. 내부 원소가 겹치면 무시 set.insert("Hello"); set.insert("World"); set.insert("Hello"); > "Hello World" std::multiset<T> // 중복 원소 허용 집합 std::map<key, value> // key값에 sort 돼있음 .first // key 출력 .second // value 출력 std::multimap<key, value> // 중복 키값 허용 map multimap.insert(std::pair('a', 10)); //Before c++14, pair<char, int>('a', 10) std::stack .push // push adds a copy .emplace // constructs a new object std::queue std::priority_queue //sort 해주는 queue, 사용자 지정 클래스를 우선순위 큐로 만들면, 크기 비교 연산자 오버로딩을 해줘야함 STL Iterator (반복자) vector<int> container; for (int i = 0; i < 10; ++i) container.push_back(i); vector<int>::const_iterator itr; //vector<int>::iterator itr; itr = container.begin(); while(itr != container.end()) { cout << *itr << " "; ++ itr; } 어떤 컨테이너든 같은 코드로 순회가 가능하기 때문에 사용한다. → vector 를 list 또는 set 등으로 바꿔도 바로 동작함. for (auto itr = container.begin(); itr != container.end(); ++itr) cout << *itr << " "; for (auto &e : container) cout << e << " "; 모두 동일하게 동작한다. std::string / std::wstring wide-string : 글로벌 std::locale 사용시 주로 사용 (다양한 Unicode 지원) //int 값을 string으로 변환 → 문자열로 처리됨 std::string my_str(std::to_string(4)); //string을 int값으로 변환 int i = std::stoi(my_str); //std::ostringstream (output) / std::istringstream (input) string도 vector와 마찬가지로 길이와 용량이 다르다. C와 다르게, string의 뒤에는 null값 ('\0')이 포함되지 않는다. string.length() string.size() string.capacity() //용량 string.max_size() //최대 크기 string.reserve(1000) //용량 확보 (최소 용량) string my_str("abcdefg"); try { my_str[100] = 'X'; → 예외처리 X my_str.at(100) = 'X'; → 예외처리 O } catch { } my_str.c_str() == .data() → C 스타일로 사용할경우 마지막에 null값 ('\0') 포함 .append() → string의 끝에 문자열 붙이기 istream (Input Stream) #include <iomanip> char buf[10] cin >> setw(5) >> buf; //최대 5글자만 받도록 해줌. <iomanip> include 필요 setw()으로 글자수 제한을 하면, cin 버퍼에 남아있기 때문에 계속 존재함. cin이 빈칸을 구분자로 사용하지만, 빈칸까지 읽게하려면 cin.get(var) 을 사용 cin.get() 으로 읽은 후, cin.gcount() 를 통해 몇글자를 읽었는지 확인 가능 cin.getline() 은 라인 단위로 읽음. → 줄바꿈 문자까지 같이 읽어짐 ('\n') string buf; getline(cin, buf); cin.ignore() 는 한 글자를 무시한다. cin.peek() 은 버퍼에서 꺼내지 않고, 다음에 올 글자를 확인함 cin.unget() 은 마지막에 읽은 문자를 버퍼로 다시 넣음 cin.putback() 은 원하는 글자를 버퍼에 넣음 ostream (Output Stream) cout.setf(std::ios::showpos) 는 기호 (+, -)를 숫자 앞에 표시한다. cout.unsetf() 은 위의 플래그 삭제 cout.setf(std::ios::hex, std::ios::basefield) 는 16진수로 출력 == cout cout.setf(std::ios::uppercase) 는 16진수의 영문자를 대문자로 표시 cout << std::boolalpha 를 통해 bool 값 출력 cout << std::setprecision(3) 은 소숫점 자릿수 설정 cout << std::fixed 는 소숫점 자릿수 고정 cout << std::scientific 은 부동 소수점 방식 표기법 cout << std::showpoint 소수점 '.' 표기 cout << std::setw(10) << std::left(right / internal) << -12345 << endl; //출력 정렬 cout.fill("*") 빈 칸을 별로 채워줌 sstream (String Stream) #include <sstream> stringstream os; os << "Hello World"; // 버퍼에 덧붙임 os.str("Hello World"); //버퍼 치환 string str; str = os.str(); //os.str(""); 파라미터로 공백을 넣으면 치환됨 os >> str; cout << str; stream state void printStates(const std::ios& stream) { stream.good(); stream.eof(); stream.fail(); stream.bad(); } void printCharacterClassification(const int& i) { bool(std::isalnum(i)); bool(std::isblank(i)); bool(std::isdigit(i)); bool(std::islower(i)); bool(std::isupper(i)); // → return 값이 int이므로 bool로 캐스팅 } Regular Expressions (정규 표현식) #include <regex> // C++11 부터 지원 regex re("\d"); //digit 1개 == regex re("[[:digit:]]{1}"); regex re("\d+"); //1개 이상의 숫자 regex re("[ab]"); //a, b만 regex re("[A-Z]{1, 5}"); //1개 이상 5개 이하의 A-Z 문자 regex re("([0-9]{1})([-]?)([0-9]{1,4})"); // 0-9 숫자 1개 + '-'이 있어도 되고 없어도됨 + 0-9 숫자 1개이상 4개 이하 //regex_match를 통해 매치되는지 판별 string str; getline(cin, str); if (std::regex_match(str, re)) cout << "match" << endl; else cout << "No match" << endl; //매치되는 것만 출력 auto begin = std::sregex_iterator(str.begin(), str.end(), re); auto end = std::sregex_iterator(); for (auto itr = begin; itr != end; ++itr) { std::smatch match = *itr; cout << match.str() << " "; } cout << endl; fstream (File Stream - 파일 입출력) #include <fstream> //ASCII code - outputstream ofstream ofs("my_first_file.dat"); // ostream ofs("my_first_file.dat", ios::app) → append mode : 데이터를 추가 //ofs.open("my_first_file.dat"); ofs << "File Detail" << endl; //ofs.close() → 영역을 벗어나면 소멸자가 닫아줌. 수동으로 처리할 필요 X //ASCII code - inputstream ifstream ifs("my_first_file.dat"); while (ifs) { std::string str; getline(ifs, str); std::cout << str << endl; } //Binary code - outputstream const unsigned num_data = 10; ofs.write((char*)&num_data, sizeof(num_data)); //데이터 개수 정의 for (int i = 0; i < num_data; ++i) ofs.write((char*)&i, sizeof(i)); //Binary code - inputstream unsigned num_data = 0; ifs.read((char*)&num_data, sizeof(num_data)); //데이터 개수 확인 for (unsigned i = 0; i < num_data; ++i) { int num; ifs.read((char*)&num, sizeof(num)); std::cout << num << endl; } 임의 위치 접근 ifstream ifs("my_file.txt"); ifs.seekg(5); //5바이트 이동 후 읽기 시작 ifs.seekg(5, ios::cur); //이전에 이동했던 위치에서 5바이트 더 이동 후 읽기 시작 ifs.seekg(0, ios::end); //끝에서 0번째 (마지막 위치) ifs.tellg(); //현재 위치 파일 열고, 읽고 쓰기 한번에 fstream iofs(filename); iofs.seekg(5); cout iofs.seekg(5); iofs.put('A'); //write <hr/> Lambda (람다 함수) : 익명 함수 auto func = [](const int& i) -> void { cdout << "Hello, World!!" << endl; }; { string name = "JackJack"; <a href="">&</a> { std::cout << name << endl; } (); } //lambda의introducer인 []에 &을 넣으면, 밖에있는 것을 레퍼런스로 가져올 수 있음. == name <hr/> std::function std::function<void(int)> func3 = func2; std::function func4 = std::bind(func3, 456); //반환값 파라미터 bind Object instance; auto f = std::bind(&Object::hello, &instance, std::placeholders::_1); //멤버함수를 instance에 바인딩 //파라미터가 1개이므로 _1, 늘어나면 매개변수 추가(_2, _3, ...) <hr/> 함수에서 리턴값 여러개 반환하기 (C++17) #include auto my_func() { return tuple(123, 456, 789); } int main() { auto [a, b, c, d] = my_func(); std::cout << a << " " << b << " " << c << " " << d << endl; return 0; } <hr/> std::thread - 멀티 스레딩 (C++11) std::thread t1 = std::thread( { while (true) { } }); t1.join(); //thread가 있는데, main이 끝나버릴 수 있으므로 t1이 끝날 때까지 대기해줌 - 스레드가 여러개 있다면, 동시에 실행된다. mutex mtx; //mutual exclusion (상호 배제) auto work_func = [](const string& name) { for (int i = 0; i mtx.lock(); cout << name << " " << std::this_thread::get_id() << " is working " << i << endl; mtx.unlock(); } }; std::thread t1 = std::thread(work_func, "JackJack"); std::thread t2 = std::thread(work_func, "Dash"); t1.join(); t2.join(); <hr/> Race Condition - std::atomic, std::scoped_loc #include //int shared_memory(0); → t1 스레드가 더하는 순간 t2가 가로채는 문제 발생할 수 있음. atomic shared_memory(0); //문제 해결 int main() { auto count_func = { for (int i = 0; i thread t1 = thread(count_func); thread t2 = thread(count_func); t1.join(); t2.join(); cout << "After" << endl; cout << shared_memory << endl; } - 다른 스레드가 메모리를 가로채는 문제 - std::atomic뿐만 아니라 mutex를 사용할 수도 있음. - atomic 남용시 성능 하락 우려 존재 ... std::lock_guard lock(mtx); shared_memory++; ... - lock 후 unlock을 할 수 없을 때도 있으므로, `std::lock_guard` 사용을 권장 ... std::scoped_lock lock(mtx); ... - (C++17 이상) `std::scoped_lock` 권장 <hr/> 작업 기반 비동기 프로그래밍 (Task) #include int main() { { //multi-threading int result; std::thread t([&] {result = 1 + 2;} ); t.join(); std::cout { //task-based parallelism auto fut = std::async([] {return 1 + 2;}); std::cout << fut.get() << std::endl; } { //future and promise std::promise<int> prom; auto fut = prom.get_future(); auto t = std::thread([](std::promise<int>&& prom) { prom.set_value(1 + 2); }, std::move(prom)); cout << fut.get() << endl; t.join(); } } - async는 join()으로 기다리지 않아도 된다 <hr/> std::forward - 완벽한 전달 #include struct MyStruct {}; void func(MyStruct& s) { cout void func(MyStruct&& s) { cout //Template을 사용하면 L-value와 R-value 구분을 못한다. template void func_wrapper(T t) { func(t); } ↓ Perfect Forwarding template void func_wrapper(T&& t) { func(std::forward (t)); } int main() { MyStruct s; func_wrapper(s); func_wrapper(MyStruct()); //func(s); //func(MyStruct()); } <hr/> 자료형 추론 - `auto` / `template<typename>` - auto는 변수의 const와 &, volatile를 모두 떼버린다. - `const auto& auto_crx2 = crx`, `volatile auto vavx = vs` 와 같이 선언해야 한다. - `decltype` (== typeof) typedef decltype(lhs ** rhs) product_type; product_type prod2 = lhs * rhs; == decltype(lhs * rhs) prod3 = lhs * rhs; typedef decltype(x) x_type; typedef decltype((x)) x_type; → &를 붙여줌 ```

2025.03.01.

글 보기

일학습병행 첨단산업아카데미 회고 (가상훈련콘텐츠SW개발_L5)

1년간의 일학습병행 과정을 마치고 학교를 졸업했다. 1년 전에 학교에 올라온 일학습병행 훈련생 모집공고를 보고 지원했던게 엊그제같은데 이제는 학교를 졸업하고 인턴 생활도 마쳤다. 우리 학교의 일학습병행 일정은 아래와 같았다. 2024년 1월 ~ 2월 - 기업 지원 및 매칭 2024년 3월 ~ 6월 - 학교에서 NCS기반의 강의를 들으며 평소와 같이 학습 7월 ~ 2025년 2월 - 매칭된 기업으로 출근 1학기는 강의를 듣고 여름방학부터 2학기, 겨울방학까지 계속 인턴 근로를 한다고 보면된다. 인턴자리 구하기도 어렵고 요새 어디를 가도 경력직을 요구하는 세상에서 좋은 제도라고 생각된다. 다만 매칭된 기업에 따라 요구하는 기술, 능력이 다르다보니 자신의 능력이 모자르면 과정과 전혀다른 일을 할 수도 있다. 실제로 다른 기업에 매칭된 학생들에게 요구한 기술은 언리얼엔진과 C++이었는데, 이 부분이 부족한 학생들은 영업, 마케팅 등의 다른 직무에 배치되었다. 이런 경우에는 자신이 원하는 직무와는 다른 직무이기 때문에 여러모로 아쉬울 수 있다. 내가 지원한 기업은 우리학교에서 나 포함 4명이 함께갔다. 이곳에서는 유니티를 활용했고, 이를 통해 인턴끼리 프로그램을 개발하게 되었다. 개발하던 프로그램이 좋은 평가를 받게 되어서 대학원을 진학하는 두 명을 제외하고는 정규직으로 계속 근무하게 되었다. 인턴 생활이 마무리될때쯤 해서 일학습병행 내부/외부평가를 진행하게 되는데, 두 가지 평가를 모두 합격하면 기사급의 자격증이 주어진다. 이 내부평가도 웬만하면 통과가 되고 문제는 외부평가인데, 과목마다 평가방식이 다르지만 가상훈련콘텐츠SW개발_L5 의 경우는 10개의 모듈 중 7개가 PASS 되어야 합격이고, 8개의 모듈은 지필로, 2개의 모듈은 면접방식으로 진행된다. 지필로 진행되는 모듈은 문제 수가 3개이다. 1개 모듈의 합격 컷은 60점이어서, 3문제 중에 2개를 맞춰야 통과된다. 문제 수가 매우 적고 배점이 크므로 어려운 부분이 있다. 면접으로 진행되는 모듈은 면접관님들이 최대한 점수를 획득할 수 있도록 노력해주신다. 면접에서 가장 중요한건 키워드기 때문에 어떻게든 키워드를 끄집어낸다면 점수를 받을 수 있다. 다행이 한번에 합격을 했고, 전체 합격자를 볼 수 있길래 조회해보니 이번 시험의 가상훈련콘텐츠SW개발_L5 합격자는 나밖에 없다. 다른 학교에서는 진행이 안되는 과정일 수도 있고, 여러모로 까다롭다보니 합격자를 찾기가 좀 어려운 것 같다. 1년간 나름 열심히 했다고 생각했고, 좋은 결과로 마무리 할 수 있어서 다행이다.

2025.01.07.

글 보기

Unity Multiplayer Play Mode Package 활용하기

만약 유니티를 사용하는 개발자라면, 특히 멀티플레이를 지원하는 프로그램을 개발해본 개발자라면 ParrelSync 에 들어봤을 것이다. 패럴싱크란? https://velog.io/@totohoon01/Unity-ParrelSync 유니티6가 신규 출시할 때, 중점적으로 다루며 홍보했던 Multiplayer Play Mode (이하 MPPM)가 ParrelSync와 비슷한 역할을 한다. 다만 ParrelSync는 프로젝트를 복사하여 에디터를 1개 더 여는 형식이라 아무래도 리소스를 더 먹고 로딩시간도 길다. 가장 불편한 점은 스크립트나 씬 등 프로젝트에 수정사항이 있으면 ParrelSync로 복사한 프로젝트에서도 컴파일이 한 번 더 이뤄지기 때문에 시간이 더 걸리는 점이다. (각각 별도의 프로젝트 처럼 컴파일됨) 유니티의 MPPM을 사용하면 이 부분에서 자유로워진다. 프로젝트의 수정사항이 생겨도 에디터에서 한 번만 컴파일되면 MPPM은 같이 컴파일되기 때문에 중복해서 기다릴 필요가 없다. 잉걸불을 개발하면서도 이 기능을 적극 활용하고 있는데, 내가 활용하는 방법은 다음과 같다. Main Editor (Client) MPPM 2 (Server) MPPM 3 (Client) MPPM 4 (Client) MPPM은 최대 4개(메인 에디터 포함)까지 동작시킬 수 있으므로 한 개를 Dedicated Server로 활용하는 것이다. public override void Start() { #if UNITY_EDITOR try { if (CurrentPlayer.ReadOnlyTags()[0] == "Server") StartServer(); } catch { DebugUtils.Log("Client 동작 실행"); } #endif } CurrentPlayer.ReadOnlyTags() 라는 API 함수를 활용할 수 있는데, 이 함수를 통해 MPPM에 등록된 Tags를 가져올 수 있다. 어짜피 하나의 태그만 작성했기 때문에, 첫번째 태그를 가져와서 이 태그가 "Server"인 Player에서만 서버를 실행시켜주면 Dedicated Server처럼 활용할 수 있다. 이를 통해 Main Editor에서 Play Mode를 시작하면 아래 사진의 Player 2는 자동으로 서버를 시작한다. Player 2가 Server로 실행되었다. 다른 플레이어인 Player 3는 클라이언트의 역할을 할 수 있다. 우측 상단에 위치한 Layout 버튼을 누르면 Console, Game, Hierachy, Inspector, Scene 중 어떤 창을 띄울건지 선택할 수 있다. 플레이모드를 시작했을 때만 체크버튼이 활성화가 되니 참고바란다. 이와같이 Unity에서 공식적으로 ParrelSync의 기능을 지원하고, 사용하기도 더 편리하니 적극 활용하면 개발함에 있어서 훨씬 편할 것이다.

2025.01.07.

글 보기

Unity Game Project - 잉걸불 (Embers)

개발 엔진: 유니티 6 RP: URP 장르: MMORPG API: MariaDB, Mirror Network HomePage: https://embers.nolda.site/ 최우선 목표는 할만한 가치가 있는 게임 임과 동시에, 유니티 개발 패턴(MVC, 싱글톤 등)과 SOLID원칙을 지키며 볼만한 가치가 있는 프로젝트 를 만드는 것이다. 다음은 유니티6의 최신 기술들을 적극 활용하여 최적화와 프로젝트 제작 능률을 올리는 것이다. 예를 들면, 기존의 묵은 Coroutine을 대신해 유니티6에서 새로 도입된 비동기 Awaitable을 사용하는 것이다. 이를 활용하면서 비동기 프로그래밍에 대한 이해도도 증가시키려고 한다. 게임 장르를 MMORPG를 설정한 이유는 개인적으로 좋아하는 장르이기 때문이기도 하지만 다중 접속을 위한 게임이기 때문에 최적화가 중요하며, 네트워크와 DB의 활용도가 높다. 이에 대해 익숙해지기 위함이다. 또한, MMORPG라는 장르의 특성상 들어갈 수 있는 콘텐츠의 양이 방대하다. 이는 내가 원하는 기능을 집약시킬 수 있다는 의미이며 하나의 프로젝트를 통해 실력을 비약적으로 상승시킬 수 있기 때문이다. 앞으로 개발 기록을 통해 기억해둬야할 내용과 기술에 대해 기록하며, 꾸준함과 열정으로 개발해나갈 생각이다.

2024.06.02.

글 보기

리액트로 velog 포스트 가져오기(파싱)

velog에서는 rss 피드형식으로 된 api를 제공하고 있다. https://api.velog.io/rss/@계정명 @ 부분에 자신의 아이디를 가져다 놓으면 자신이 작성한 글의 title, pubData(작성일), description(글 내용) 등을 가져올 수 있다. 이를 정적 React 페이지에서 XML을 파싱하여 필요한 정보를 추출하고 렌더링 시킬 수 있다. 본인은 github-pages로 포트폴리오 사이트를 운영하고 있기에 동적으로는 불가능하여 이런 방법을 찾아보았다. function App() { const [posts, setPosts] = useState([]); useEffect(() => { fetch('https://api.velog.io/rss/@계정명') .then(response => response.text()) .then(data => { const parser = new DOMParser(); const xml = parser.parseFromString(data, 'application/xml'); const items = xml.querySelectorAll('item'); const postsArray = Array.from(items).map(item => ({ title: item.querySelector('title').textContent, link: item.querySelector('link').textContent, description: item.querySelector('description').textContent, })); setPosts(postsArray); }) .catch(error => console.error('Error fetching the RSS feed:', error)); }, []); return ( Velog Posts {posts.map((post, index) => ( {post.title} {post.description.substring(0, 100)}... ))} ); } fetch를 사용해 api를 가져올 수 있는데 이렇게 바로 가져오면 CORS 정책 오류 가 발생한다. 이를 해결하기 위해선 프록시 서버를 사용해야하는데 단순하게 무료로 사용할 수 있는 프록시가 있다. https://proxy.cors.sh/https://api.velog.io/rss/@계정명 이런식으로 fetch 값 에 넣어주면 되는데, 무료인만큼 호출횟수 제한이 존재한다. 다른 CORSProxy 서버를 알고있다면 사용해도 무방하다. 위와 같이 XML을 파싱해오고 CSS를 건드려주면 아래와 같이 가져올 수 있다. 이미지는 썸네일을 가져오는건 지원하지 않아 각 글의 첫번째 이미지를 가져오도록 했다. 썸네일을 올릴 때 파일업로드가 아닌, 이미지 파일을 Ctrl + V로 넣어주면 썸네일 이미지를 첫번째 이미지로 받아들여 표시해준다. .then(data => { const imageUrl = firstImage ? firstImage.getAttribute('src') : null; ... {post.imageUrl && <img src={post.imageUrl} alt="Post thumbnail"/>} 제공되는 값에 대해 여러가지 가공과정을 거칠 수 있으므로, 글 목록 갯수를 원하는대로 지정한다거나 내용을 몇글자까지 표출할지 지정하는 등 자신이 원하는대로 출력 형식을 지정할 수 있을 것이다. 일련의 과정을 거친다면 새로운 글을 작성해도 정상적으로 바로바로 잘 가져오는걸 확인할 수 있고, 이런 방법을 통해 정적 페이지에서도 동적으로 값을 가져올 수 있다.

123