1. 넥슨 코리아 데브캣 스튜디오 드래곤하운드(프로젝트DH) 개발팀
전형규(henjeon@nexon.co.kr)
UE4에서 Lua 사용하기
SilvervineUE4Lua
NDC2019
2. • 현재, 드래곤하운드(프로젝트DH) 개발팀 엔지니어링 책임자
발표자 소개
• 참여 프로젝트
• GEARS(2001)
• 마비노기(2004), 마비노기 XBOX360, 마비노기2
• 링토스 세계여행(2014)
• 마비노기 듀얼(2015)
• 주요 관심사: UE4, 육아, 그리고 AI
• 자율 주행 엔지니어를 찾고 있습니다(농담아님).
3. 발표 내용
• UE4 Blueprint Overview
• Blueprint의 장점과 단점 요약
• 관련 작업
• UE4 스크립트 플러그인 몇 가지 소개
• SilvervineUE4Lua 소개
• 개발 동기 및 특징 설명
4. 발표 내용
• 드래곤하운드 사례 분석
• Lua를 적용한 작업 몇 가지를 예로 들어서 설명
• SilvervineUE4Lua 스크립팅 가이드
• 플러그인 사용 방법과 유용한 팁 소개
• SilvervineUE4Lua 설계 리뷰
• 스크립트 플러그인 제작 시 참고할 만한 경험을 공유
5. 발표 자료 URL
• NDC 다시 보기
• http://ndcreplay.nexon.com/#
• 데브캣 스튜디오 슬라이드셰어
• https://www.slideshare.net/devcatpublications
• 올해부터 다른 곳에 올릴 가능성이 있음(구글 프리젠테이션, GitHub, …)
7. UE4 Blueprint
• 노드 기반의 비주얼 스크립팅 시스템
• 엔지니어의 도움 없이 디자이너가 직접 기능을 구현할 수 있다.
http://api.unrealengine.com/images/Engine/Blueprints/GettingStarted/Level_Blueprint_Main.jpg
8. UE4 Blueprint의 장점
• 사용자 관점에서
• 배우기 쉽다.
• 사용법이 간단하다.
• 작업 진행 속도가 빠르다(프로토타이핑에 유리).
• 언어 관점에서
• 데이터의 흐름, 상태를 기술할 때 편하다
• [+] 셰이더(머티리얼) 프로그래밍
• [+] 애니메이션 상태 기계 프로그래밍
9. UE4 Blueprint의 단점
• 복잡도 관리가 어렵다
• 시간이 지나면... 괴물이 된다.
https://blueprintsfromhell.tumblr.com/image/180629779201
10. UE4 Blueprint의 단점
• 공동 작업이 어렵다.
• 바이너리 형식이라서 ‘브랜치 후 머지’ 방식을 쓸 수 없다.
• 배타적 체크아웃으로 인한 작업 병목이 심하다.
15. UE4 ScriptPlugin
• Epic의 순정 코드
• Engine/Plugins/ScriptPlugin
• 거의 모든 UE4 스크립트 플러그인의 레퍼런스로 사용됨
• UHT(Unreal Header Tool)로 Lua 바인딩 코드를 생성하는 코드가 아주 유용함
• 바로 쓸 수 있는 상태는 아님
• 2014년 이후 거의 변경되지 않음
• 문서도 없음
17. Unreal.js https://github.com/ncsoft/Unreal.js/
• Javascript를 통합
• NCSOFT에서 개발한 오픈 소스
• NDC2017 발표 참고(https://www.slideshare.net/crocuis/unrealjs-ue4-75499471)
https://github.com/ncsoft/Unreal.js/blob/master/doc/images/UnrealJs_JavascriptConsole.gif
18. UE4 Python Editor Script Plugin
• Epic의 순정 코드
• Engine/Plugins/Experimental/PythonScriptPlugin
• Python을 스크립트 언어로 사용 가능
• 문서화됨: http://api.unrealengine.com/KOR/Engine/Editor/ScriptingAndAutomation/Python/
• 주의: 에디터 플러그인
• 런타임 플러그인이 아니라서 게임 플레이 로직에 사용할 수 없음
• 개조하면 될 것 같기도 하다.
19. MonoUE https://mono-ue.github.io/
• C#을 지원!!
• 오픈 소스(https://github.com/mono-ue)
• 꿈 같다… 너무 좋다…
• 하지만…
• 개발 속도가 다소 늦다(1인 개발)
• 충분히 검증되지 않음
https://mono-ue.github.io/code.png
24. SUE4Lua 개발 동기
• Blueprint 지옥에서 탈출하고 싶었다
• 팀 규모가 커지자 문제가 심각해짐
• 한 여름 우유보다 잘 상하는 우리 팀의 Blueprint 코드들
• 체크아웃 전쟁… 끝없는 노드 리프레시… 선 정리…
• C++로 탈출을 시도했으나 만족스럽지 않았다
• 몇 개월에 걸쳐 Blueprint 코드를 C++로 포팅함(일명 BP2CPP)
• BP2CPP 과정에서 오류가 빈번하게 발생
• BP2CPP 작업자가 스트레스를 많이 받음
• 구현 속도 및 작업 난이도 증가
25. 목표
• Blueprint 복잡도를 낮춘다.
• Blueprint를 안 쓸 수는 없다: 빠른 작업 이터레이션에 필수적
• Blueprint 사용을 최소화
• 협업이 쉬워야 한다.
• 공동 작업이 가능해야 한다.
• 브랜치, 머지, 코드 리뷰가 쉬워야 한다. 즉, git과 같은 버전 관리가 가능해야 한다.
• 디버깅이 쉬워야 한다.
26. 달성 방법
• 텍스트 스크립트 언어를 도입
• 복잡한 Blueprint 그래프를 텍스트 스크립트로 표현
• 스크립트를 소스코드로 취급하고 UE4 에디터에서 편집하지 않는다.
• 스크립트 언어로 Lua를 선택
• 따.. 딱히 Lua를 좋아해서 선택한 것은 아님(강타입 언어를 선호함)
• 이전 프로젝트(마비노기 듀얼)에서 Lua를 사용했기 때문에 별 고민 없이 선택
• 충분한 시간과 자원이 있었다면 C#을 선택했을 것
27. 자체 제작을 결심한 이유
• 다른 플러그인들은 스크립트 자유도가 너무 높다
• 스크립트로 파생 타입을 정의할 수 있으면 복잡도 관리가 어려울 것으로 판단
• 객체의 정의는 가급적 C++로 제한
• 문서화 및 코드 품질이 중요
• 영어까지는 괜찮은데 중국어를 할 줄 모름
• UE4 코딩 컨벤션을 지켰으면 함
28. SilvervineUE4Lua(SUE4Lua)
• 자체 개발한 UE4 Lua 통합 플러그인
• 2018년 6월 부터 개발
• 2019년 4월에 오픈 소스화(https://github.com/devcat-studio/SilvervineUE4Lua)
• ‘드래곤하운드’ 개발에 사용 중
29. SUE4Lua 특징
• 스크립트를 소스코드와 동등하게 취급
• Lua 파일을 uasset으로 저장하지 않는다.
• 프로젝트 Source 폴더에 스크립트를 둔다.
• Visual Studio나 Visual Studio Code로 편집한다.
30. SUE4Lua 특징
• Visual Studio Code로 디버깅
• VSCodeLuaDebug 사용(https://github.com/devcat-studio/VSCodeLuaDebug)
• 디버그 로그 리다이렉션
• 브레이크 포인트 및 데이터 검사 지원
35. 사례: AI 커스터마이징
• AI 커스터마이징 데이터를 자체 텍스트 형식에서 Lua 테이블로 변경
• 수동으로 작성한 텍스트 라인 파서 코드를 제거할 수 있었다.
• 문법 오류 검사 및 문법 강조(Syntax Highlight) 기능을 공짜로 얻게 됨
{-- DecisionMaker
Class = "Normal",
Description = "기본피격반응",
bRandomSelection = false,
Weight = 2.0,
Evaluators =
{
{
bGenerateOrganStateActions = true,
},
{
bGenerateLegLimpHelper = true,
},
{
bGenerateStakeResponses = true,
},
{
bGenerateHitActionEffects = true,
Args = {
-- 함포 사격을 받으면 넘어짐
bArtilleryHitFall = true,
},
},
},-- end of evaluators
},
36. 사례: 지스타2018 전투 미션 제작
• 5분 정도의 멀티 플레이 전투 데모
• Lua로 미션 스크립트를 구현함
• 약 3000라인 / 작성자 6명 / 358 커밋
내부 리뷰 자료 일부
37. ScenarioMission_GSTAR2018.lua 일부
--[[
Phase: 인트로 컷씬
]]
local function SpawnFlyingDragon_IntroCutScene(self)
if 0 < #self.LuaContext.TargetDragons then return end
SUE4Lua.Log("염화룡 등장")
if UDhFieldNetworkFunctionLibrary.IsServer(self) then
-- 염화룡 스폰
local SpawnSpots = {
self.LuaContext.WorldActors['M9Spawn’]
}
for _, Spot in pairs(SpawnSpots) do
local SpawnTM = Spot:GetActorTransform()
SpawnTM.Scale3D = UE4.Vector.new(4, 4, 4)
local Monster = SpawnFireDragonMiddle(self, SpawnTM, false, Spot, "")
table.insert(self.LuaContext.TargetDragons, Monster)
Monster:GetController():GetAIWrapperComponent():ToggleEnableTargeting(0)
end
38. ScenarioMission_GSTAR2018.lua 일부
--[[
Phase: 관문 보여주기 컷씬
]]
function Mission_GSTAR2018.EnterPhase_GateShowCutScene(self)
SUE4Lua.Log("EnterPhase_GateShowCutScene: "..self.LuaContext.Phases[self.LuaContext.Phase].DebugName)
SetGateClosed(self)
RefreshContextPlayers(self)
TeleportActorsToSpots(self, self.LuaContext.LocalPlayers, self.LuaContext.StartPosList_Gate)
if UDhFieldNetworkFunctionLibrary.IsClient(self) then
-- Town
self.LuaContext.WorldActors['GSTAR_Town']:SetActiveTown(false)
self.LuaContext.WorldActors['GSTAR_Town']:SetActiveTownMesh(false)
self.LuaContext.WorldActors['GSTAR_Town']:SetActiveInteriorProps(false)
self.LuaContext.WorldActors['GSTAR_Town']:SetActiveExteriorProps(false)
self.LuaContext.IsTownMove = false
StopLocalPlayer(self)
SetEnablePlayerInput(self, false)
self.LuaContext.WidgetGSTARMain:HideNPCTracker()
end
39. 사례: 지스타2018 전투 미션 제작
• 엔지니어, 디자이너 모두 Lua를 사용
• 엔지니어는 VS2017이나 VS Code를 사용
• 디자이너는 VS Code를 사용
• 저장소는 git을 사용
ScenarioMission_GSTAR2018.lua 커밋 로그 일부
• 텍스트 스크립트의 효율성을 확인함
• Blueprint로 제작했다면… 체크아웃 병목이 심했을 것
• C++로 제작했다면… 작업 진행 속도가 느렸을 것
• 덕분에 일정에 늦지 않게 작업을 완료할 수 있었다.
40. 사례: UMG 위젯 구현
• 복잡한 위젯 함수 구현을 Lua로 옮김
• SUE4Lua의 디스패치 기능을 사용
• UMG Blueprint는 이벤트 정의와 애니메이션 처리만 담당
• C++은 함수를 정의만 해둠
void UDhTownItemDetailWidgetCpp::ToggleSubStatExpand(FName StatName)
{
SUE4LUA_DISPATCH(GetLuaBridge(), StatName);
}
41. function TownStatGaugeWidget:ToggleExpand(Params)
if not self:IsValidWidget() then
return
end
self.bSubStatExpanded = not self.bSubStatExpanded
if self.bSubStatExpanded then
self.IMG_ExpandShrink:SetBrushFromTexture(self.ShrinkImageTexture)
self.IMG_ExpandShrink:SetColorAndOpacity(self.SubStatExpandColor)
self.BDR_SubStat:SetVisibility(ESlateVisibility.Visible)
UGameplayStatics.PlaySound2D(self:GetWorld(), self.OnExpandSound)
else
self.IMG_ExpandShrink:SetBrushFromTexture(self.ExpandImageTexture)
self.IMG_ExpandShrink:SetColorAndOpacity(self.SubStatShrinkColor)
self.BDR_SubStat:SetVisibility(ESlateVisibility.Collapsed)
UGameplayStatics.PlaySound2D(self:GetWorld(), self.OnShrinkSound)
end
end 코드 변환 샘플
42. local function RefreshItemState(self)
self.TXT_ItemState:SetText("")
self.TXT_Remark:SetText("")
self.TXT_Equipped:SetText("")
if self.TargetItem.EquippingCharacter then
local ShortEquippedText, _LongEquippedText = UDhTownFunctionLibrary.G
self.TXT_Equipped:SetText(ShortEquippedText)
self.TXT_Equipped:SetVisibility(ESlateVisibility.SelfHitTestInvisible
else
self.TXT_Equipped:SetVisibility(ESlateVisibility.Collapsed)
end
local ShortLevelRequirementText, LongLevelRequirementText = UDhTownFunctio
self.TXT_ItemState:SetText(ShortLevelRequirementText)
if self.TargetItem:NeedsMoreLevel(UDhSyncedPlayer.Get(self).CurrentCharact
self.TXT_Remark:SetText(LongLevelRequirementText)
self.TXT_ItemState:SetColorAndOpacity(self.NeedMoreLevelStateColor)
else
self.TXT_ItemState:SetColorAndOpacity(self.NotNeedMoreLevelStateColor
end
if self.TargetItem:IsBroken() then
self.BDR_Main:SetBrushColor(self.BrokenBorderColor)
local ShortBrokenText, LongBrokenText, RepairText = UDhTownFunctionLi
self.TXT_ItemState:SetText(ShortBrokenText..".")
self.TXT_ItemState:SetColorAndOpacity(self.BrokenStateColor)
self.TXT_Remark:SetText(LongBrokenText)
if self.ItemViewPurpose == EDhItemViewPurpose2.Equipment then
self.TXT_RightClick:SetText(RepairText)
self:PlayAnimation(self.Anim_ClickBlink)
end
end
end
코드 변환 샘플
43. void UDhTownTooltipItemWidgetCpp::RefreshItemSet()
{
if (!IsValidWidget())
{
return;
}
FDhCommonSheetTownModifierRowData ModifierRowData;
if (UDhCommonSheetContainerEx::Get()->FindModifierBySetID(TargetItem->SetID, ModifierRowData))
{
SetItemTextBlock->SetVisibility(ESlateVisibility::HitTestInvisible);
SetItemTextBlock->SetText(UDhTownFunctionLibrary::GetModifierFullName2(ModifierRowData));
if (TargetItem->IsBroken())
{
SetItemTextBlock->SetColorAndOpacity(BrokenItemSetColor);
}
else
{
SetItemTextBlock->SetColorAndOpacity(SetItemTextColor);
}
}
else
{
SetItemTextBlock->SetVisibility(ESlateVisibility::Collapsed);
}
}
local function RefreshItemSet(self)
local bModifierFound, ModifierRowData =
UDhSheetManager:GetDhSheetManager().CommonSheetEx:FindModifi
if bModifierFound then
self.TXT_SetItem:SetVisibility(ESlateVisibility.HitTest
self.TXT_SetItem:SetText(UDhTownFunctionLibrary.GetModi
if self.TargetItem:IsBroken() then
self.TXT_SetItem:SetColorAndOpacity(self.BrokenIte
else
self.TXT_SetItem:SetColorAndOpacity(self.SetItemTe
end
else
self.TXT_SetItem:SetVisibility(ESlateVisibility.Collaps
end
end 코드 변환 샘플
44. 사례: UMG 위젯 구현
• Lua로 변경한 결과
• 위젯 코드 변경점을 쉽게 알 수 있었다.
• 컴파일 과정 없이 로직을 바로 변경할 수 있어서 작업 속도가 빨라졌다.
47. Lua 코딩 컨벤션
• UE4 코딩 스타일을 따른다.
• UE4 코딩 스타일을 좋아하지 않지만 여러 스타일이 섞이는 것보다 낫다고 판단
function Sedan:OnMoveRight(Params)
--SUE4Lua.Log("Sedan:OnMoveRight() was called.")
local AxisValue = Params.AxisValue
-- Steering inupt
self.VehicleMovement:SetSteeringInput(AxisValue)
end
49. SUE4Lua 팁: 기본 라이브러리의 활용
• 기본적인 Blueprint 라이브러리를 연결해 둠
• KismetSystemLibrary, 각종 수학 라이브러리, …
• ‘Find in Files’로 키워드를 검색하면 설명과 함께 샘플 코드를 찾을 수 있다.
50. SUE4Lua 팁: Main.lua
• VM의 시작 파일을 Main.lua로 설정하는 것을 추천
• 이렇게 하면 Main.lua가 가장 먼저 실행된다.
• 추가로 필요한 파일들을 Main.lua에서 ExecuteFile()로 실행하자.
• 자주 사용하는 타입을 전역변수로 설정해두면 편리하다.
-- Engine 타입 선언
ECollisionChannel = SUE4Lua.GetEnumTable("ECollisionChannel")
ECollisionResponse = SUE4Lua.GetEnumTable("ECollisionResponse")
ECollisionEnabled = SUE4Lua.GetEnumTable("ECollisionEnabled")
UObject = UE4.FindClass("Object")
UGameplayStatics = UE4.FindClass("GameplayStatics")
UWidgetBlueprintLibrary = UE4.FindClass("WidgetBlueprintLibrary")
UWidgetLayoutLibrary = UE4.FindClass("WidgetLayoutLibrary")
ALevelSequenceActor = UE4.FindClass("LevelSequenceActor")
SUE4Lua.ExecuteFile("SUE4Lua/Tests/AllTests.lua")
51. SUE4Lua 팁: Lua Hot Reloading
• SUE4Lua.OnFileModified() 를 활용
• 파일 변경이 감지되면 SUE4Lua.OnFileModified() 함수가 호출된다(개발 빌드 전용)
• 지정된 파일이 변경되면 다시 실행해서 핫 리로딩을 흉내 낼 수 있다.
• 디스패치 핸들러 파일은 플러그인에서 자동으로 다시 실행해 준다.
local AdditionalExecuteFilenames = {
"Game/Utility.lua",
"Game/UI.lua",
}
-- 개발전용: 파일 변경 처리
SUE4Lua.OnFileModifiled = function (Filename)
for _, ExecutedFilename in pairs(AdditionalExecuteFilenames) do
if Filename:lower() == ExecutedFilename:lower() then
SUE4Lua.Log("Re-Execute", Filename)
SUE4Lua.ExecuteFile(Filename)
break
end
end
end
52. SUE4Lua 팁: LuaContext
• Lua 상태를 저장하는 변수 이름은 ‘LuaContext’을 사용
• Blueprint나 C++ 클래스에 ‘LuaContext’라는 이름의 LuaValue를 추가하고,
• Lua 구현을 위해 필요한 변수들을 저 곳에 저장해 두면 좋다.
• Lua 값과 나머지 값을 격리할 수 있어서 코드 유지 보수에 유리하다.
self.LuaContext = {
ColumnMax = Params.Column,
CurCol = 0,
CurRow = 0,
}
53. SUE4Lua 팁: 대소문자 구별
• 함수, 속성 이름의 대소문자를 구별할 수 없다
• UProperty와 UFunction이 내부적으로 FName 해시 테이블에 저장되기 때문
• Lua에서 외부 속성, 함수에 접근할 때 주의할 것
-- self.IsInCar = State 와 결과가 같다
self.isincar = State
54. SUE4Lua 팁: 이벤트 핸들러의 디스패치
• 디스플레이 이름과 실제 이름이 다르므로 주의할 것
• 예를 들어, Tick 이벤트의 실제 이름은 ReceiveTick
• 이벤트를 추가하면 자동 생성되는 이름도 있다.
• 디스패치 노드의 FunctionName을 활용하면 좋다.
실제 이름은 InpActEvt_ResetVR_K2Node_InputActionEvent_0와 InpActEvt_ResetVR_K2Node_InputActionEvent_1
55. SUE4Lua 팁: Hard Reference
• SUE4Lua는 Lua 안에 UObject에 대한 강참조를 만들지 않는다
• 어셋 참조가 필요하다면 외부 클래스에 변수를 만들어서 저장하거나,
• 해당 객체를 반환하는 Blueprint 함수를 만들자.
-- Print vehicle speed
self:DrawText({
Text = Sedan.SpeedDisplayString,
TextColor = UE4.LinearColor.new(1.0, 1.0, 1.0, 1.0),
ScreenX = HUDXRatio * 805,
ScreenY = HUDYRatio * 455,
Font = self.Font,
Scale = HUDYRatio * 1.4,
})
57. 지원하는 Lua Version
• Lua 5.3.4 사용
• Lua 5.1.5도 사용 가능할 것 같다. 다만 pairs()가 동작하지 않을 것이다.
• Lua JIT는 지원하지 않는다.
• 플러그인 내부에 Lua 소스코드가 포함됨
• 직접 Lua 코드를 구해서 연결할 필요 없다.
• luasocket 및 op_halt 패치가 적용되어 있다.
• https://github.com/devcat-studio/lua-5.3.4-op_halt
58. Lua 소스코드 관리
• 파일 로더 클래스를 세분화
• LocalFileLoader : git에 저장된 Lua 소스코드를 사용
• BundleFileLoader: 빌드머신에서 배포한 Lua 파일 번들(zip)을 사용
• 사용할 로더는 실행 환경마다 다름
• 엔지니어, 디자이너: LocalFileLoader
• 나머지 개발자: BundleFileLoader
• 배포 빌드: BundleFileLoader
61. SilvervineUE4LuaCodeGen
• UHT(Unreal Header Tool) 플러그인
• 프로젝트 모듈을 빌드하기 전에 먼저 실행된다.
• 스태틱 바인딩 코드와 함수 디폴트 파라메터 테이블을 생성한다.
1>------ Build started: Project: SUE4LuaSample, Configuration: DebugGame_Editor x64 ------
1>Performing full C++ include scan (building a new target)
1>Using 'git status' to determine working set for adaptive non-unity build (D:devgithubSilvervineUE4Lua).
1>Creating makefile for SUE4LuaSampleEditor (source directory changed)
1>Using Visual Studio 2017 14.16.27023 toolchain (C:Program Files (x86)Microsoft Visual Studio2017...
1>UnrealBuildTool : warning : Missing binary: D:devEpic GamesUE_4.21EngineSource$(ProjectDir)Plugins...
1>Building UnrealHeaderTool...
1>Performing full C++ include scan (building a new target)
1>Using 'git status' to determine working set for adaptive non-unity build (D:devgithubSilvervineUE4Lua).
1>Creating makefile for UnrealHeaderTool (ini files are newer than UBTMakefile)
1>Using Visual Studio 2017 14.16.27023 toolchain (C:Program Files (x86)Microsoft Visual Studio2017...
1>Parsing headers for SUE4LuaSampleEditor
1> Running UnrealHeaderTool "D:devgithubSilvervineUE4LuaSUE4LuaSample.uproject" "D:devgithubSilvervineUE4Lua...
1>Reflection code generated for SUE4LuaSampleEditor in 11.5833797 seconds
1>Target is up to date
1>Deploying SUE4LuaSampleEditor Win64 DebugGame...
1>Total build time: 59.99 seconds (NoActionsToExecute executor: 0.00 seconds)
1>Done building project "SUE4LuaSample.vcxproj".
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
62. SilvervineUE4LuaCodeGen
• 스태틱 바인딩 코드 생성
• IScriptGeneratorPluginInterface를 사용함
• 엔진 및 게임 클래스의 UFunction을 호출하는 함수를 생성
• 엔진 및 게임 클래스, 구조체의 UProperty 접근자 생성
• 두 개의 (거대한) inl 파일을 생성
• SilvervineUE4LuaCodeGen_Engine.g.inl : 약 80k 라인(3.9MB)
• SilvervineUE4LuaCodeGen_Game.g.inl : 게임 마다 다름
65. SilvervineUE4LuaCodeGen
• 간단한 성능 비교
• 평균적으로 스태틱 바인딩 코드 패스가 리플렉션 코드 패스에 비해 빠르다.
• Scripts/Test/PerformanceTest.lua 참고
0 0.01 0.02 0.03 0.04 0.05 0.06
UObject* Getter
UObject* Setter
int32 Getter
int32 Setter
TestArg(…)
TestArg(Args)
Reflection Static Binding
66. SilvervineUE4LuaCodeGen
• 스태틱 바인딩 활성화가 기본값
• 실행 속도가 빠르기 때문에 기본값이 True
• 대신, 빌드 속도가 느리다(10~30초 추가)
• 원한다면 비활성화 할 수 있다(DefaultSilvervineUE4Lua.ini 수정)
[/Script/SilvervineUE4Lua.SUE4LuaSettings]
bEnableStaticBinding = False
67. SilvervineUE4LuaCodeGen
• 디폴트 파라메터 테이블
• 함수 호출 시 생략 가능한 파라메터의 기본값이 저장되어 있는 Lua 테이블을 생성한다.
• 함수 파라메터가 생략되면 Lua 테이블에서 기본값을 찾는다.
• 메타 정보가 에디터 빌드에서만 접근 가능하기 때문에 이와 같은 파일이 필요하다.
• 모듈 의존성을 제거하기 위해 C++이 아닌 Lua 코드로 생성한다.
--SpawnDecalAttached
DefaultParameters.GameplayStatics.SpawnDecalAttached = Class(DefaultParameterClass)
DefaultParameters.GameplayStatics.SpawnDecalAttached.AttachPointName = "None"
DefaultParameters.GameplayStatics.SpawnDecalAttached.LocationType = 0 --EAttachLocation::Type::KeepRelativeOffset
DefaultParameters.GameplayStatics.SpawnDecalAttached.LifeSpan = 0.000000
생성된 코드 샘플
68. SilvervineUE4Lua
• 예외 처리
• VM에 에러 핸들러가 구현되어 있음
• C++에서 Lua 함수를 호출할 때는 항상 lua_pcall() 사용
• Lua xpcall에서 사용 가능(SUE4Lua.EventHandler)
[Log] [Game/Main.lua:10] DispatchHandlerFactory: Sedan_C
[Log] [SUE4Lua/Framework/SUE4LuaBinding.lua:103] Dispatch Handler Registered: Sedan_C
Game/Sedan.lua
[Log] [Game/Sedan.lua:62] Sedan:ReceiveBeginPlay() was called.
[Log] [Game/Main.lua:10] DispatchHandlerFactory: CameraComponent
[Error] [Game/Sedan.lua:23] 'Activatee': is not a member of 'CameraComponent'.
[Error] [Game/Sedan.lua:23] FATAL: attempt to call a nil value (method 'Activatee')
[Log] [Game/Main.lua:10] DispatchHandlerFactory: VehicleHUD_C
[Log] [SUE4Lua/Framework/SUE4LuaBinding.lua:103] Dispatch Handler Registered: VehicleHUD_C
Game/VehicleHUD.lua
[Log] [FSUE4LuaVMContext::Dispose] Disposing '00000153CEEA5608'...
70. SilvervineUE4Lua
• 디버깅
• VSCodeLuaDebug에 크게 의존하고 있다.
• 디버기 설정은 샘플 프로젝트의 Main.lua를 참고(SUE4LuaSample/Scripts/Main.lua)
• 파일 로딩 전에 설치된 브레이크포인트가 작동하지 않는 문제가 있다.
• 개발 빌드는 시작할 때 모든 Lua 파일을 미리 로딩해 둠
71. SilvervineUE4Lua
• Lua 객체를 C++에 저장하기
• LuaValue(USUE4LuaValue)를 통해 모든 Lua 값을 C++에 저장할 수 있다.
• 저장된 값은 LuaValue 파괴 전까지 gc되지 않는다.
• 저장된 값을 C++에서 사용하기
• boolean, integer, number, string 면 해당 값을 C++ 타입으로 반환
• 테이블 필드에도 접근 가능하다.
• 함수면 직접 호출할 수 있다.
• UObject(userdata)는 UObject*로 반환
• UStruct, TArray 등은 (아직) 지원 안함
72. SilvervineUE4Lua
• Lua 스택에 Push 할 수 있는 타입
• C++ 기본 타입: (u)int8/16/32/64, float/double, bool, char*, TChar*(wchar)
• UE4 스트링: FName, FString, FText
• Enum : C++ enum, enum class, UEnum
• UObject: UObject 및 모든 파생 클래스 -> userdata
• UStruct: FVector, FColor 등의 구조체는 미리 정의된 table, 나머지는 userdata
• 그리고 LuaValue: 저장된 Lua 값을 푸시
• TArray 등의 컨테이너 타입은 지원하지 않는다.
• 하지만 함수 파라메터로는 전달할 수 있다.
• UFunction이 UStruct의 파생 클래스이기 때문
73. SilvervineUE4Lua
• Lua에서 UProperty에 접근
• Lua에서 UObject, UStruct의 UProperty를 읽고 쓸 수 있다.
• UE4 리플렉션 시스템 및 스태틱 바인딩을 사용해서 구현
• 배열 및 컨테이너 지원
• 배열(int32[]), 컨테이너(TArray, TMap, TSet) 모두 지원한다.
• pairs()로 원소 순회 가능
74. SilvervineUE4Lua
• 기본 타입이 아닌 UProperty는 프록시 객체를 생성
• UObject, UStruct, 배열, TArray, TMap, TSet, Delegate가 해당
• userdata 형식의 프록시를 생성하고 weak 테이블에 캐싱함
• 프록시 객체는 소유자 UObject에 대한 약참조를 생성
• C++은 Lua 값에 대한 강참조 가능
• Lua는 C++에 대한 약참조만 가능
• Lua에서 강참조가 필요하다면 C++에 저장하면 된다.
75. SilvervineUE4Lua
• 배열과 컨테이너의 원소 반환
• 값을 반환 : 안전하지만 복사 비용이 발생
• 참조를 반환 : 위험하지만 비용 부담이 없다.
• 고정 크기 배열은 참조를 반환해도 안전
• 메모리가 재할당되지 않기 때문
• TArray 등은 참조를 반환하면 위험
• 내부 버퍼가 재할당될 수 있기 때문
• 재할당이 발생하지 않는 경우에만 참조 반환을 쓰자.
76. SilvervineUE4Lua
• Lua에서 클래스 함수 호출
• C++, Blueprint 함수 모두 호출할 수 있다.
• UE4 리플렉션 및 코드젠된 스태틱 바인딩 코드를 사용함
• 파라메터 전달 과정이 약간 복잡한데 이후 슬라이드에서 설명
• 코딩 편의를 위한 추가 기능
• UClass 타입은 편의상 대상 클래스의 UFunction을 바로 호출할 수 있다.
• 이름에 ‘K2_’ 접두사가 붙은 함수는 접두사 없는 이름으로 호출 가능
77. SilvervineUE4Lua
• Lua에서 UFunction이 아닌 클래스 함수 호출
• 예를 들면 UObject::IsA()
• 간단하게 바인딩 함수를 구현할 수 있다
• Public/Bindings/*.h 참고
//
// UObject에 대한 lua 바인딩
//
UCLASS(NotBlueprintType)
class SILVERVINEUE4LUA_API USUE4LuaObjectBinding : public USUE4LuaBinding
{
GENERATED_BODY()
public:
// Begin USUE4LuaBinding Interface
virtual UClass* GetBindingClass() const override;
// End USUE4LuaBinding Interface
UFUNCTION()
static FString LuaGetName(UObject* InSelf);
UFUNCTION()
static UClass* LuaGetClass(UObject* InSelf);
UFUNCTION()
static bool LuaIsA(UObject* InSelf, FName ClassName);
UFUNCTION()
static class UWorld* LuaGetWorld(UObject* InSelf);
};
function VehicleHUD:ReceiveDrawHUD(Params)
--SUE4Lua.Log("VehicleHUD:ReceiveDrawHUD() was called.")
local SizeX = Params.SizeX
local SizeY = Params.SizeY
local OwningPawn = self:GetOwningPawn()
local Sedan = (UE4.IsValid(OwningPawn) and OwningPawn:IsA('Sedan_C’)) …
78. SilvervineUE4Lua
• Lua에서 클래스 함수 호출 시 파라메터 및 반환값 전달
• (거의) 모든 타입을 지원: int32부터 TArray까지
• 파라메터는 테이블로 전달할 수 있다.
• 참조형 파라메터(출력값)도 지원(파라메터가 테이블 값이 갱신된다).
• 반환값 및 출력값은 순서대로 Lua 함수의 반환값으로 전달된다.
UFUNCTION()
int32 TestArgs(int32 InIntArg1, int32 InIntArg2, const int32& InIntArg3, int32& OutIntArg1, int32& OutIntArg2);
local Ret, OutIntArg1, OutIntArg2 = self:TestArgs({
InIntArg1 = 1,
InIntArg2 = 2,
InIntArg3 = 3,
OutIntArg1 = 4,
OutIntArg2 = nil,-- 생략 가능
})
79. SilvervineUE4Lua
• C++에서 Lua 함수 호출
• FSUE4LuaFunction 클래스를 사용하거나,
• 딜리게이트에 Lua 함수를 바인딩해서 호출할 수 있다.
• 딜리게이트 Lua 바인딩 구현이 약간 특이함
• 일단, 딜리게이트에 비어 있는 더미 함수를 바인딩 해둔다.
• ProcessEvent() 함수를 오버라이드한 후 그 안에서 Lua 함수를 호출
• 자세한 사항은 USUE4LuaValue::ProcessEvent() 구현을 참고
80. SilvervineUE4Lua
• Blueprint 디스패치
• 다른 스크립트 플러그인에는 없는 독특한 기능.
• Blueprint 스택을 읽어서 Lua 스택에 바로 푸시한다.
• 때문에 Blueprint 노드 파라메터를 디스패치 노드로 연결하는 작업이 필요없다.
function SomeClass:Func(Params)
local InParams1 = Params.InParam1
local InParams2 = Params.InParam2
local InParams3 = Params.InParam3
-- ...
Params.OutParam1 = ...
Params.OutParam2 = ...
end
81. SilvervineUE4Lua
• 네이티브(C++) 디스패치
• Blueprint 디스패치와 비슷한 기능
• 디스패치라고 부르고 있지만 사실 단순한 LuaFunction 호출이다.
• 리플렉션을 사용하지 않기 때문에 파라메터를 배열로 Lua에 전달한다.
// NativeDispatch()를 쉽게 사용하기 위한 매크로입니다.
//
// 사용 예:
//void UMyClass::Foo(int32 P1, int32 P2)
//{
// SUE4LUA_DISPATCH(LuaBridge, P1, P2);
//}
#define SUE4LUA_DISPATCH(LuaBridge, ...)
if( LuaBridge)
{
LuaBridge->NativeDispatch(this, ANSI_TO_TCHAR(__func__), __VA_ARGS__);
}
82. SilvervineUE4Lua
• 디스패치 핸들러 팩토리
• 클래스의 디스패치에 사용할 Lua 파일(디스패치 핸들러) 이름을 반환하는 함수를 말한다.
• Lua로 구현해서 VM에 설정한다.
SUE4Lua.SetDispatchHandlerFactory(function (CallerClass)
local ClassName = CallerClass:GetName()
SUE4Lua.Log("DispatchHandlerFactory: ", ClassName)
local Filenames = {
Sedan_C = "Game/Sedan.lua",
VehicleHUD_C = "Game/VehicleHUD.lua",
}
return Filenames[ClassName] or ""
end)
83. SilvervineUE4Lua
• 디스패치 핸들러 직접 호출
• 디스패치 핸들러 Lua 함수에서 다른 디스패치 핸들러의 Lua 함수를 바로 부를 수 있다.
• 구조적으로 마음에 들지 않지만 확실히 컨텐츠 구현할 때 유용하다...
• 최소한 함수 네이밍이라도 다르게 해야 할 것 같다(내부 논의 중)
function ClassA:FuncA(Params)
self.ObjectB:FuncB(1, 2, 3)
end
function ClassB:FuncB(P1, P2, P3)
SUE4Lua.Log(P1, P2, P3) -- 1 2 3
end
84. SilvervineUE4Lua
• 코루틴 및 멀티스레드
• 멀티스레드는 지원하지 않는다. 필요하다면 VM을 분리해서 사용.
• 최근에 코루틴을 지원하기 시작했다. 아직 불안정함.
85. SilvervineUE4Lua
• 알고 있는 문제들
• UE4 핫 리로딩 이후에 Lua에서 잘못된 함수를 호출하는 문제
• 딜리게이트에 시그니처가 맞지 않는 함수를 바인딩하면 호출할 때 크래시
• SUE4Lua 오류 메시지가 너무 불친절함
• 이후 작업
• 오류 메시지를 이해하기 쉽게 수정
• REPL(Read-Eval-Print-Loop) 지원
87. SilvervineUE4Lua
• UE4 Blueprint의 단점을 보완하기 위해 Lua를 사용
• Lua와 같은 텍스트 스크립트 언어를 사용하여 Blueprint 복잡도를 낮췄다.
• 텍스트 자료형을 사용하여 협업에 어려움이 없도록 개선했다.
• 실제 업무에 적용하여 좋은 결과를 얻고 있다.
• 플러그인 소스코드 공개
• GitHub에 소스코드를 공개함. 많은 피드백 부탁 드립니다!
• 스크립트 플러그인을 제작할 때 도움이 될 만한 경험을 공유