본문 바로가기

구조체와 열거형의 기본 이해

구조체로 관련 데이터를 하나의 값으로 묶고 열거형으로 유한한 상태를 정의해 변수와 분기 로직을 명확히 만듭니다.

캐릭터 블루프린트에 Health, AttackDamage, Defense, CharacterName 변수를 하나씩 만들어 두었다고 생각해 봅니다. 처음에는 문제가 없지만, 이 네 값을 함수에 넘기거나 다른 블루프린트에 전달하려고 하면 핀 네 개를 매번 따로 연결해야 합니다. 능력치가 하나 늘면 그 값을 쓰는 모든 곳을 찾아 고쳐야 하고, 그중 하나를 빠뜨리면 값이 어긋납니다. 게임 상태를 "Playing", "Paused" 같은 문자열로 관리할 때도 비슷한 문제가 생깁니다. "Pasued"처럼 철자를 하나 틀려도 에디터는 알려 주지 않고, 분기만 조용히 어긋납니다.

지금까지 변수와 함수, 이벤트, 흐름 제어, 통신 기법처럼 게임의 행동을 정의하는 요소를 익혔다면, 이번 장에서는 그 행동이 다루는 데이터를 체계적으로 관리하는 방법을 다룹니다. 이 글에서는 관련된 값을 하나로 묶는 구조체(Structure, Struct)와, 변수가 가질 수 있는 값을 정해진 이름 목록으로 제한하는 열거형(Enumeration, Enum)을 만들고 블루프린트에서 사용해 봅니다.


흩어진 변수를 하나로 묶는 구조체

구조체는 서로 다른 타입의 변수 여러 개를 하나의 묶음으로 정의하는 사용자 정의 데이터 타입입니다. 사람을 설명할 때 이름, 나이, 키를 따로 들고 다니기보다 "사람 정보"라는 한 장의 카드로 묶어 다루는 것과 비슷합니다. 구조체로 묶은 값은 변수 하나, 핀 하나로 주고받을 수 있으므로 그래프가 깔끔해지고, 어떤 값들이 서로 관련되어 있는지도 타입 이름만 보고 알 수 있습니다.

한 번 정의한 구조체는 여러 블루프린트에서 같은 모양으로 재사용됩니다. 그래서 캐릭터, 적, UI가 모두 같은 능력치 구조체를 쓰면 멤버를 하나 추가할 때 정의 한 곳만 고치면 되고, 값을 하나씩 따로 연결하다가 생기는 실수도 줄어듭니다.

구조체 에셋 만들기

콘텐츠 브라우저의 빈 곳을 오른쪽 클릭하고, 고급 에셋을 만드는 영역(Create Advanced Asset)의 블루프린트 하위 메뉴에서 Structure를 선택합니다. 이름은 F_CharacterStats로 짓습니다. C++의 구조체는 F 접두사를 붙이는 관례가 있고, 블루프린트 에셋의 이름은 프로젝트의 규칙을 따르면 되므로 이 글에서는 F_CharacterStats를 씁니다.

만든 에셋을 더블클릭하면 구조체 에디터가 열립니다. 변수 추가 버튼으로 멤버를 넣고 각 멤버의 이름과 타입을 정합니다. 이 예제에서는 Health(Float), AttackDamage(Float), Defense(Float), CharacterName(Text) 네 멤버를 만들고, 필요하면 각 멤버의 기본값도 정합니다. 마지막으로 저장하면 이 구조체를 변수 타입으로 고를 수 있게 됩니다.

구조체 변수와 Make, Break 노드

정의한 구조체는 일반 변수처럼 씁니다. BP_PlayerCharacter 같은 액터 블루프린트를 열고 마이 블루프린트(My Blueprint) 패널에서 새 변수를 추가한 뒤, 변수 타입 선택 메뉴의 검색창에 F_CharacterStats를 입력해 고릅니다. 변수 이름은 MyStats로 정합니다.

구조체 안의 값을 읽을 때는 Break 노드를 씁니다. 이벤트 그래프에 MyStats의 Get 노드를 놓고 출력 핀에서 끌어 Break F_CharacterStats를 고르면, 구조체가 멤버별 출력 핀으로 나뉘어 각 값을 따로 쓸 수 있습니다. 반대로 개별 값을 모아 구조체 하나를 만들 때는 Make F_CharacterStats 노드를 씁니다. 이 노드의 입력 핀에 멤버 값을 연결하고 출력 핀을 Set MyStats의 입력에 이으면, 구조체 전체를 한 번에 저장합니다.

예를 들어 Event BeginPlay에서 Make F_CharacterStats로 Health를 100, AttackDamage를 20 등으로 채워 Set MyStats에 연결하면 능력치가 초기화됩니다. 그 뒤 Break F_CharacterStats로 CharacterName과 Health를 꺼내 Print String에 연결하면 초기화한 값을 화면에서 확인할 수 있습니다. 멤버 하나만 바꾸고 나머지는 그대로 두고 싶다면 Set Members in F_CharacterStats 노드를 씁니다. 디테일 패널에서 바꿀 멤버만 핀으로 드러낼 수 있고, 드러내지 않은 멤버는 원래 값을 유지합니다.

Break 노드가 내놓는 값은 구조체에서 읽어 온 값일 뿐, 그 값을 계산에 쓰거나 출력한다고 원래 구조체가 바뀌지는 않습니다. MyStats의 Health를 줄이려면 바뀐 값을 담은 구조체를 Set MyStats로 저장하거나 Set Members 노드를 실행 선에 연결해야 합니다. 이 차이는 연습 문제 1에서 직접 따라가 봅니다.


정해진 선택지만 허용하는 열거형

열거형은 이름이 붙은 값의 목록을 정의하는 사용자 정의 데이터 타입입니다. 요일을 월, 화, 수, 목, 금, 토, 일로 정해 두듯, 변수가 가질 수 있는 값을 몇 가지로 제한할 때 씁니다. 각 이름은 내부적으로 정수에 대응하며, 블루프린트 열거형은 바이트(Byte) 크기의 값으로 저장됩니다.

열거형을 쓰면 숫자 대신 의미 있는 이름으로 상태를 읽을 수 있습니다. 게임 상태 변수가 0인지 1인지를 기억할 필요 없이 MainMenu인지 Playing인지를 보면 되고, 변수에 값을 넣을 때도 정해진 목록에서 고르므로 임의의 숫자나 철자가 틀린 문자열을 넣는 실수가 줄어듭니다. 또 Switch on Enum 노드와 함께 쓰면 각 값마다 실행 핀이 하나씩 생겨 분기가 그래프에 그대로 드러납니다.

다만 열거형이 모든 잘못된 값을 막아 주는 것은 아닙니다. 외부 데이터에서 읽은 정수를 열거형으로 바꾸거나, 예전에 저장한 값을 불러올 때처럼 열거형 바깥에서 들어오는 값은 따로 확인해야 합니다. 열거형이 막아 주는 것은 에디터에서 목록에 없는 이름을 고르는 실수입니다.

열거형 에셋 만들기

콘텐츠 브라우저의 빈 곳을 오른쪽 클릭하고, 블루프린트 하위 메뉴에서 Enumeration을 선택합니다. 이름은 E_GameState로 짓습니다. 열거형 이름에는 흔히 E_ 접두사를 붙입니다.

만든 에셋을 더블클릭해 열거형 에디터를 열고, Add Enumerator 버튼으로 항목을 추가한 뒤 각 항목의 표시 이름(Display Name)을 정합니다. 이 예제에서는 MainMenu, Playing, Paused, GameOver 네 항목을 만들고 저장합니다.

상태 변수와 Switch on Enum

열거형은 상태를 나타내는 변수에 특히 잘 맞습니다. BP_GameManager처럼 게임 상태를 관리할 블루프린트를 열고 새 변수를 추가한 뒤, 타입 선택 메뉴에서 E_GameState를 고르고 이름을 CurrentGameState로 정합니다. 컴파일한 뒤 디테일 패널에서 기본값을 MainMenu로 정할 수 있습니다.

이제 Event BeginPlay에서 Set CurrentGameState로 상태를 MainMenu로 정하고, 그 실행 출력을 분기 노드에 잇습니다. CurrentGameState의 Get 노드를 놓고 출력 핀에서 끌어 Switch on E_GameState를 고르면, 현재 값에 따라 다른 실행 핀을 활성화하는 노드가 만들어집니다. Set CurrentGameState의 실행 출력을 이 Switch의 실행 입력에 연결하고, MainMenu 핀 뒤에는 메인 메뉴를 표시하는 처리를, Playing 핀 뒤에는 게임플레이를 시작하는 처리를 잇습니다.

여기서 실행 선의 연결이 빠지면 Switch는 실행되지 않습니다. 열거형 변수의 값을 바꾸는 것과 Switch가 그 값을 읽어 분기하는 것은 별개의 실행이며, 값을 바꿨다고 Switch가 저절로 다시 실행되지는 않습니다. 상태가 바뀔 때마다 분기를 실행하고 싶다면, 상태를 바꾸는 경로마다 Switch까지 실행 선을 이어야 합니다.


구조체 안에 열거형 넣기

구조체의 멤버로 열거형을 쓸 수도 있습니다. 아이템의 이름과 무게를 구조체로 묶으면서 ItemType 열거형 멤버로 무기, 소비품 같은 종류를 함께 담으면, 아이템 데이터 하나를 받은 쪽이 같은 묶음 안의 ItemType으로 바로 분기할 수 있습니다. 데이터와 그 데이터를 어떻게 처리할지 정하는 기준이 한 묶음으로 전달되는 셈입니다.

이렇게 구조체와 열거형을 함께 쓰면 핀 여러 개를 따로 연결하던 그래프가 짧아지고, 상태를 이름으로 읽을 수 있어 유지보수가 쉬워집니다. 다음 글에서 다룰 데이터 테이블도 각 행의 모양을 구조체로 정의하므로, 구조체는 이번 장 전체에서 계속 쓰입니다.


정리

  • 구조체는 서로 다른 타입의 값을 하나의 사용자 정의 타입으로 묶어, 변수 하나와 핀 하나로 주고받게 합니다.
  • Make 노드는 개별 값으로 구조체를 만들고, Break 노드는 구조체를 멤버별로 나누어 읽습니다. 멤버 일부만 바꿀 때는 Set Members 노드를 씁니다.
  • Break로 꺼낸 값을 계산하거나 출력해도 원래 구조체 변수는 바뀌지 않으며, 바꾸려면 저장하는 실행 노드를 연결해야 합니다.
  • 열거형은 변수가 가질 수 있는 값을 이름 목록으로 제한하고, Switch on Enum과 함께 쓰면 값마다 실행 핀으로 분기합니다.
  • 열거형 값을 바꾸는 것과 Switch가 실행되는 것은 별개이며, 열거형 바깥에서 들어오는 값은 따로 확인해야 합니다.

연습 문제

1. 꺼내서 계산한 값과 저장된 값

MyStats의 Health는 100입니다. Break로 읽은 Health에서 20을 빼 Print String에만 연결하고, MyStats를 바꾸는 Set이나 멤버 갱신은 실행하지 않았습니다. 화면에 80이 출력된 뒤 MyStats의 Health를 다시 읽으면 얼마일까요?

해설 보기

100입니다. Break는 멤버 값을 읽어 계산에 쓸 수 있게 해 줄 뿐이고, 그 계산 결과를 출력했다고 원래 구조체가 바뀌지는 않습니다. 화면의 80은 "100에서 20을 뺀 값"을 보여 준 것이지 저장된 값이 아닙니다.

체력을 실제로 줄이려면 나머지 멤버는 그대로 두면서 바뀐 Health를 담은 구조체를 MyStats에 저장하는 실행까지 연결해야 합니다. Set Members in F_CharacterStats로 Health만 드러내 연결하는 방법이 가장 간단합니다.

2. 값을 바꿨는데 분기가 일어나지 않음

일시 정지 버튼을 누르면 Set CurrentGameState로 상태를 Paused로 바꾸게 했습니다. 그런데 Switch on E_GameState의 Paused 핀 뒤에 연결한 일시 정지 화면이 나타나지 않습니다. BeginPlay에서는 같은 Switch가 MainMenu 핀을 잘 실행합니다. 무엇을 먼저 확인해야 할까요?

해설 보기

일시 정지 버튼의 실행 경로에서 Set CurrentGameState 뒤에 Switch까지 실행 선이 이어져 있는지 확인합니다. BeginPlay에서 Switch가 동작한 것은 BeginPlay의 실행 선이 Switch까지 이어졌기 때문이고, 버튼 경로에서 값을 Paused로 바꿨다고 그 Switch가 다시 실행되지는 않습니다.

값이 실제로 Paused로 바뀌었는지는 Set 뒤에 Print String으로 상태를 출력해 확인할 수 있습니다. 값은 바뀌었는데 화면이 안 나온다면 실행 선이 빠진 것이고, 값조차 바뀌지 않았다면 버튼 이벤트가 실행되는지부터 봅니다.

3. 문자열 상태와 열거형 상태

동료가 게임 상태를 String 변수로 관리하면서 Switch on String에 "Playing", "Paused"를 등록했고, 다른 곳에서는 상태를 "Pasued"로 잘못 적어 저장하고 있습니다. 어떤 문제가 생기며, 열거형으로 바꾸면 무엇이 달라질까요?

해설 보기

"Pasued"는 등록한 어느 문자열과도 같지 않으므로 "Paused" 핀으로 가지 않고 기본(Default) 핀으로 빠집니다. 기본 핀에 연결한 처리가 없다면 아무 일도 일어나지 않습니다. 에디터는 이런 철자 차이를 컴파일 오류로 알려 주지 않으므로, 실행해 보기 전까지 문제를 찾기 어렵습니다.

E_GameState 열거형으로 바꾸면 상태를 저장하는 곳과 분기하는 곳이 모두 같은 목록에서 값을 고르므로 철자가 어긋날 수 없습니다. 상태 이름을 바꿀 때도 열거형 에셋에서 표시 이름을 고치면 그 값을 쓰는 노드에 함께 반영됩니다.

구조체의 Make, Break, Set Members 노드는 언리얼 엔진 공식 문서의 블루프린트 구조체 변수에서 확인할 수 있습니다.

다음 글 데이터 테이블과 블루프린트 연동에서는 구조체로 행의 모양을 정의한 데이터 테이블을 만들고, 블루프린트에서 행을 찾아 읽는 방법을 다룹니다.