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.
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:
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.
- 1
- 1
51 Comments
Recommended Comments