Jump to content
  • entries
    943
  • comments
    5,899
  • views
    924,334

Using Multiple Entity Scripts in Turbo Game Engine


Josh

12,467 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



You will also get a set of very easy to use command to get and set values in Lua from C++. There are all part of the Entity class:

void Entity::SetValue(const std::string& name, shared_ptr<Object> o);
void Entity::SetValue(const std::string& name, const std::string& s);
void Entity::SetValue(const std::string& name, const bool b);
void Entity::SetValue(const std::string& name, const float f);
std::string Entity::GetValueType(const std::string& name);
std::string Entity::GetValueString(const std::string& name);
bool Entity::GetValueBool(const std::string& name);
float Entity::GetValueNumber(const std::string& name);
shared_ptr<Object> Entity::GetValueObject(const std::string& name);

An entity does not need to have any script attached to it to have values added, so you can use these to attach values to your entities even if you aren't using Lua!

Link to comment

I've waited a few years for this. Hooray! Why the change of heart?

The implementation seems a little strange to me. I get you're trying to keep it simple and avoid having to prefix variables and methods with the script name but sometimes you may just really want to call one scripts function and not have all the ones with the same name called.

I would rewrite your HurtEnemy() method like the below and then it's not that bad. This assumes GetScript() on an entity gets the instance of said script on that entity and that system is in place. This allows us to not have to type self.enemy as much and also reduces the indentation so it's easier to follow.

 

function Script:HurtEnemy(amount)
	if self.enemy == nil then return end
	
	local healthMgr = self.enemy:GetScript("HealthManager")
	
	if healthMgr == nil then return end
	if healthMgr.TakeDamage == nil then return end
	
	healthMgr:TakeDamage(amount);
end

I don't see anything wrong with the above. It's short and to the point. It really only does 1 extra check that a script with that name is attached. To call multiple functions have some sort of self:CallMethods("TakeDamage", args)? This might not be bad anyway as it can do the check internally if it exists so we don't have to continually do that check each time we want to call a function that we think might exist.

  • Haha 1
Link to comment

@Rick that is EXACTLY the kind of thing I want to avoid, and this system does a nice job of it! :D

However, you could do put all of one script's values in a special table if you wanted:

function Script:HurtEnemy(amount)
	if self.enemy == nil then return end
	
	local healthMgr = self.enemy.HealthManager
	
	if healthMgr == nil then return end
	if healthMgr.TakeDamage == nil then return end
	
	healthMgr:TakeDamage(amount);
end

Or even simpler, just add a prefix to the function name that is unlikely to be used by another script:

function Script:HurtEnemy(amount)
	if self.enemy == nil then return end
	if self.enemy.HealthManager_TakeDamage == nil then return end
	self.enemy.HealthManager_TakeDamage(amount);
end

But the default behavior is just going to call all occurrences of the TakeDamage function like this:

function Script:HurtEnemy(amount)
	if self.enemy == nil then return end
	if self.enemy.TakeDamage == nil then return end
	self.enemy.TakeDamage(amount);
end

I hope to see some really neat emergent behavior come from this system when two scripts work together that weren't written with the other in mind.

Link to comment

 

29 minutes ago, Josh said:

I hope to see some really neat emergent behavior come from this system when two scripts work together that weren't written with the other in mind.

This is the goal of having multiple scripts which is why some of us asked for it. I think the implementation is a little strange but we'll see how it works out.

  • Like 2
Link to comment

I'll probably never use the multiple return value feature and mostly stick with mono-scripting mostly However, it's really cool to have multiple script attachments for those cases in which multiple scripts would make sense. I rarely run into it, but it does happen.

Overall the Lua implementation looks much better than it is in Leadwerks. Really stoked about using an entity as a table for other scripts to add, remove or manipulate. You made this much better, hopefully you can hit the same bar with the flowgraph/io system. ?

Link to comment
Quote

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

This is to this day how I would have liked the lua scripts to have worked in Leadwerks. Albeit slightly differently scripted.

The example you give really describes the problem with singular scripts. The player script has no business seeking out a healthmanager subscript that should be managed by the enemy. It is the enemy's script responsibility to expose any of its subcomponents to the outside world. Personally I never check if something is a function either. By convention variables are lowercase and function are not. 

In this case the player only check if it has enemy and that gives it damage.

--player
function Script:HurtEnemy(amount)
    if self.enemy ~= nil then
        self.enemy:ApplyDamage(amount)
    end
end

The enemy has a reference to the health manager which it retrieves in the start function. If it is not there, the game should simply report an error since something is wrong in your setup. Lastly, the enemy script passes the damage value on to the right component(s). In this case only the healthmanager. Ideally you would set this up by events.

--enemy script
function Script:Start()
	self.healthManager = self:GetScript("healthManager")
	if (self.healthManager == nil)
		error("Enemy has no healthmanager. Something is wrong in your setup.")
end

function Script:ApplyDamage(amount)
	self.healthmanager:ReduceHealth(amount)
end

The health manager finally actually does something with it. The amount variable should only be changed by the healthmanager itself and not by other scripts. 

--healthmanager
function Script:ReduceHealth(amount)
	self.amount = self.amount - amount
end

Now you have the logical separated scripts that are far easier to maintain due to small amount of code lines, the absence of constant nil checks and by keeping responsibility to the component and its owners.

 

9 hours ago, Rick said:

The implementation seems a little strange to me. I get you're trying to keep it simple and avoid having to prefix variables and methods with the script name but sometimes you may just really want to call one scripts function and not have all the ones with the same name called.

I would rewrite your HurtEnemy() method like the below and then it's not that bad. This assumes GetScript() on an entity gets the instance of said script on that entity and that system is in place. This allows us to not have to type self.enemy as much and also reduces the indentation so it's easier to follow.


function Script:HurtEnemy(amount)
	if self.enemy == nil then return end
	
	local healthMgr = self.enemy:GetScript("HealthManager")
	
	if healthMgr == nil then return end
	if healthMgr.TakeDamage == nil then return end
	
	healthMgr:TakeDamage(amount);
end

I don't see anything wrong with the above. It's short and to the point. It really only does 1 extra check that a script with that name is attached. To call multiple functions have some sort of self:CallMethods("TakeDamage", args)? This might not be bad anyway as it can do the check internally if it exists so we don't have to continually do that check each time we want to call a function that we think might exist.

This would be my choice too Rick. Maybe that is because this is far more similar to how Unity does it with GetComponent<> or Godot with get_node. I think what plays along here is that Josh really likes to implement his own methods rather than just 'copying' the same methods others use. Both systems work, and Lua is awesome in this kind of flexibility.

@Josh: have you considered using a GetComponent feature? People using other engines will recognize this function and find it perhaps an argument as too why they could potentially switch.  A future documentation page I would recommend: Switching from Unity to Turbo/Leadwerks. "If you are used to using GetComponent<>(), with Leadwerks you are just as easy on your way with GetComponent(YourScript)." 

 

8 hours ago, reepblue said:

I'll probably never use the multiple return value feature and mostly stick with mono-scripting mostly However, it's really cool to have multiple script attachments for those cases in which multiple scripts would make sense. I rarely run into it, but it does happen.

From the day I learned that this was possible I never even considered using multiple return values. Not that is not powerful, but merely the complexity this suddenly adds, stops me from using it. This already happens when you have a function that can have any number of arguments. If you don't deal with all these arguments you will eventually kick yourself in the knee. Especially if you program in a team, you expect that a user does something with the given values or you would need to have strict guidelines in how to deal with them.

 

5 hours ago, Josh said:

Do you guys have any ideas for changes or things you would like to see in the flowgraph system?

Thought you would never ask. Lets start with some basics:

  • Zooming in and out of a current layer.
  • Flowgraph works by creating scene level graphs and prefab/model based graphs.
  • Level based
  • Layers:
    • When working with FPS levels (like the elevator demo), you want to add level logic for a certain area in a layer. eg:
      • Starting area layer
      • Elevator room area layer
  • Panels
    • These are more UI containers for your 'graph nodes' to attach to. Naming or colouring the panels is a nice addition.
    • Image result for flow editor game visual
  • Improved input types:
    • curve property
    • material selector

More advanced:

  • Nested flows/ Object flows. 
    • Nested flows are excellent for reuseablity. 
    • Nested flows are fixed flows per entity/prefab. This means a flow is always similar regardless of what level you are playing.
    • Example
      • Lets say you have to 3 script in a flow: CollisionTrigger, timertrigger and EnemyTurret.
      • The CollisionTrigger.OnCollision can activate the Turret
      • The TimerTrigger.OnCollision can also activate the Turret after a given time
      • 1401584236_Levellayer.jpg.311ccee461d06b059f4a3e38ffdc1153.jpg
      • Double click on the EnemyTurret opens the subflow of this turret. This flow is on an object level instead of scene level. 
        • The subflow always has its parent entry arguments coming in
        • The subflow always has its parent exit arguments going out.
        • This subflow produces a rocket that can again also be double clicked since it is an object flow.
        • 1315515817_Objectsublayer.jpg.28084323cbae997c7edec6ee04b92389.jpg
  • Upvote 2
Link to comment

I can give you a concrete example of how this helps. We are making  bunch of VR demos right now, and we have a bunch of different scripts for different controller tools. Depending on the demo, the tools and controls we want to use are different. The demos also usually need a reset option so that they can be quickly reset back to the original scene without reloading anything. These scripts often create sprites for visual indicators, change the texture of a controller, or make other modifications that need to be rolled back to reset the scene.

With the system I describe above, we can have this code in any of those scripts:

function Entity:UpdateControls()
	if VRControllerButtonPressed(self.hand,VR_BUTTON_MENU) then
		self:Reset()
	end
end

If the code for either the right or the left controllers calls Reset() like above, then the Reset() function in both scripts will get called, resetting the appearance of both controllers. Of course if you want to make sure you are calling one specific function you can add a prefix and call the function something like MeasuringTool_Reset().

Now for a theoretical example. Let's say I want to make a script that adds massive amounts of blood and a scream when an entity receives a large amount of damage, something like this. All I have to do is add another TakeDamage function into the script, and I can be sure it will always get called automatically on top of any other functions that an AI script contains:

function Entity:Start()
	self.screamsound=LoadSound("Sound/ouch.ogg")
	self.splatter=CreateEmitter()
	self.splatter:SetMaterial(LoadMaterial("Materials/blood.mat"))
	self.splatter:Hide()
end

function Entity:TakeDamage(amount)
	if amount > 20 then
		self.entity:EmitSound(self.screamsound)	
		local mess = self.splatter:Instance()
		mess.position = self.position
		mess:Show()
	end
end

This can be added to any AI script and the added behavior will be automatically created, without the calling script having to say something like "BloodManager.MakeMess()" or something like that.

Of course it would be possible for your calling script to loop through all different scripts like this:

for n,script in entity.scripts do
	script.TakeDamage(entity,amount)
end

But in practice no one will code scripts this way: you just want to check if the function is there and then call it if it is available. Basically what I have done is made the above for loop the automatic default behavior.

Another example: Let's say you want to add a dissolve effect like in Doom 3 so when an enemy is killed they look like they are burning up and disintegrating. With this system you can do it like this, assuming that we make Kill() a commonly used convention:

function Entity:Start()
	self.dissolvematerial = LoadMaterial("Materials/Effects/dissolve.mat")
end

function Entity:Kill()
	self.dissolvestarttime = CurrentTime()
	self.entityLSetMaterial(self.dissolvematerial)
end

function Entity:Update()
	if self.dissolvestarttime ~= nil then
		self.entity:SetColor(1,1,1,(self.CurrentTime()-self.dissolvestarttime) / 3000)
	end
end

Now here is a zombie script you can add to any entity with some AI. This assumes that Reset() is a commonly used function. This will being dead enemies back to life:

function Entity:Kill()
	self.deathtime = CurrentTime()
end

function Entity:Update()
	if self.deathtime ~= nil then
		if CurrentTime() - self.deathtime > 10000 then
			if self.Reset ~= nil then self:Reset() end
		end
	end
end

With this Reset() convention in mind, let's make it so our dissolve effect will also be automatically reset:

function Entity:Start()
	self.originalmaterial = self.material
	self.dissolvematerial = LoadMaterial("Materials/Effects/dissolve.mat")
end

function Entity:Kill()
	self.dissolvestarttime = CurrentTime()
	self.entityLSetMaterial(self.dissolvematerial)
end

function Entity:Update()
	if self.dissolvestarttime ~= nil then
		self.entity:SetColor(1,1,1,(self.CurrentTime()-self.dissolvestarttime) / 3000)
	end
end

function Entity:Reset()
	self.entity.material = self.originalmaterial
	self.entity.color = Vec4(1)
end

What this really helps with is scripts that add behavior and interact smoothly other scripts.

  • Like 1
Link to comment

I see your point Josh and the advantages to this method are plentiful based on your examples alone. It is an interesting idea with lots of flexibility.

Maybe this is just me, but I see the level of flexibility as an increase to the complexity of the game. New users will lose track of what functions are attached to an entity. I am not trying to shoot down your ideas here btw, I am just trying to see if the advantage way up to possible disadvantages. For instance, I wonder how this works in practice when you are debugging an enemy hat has 8 different scripts applied that all have the Kill() function. With this method it seems that infinite recursion and stack overflow exceptions are constantly lurking around the corner here. 

If I have a bug and start debugging I would expect some sort of ListAllFunctions() that gives you an overview of all execution ordered function (duplicates) on an entity. As long as you can properly see the stack trace and easily verify which Kill() function has already been called you can can away with it. 

Either way, the concept of attaching these scripts is interesting and the discussion about it as well. Keep on improving and blogging.

Link to comment
1 hour ago, Josh said:

What this really helps with is scripts that add behavior and interact smoothly other scripts.

In practice I don't think you will get this smooth interaction. If you look at the examples in this blog you'll see that 3 different people named a function with similar functionality differently. ReduceHealth()/TakeDamage()/HurtEnemy(). The chances that different programmers all name their functions the same in order to add different functionality is pretty slim. There will just be little cohesion to make this act as the bridge for adding different scripts from different programmers who weren't working together.

While this helped your situation, which unless you had 25+ different scripts attached I don't see why calling the specific scripts reset is that huge of a deal, this is a pretty specialized situation for the NASA stuff you're working on.

I think you'd be better off using an event system to get what you want so things are more explicit. Define an event in one master script on the entity and in it's Start() get references to all the scripts attached and add their Reset() methods to the event. Then raise the event and it'll chain call all those methods. While for your specific case it may seem like more work, for a more generic case, like our games, it gives more flexibility in chaining functions with different names by raising 1 event, it gives explicit intent which makes debugging easier, and it makes it so the community isn't trying to come up with consistent naming conventions which will never happen.

Link to comment

This seems like the perfect system to me. You can use just one script like before, you can automatically "combine" many scripts and still use them without knowing how they are called and you can separate functions into different tables if you want that. Lua is so good.

Link to comment
2 hours ago, Rick said:

In practice I don't think you will get this smooth interaction. If you look at the examples in this blog you'll see that 3 different people named a function with similar functionality differently. ReduceHealth()/TakeDamage()/HurtEnemy(). The chances that different programmers all name their functions the same in order to add different functionality is pretty slim. There will just be little cohesion to make this act as the bridge for adding different scripts from different programmers who weren't working together.

I think you'd be better off using an event system to get what you want so things are more explicit. Define an event in one master script on the entity and in it's Start() get references to all the scripts attached and add their Reset() methods to the event. Then raise the event and it'll chain call all those methods. While for your specific case it may seem like more work, for a more generic case, like our games, it gives more flexibility in chaining functions with different names by raising 1 event, it gives explicit intent which makes debugging easier, and it makes it so the community isn't trying to come up with consistent naming conventions which will never happen.

hehe, starting to have a déja vu again. I think for the past 5 years there has always been a topic each year mentioning the 'similar named' functions like "Kill", "Hurt" as well as using events. I don't see an event system happening for Turbo either unfortunately.

  • Like 1
Link to comment

Imo, it should be up to the end user to decide how they want to build their systems. The engine should do just this, allow multiple scripts to be attached to the entire and handle duplications if there is any.

Would I personally attach multiple scripts with the same function and value names? Most likely not, but that's my choice as a programmer. Multiple scripts will remove needing to rewrite effects per object type and allow things to be more modular.

I see this as another tool in the toolbox. As long as it doesn't hinder what a programmer wants to do (Like if Rick wants to use an event system.) Its fine by me.

Link to comment
30 minutes ago, reepblue said:

I see this as another tool in the toolbox. As long as it doesn't hinder what a programmer wants to do (Like if Rick wants to use an event system.) Its fine by me.

It currently does cause issues with script variables being overwritten. You can't have independent script variables with the same name as they'll just merge into 1 variable that all scripts share. So people have to be careful with their script variable names especially if using a script someone else wrote as it could cause unforeseen and hard to track issues. What you just said is the main reason to just create a table on the entity for each script attached to store the instance of that script and then reference any function or variable by using that scripts instance because it's the most flexible way that satisfies the most use cases, which is not what this system is doing. Surprisingly, since josh usually hates forcing convention, this system forces convention even if you don't want it. ie. if 2 scripts have the same function name calling one anywhere will automatically call both every time no matter what. To get around that is a naming convention that you hope other scripts from other programmers don't have a collision with your function names. I'm assuming a very specific use case for the NASA stuff Josh is working on caused this.

Link to comment

It sounds like we will have to try it out with real scripts before we can really know all the details. It will be interesting to see how this develops in the beta. Nothing is set in stone yet, but I am excited about the possibilities.

Link to comment

It would be a good idea to start planning out some common functions and building a set of common behaviors, like below:

------------------------------------------
-- General
------------------------------------------

function Entity:Activate()
    if self.disabled==true then return end
    self.active=true
end

function Entity:Deactivate()
    if self.disabled==true then return end
    self.active=false
end

function Entity:Disable()
    self.enabled=true
end

function Entity:Enable()
    self.enabled=false
end

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

------------------------------------------
-- Health and Wellness
------------------------------------------

function Entity:Hurt(damage,distributorOfPain)
    if self.disabled==true then return end
    if type(self.health)=="number" then
        self.health = self.health - damage
        if self.health<=0 then self:Kill(distributorOfPain) end
    end
end

function Entity:Kill(culprit)
    if self.disabled==true then return end
    if type(self.health)=="number" then
        if self.health>0 then self.health=0 end
    end
    self.dead=true
end

------------------------------------------
-- AI
------------------------------------------

function Entity:Wake()
    if self.disabled==true then return end
    if self.target~=nil then
        if self.team==self.target.team then
            self.mode="follow"
        else
            self.mode="attack"
        end
    else
        self.mode="idle"
    end
end

function Entity:Sleep()
    if self.disabled==true then return end
    self.mode="sleep"
end

function Entity:SetTarget(target)
    if self.disabled==true then return end
    self.target=target
    if self.target~=nil then
        if self.mode~="sleep" then
            if self.team==self.target.team then
                self.mode="follow"
            else
                self.mode="attack"
            end
        end
    else
        if self.mode~="sleep" then
            self.mode="idle"
        end
    end
end

Let's say I want to make a script that modifies any AI so that when it sees the player, it will automatically alert all nearby friends, and they will all come to attack. The script below can be added to the enemy characters, and it will modify the behavior of any enemy to add this little additional behavior:

function Entity:Start()
    self.sound_upthere=LoadSound("Sounds/Dialog/Hes up there.mp3")
    self.sound_downthere=LoadSound("Sounds/Dialog/Hes down there.mp3")
    self.sound_overhere=LoadSound("Sounds/Dialog/We got em.mp3")
end

function Entity:SetTarget(target)
    if target~=nil then
        if self.mode~="sleep" then
            if self.team~=target.team then

                if target.position.y-self.position.y>10 then

                    --Play "He's up there!"
                    self.entity:EmitSound(self.sound_upthere)

                elseif target.position.y-self.position.y<10 then

                    --Play "He's down there!"
                    self.entity:EmitSound(self.sound_downthere)                  

                else

                    --Play "Hey guys, I found him!"
                    self.entity:EmitSound(self.sound_overhere)

                end

                --Look for friends and alert them
                local entities=self.world:GatherEntities(self.position-20,self.position+20)
                for n,entity in ipairs(entities) do
                    if entity.team==self.team and entity~=self then
                        entity:SetTarget(target)
                    end
                end

            end
        end
    end
end

 

  • Thanks 1
Link to comment

Hey guys. 

Remember, that some of us are Artists, others of us are programmers, and occasionally you get someone that has tried to tackle both. Fluidity in process is key. The Flowgraph editor is still very useful to those of us that still have to "Cling to the handrail" pertaining to interactivity. I hate to say it, but I need to continue learning LUA, and I need to learn to structure my code to properly execute it. That would mean long lead times for me as a developer. 

I think, and this is an odd idea I must say, lets take a look at the documentation for the LUA scripts tutorial pages, we have several sections there. Variables, Constants, loops and a few other LUA API commands. I am trying to think of using LUA as a car mechanic goes to his toolbox looking for the "right tool for the job". Sometimes, I'm not sure what the right tool is but I know its in that toolbox. 

using a program like Raspberry pi "scratch" lays all the tools out for you and color codes connectable nodes. Maybe, you could color code nodes or even color code functions and constants, variables, etc different colors to show people that certain "tools are for certain jobs". I have a feeling some of the up and coming LEADWERKS users may be trying to hammer a nail with a protractor, or trying to tighten a bolt with a magnifying glass. (Oh, the mental images)

Just an idea. for what its worth. 

Link to comment
9 minutes ago, Josh said:

It would be a good idea to start planning out some common functions and building a set of common behaviors, like below:

I'm very shocked I'm hearing you say this. This is a complete 180 from your normal style. There are pluses to doing this but it would have to be built in and promoted by you, the creator, to work. You're basically making more game specific UpdateWorld() type script functions. I never thought I'd see the day.

  • Haha 1
Link to comment

wait, why is this a bad idea rick? Scratch has been very popular with incumbent youth seeking programming knowledge. I think josh could write:

"Did you ever use Scratch on R-pi? Well then you will dig the skittles out of this" 

 

Link to comment
13 minutes ago, GSFC_1 said:

wait, why is this a bad idea rick? Scratch has been very popular with incumbent youth seeking programming knowledge. I think josh could write:

"Did you ever use Scratch on R-pi? Well then you will dig the skittles out of this" 

I hope my post didn't come off as it was a bad idea. I said there are pluses to doing it this way and just needs to be promoted and supported by Josh (he can't just tell people to name their functions this way he needs to built them into entity scripts just like UpdateWorld() and others are).

I mentioned that this is completely different from what Josh has wanted in the past. Josh was always more of a library vs framework guy. LE was always a library. it gave you generic ways to create games and/or multimedia apps. What he's saying now is way more specific and funnels people towards a common way of creating these apps/games.

A library is more like a bunch of baking tools and ingredients. You have everything you need to bake something but it's 100% up to you on how to use those tools and ingredients to do it. A framework is more like following directions on how to bake something. It gives detailed guidelines on how to do it, but still has a little room for you to add your own special mix to set it apart. Josh was always a library guy with LE and with this blog we can see the shift which is surprising to us who have been around awhile.

Link to comment
54 minutes ago, Rick said:

I'm very shocked I'm hearing you say this. This is a complete 180 from your normal style. There are pluses to doing this but it would have to be built in and promoted by you, the creator, to work. You're basically making more game specific UpdateWorld() type script functions. I never thought I'd see the day.

I think this design makes it a lot easier to plan and visualize. Looking at my AI alert example above, basically, what it is doing is taking a function that is like this:

function Entity:foo()

	block of code 1...

	block of code 2...

	block of code 3...

end

And breaking it into three scripts that can be mixed and matched:

function Entity:foo()
	block of code 1...
end
function Entity:foo()
	block of code 2...
end
function Entity:foo()
	block of code 3...
end

 

Link to comment

Because values are attached directly to the object, there's actually no reason you can't add values to types of objects other than entities:

local material=LoadMaterial("Materials/Wood/floor1.mat")
material.stepsound=LoadSound("Sound/Steps/wood.wav")

In fact there is no reason scripts can't be added to all types of objects, not just entities:

local sound=LoadSound("Sound/Music/battle.ogg")
sound:AddScript("Scripts/Sound/I have no idea.lua")

However, I have trouble coming up with ideas of what a script attached to something other than an entity would even do. I supposed you could make a script for a material that makes the material change color over time like this:

function Material:Update()
    self:SetColor(Sin(Millisecs())*0.5+0.5)
end

I can't really think of any other types of objects this could be useful for, except GUI widgets of course, which is already supported.

Link to comment

Flowgraph connections can be created programmatically like so:

entity->Connect("Activate",door,"Open");

Ready to have your mind blown?

//Create a model and add a script with a Collision() function
auto box=CreateBox(world);
box->AddScript("Scripts/Objects/Physics/ImpactNoise.lua");

//Create a material and add a script that changes the color
auto mtl=CreateMaterial();
box->SetMaterial(mtl);
mtl->AddScript("Scripts/Objects/Appearance/ChangeColor.lua");

//Create a flowgraph connection
auto connection = box->Connect("Collision",mtl,"SetColor");
connection->AddArgument(Vec4(1,0,0,1));

The box material will turn red when the box collides with something. ?

Link to comment
25 minutes ago, Josh said:

I think this design makes it a lot easier to plan and visualize. Looking at my AI alert example above, basically, what it is doing is taking a function that is like this:

I'm well aware of the benefits of adding multiple scripts to entities. Some have been asking for it and simulating it in LE for a long time :). That's not the question. The question really is the best way to do it. Giving the ability while not pigeonholing you into a style is generally what I think is best/safest for the core part of the engine. Then you can add something like this on top of that.

The value attaching thing is a great idea for sure!

Link to comment
4 minutes ago, Josh said:

The box material will turn red when the box collides with something.

I just want to note that the high level concept you are doing in that example is basically connecting event(s) to functions. A great way to program :). I have multiple blogs on that from Roland and me back a  year or so ago. It handles arguments and other things like criteria to call the function or not (returns a boolean to call or not. comes in handy).

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...