PART 9 - 강의 2/4
플러그인 생성
.uplugin 설정, 모듈 타입(Runtime/Editor), LoadingPhase 이해
01
플러그인 vs 모듈
플러그인과 모듈의 차이점 및 플러그인 사용 시점
핵심 차이점
| 특성 | 모듈 (Module) | 플러그인 (Plugin) |
|---|---|---|
| 구조 | 단일 코드 유닛 | 하나 이상의 모듈 + 에셋 + 설정 |
| 독립성 | 프로젝트에 종속 | 완전히 독립적, 재사용 가능 |
| 활성화 | 항상 활성화 | 선택적 활성화/비활성화 |
| 배포 | 프로젝트 일부로 배포 | 마켓플레이스, 독립 배포 가능 |
| 콘텐츠 | 코드만 포함 | 코드 + 에셋 + 설정 포함 가능 |
플러그인을 사용해야 하는 경우
- 여러 프로젝트에서 재사용할 기능
- 선택적으로 활성화/비활성화가 필요한 기능
- 에셋(블루프린트, 머티리얼 등)과 코드가 함께 필요한 기능
- 마켓플레이스 배포 또는 팀 간 공유 예정인 기능
- 에디터 확장 도구
플러그인 디렉토리 구조
Plugin Directory Structure
MyPlugin/
├── MyPlugin.uplugin // 플러그인 설명자 (필수)
├── Source/
│ ├── MyPlugin/ // 런타임 모듈
│ │ ├── Public/
│ │ │ └── MyPlugin.h
│ │ ├── Private/
│ │ │ └── MyPlugin.cpp
│ │ └── MyPlugin.Build.cs
│ │
│ └── MyPluginEditor/ // 에디터 전용 모듈
│ ├── Public/
│ ├── Private/
│ └── MyPluginEditor.Build.cs
│
├── Content/ // 에셋 폴더 (선택적)
│ ├── Blueprints/
│ ├── Materials/
│ └── Textures/
│
├── Config/ // 설정 파일 (선택적)
│ └── DefaultMyPlugin.ini
│
└── Resources/ // 리소스 (아이콘 등)
└── Icon128.png
02
.uplugin 설명자 파일
플러그인의 메타데이터와 모듈 설정을 정의
기본 .uplugin 구조
MyPlugin.uplugin
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0.0",
"FriendlyName": "My Awesome Plugin",
"Description": "RPG 게임을 위한 인벤토리 및 아이템 시스템",
"Category": "Gameplay",
"CreatedBy": "Game Studio",
"CreatedByURL": "https://example.com",
"DocsURL": "https://docs.example.com/myplugin",
"MarketplaceURL": "",
"SupportURL": "https://support.example.com",
"EnabledByDefault": true,
"CanContainContent": true,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "MyPlugin",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyPluginEditor",
"Type": "Editor",
"LoadingPhase": "Default"
}
],
"Plugins": [
{
"Name": "GameplayAbilities",
"Enabled": true
},
{
"Name": "GameplayTags",
"Enabled": true
}
]
}
모듈 타입 (Module Type)
| Type | 설명 | 포함되는 빌드 |
|---|---|---|
Runtime |
게임 런타임 코드 | 모든 빌드 (게임, 에디터, 서버) |
RuntimeNoCommandlet |
런타임 (Commandlet 제외) | 게임, 에디터 |
Editor |
에디터 전용 기능 | 에디터만 |
EditorNoCommandlet |
에디터 (Commandlet 제외) | 에디터만 |
Developer |
개발 도구 | Development/Debug 빌드 |
UncookedOnly |
쿠킹되지 않은 콘텐츠 전용 | 에디터, 비쿠킹 빌드 |
로딩 단계 (Loading Phase)
Loading Phase Options
// 사용 가능한 LoadingPhase 값들
"EarliestPossible" // 가능한 가장 빠른 시점
"PostConfigInit" // 설정 초기화 후
"PostSplashScreen" // 스플래시 화면 후 (에디터만)
"PreEarlyLoadingScreen" // 초기 로딩 화면 전
"PreLoadingScreen" // 로딩 화면 전
"PreDefault" // Default 전 (엔진 모듈 로드 후)
"Default" // 기본값 - 대부분의 플러그인에 적합
"PostDefault" // Default 후
"PostEngineInit" // 엔진 완전 초기화 후
"None" // 자동 로드 안 함 (수동 로드 필요)
LoadingPhase 선택 시 주의
대부분의 플러그인은 "Default"로 충분합니다. 특수한 경우에만 다른 단계를 사용하세요:
- EarliestPossible: 로깅, 설정 시스템 등 다른 모듈이 의존하는 기초 기능
- PostEngineInit: 전체 엔진 기능이 필요한 경우
- None: 동적으로 로드해야 하는 선택적 기능
03
플러그인 코드 구현
Runtime 및 Editor 모듈의 구현 방법
Runtime 모듈 Build.cs
MyPlugin.Build.cs
using UnrealBuildTool;
public class MyPlugin : ModuleRules
{
public MyPlugin(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// 필수 의존성
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore"
});
// 게임플레이 의존성
PrivateDependencyModuleNames.AddRange(new string[]
{
"GameplayAbilities",
"GameplayTags",
"GameplayTasks"
});
}
}
Runtime 모듈 헤더
Public/MyPlugin.h
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
// 플러그인 로그 카테고리
DECLARE_LOG_CATEGORY_EXTERN(LogMyPlugin, Log, All);
class FMyPluginModule : public IModuleInterface
{
public:
/** IModuleInterface 구현 */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
/** 모듈 싱글톤 접근 */
static inline FMyPluginModule& Get()
{
return FModuleManager::LoadModuleChecked<FMyPluginModule>("MyPlugin");
}
static inline bool IsAvailable()
{
return FModuleManager::Get().IsModuleLoaded("MyPlugin");
}
/** 플러그인 기능 API */
void RegisterInventorySystem();
void UnregisterInventorySystem();
};
Runtime 모듈 구현
Private/MyPlugin.cpp
#include "MyPlugin.h"
DEFINE_LOG_CATEGORY(LogMyPlugin);
#define LOCTEXT_NAMESPACE "FMyPluginModule"
void FMyPluginModule::StartupModule()
{
UE_LOG(LogMyPlugin, Log, TEXT("MyPlugin module starting up"));
// 인벤토리 시스템 등록
RegisterInventorySystem();
// 에셋 레지스트리 콜백 등록 (필요시)
// IAssetRegistry::Get().OnAssetAdded().AddRaw(...);
}
void FMyPluginModule::ShutdownModule()
{
UE_LOG(LogMyPlugin, Log, TEXT("MyPlugin module shutting down"));
// 정리 작업
UnregisterInventorySystem();
}
void FMyPluginModule::RegisterInventorySystem()
{
// Subsystem이 자동으로 처리하지 않는 초기화 작업
UE_LOG(LogMyPlugin, Log, TEXT("Inventory system registered"));
}
void FMyPluginModule::UnregisterInventorySystem()
{
UE_LOG(LogMyPlugin, Log, TEXT("Inventory system unregistered"));
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FMyPluginModule, MyPlugin)
04
Editor 전용 모듈
에디터에서만 로드되는 도구 및 확장 기능
Editor 모듈 Build.cs
MyPluginEditor.Build.cs
using UnrealBuildTool;
public class MyPluginEditor : ModuleRules
{
public MyPluginEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"MyPlugin" // 런타임 모듈 참조
});
PrivateDependencyModuleNames.AddRange(new string[]
{
// 에디터 전용 모듈
"UnrealEd",
"Slate",
"SlateCore",
"EditorStyle",
"PropertyEditor",
"EditorWidgets",
"AssetTools",
"ContentBrowser"
});
}
}
Editor 모듈 구현
Private/MyPluginEditor.cpp
#include "MyPluginEditor.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Framework/Docking/TabManager.h"
#include "LevelEditor.h"
#define LOCTEXT_NAMESPACE "FMyPluginEditorModule"
void FMyPluginEditorModule::StartupModule()
{
// 메뉴 확장 등록
RegisterMenuExtensions();
// 커스텀 에셋 타입 액션 등록
RegisterAssetTypeActions();
// 커스텀 디테일 패널 등록
RegisterDetailCustomizations();
// 툴바 버튼 등록
RegisterToolbarExtension();
}
void FMyPluginEditorModule::ShutdownModule()
{
// 등록 해제
UnregisterMenuExtensions();
UnregisterAssetTypeActions();
UnregisterDetailCustomizations();
}
void FMyPluginEditorModule::RegisterMenuExtensions()
{
// Tools 메뉴에 커스텀 항목 추가
FLevelEditorModule& LevelEditorModule =
FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender);
MenuExtender->AddMenuExtension(
"Tools",
EExtensionHook::After,
nullptr,
FMenuExtensionDelegate::CreateRaw(this,
&FMyPluginEditorModule::AddMenuEntry)
);
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
}
void FMyPluginEditorModule::AddMenuEntry(FMenuBuilder& MenuBuilder)
{
MenuBuilder.BeginSection("MyPlugin", LOCTEXT("MyPluginMenu", "My Plugin"));
{
MenuBuilder.AddMenuEntry(
LOCTEXT("OpenTool", "Open My Tool"),
LOCTEXT("OpenToolTooltip", "Opens the plugin tool window"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateRaw(this,
&FMyPluginEditorModule::OpenToolWindow))
);
}
MenuBuilder.EndSection();
}
void FMyPluginEditorModule::OpenToolWindow()
{
// 커스텀 에디터 탭/윈도우 열기
FGlobalTabmanager::Get()->TryInvokeTab(FName("MyPluginToolTab"));
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FMyPluginEditorModule, MyPluginEditor)
Runtime vs Editor 모듈 분리 이유
- 패키지 크기 최소화: Editor 코드는 Shipping 빌드에서 제외됨
- 의존성 관리: UnrealEd 등 에디터 전용 모듈 분리
- 컴파일 시간: 런타임 변경 시 Editor 모듈 재컴파일 불필요
05
플러그인에서 Subsystem 활용
자동 라이프사이클 관리로 플러그인 사용성 향상
플러그인용 GameInstanceSubsystem
Public/MyInventorySubsystem.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "MyInventorySubsystem.generated.h"
/**
* 인벤토리 관리를 위한 Subsystem
* 플러그인 사용자는 별도 초기화 없이 바로 사용 가능
*/
UCLASS()
class MYPLUGIN_API UMyInventorySubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
// 초기화
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// Subsystem 생성 조건 (선택적)
virtual bool ShouldCreateSubsystem(UObject* Outer) const override
{
// 특정 조건에서만 생성
return true;
}
/** 아이템 추가 */
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool AddItem(const FName& ItemId, int32 Quantity = 1);
/** 아이템 제거 */
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool RemoveItem(const FName& ItemId, int32 Quantity = 1);
/** 아이템 수량 확인 */
UFUNCTION(BlueprintPure, Category = "Inventory")
int32 GetItemCount(const FName& ItemId) const;
/** 아이템 변경 이벤트 */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInventoryChanged,
FName, ItemId, int32, NewQuantity);
UPROPERTY(BlueprintAssignable, Category = "Inventory")
FOnInventoryChanged OnInventoryChanged;
private:
// 아이템 저장소
TMap<FName, int32> ItemInventory;
};
Subsystem 구현
Private/MyInventorySubsystem.cpp
#include "MyInventorySubsystem.h"
#include "MyPlugin.h"
void UMyInventorySubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogMyPlugin, Log, TEXT("Inventory Subsystem initialized"));
// 저장된 인벤토리 로드 (필요시)
// LoadInventory();
}
void UMyInventorySubsystem::Deinitialize()
{
UE_LOG(LogMyPlugin, Log, TEXT("Inventory Subsystem deinitializing"));
// 인벤토리 저장 (필요시)
// SaveInventory();
ItemInventory.Empty();
Super::Deinitialize();
}
bool UMyInventorySubsystem::AddItem(const FName& ItemId, int32 Quantity)
{
if (Quantity <= 0)
{
return false;
}
int32& CurrentCount = ItemInventory.FindOrAdd(ItemId);
CurrentCount += Quantity;
// 이벤트 브로드캐스트
OnInventoryChanged.Broadcast(ItemId, CurrentCount);
UE_LOG(LogMyPlugin, Log, TEXT("Added %d of %s. Total: %d"),
Quantity, *ItemId.ToString(), CurrentCount);
return true;
}
bool UMyInventorySubsystem::RemoveItem(const FName& ItemId, int32 Quantity)
{
int32* CurrentCount = ItemInventory.Find(ItemId);
if (!CurrentCount || *CurrentCount < Quantity)
{
return false; // 수량 부족
}
*CurrentCount -= Quantity;
if (*CurrentCount == 0)
{
ItemInventory.Remove(ItemId);
}
OnInventoryChanged.Broadcast(ItemId, *CurrentCount);
return true;
}
int32 UMyInventorySubsystem::GetItemCount(const FName& ItemId) const
{
const int32* Count = ItemInventory.Find(ItemId);
return Count ? *Count : 0;
}
사용 예시
Usage Example
// 게임 코드에서 Subsystem 사용
void AMyCharacter::PickupItem(FName ItemId)
{
// GameInstance에서 Subsystem 가져오기
if (UGameInstance* GI = GetGameInstance())
{
if (UMyInventorySubsystem* InvSS = GI->GetSubsystem<UMyInventorySubsystem>())
{
InvSS->AddItem(ItemId, 1);
}
}
}
// Blueprint에서는 더 간단:
// Get Game Instance -> Get Subsystem (MyInventorySubsystem) -> Add Item
플러그인에서 Subsystem 사용의 장점
- 자동 초기화: 사용자가 별도 설정 없이 바로 사용 가능
- 라이프사이클 관리: 엔진이 생성/소멸 타이밍 관리
- Blueprint 지원: UFUNCTION으로 쉽게 노출
- 테스트 용이성: 모킹 및 단위 테스트 가능
SUMMARY
핵심 요약
- 플러그인은 하나 이상의 모듈 + 에셋 + 설정을 포함하는 독립적 패키지
- .uplugin 파일에서 모듈 타입(Runtime/Editor)과 LoadingPhase 설정
- Runtime 모듈은 게임에 포함, Editor 모듈은 에디터에서만 로드
- Subsystem을 활용하면 플러그인 사용자가 별도 초기화 없이 기능 사용 가능
- 대부분의 플러그인은 LoadingPhase: Default로 충분
다음 강의 예고
다음 강의에서는 Game Feature Plugin을 통한 모듈형 게임플레이 콘텐츠 구현과 런타임 활성화/비활성화를 학습합니다.
PRACTICE
도전 과제
배운 내용을 직접 실습해보세요
실습 1: 빈 플러그인 생성
에디터의 Edit > Plugins > New Plugin으로 RPGInventoryPlugin을 생성하세요. .uplugin 파일의 구조, Modules 배열의 Type(Runtime, Editor), LoadingPhase 설정을 이해하세요.
실습 2: 플러그인 모듈 코드 작성
플러그인의 IModuleInterface를 구현하고, StartupModule()에서 커스텀 콘솔 명령을 등록하세요. FAutoConsoleCommand로 인벤토리 디버그 명령(showInventory, giveItem)을 추가하세요.
심화 과제: 플러그인 배포 및 공유
플러그인을 독립적으로 패키징하고, 다른 UE5 프로젝트에 설치하는 워크플로우를 구현하세요. Content-only 플러그인과 Code 플러그인의 차이를 이해하고, Marketplace 배포 요구사항을 확인하세요.