Jump to content
  • entries
    945
  • comments
    5,899
  • views
    930,074

A second look at entity scripts


Josh

3,149 views

 Share

With Luabind, it turns out we don't even need a table associated with an entity. We can just add values and functions straight to Lua's representation of that entity in the virtual machine! So instead of this:

object.health=100
object.entity:SetPosition(1,2,3)

You can just do this, which is much nicer!:

entity.health=100
entity:SetPosition(1,2,3)

So there's no object/actor nonsense, you just work directly with the entity itself. :)

 

Entity Keys

The Get/SetKey convention from Leadwerks Engine 2 is going away, to be replaced with more direct entity access functions. You can still store "keys" with strings, but this will directly set the values of the entity table in Lua, so they can be access from script more easily:

void Entity::SetString(const std::string& name, const std::string& value);
std::string Entity::GetString(const std::string& name);

void Entity::SetFloat(const std::string& name, const float& value);
float Entity::GetFloat(const std::string& name);

void Entity::SetInt(const std::string& name, const int& value);
int Entity::GetInt(const std::string& name);

void Entity::SetObject(const std::string& name, Object* o);
Object* GetObject(const std::string& name);

Here's a sample "mover" script that performs simple movement and rotation each frame:

function entity:Start()
self.movespeed=Vec3(0)
self.turnspeed=Vec3(0)
end

function entity:Update()
self:Move(self.movespeed)
self:Turn(self.turnspeed)
end

And this is how we would set an entity up in C++ to turn 2 degrees on the Y axis each frame:

Entity* box = CreateBox();
box->AttachScript("Scripts/Entity/Mover.lua")
box->SetObject("turnspeed",Vec3(0,2,0))

Getters and Setters

LuaBind does support getter and setter functions, and I decided it would be nice if we could have an object-oriented command set for the surface commands (even though vertices are NOT an object-oriented structure, at all!). If I can get vectors to be accessible in Lua, then we will be able to write code like this script, which performs a "breathing" effect on any model, ala Quake 3 Arena:

function Entity:Start()
this.speed = 1
this.amplitude = 0.1
end

function Entity:Update()
local n,v.d

--Make sure this is a model entity before calling commands
if this.class = CLASS_MODEL then

	--Get the distance the vertex should move
	d = math.sin(AppTime() * speed) * amplitude - amplitude

	--Apply movement to all vertices
	for n = 0,this.surface:length()-1 do
		for v = 0,this.surface[n].vertex:length()-1 do
			this.surface[n].vertex[v].position += d * this.surface[n].vertex[v].normal
		end
	end

end

end

Multiple Script Attachments

This is a very tricky thing to handle, because the behavior is so hard to define. Right now I have it set up so predefined functions will be called, in order of their attachment. As for user-defined functions, that's a lot hard to pin down. If two scripts contain a function called "Kill()", and the script itself calls the function, should both functions be called? It will take more time and testing to see how this should work. A big problem is if the user defines a function that returns a value, and two scripts contain the same function. If another script calls the function, what should be returned? So then I start thinking about separate spaces for each script attachment, with their own set of members and functions, and I realized how incredibly hard to understand that would be, if you were accessing this entity from an outside script. :o

 

In the absence of any compelling technological advantage, simple is best. For now, only the predefined functions get executed in sequence, and none of those return a value. My prediction is multiple script attachments will be used primarily by non-programmers who just want to combine a few behaviors and see results without touching any code. When you get into more complex behvior I think script programmers will generally use one script per entity that does what they want.

 

It's still only 1:30 in the afternoon, so I am going to go get lunch and spend the rest of the day working bug reports. The model reloading issue that's active is a tough one. There's also a PHP issue uploading files to the site, so I will try to get that resolved.

 Share

9 Comments


Recommended Comments

I love how my research and recommendation to use LuaBind over many others turns out every day to be more powerful :)

You have probably exploited LuaBind more than anyone else ever has in a 3D engine, and this will be years ahead of all other engines.

Link to comment

I've got iterators working, so you can do this:

for child in entity.kids do
Print(child.name)
end

Unfortunately, the Entity returned by the iterator does not match the lua object the main program uses, and so you can't set attributes from the main program and detect them elsewhere. This is pretty complex stuff.

Link to comment

Great Josh , really great :)

When you get into more complex behvior I think script programmers will generally use one script per entity that does what they want.

That is absolutely OK

Link to comment

Perhaps you can give each script a name property which will be the script file name itself and we can access it like:

 

FindScript("characterstats"):AddHealth(5)

 

or somehow be able to prefix the script name directly like characterstats:AddHealth(5)

 

Then require that you can't attach 2 scripts with the same name. This would help reduce confusion for newbies and pros both.

 

I would suggest also a FindEntity("") that can be called from any entity to find another entity in the world. Then FindEntityTag("") to be able to group entity types in the case of dynamic entities.

Link to comment

I considered something like that, but I'm not convinced it's that great::

function door:Open()
self:Turn()--Oh wait, we can not do that, because this is the door...

--What if we say "entity" is a value in "door"?  It would be entity.door.entity:
this.entity:Turn()--Okay, this works alright
end

function door:Update()
this.health --wait, that does not work, unless you want the value contained within "door", in which case GetString("health") would not return it
this.entity.health = 100--okay...
end

It seems like a lot of mess for something that is supposed to be as simple as possible. I really do not like the infinite recursive loop of tables.

 

I suppose you could do something like this in C++:

entity->SetInt("door.openstate","1")

Which doesn't look too bad.

 

Of course, you could simple add a prefix to all your functions and attributes you don't want messed with, and achieve the same exact outcome with a lot less complexity:

function entity:Door_Open()
this:Turn(1,0,0)
this.door_openstate=1
end

Link to comment

I personally don't think the entities should have variables, just the scripts attached to them. Each script should be creating it's own "instance" for that entity. So for example the variables in a script that is attached to 4 different entities would have it's own instance of those variables per entity, essentially making those variables apart of the entity itself, but indirectly via the scripts.

 

In my view entities should basically be dumb. They are just placeholders for scripts and other objects like a model or a sound object, or nothing but a script that runs game logic. You build on these things to an entity to make it smart but it's not the entity that is holding anything but the objects attached to it.

Link to comment

If that were the case, we'd be looking at something like this for the mover script:

function script:Start()
self.movespeed=Vec3(0) end
self.turnspeed=Vec3(0) end
end

function script:UpdateMatrix()
Print(self.entity.position.x..","..self.entity.position.y..","..self.entity.position.z)	
end

function script:Update()
if self.movespeed~=nil then
	self.entity:Move(self.movespeed)
end
if self.turnspeed~=nil then
	self.entity:Turn(self.turnspeed)
end
end

It's not bad, although the extra .entity table is not preferable.

 

Let's say you have a rocket script, and it collides with some entity. If all functions were stored on the entity, you could do this:

function script:Collision(entity,position,normal)
if entity.Hurt~=nil then
	entity:Hurt(5)
end
end

But if all information is stored in script subobjects, you would only be able to interact with some classes:

function script:Collision(entity,position,normal)
if entity.player~=nil then
	entity.player:Hurt(5)
end
end

Link to comment

It's not bad, although the extra .entity table is not preferable.

 

I would almost prefer that so it's more explicit to what you are doing.

 

entity:Hurt(5)

 

Like you pointed out this leads to issues when Hurt() is defined in 3 different scripts attached. Prefixing the script name of 'player' removes that issue. If you want to have a way to call multiple functions with the same name in different scripts I would keep your SendMessage() method of the entity and instead of firing a ReceiveMessage() it fires the actual method name. That would still give the ability to call all similar named methods if needed because that could be valuable if you need to inform every script attached. You would just make it not return a value, and I think Lua can handle the dynamic parameter count.

 

you would only be able to interact with some classes:

 

Why only some? You would be able to interact with every script attached to the entity.

Link to comment

The idea is we might be writing a rocket script right now, but some time in the future we might come up with an entirely new entity behavior that we want the rocket to interact with, without altering the original rocket script. Maybe I'll want to make a special button you shoot to activate, so it has to detect when a rocket hits it.

 

A good case could also be made for having two scripts attached of the same type. A lot of oscillating and gradual motions might use two or three script attachments of the same type, with various settings adjusted to produce a combined output. For example, my vegetation waver code is the combination of a few different sine curves with different frequencies and amplitudes. You wouldn't want to do that in a script, but that's an example of the idea.

 

Each script could just be appended to the end of an entity.scripts table, in which case you could just do this:

for script in entity.scripts do
if script.Hurt~=nil then
	script:Hurt(5)
end
end

Then you could just call the function for all attached script, without knowing or caring what other scripts are doing.

 

I need to come up with some more usage examples and see how they would work out.

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