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 배포 요구사항을 확인하세요.