ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Unreal Engine] LuaMachine : 함수 중복 사용하기 (LuaComponent)
    Unreal Engine 5._ 2023. 10. 6. 15:30

    지난 글 (LuaMachine : Install & Hello World)에서 LuaMachine 플러그인에 대해 소개드렸습니다.

    저는 이 플러그인을 모딩 기능을 구현하기 위해 사용했습니다.

    제가 사용할 Lua 스크립트의 특징은 다음과 같습니다.

    • 스크립트의 이름은 다음과 같은 형태를 합니다. Script.{ID}.lua ({ID}는 1000~9999까지의 자연수입니다.)
    • 스크립트는 특정한 템플릿에 맞춰 작성되어야 합니다

     

    두 번째 특징인 특정 템플릿의 예시는 다음과 같습니다.

    더보기
    function Start()
    end
    
    function Process()
    end
    
    function Finish()
    end

    이름에서 알 수 있듯이, 게임의 특정 기능에 대하여 지정된 함수가 호출되는 형식입니다.

    모더는 위 함수의 구현부를 추가하여 각각의 모드를 제작할 수 있도록 하였습니다.

     

    이번 글에서는 해당 기능을 구현하며 발생한 문제점에 대해 살펴보도록 하겠습니다.

     


     

    해당 기능은 두 개의 액터로 구현했습니다.

    두 액터는 각각 MyMod와, MyModManager입니다.

    첫 번째로, MyModManager에서 MyMod를 생성하는 코드입니다.

    더보기
    void MyModManager::InitMod() {
        TArray<FString> findFiles;
        // findFiles file search code
        for(auto& i : findFiles) {
            auto mod = GetWorld()->SpawnActor(MyMod::StaticClass(), FTransform());
            mod->InitMod(i);
            MyModArr.Add(mod);
        }
    }

    MyModManager는 스크립트 파일의 이름을 읽어서, 그 수에 맞게 MyMod를 생성합니다.

     

    다음은 MyMod입니다.

    더보기
    void MyMod::InitMod(FString fileName) {
        auto readLua = 
            ULuaBlueprintFunctionLibrary::LuaRunFile(GetWorld(), LuaState, fileName);
        ...
    }
    
    void MyMod::ContentStart() {
        auto readLua = 
            ULuaBlueprintFunctionLibrary::LuaGlobalCall(GetWorld(), LuaState, TEXT("Start"), TArray<FLuaValue>());
        ...
    }
    
    void MyMod::Tick(float DeltaTime) {
        ...
        auto readLua = 
                ULuaBlueprintFunctionLibrary::LuaGlobalCall(GetWorld(), LuaState, TEXT("Process"), TArray<FLuaValue>());
        ...
    }
    
    void MyMod::ContentFinish() {
        auto readLua = 
                ULuaBlueprintFunctionLibrary::LuaGlobalCall(GetWorld(), LuaState, TEXT("Start"), TArray<FLuaValue>());
        ...
    }

    MyMod는 각각의 상황에 대하여 지정된 Lua함수를 호출합니다.

    호출하기 위한 스크립트의 파일명은 InitMod에서 지정되며, 이는 MyModManger에서 읽은 파일명이 됩니다.

     

    이제 특정 상황에 대해 지정된 MyMod의 지정 함수를 호출하면 됩니다.

    위 코드는 약식으로, 실제 구현에서는 MyMod를 Map으로 관리하여 구분했습니다.

     

    1. 문제점

     

    더보기

    위 코드의 문제점은 다음과 같습니다.

    LuaGlobalCall은 LuaState의 Table을 참조하여 함수를 호출합니다.
    위 코드의 구현에서, 다수의 MyMod객체는 동일한 LuaState를 사용합니다.
    MyMod는 생성 시 LuaRunFile을 실행하며, 지정된 스크립트의 함수가 LuaState의 Table에 저장됩니다.
    이 때, 다수의 MyMod를 생성할 경우, 이름이 동일한 함수가 덮어쓰기됩니다.

    결과적으로, MyMod에서 호출하는 함수는 마지막에 생성된 MyMod가 읽은 스크립트의 함수가 됩니다.

    모든 MyMod객체에서 같은 함수가 호출되는 상황이 발생했습니다.

     

    2. 해결 : LuaComponent

     

    더보기

    위 문제는 MyMod객체에서 함수를 호출할 때, LuaState의 Table을 참조해서 생긴 문제였습니다.

    해당 문제를 해결하기 위해 LuaState를 늘려보는 것을 시도했지만, LuaState는 Singleton처럼 작동하여 의도대로 해결되지 않았습니다.

     

    문서를 찾아보던 도중 LuaCallFunction이라는 함수를 찾을 수 있었고, 이 함수는 호출하는 함수의 Global여부를 지정할 수 있었습니다.

    LuaCallFunction은 LuaComponent의 멤버 함수로, 기존의 MyMod를 LuaComponent로 수정하기로 했습니다.

    LuaComponent는 ActorComponent객체의 하위 객체로, 기존의 MyModManager가 MyMod를 스폰하는 코드는 MyMod컴포넌트를 MyModManager에 추가하는 형태가 되었습니다.

     

    다음은 MyModManager의 수정된 코드입니다.

    void MyModManager::InitMod() {
        TArray<FString> findFiles;
        // findFiles file search code
        for(auto& i : findFiles) {
        	auto mod = Cast<MyMod>(
                AddComponentByClass(MyMod::StaticClass(), true, FTransform(), true));
            mod->LuaState = LuaState::StaticClass();
            
            FLuaValue readLua = 
                ULuaBlueprintFunctionLibrary::LuaRunFile(GetWorld(), LuaState, i);
            mod->LuaSetField(TEXT("Start"), readLua.GetField(TEXT("Start")));
            mod->LuaSetField(TEXT("Process"), readLua.GetField(TEXT("Process")));
            mod->LuaSetField(TEXT("Finish"), readLua.GetField(TEXT("Finish")));
            
            MyModArr.Add(mod);
        }
    }

    이제 스크립트 파일을 MyModManager에서 읽습니다.

    읽어온 파일을 생성된 LuaComponent의 SetField함수를 통해 전달합니다.

    이렇게 될 경우, LuaState의 Field에 저장되는 함수는 마지막에 읽어 온 파일이 되지만, 각각의 LuaComponenet의 Field에는 각각의 함수가 저장될 수 있습니다. 

     

    다음은 MyMod의 수정된 코드입니다.

    void MyMod::ContentStart() {
        auto readLua = 
            LuaCallFunction(TEXT("Start"), TArray<FLuaValue>(), false);
        ...
    }
    
    void MyMod::Tick(float DeltaTime) {
        ...
        auto readLua = 
            LuaCallFunction(TEXT("Process"), TArray<FLuaValue>(), false);
        ...
    }
    
    void MyMod::ContentFinish() {
        auto readLua = 
            LuaCallFunction(TEXT("Finish"), TArray<FLuaValue>(), false);
        ...
    }

    기존의 스크립트 파일을 읽어오는 기능을 MyModManager가 수행하므로, InitMod는 더이상 필요하지 않습니다.

    또한, 각각의 Start, Process, Finish를 LuaCallFunction함수를 통해 수행합니다.

    세 번째 인자인 bool은 bGlobal로, 해당 함수가 Global Table에서 참조될지의 여부를 지정합니다.

    true를 지정할 경우, LuaState의 Table에서 함수를 참조하여 마지막에 읽은 스크립트 파일의 함수를 호출하게 됩니다. 

     


     

    동일한 LuaState에서 동일한 함수명을 독립적으로 사용하는 방법에 대해 알아보았습니다.

    다만 문제점으로, 각각의 스크립트에서 사용되는 변수명이 공유된다는 점이 있습니다.

    이 문제점은 아직 해결방법을 찾지 못 한 상태로, 임시로 변수명을 제약하는 방법을 사용했습니다.

    관련된 내용을 찾을 경우 추가적으로 포스팅하도록 하겠습니다.

     

    이번 글이 도움이 되셨기를 바랍니다.

    감사합니다.

    댓글

Designed by Tistory.