Multiple AIs with one Lua state

Started by
23 comments, last by ddn3 14 years, 11 months ago
I'm starting to wonder if Lua is even appropriate for the kind of scripting I want to accomplish here.

Like the creator of the thread Sneftel linked to, I sort of want my scripts to be something that any object can just run; e.g. an NPC's think() method is runAIScript("myScript.lua"). I wanted to be a bit more elaborate by having these scripts store functions the objects could choose from, but it's the same spirit as far as I can tell. But with Lua's global name space it seems I either have to use multiple Lua states, jumble my scripts up with tables and stuff, or maybe just give up and write my entire game in Lua.

I chose Lua because it was popular, fairly minimal, and could also be used for data definition. However, is there perhaps another scripting language that would be a better fit for this structure.
Advertisement
Quote:Original post by Guy Meh
I'm starting to wonder if Lua is even appropriate for the kind of scripting I want to accomplish here.

Like the creator of the thread Sneftel linked to, I sort of want my scripts to be something that any object can just run; e.g. an NPC's think() method is runAIScript("myScript.lua"). I wanted to be a bit more elaborate by having these scripts store functions the objects could choose from, but it's the same spirit as far as I can tell. But with Lua's global name space it seems I either have to use multiple Lua states, jumble my scripts up with tables and stuff, or maybe just give up and write my entire game in Lua.

If you consider hierarchical organization with tables to be "jumbling things up", you will almost certainly be unhappy with Lua.
this old post seems to deal with the exact same problem. The idea is to surround the code from a script file in a table. I just don't quite understand how to reproduce this with luabind (still pretty new to lua/luabind). and i'd like to hide all this from the enduser, of course. can you think of a way to do this?
Wunderwerk Engine is an OpenGL-based, shader-driven, cross-platform game engine. It is targeted at aspiring game designers who have been kept from realizing their ideas due to lacking programming skills.

blog.wunderwerk-engine.com
It's the same issue but slightly different take on the problem. I stuff my functions into a global table using a unique key (for dispatching later), the other adds the functions to a global table but doesn't distinguish multiple additions, they just overwrite each other, but it's simple enough to get around that problem.

You can't dispatch the function directly since multiple scripts redefine the same function and the only distinguishing factor is the file which they come from.

This question is asked so often it should be in the FAQ. Here's what I would do in this case:

Provide C or Lua function to load up and register global functions of a given script into a global dispatch table. A global function is one which isn't defined using the local keyword. Some common global functions it could define are Initialize,Shutdown,Update etc..

After loading the script up the loader function auto-registers any global functions into a dispatch function table using the scripts file as a key. This eliminates the need for the script to do this themselves. It then clears out the global functions just loaded to prevent global name space pollution.

Anyone wishing to call a registered function from a script must pass on the script file name ( or some other convenient key ) which will be used as a key to lookup that function with the global dispatch table.

Registered scripts functions are passed an instance table (anonymous tables which are used to store state about the script). Because the scripts are shared between multiple instances they can't use local data to store their state, this assumes there is a one to many relationship between the scripts and objects using them.

These anonymous tables are stuffed in the objects (which own the scripts?) using the unique key of the given script.

To call a particular function from LuaBind you would call a dispatch function which needs 3 pieces of information, the script key ( the unique identifier for a particular script), function you want to call and the object itself ( because the instance data lives in that object ).

That's how I would do it.

I can only imagine why anyone would want to do this is because they want to write scripts which are nearly wholly self contained with a common API (Initialize,Shutdown,Update etc..) so they can be auto-registered more easily.

Good Luck

-ddn
ddn3, could you illustrate this with some (pseudo)code? What you're describing sounds exactly like what I'm looking for, but I can't quite follow you to the end. It's not that I want others to write my code, but I'm still pretty new to Lua, and it's C API just confuses me.

Thank you all for your patience!
Wunderwerk Engine is an OpenGL-based, shader-driven, cross-platform game engine. It is targeted at aspiring game designers who have been kept from realizing their ideas due to lacking programming skills.

blog.wunderwerk-engine.com
Better than pseduo code, I'll give u Lua code! :D Here ya go a simple example minus the C function used to load run the script.

--here is where all registered functions will liveGLOBAL_DISPATCH={}--use this function to load and register a scripts funcitonsfunction RegisterScript(scriptFile)	local key = scriptFile;		if (GLOBAL_DISPATCH[key]~=nil) then		print("script already registered");		return true;	end		local funcTable={}		--make sure these globals functions are nil before hand	Initialize=nil;	Shutdown=nil;	Update=nil;		--nfLoadAndRun is a C fucntion which loads and runs a script	--i don't know how to do  this from Lua, but in C you do 	--loadfile the do a pcall and that will load and run the script	local err = nfLoadAndRun(scriptFile)		if (err~=nil) then		print("Failed to load and execute script");		print(err);	--this should contain error message		return false;	end		--after the script has been loaded we can now store away these	--global, doesn't mater if they are nil, since dsipatcher will	--not execute nil funcitons.	funcTable["Initialize"]=Initialize;	funcTable["Shutdown"]=Shutdown;	funcTable["Update"]=Update;		--nil them out again to keep namespace clear	Initialize=nil;	Shutdown=nil;	Update=nil;		--store away our function table for later use	GLOBAL_DISPATCH[key]=funcTable;		return true;end--you'll use the resister function like soRegisterScript("Scripts/AI_Ogre")--here is an example script for say AI_Ogre (note that myData is passed in)function Initialize(myData)	myData.name="Mr Orgre";	myData.age=24;	myData.weapons={"battle axe","dagger"};	myData.dislikes={"elves","hobbits","brocolli");	myData.state="sleeping";end--note any function used by the script internally should be declared--local to prevent pollution and possible overwrite by other scriptslocal function UpdateAwake(myData)    if (math.random(0,100)==0) then	myData.state="sleeping";   endend--this is how a script uses stateful data for a particular instancefunction Update(myData)	if (myData.state=="sleeping") then		if (math.random(0,100)==0) then			myData.state="awake";		end	elseif (myData.state=="awake") then	    UpdateAwake(myData);	endend--doesnt define Shutdown but thats ok..--now to dispatch any function we need 3 thingsfunction Dispatch(funcName,scriptFile,object)	local key = scriptFile;	local funcTable = GLOBAL_DISPATCH[key];		if (funcTable==nil) then		print("cannot find function table for given script");		return false;	end		if (funcTable[funcName]==nil) then		--it's not really an error for script to not have this function?		--is it? depends on your implemenation.		return true;	end		--the object should already contain a table use to store state	--specificly for this script, but in this case we can create one 	--if it doesn't	local data = object[key];		if (data==nil) then		object[key]={};		data=object[key];	end		--now we dispatch the function with the given data table associated	--with it.	funcTable[funcName](data);		--all ok	return true;end


I didn't check if this runs, you might have to use an anonymous function instead of directly assigning the functions like so:

instead of :
funcTable["Initialize"]=Initialize;

use this maybe?
funcTable["Initialize"]=function(data) Initialize(data) end;

Give it a try! :D

Good Luck!

-ddn
So...if I have a script like this:
----# ai.lua# May or may not have local variables up here (which could be declared# local but might not be because I'm lazy)# And remember: more than one NPC can use this script, so the state# variables declared here would have different values for each NPC.function nextAction()    # Do cool stuff    ...   return stuffendfunction getState()    # More stuff    ...    return stateCodeend# Not called in C++, so I might want to declare it local.function helperFunc(moreStuff)    ...end----


The suggestions here have generally been to perform extra processing in the script files (this is what I called "jumbling things up"). In addition to the actual AI code, these scripts would contain processing code to differentiate each script/chunk in the Lua interpreter. Code that would probably be the same across scripts besides maybe a few name changes.

Is there no way to put this processing inside the C++ half?
Problem is your witting the state into free local variables of the script. If your not going to make any concession about where you store the variables then you'll have to clone the entire script in memory and that way each instance clones all its internal state. You'll still need a mechanism to dispatch functions inside that script which are unique to it.

I believe its possible to load up a script file in Lua, stuff that compiled script into a table and its also possible to do a deep copy of a table. So you'll load up the script and when u need an instance you would clone it and stuff that clone somewhere accessible only by the object which owns it.

Just declare all functions and variables of the script local so it doesn't write into the global table overwritting each others implementation, unless that is what u want ( ie a shared global data or function between all the instances of the script ).

Again alot of convoluted steps just so you can write scripts which use exact same named functions ? Is this a concession to the designers who don't want to or know about namespace and OOP ?

Good Luck!

EDIT: ofcoruse u can put it all in C++/C Lua has a complete C api which duplicates every single facet of the normal Lua syntax, but why would u want to do that when u can dispatch any global Lua function from C/C++ with ease?

Apparently this is what the require() function already does for Lua. It loads up a script and returns a copy of it. This copy is a complete clone of the script so any locals declared inside it are unique only to it. So something like this :

local clone = require("Scripts/AI_Ogre");

now u can dispatch functions in clone ( make sure its local only to clone )

clone.Iniitlizaion();

That should work.. Good Luck! :D

see thread :

http://lua-users.org/lists/lua-l/2007-08/msg00005.html

for more details...

-ddn

[Edited by - ddn3 on April 26, 2009 12:14:20 PM]
Quote:Original post by ddn3
Just declare all functions and variables of the script local so it doesn't write into the global table overwritting each others implementation, unless that is what u want ( ie a shared global data or function between all the instances of the script ).


This was sort of what I was trying to get at. NPCs with the same AI script share the functions but keep their own instances of the local variables. I suppose, to make it easier, any persistent local variables an AI would use would instead be stored in a single table instead of be scattered around the script file, though that doesn't solve the function name clashing between multiple AI scripts.

Quote:Again alot of convoluted steps just so you can write scripts which use exact same named functions ? Is this a concession to the designers who don't want to or know about namespace and OOP ?


Technically, yes. My scripts in general are basically implementations of C++ methods except that (a) I don't want to have to recompile and (b) I want to program in a higher level language. I'm just scared by all the solutions offered so far because they look like a lot of boilerplate code, and I wanted my scripts to be fairly minimal. Besides, I figured much of the work done in these scripts would be procedural in nature anyway (all of my OOP is in the C++ side).

Quote:EDIT: ofcoruse u can put it all in C++/C Lua has a complete C api which duplicates every single facet of the normal Lua syntax, but why would u want to do that when u can dispatch any global Lua function from C/C++ with ease?


So that I'd be able to put less code in my scripts. If my scripts need code to manage the scripts, I feel I might as well code my entire game in Lua.

Again, if Lua is not designed for the kind of scripting I'm trying to fit it in, is there another language that follows my way of thinking better?

</rant>

In an attempt to prove that I've learned something from this thread; if this is what my original script looked like:
local variable1 = foo...local variableN = baz# Functions called by NPC  objects in C++function nextAction(arg1, ... , argN) # Could these be local?    ...    return actionCodeendfunction currentState(arg1, ... , argN)    ...    return stateCodeend...# Functions only this script useslocal function blarg(arg1, ... , argN)    ...end...

Then I have to rewrite the script like this?
AI{    variable0 = foo,    ...    variableN = baz,    # Functions called by NPC  objects in C++    nextAction = function(arg1, ... , argN)        ...        return stateCode    end, # Since this is a table, does a comma go here?    currentState = function(arg1, ... , argN)        ...        return stateCode    end,    ...    # Functions only this script uses    blarg = function(arg1, ... , argN)        ...    end,    ...}

Here, is AI treated as a function that returns a table (in C++)? And I think I missed how to handle different NPCs that use the same script (with different values for their "local variables").
Your close, but in Lua there is a better way. It involves using the metatable though, this is the way u keep per instance data but use a common implementation. It's analogous to the C++ classes and objects.

here is a sample implementation. Note this is just one way to do it, i'm sure there are many ways to do this.

This is the main program..

---------------------------------------------------------------demonstrate script loading with consistent API across all--scripts--global accessible to all scriptsglobalValue = 1.123;--original instances of these loaded scripts, we keep them around and use them to create new instances of the objects we need.local scriptA	= require "scripts/ScriptA";local scriptB   = require "scripts/ScriptB";--when we need an instance we new itlocal a = scriptA:new();local b = scriptA:new();local c = scriptB:new();--some test seting local valuesa:set(1);b:set(2);c:set(3);--setting a shared value between all instance of that scripta:setShared(7.89);--now we print out the states, note how we call functions to an object using--the : convention, this is important.a:foo();b:foo();--set global valueglobalValue = 3.124;--check on the unrelated scriptsc:foo();


This is script A

---------------------------------------------------------------------common header to all scritps using this scheme of auto registeration--dont need to change anything in this header.local Script = { }	--local table no possible namespace collisionfunction Script:new()   local __object = { }   setmetatable(__object, {__index = Script})   __object:init();   return __object; end------------------------------------------------------------------- ---------------------------------------------------------------------shared values/functions will be accessible to all instance of this particular scriptlocal sharedValue = 0;--this function is only accessible to instances of this scriptlocal function sharedFunction()	print("Shared function called");end--since this is global any script can call this, even other AIsfunction globalFunction()	print("Global function called");end---------------------------------------------------------------------init function called first thing upon object creation--any variables defined in here are only accessible to a particualr--instance, this is where all per instance state data is stored.--even though this function doesn't have the keyword local is it --local by the fact that its owner table Script is declared local above.-------------------------------------------------------------------function Script:init()	print("init called");        --define new member variables here, this is per instance data        --accessed using the self. convention.	self.x = 0;end ---------------------------------------------------------------------update function-------------------------------------------------------------------function Script:update() 	--do something?end---------------------------------------------------------------------shutdown function-------------------------------------------------------------------function Script:shutdown() 	--cleanupend---------------------------------------------------------------------misc functions accessible to this object-------------------------------------------------------------------function Script:foo()	print("local "..self.x.." global "..globalValue.." shared "..sharedValue);end function Script:set(val) 	self.x=val; endfunction Script:setShared(val) sharedValue=val; end---------------------------------------------------------------------automatically returns table for this object, in this scheme--always needs to be the last line.return Script;-------------------------------------------------------------------


This is script B

local Script = { }	--local table no possible namespace collision function Script:new()   local __object = { }   setmetatable(__object, {__index = Script})   __object:init();   return __object; end -------------------------------------------------------------------local sharedValue = 0;--init function called first thing upon object creationfunction Script:init()	print("init2 called");	self.x = 0;end function Script:update() 	--do something?endfunction Script:shutdown() 	--cleanupend--misc functions accessible to this objectfunction Script:foo()	print("local "..self.x.." global "..globalValue.." shared "..sharedValue);end function Script:set(val) 	self.x=val; end---------------------------------------------------------------------automatically returns table of this objectreturn Script;


When u run the main script you'll get something like this.

init called
init called
init2 called
local 1 global 1.123 shared 7.89
local 2 global 1.123 shared 7.89
local 3 global 3.124 shared 0

It has private instance data ( accessed through the self. notation ) shared local data and global data access. All scripts can use a common api Script:init, Script:shutdown, etc.. You can change the table name from Script to whatever you want but u need to define functions local to this script using the : notation.

That's the simplest scheme I can think of. I dont know of any other scripting language which will just let you do what you want out of the box, maybe Python but i've not used it.

Good Luck!

-ddn

This topic is closed to new replies.

Advertisement