Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to bind multiple lua scripts to the same blueprint class? #658

Closed
JenkinsGage opened this issue Oct 18, 2023 · 4 comments
Closed

How to bind multiple lua scripts to the same blueprint class? #658

JenkinsGage opened this issue Oct 18, 2023 · 4 comments

Comments

@JenkinsGage
Copy link

I'm trying to figure out a mod system solution for my game which requires to bind multiple lua scripts to the same blueprint at runtime.
I mean there will be many mods came from different authors who probably want to override the same class but they don't know each other in advance.

@jozhn
Copy link
Contributor

jozhn commented Oct 23, 2023

You cannot directly do it in UnLua, because a UClass should only bind to one lua module. But there's another way to do the same thing by duplicating a class from the source class and dynamicly bind it to the same lua file.

Example function to duplicate from a BlueprintGeneratedClass

void BeginGenerator(UClass* InClass, UClass* InParentClass)
{
	InClass->PropertyLink = InParentClass->PropertyLink;
	InClass->ClassWithin = InParentClass->ClassWithin;
	InClass->ClassConfigName = InParentClass->ClassConfigName;
	InClass->SetSuperStruct(InParentClass);
}

UClass* CreateBlueprintGeneratedClass(UClass* InClass)
{
	const auto ClassNameString = FString::Printf(TEXT("LUA_GENERATED_%s"), *InClass->GetName());
	const auto ClassName = MakeUniqueObjectName(GetTransientPackage(), InClass, FName(*ClassNameString));
	UBlueprintGeneratedClass* Class = NewObject<UBlueprintGeneratedClass>(GetTransientPackage(), ClassName, RF_Public | RF_Transient);
	Class->UpdateCustomPropertyListForPostConstruction();
	const auto Blueprint = NewObject<UBlueprint>(Class);
	Blueprint->AddToRoot();
	Blueprint->SkeletonGeneratedClass = Class;
	Blueprint->GeneratedClass = Class;
#if WITH_EDITOR
	Class->ClassGeneratedBy = Blueprint;
#endif
	BeginGenerator(Class, InClass);
	EndGenerator(Class);
	Class->ClassFlags |= InClass->ClassFlags & CLASS_NewerVersionExists;
	Class->AddToRoot();
	return Class;
}

And then in lua:

local Newclass1 = UE.YourLibrary.Createclass(YourBlueprintClass) -- duplicate from YourBlueprintClass
local Object1 = Newobject(Newclass1GetCurrentWorld(),nil"test1") -- bind to test1.lua
local Newclass2 = UE.YourLibrary.Createclass(YourBlueprintClass)
local Object2 = NewObject(Newclass2GetcurrentWorld(),nil, "test2")

Don't forget to remove the class from root when it's useless.

@JenkinsGage
Copy link
Author

Really appreciate your example which is pretty clear and straightforward!

But I have another question here, let's say we have two lua scripts:
test1.lua

local Screen = require "Tutorials.Screen"

local M = UnLua.Class()

function M:ReceiveBeginPlay()
    local msg = self:SayHi("Hi from test1")
    Screen.Print(msg)
end

return M

test2.lua

local Screen = require "Tutorials.Screen"

local M = UnLua.Class()

function M:ReceiveBeginPlay()
    local msg = self:SayHi("Hi from test2")
    Screen.Print(msg)
end

function M:SayHi(name)
    local origin = self.Overridden.SayHi(self, name)
    return "overridden:" .. origin
end

return M

and blueprint BP_Test with function SayHi that just returns the name.

now I want to 'merge' test1.lua and test2.lua at runtime based on the loading order (test1.lua comes from mod1, test2.lua comes from mod2 and player can decide to load the mod1 first and then the mod2)
so I want to get a 'merged' lua script which looks like:
test_merged.lua

local Screen = require "Tutorials.Screen"

local M = UnLua.Class()

function M:ReceiveBeginPlay()
    local msg = self:SayHi("Hi from test1")
    Screen.Print(msg)

-- merged from test2.lua
    local msg = self:SayHi("Hi from test2")
    Screen.Print(msg)
--

end

-- merged from test2.lua
function M:SayHi(name)
    local origin = self.Overridden.SayHi(self, name)
    return "overridden:" .. origin
end
--

return M

and then bind this test_merged.lua to the blueprint.

I'm not quite familiar with lua, maybe there is already an elegant solution to achieve this but what comes to my mind is to integrate a 'git' tool to our game and generate some intermediate files from users' mods and bind them like the traditional ways.

Why do we need this?

We are trying to make a mod system that allows the community to write third-party mods. And we want to make all of the loaded mods compatible.
For example, two people just made two mods but both of them want to override some functions of the same player character blueprint. Such as mod1 wants to override M:ReceiveBeginPlay() to spawn some particles at begin play; mod2 wants to override something related to the input events.
And what we want is to spawn an actor bound with the merged lua module instead of 2 actor instances with different lua module.

@jozhn
Copy link
Contributor

jozhn commented Oct 25, 2023

It's not easy to merge different lua files, because This approach may lead to timing issues.

I think you may merge two lua files at runtime. Firstly, use a template lua file:
template.lua

local Screen = require "Tutorials.Screen"

local M = UnLua.Class()

function M:ReceiveBeginPlay()
end

return M

then require the template file and merge two lua files to the template:

-- you should empty the required template.lua first so that unlua can bind to the new module
if packege.loaded["template"] then
    packege.loaded["template"] = nil
end

-- require template
local template = require("template")
local mod1 = require("test1")
local mod2 = require("test2")

-- merge functions to template
for k,v in pairs(mod1):
    -- skip overrided function
    if k ~= "ReceiveBeginPlay" then
        template[k] = v
    end
end

for k,v in pairs(mod2):
    -- skip overrided function
    if k ~= "ReceiveBeginPlay" then
        template[k] = v
    end
end

-- merge overrided function like ReceiveBeginPlay, don't forget to pass self and other params to functions
template.ReceiveBeginPlay = function(param_self)
    if mod1.ReceiveBeginPlay then
        mod1.ReceiveBeginPlay(param_self)
    end
    if mod2.ReceiveBeginPlay then
        mod2.ReceiveBeginPlay(param_self)
    end
end

-- dynamicly bind the merged lua to a new uobject
local MergedObject = Newobject(YourClassGetCurrentWorld(),nil"template")

I still have to say it's not easy, because you cannot imagine what kind of codes the community could write.

@JenkinsGage
Copy link
Author

Thank you and this really helps me a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants