Jump to content
  • entries
    940
  • comments
    5,894
  • views
    863,962

Using Multiple Entity Scripts in Turbo Game Engine


Josh

10,718 views

 Share

During development of Leadwerks Game Engine, there was some debate on whether we should allow multiple scripts per entity or just associate a single script with an entity. My first iteration of the scripting system actually used multiple scripts, but after using it to develop the Darkness Awaits example I saw a lot of problems with this. Each script used a different classname to store its variables and functions in, so you ended up with code like this:

function Script:HurtEnemy(amount)
	if self.enemy ~= nil then
		if self.enemy.healthmanager ~= nil then
			if type(self.enemy.healthmanager.TakeDamage)=="function" then
				self.enemy.healthmanager.TakeDamage(amount)
			end
		end
	end
end

I felt this hurt script interoperability because you had to have a bunch of prefixes like healthmanager, ammomanager, etc. I settled on using a single script, which I still feel was the better choice between these two options:

function Script:HurtEnemy(amount)
	if self.enemy ~= nil then
		if type(self.enemy.TakeDamage)=="function" then
			self.enemy.TakeDamage(amount)
		end
	end
end

Scripting in Turbo Game Engine is a bit different. First of all, all values and functions are attached to the entity itself, so there is no "script" table. When you access the "self" variable in a script function you are using the entity object itself. Here is a simple script that makes an entity spin around its Y axis:

function Entity:Update()
    self:Turn(0,0.1,0)
end

Through some magic that is only possible due to the extreme flexibility of Lua, I have managed to devise a system for multiple script attachments that makes sense. There is no "component" or "script" objects itself, adding a script to an entity just executes some code that attached values and functions to an entity. Adding a script to an entity can be done in C++ as follows:

model->AttachScript("Scripts/Objects/spin.lua");

Or in Lua itself:

model:AttachScript("Scripts/Objects/spin.lua");

Note there is no concept of "removing" a script, because a script just executes a bit of code that adds values and functions to the entity.

Let's say we have two scripts named "makeHealth100 and "makeHealth75".

MakeHealth100.lua

Entity.health=100

MakeHealth75.lua

Entity.health=75

Now if you were to run the code below, which attaches the two scripts, the health value would first be set to 100, and then the second script would set the same value to 75, resulting in the number 75 being printed out:

model->AttachScript("Scripts/Objects/MakeHealth100.lua");
model->AttachScript("Scripts/Objects/MakeHealth75.lua");
Print(entity->GetNumber("health"));

Simple enough, right? The key point here is that with multiple scripts, variables are shared between scripts. If one scripts sets a variable to a value that conflicts with another script, the two scripts won't work as expected. However, it also means that two scripts can easily share values to work together and create new functionality, like this health regeneration script that could be added to work with any other scripts that treat the value "health" as a number.

HealthRegen.lua

Entity.healthregendelay = 1000

function Entity:Start()
	self.healthregenupdatetime = CurrentTime()
end

function Entity:Update()
	if self.health > 0 then
		if CurrentTime() - self.healthregenupdatetime > self.healthregendelay then
			self.health = self.health + 1
			self.health = Min(self.health,100)
		end
	end
end

What about functions? Won't adding a script to an entity overwrite any functions it already has attached to it? If I treated functions the same way, then each entity could only have one function for each name, and there would be very little point in having multiple scripts! That's why I implemented a special system that copies any added functions into an internal table. If two functions with the same name are declared in two different scripts, they will both be copied into an internal table and executed. For example, you can add both scripts below to an entity to make it both spin and make the color pulse:

Spin.lua

function Entity:Update()
	self:Turn(0,0.1,0)
end

Pulse.lua

function Entity:Update()
	local i = Sin(CurrentTime()) * 0.5 + 0.5
	self:SetColor(i,i,i)
end

When the engine calls the Update() function, both copies of the function will be called, in the order they were added.

But wait, there's more.

The engine will add each function into an internal table, but it also creates a dummy function that iterates through the table and executes each copy of the function. This means when you call functions in Lua, the same multi-execution feature will be available. Let's consider a theoretical bullet script that causes damage when the bullet collides with something:

function Entity:Collision(entity,position,normal,speed)
	if type(entity.TakeDamage) == "function" then
		entity:TakeDamage(20)
	end
end

If you have two (or more) different TakeDamage functions on different scripts attached to that entity, all of them would get called, in order.

What if a function returns a value, like below?:

function Entity:Update()
	if self.target ~= nil then
		if self.target:GetHealth() <= 0 then
			self.target = nil --stop chasing if dead
		end
	end
end

If multiple functions are attached that return values, then all the return values are returned.

2k6qco.jpg.47118c97ce0e02ce7f1451470dffb7c7.jpg

To grab multiple returned values, you can set up multiple variables like this:

function foo()
	return 1,2,3
end

a, b, c = foo()
print(a) --1
print(b) --2
print(c) --3

But a more practical usage would be to create a table from the returned values like so:

function foo()
	return 1,2,3
end

t = { foo() }
print(t[1]) --1
print(t[2]) --2
print(t[3]) --3

How could this be used? Let's say you had a script that was used to visually debug AI scripts. It did this by checking to see what an entity's target enemy was, by calling a GetTarget() function, and then creating a sprite and aligning it to make a line going from the AI entity to its target it was attacking:

function Entity:UpdateDisplay()
	local target = self:GetTarget()
	self.sprite = CreateSprite()
	local p1 = self.entity:GetPosition() 
  	local p2 = target:GetPosition()
	self.sprite:SetPosition((p1 +  p2) * 0.5)
	self.sprite:AlignToVector(p2 - p1)
	self.sprite:SetSize(0.1,(p2-p1):Length())
end

Now let's imagine we had a tank with a main gun as well as an anti-aircraft gun that would ward off attacks from above, like this beauty I found on Turbosquid:

01.jpg7aadaf3f-2e0c-4a62-8154-f8b5da25f02bOriginal.thumb.jpg.3528aef8665ae145a2a5001d0054799e.jpg

Let's imagine we have two different scripts we attach to the tank. One handles AI for driving and shooting the main turret, while the other just manages the little machine gun. Both the scripts have a GetTarget() function, as the tank may be attacking two different enemies at once.

We can easily modify our AI debugging script to handle multiple returned values as follows:

function Entity:UpdateDisplay()
	local targets = { self:GetTarget() } --all returned values get put into a table
	for n,target in ipairs(targets) do
		local sprite = CreateSprite()
		self.sprites.insert(sprite)
		local p1 = self.entity:GetPosition() 
	  	local p2 = target:GetPosition()
		sprite:SetPosition((p1 +  p2) * 0.5)
		sprite:AlignToVector(p2 - p1)
		sprite:SetSize(0.1,(p2-p1):Length())
	end
end

However, any scripts that are not set up to account for multiple returned values from a function will simply use the first returned value, and proceed as normal.

This system supports both easy mix and match behavior with multiple scripts, but keeps the script code simple and easy to use. Scripts have easy interoperability by default, but if you want to make your function and variable names unique to the script it is easy to do so.

Let me know if you have any other ideas for scripting in Turbo Game Engine.

  • Like 1
  • Upvote 1
 Share

51 Comments


Recommended Comments



In fact, I bet it would even be possible to add a connection to SetColor() or another engine function without any script at all on the target entity. At that point it's basically a lambda function that gets executed when the event occurs.

Image result for godlike powers

  • Like 1
Link to comment

It actually works:

auto model = CreateBox(world)	
model->AddScript(L"Scripts/Objects/test.lua");
auto connection = model->Connect("Update", model, "SetPosition");
connection->AddArgument(1);
connection->AddArgument(0);
connection->AddArgument(10);

Holy **** this is some crazy stuff. The line between visual scripting and inter-object communication has been blown wide open.

  • Like 1
Link to comment

Okay, so from the perspective of Lua, there is no difference between a Lua function like Entity:PickupWeapon() and an engine function like Entity:SetPosition(), nor is there any difference between how entity.position (an engine value) and entity.health (a script value) are treated. Either could be used as an argument in a flowgraph connection. However, outputs are still limited to Lua-defined script functions, unless I want to add a call to FireOutputs(<functionname>) in every single method in the engine (which I don't).

Link to comment

There's no more concept of a "script" object attached to an entity. There's just objects, and they can have extra values. Some of these extra values are functions.

Link to comment

Nice, I like the concept you are proposing here. Just adding my 2 ct:

 

When you said, there was no remove-function for a script, that makes perfect sense for scripts that are only setting variables, as you mentioned. However, you might want to have the option to remove these functions. Let's consider your tank-example: If you somehow lose your main cannon, you might want to remove the script associated with it, such that future calls to GetTarget() do not call the function of the main cannon anymore.

 

There is another issue with your tank-example: If you have two functions called GetTarget(), you would likely also name the corresponding setter-function SetTarget(), each. However, this would mean that you can only set the target of both at the same time, if you did not name them AISetTarget() and CanonSetTarget(). For this reason, I think, you should draw a line here so you have to make explicit whether you want to call functions from a different script or only those of the same script. I would suggest, you do the following:

  • SetTarget() will only execute the function from the current script. If there is no function named SetTarget in the current script, an error should be thrown
  • All::SetTarget() (or something similar) will execute functions from all attached scripts (as well, as the current script) with the current name (i.e. the way you proposed)
  • FILENAME::SetTarget() will only execute the function from the script called FILENAME. If this script does not exist or it does not have this function, an error should be thrown

So for each function you define in your script, you would need to internally create three different mappings. Doing this would give everyone maximal flexibility while at the same time preventing some unforeseen errors. So, everyone could still use their isolated scripts the way they used them before without even needing to change anything. If you want to call functions from other scripts, you have the choice to select either only one specific other script or all others

 

Link to comment
4 hours ago, Josh said:

model:Connect("Update", "EmitEvent(EVENT_GAME_BEGIN,model)")

Update is the "event". In all the stuff you're saying you are linking functions to "events". Then when that "event" is fired/called it's actually calling the function we assigned to it. Being able to assign multiple functions would be nice. In your example your 2nd param is basically a script in itself as it's getting executed, but before you were assigning things to straight functions to be called and so I was wondering if you could assign to multiple functions and when Update is called it'll loop through and make the call to all functions attached.

If you changed the name of Update to onUpdate then it becomes clearer that it's basically an event that the engine is raising every frame. There are other events like onCollision. Same idea. You're simply allowing users to have their functions called when these events are raised (when those engine functions are called).

That's the premise of my blog post. Raising and event is just like calling a function, except the event can have N number of functions that the user assigned to be called when the event was raised. It allows one to make events when they don't know what a user wants to do with said event. It gives them the flexibility to do whatever they want when an event is raised.

It's easier to think about a GUI button. When it's pressed the UI library "should" raise and event and the user would have attached a function (or more than 1) to that event that gets called. This is opposed to the polling that you use in your GUI system. So you either poll or use events.

Link to comment

There's no limit on the number of outgoing connections you can create.

This will call the Foo() function attached to the target entity, if it is present. Foo() can a function from a script or an engine function:

model:Connect("Collision", target, "Foo")

This will call the global Foo() function:

model:Connect("Collision", "Foo")

This will execute the passed code, and the target entity will be available in the "self" variable. In this case, the target entity doesn't even need to have a script at all:

model:Connect("Collision", target, "self:SetColor(1,2,3) self:SetPosition(3,3,3)")

This will execute the contained code:

model:Connect("Collision", "print(545)")

Multiple connections from one function are fine:

model:Connect("Collision", target, "Foo")
model:Connect("Collision", target2, "Foo")
model:Connect("Collision", target3, "Foo")

The Connect command returns a Connection object. You can add arguments to be passed to functions from other object functions:

local connection = model:Connect("Collision", target, "Foo")
connection:AddArg(button,"GetState")

Or you can use a member:

local connection = model:Connect("Collision", target, "Foo")
connection:AddArg(button,"state")

Or you can use a literal value with no object:

local connection = model:Connect("Collision", target, "Foo")
connection:AddArg(3)

Of course this is all meant to be used to in the visual editor. I don't expect people to code these things directly, but it's there if you want it, and it is the foundation of what's actually happening when you use the flowgraph system. How well does that fit with your ideas of what you want to do?

Link to comment

So :Connect() is an entity function. Can we have our own event? In the example above "Collision" can be thought of as an event. When it's called/raised you call the connected functions. Can we have our own lua defined event that we can call and it'll call all the connected functions? 

-- treating this more as an event that I can call and it'll in turn raise all the connected functions
function Script:MyScriptsEvent()
end

self:Connect("MyScriptsEvent", target, "Foo")

So when inside self I "raise" or call MyScriptsEvent function it'll call Foo() from target?

Why do you do the AddArg()? Is that for the C++ side of things? Lua can do variable number of arguments so you could capture all arguments after the 3rd one and pack them off in a table and then unpack them when you make the call and they'll fall in the right order.

 

Link to comment
22 minutes ago, Rick said:

So :Connect() is an entity function. Can we have our own event? In the example above "Collision" can be thought of as an event. When it's called/raised you call the connected functions. Can we have our own lua defined event that we can call and it'll call all the connected functions? 


-- treating this more as an event that I can call and it'll in turn raise all the connected functions
function Script:MyScriptsEvent()
end

self:Connect("MyScriptsEvent", target, "Foo")

So when inside self I "raise" or call MyScriptsEvent function it'll call Foo() from target?

Why do you do the AddArg()? Is that for the C++ side of things? Lua can do variable number of arguments so you could capture all arguments after the 3rd one and pack them off in a table and then unpack them when you make the call and they'll fall in the right order.

 

I am writing this in C++, so that is the reason for the AddArgs thing, but it is something I am still thinking about.

You could create a connection with a made-up name, and then call entity:Fire("myevent") but why would you do that when it is just as easy to call the code you want to call?

local connection = self:Connect("MyEvent",self.material,"SetColor",1,0,0)
connection:Fire("MyEvent")

This seems more straightforward to me:

self.material:SetColor(1,0,0)

 

Link to comment
1 minute ago, Josh said:

You could create a connection with a made-up name, and then call entity:Fire("myevent") but why would you do that when it is just as easy to call the code you want to call?

You do that for the reason my blog post talks about. When making reusable scripts for others to use, you create events for things that happen inside that script. Users connect whatever they want to that scripts events (I wouldn't know what they want to do at the time of writing my reusable script) and when I fire them inside my reusable script their custom code is called. 

If a health script wanted to tell the outside world when the health dropped below 50% then inside it's Hurt() function when health is < 50% is would raise or call an event. Since these are functions in  your example it can just call the function onHealthBelowFiftyPercent()  I guess but internally that function would be empty and only exists for others to connect to it so they can call their functions. So in that regard it acts more like an event since it's sole purpose is for others to just connect to it and it's body is empty.

  • Like 2
Link to comment

Just to put a more concrete example with this. Let's say the goal is to have a heartbeat sound play when health is < 25% and to also add pulsing red post process effect around the edges of the screen. A reusable way to do something like this so that others can modify it to their needs would be to:

  1. Create a Noise.lua script. This would have a property of what sound to play and a function Play() (some other properties as well).
  2. Create a HurtPostProcessing.lua script. It will do the pulsating red boarder when you call it's TurnOn() function and you can turn it off by calling it's TurnOff(). It can have properties as well to control certain aspects like how fast the pulsating is.
  3. Create a Health.lua script that has a Hurt() function and an onHealthBelowTwentyFivePercent event. It also has properties like max health and such.

 

All 3 of these scripts are separate and were created by 3 different people and put in the shop. I come along and add all 3 scripts to my player entity. I set the noise.lua wav file property to some heavy breathing sound and I connect it's Play() function to the Health scripts onHealthBelowTwentyFivePercent. I do the same for the HurtPostProcessing script. I now have 3 scripts that knew nothing about each other working together help me reach my goal effect. This is the benefit of "events" and event programming.

If we used the polling and shared variable way of doing things this wouldn't be ideal. My reusable Noise script should never know or care about a Health script. Inside it's Update() method it shouldn't be checking if self.health is < 25% because self.health has nothing to do with Noises. It couldn't possible know all the use cases for playing noises. The main benefit of attaching multiple scripts is the reusability and cleanliness of code. Put a nice user interface on top of hooking all this up and you have a very friendly system to make complex functionality without much coding by the user. This gives the coders a framework to create reusable scripts, and down the line a revenue stream by selling those scripts.

Link to comment

if you want to create a function that will not be sharing variables with other functions with the same name, are you thinking of having a using namespace convention?

Link to comment
Just now, Gonan said:

if you want to create a function that will not be sharing variables with other functions with the same name, are you thinking of having a using namespace convention?

You can prefix your variables with a unique string or add them into a table:

function Entity:Start()
  self.scriptname_value=100
end

Or:

function Entity:Start()
  self.scriptname = {}
  self.scriptname.value=100
end

 

Link to comment

@Rick That's a good example.

Let's say this is our health script (or an AI script with a health value in it):

function Entity:Start()
    self.health=100
end

function Entity:Hurt(damage)
    self.health = self.health - damage
end

function Entity:Reset()
    self.health = 100
end

And here is our sound script:

function Entity:Start()
    self.sound_heartbeat=LoadSound("Sound/heartbeat.wav")
    self.source = self:EmitSound(self.sound_heartbeat)
    self.source:Pause()
end

function Entity:Hurt(damage)
    if self.health < 25 and self.health > 0 then
        self.source:Resume()
    else
        self.source:Pause()
    end
end

function Entity:Reset()
    self.source:Pause()
end

And our overlay script:

function Entity:Start()
    self.shader_pain=LoadSound("Shaders/PostProcess/pain.shader")
end

function Entity:Hurt(damage)
    if self.health < 25 and self.health > 0 then
        if self.camera ~= nil then self.camera:AddEffect(self.shader_pain) end
    else
        if self.camera ~= nil then self.camera:RemoveEffect(self.shader_pain) end
    end
end

function Entity:Reset()
    if self.camera ~= nil then self.camera:RemoveEffect(self.shader_pain) end
end

So these all need to recognize that Hurt and Reset are common functions, and two different scripts use 25 as the threshold for the pain state. No flowgraph connection is required.

Another way would be to have the health script control the pain state:

function Entity:Start()
    self.health=100
end

function Entity:StartThePain()
end

function Entity:StopThePain()
end

function Entity:Hurt(damage)
    self.health = self.health - damage
    if self.health < 25 then
    	self:StartThePain()
    else
    	self:StopThePain()
    end
end

function Entity:Reset()
    self.health = 100
    self:StopThePain()
end

And then the other scripts would use StartThePain() and StopThePain() instead of adding another Hurt() function. Something like this is probably better because it sets a single threshold value inside the health script, so the sound and overlay scripts don't get out of sync.

Another option might be to add a getter / setter type of function that watches a variable. In this case we will use one called ithurts to store the pain stete:

function Entity:Start()
    self.health=100
    self.ithurts=false
end

function Entity:Hurt(damage)
    self.health = self.health - damage
    if self.health < 25 then self.ithurts = true else self.ithurts=false end
end

function Entity:Reset()
    self.health = 100
    self.ithurts = false
end

And your sound script would look like this:

function Entity:Start()
    self.sound_heartbeat=LoadSound("Sound/heartbeat.wav")
    self.source = self:EmitSound(self.sound_heartbeat)
    self.source:Pause()
end

function Entity:set_ithurts(value)
    if value then
        self.source:Resume()
    else
        self.source:Pause()
    end
end

What are your thoughts on this?

Link to comment

You would never have/want health information inside a sound script. That script has stopped being generic enough for reuse at that point and it's now very "leaky". Other responsibility is leaking into that script. If you want 3 instances of that script all playing sound on different events you either bloat the 1 script to handle all use cases (which no reuseable script will know all possible use cases) or you create 3 different scripts. Both are bad options.

There are all sorts of different events that you want to play a sound and trying to build in common functions or sharing information directly inside another script is just a mess waiting to happen and again kills reuse.

Being able to link the event to a function in the editor is the way to go. Those other ways you're showing can't really be done via the editor unless you start doing code generation inside the scripts themselves which leads to a whole other mess.

 

I think your StartThePain()/StopThePain() is the right idea as long as the idea is that I can connect (in another script or via editor) my sound scripts Play() and Stop() function to those functions (events). That way the 2 scripts are unaware of each other on the inside of themselves. The sound script has no knowledge internally of the health script and vice versa. A much cleaner and reusable situation.

 

  • Upvote 1
Link to comment

In that case, what you probably want is a per-entity flowgraph that can simply call engine commands. Does that sound right? I mean, why would you need a whole script for something that is just one command to play a sound?

Link to comment

Because that script has properties like the current noise does. So you need a way to capture those and combine it with the functionality. The noise script can emit events as well like onFinishedPlaying, onHalfWayThrough whatever. A script is just a class to hold all that information, then you make instances of it by attaching it to entities.

It's not engine commands. We have our own scripts that do a lot of gameplay specific events and functions that we would want to hook up as well. OnScorePoint, OnFoundTreasure, etc etc. Stuff that you can't make common commands for but are useful for our games scripts.

Now, once you've attached a bunch of scripts to an entity then yes you need a place to connect everything between those scripts and a per entity flowgraph is a good idea on how to do that actually. One could do another script that is specific to that entity and do it all by typing the code if they wanted to do that but a flowgraph for each entity would be a really cool way to do that. If you could save the entity as a prefab and it has all that information (scripts, flowgraph, model, etc) that would be cool too!

Link to comment

I'm not following why you want to programmatically generate a script to do this specific game requirement. With the idea of connecting functions to 'events' that's just configuration information between generic/reusable user created scripts that is ran when the starting 'event' is triggered.

If you're thinking about a per-entity flowgraph, all you need is the information of the entity, attached scripts, and the connecting information. After all that is loaded, read a text file that stores the config of what hooks to what and since lua is so flexible that string information in the text file should be able to be setup since everything in lua can be accessed with it's string name.

So let's imagine entity1 has a health script, sound script, postprocessing script attached to it. The per entity flowgraph could just be a text file config file I would think. When saving you're saving the 'event' variable name and what script it belongs to and the called function and what script that belongs to. All that information can be stored as a string and read back as a string and then you can get all the actual lua objects from their string names. So not following the idea of generating a script to do this functionality.

Link to comment

Because a lot of people want to choose from a list of options and create things visually, then see the code that generates.

Lua user base is 10x bigger than C++ user base. I think a non-coding userbase would be 10x bigger than Lua userbase.

Link to comment

That's why I'm confused as to why you said generated script. No lua script needs to be generated for visual coding really. That's all just configuration of what hooks to what. Code generation adds another layer into the mix.

Link to comment

The above flowchart would generate this code:

function Object:Start()
    self.source = self:EmitSound(LoadSound("Sound/ouch.wav"),50)
end

function Object:Update()
    if type(self.health)=="number" then
        if self.health < 25 then
            self.source:Resume()
        else
            self.source:Pause()
        end
    end
end

 

Link to comment

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...