Jump to content

Communicating from Lua to C++


AggrorJorn
 Share

Recommended Posts

Currently I have a trigger in my scene. The trigger has a script attached to it with the CollisionHook function

 

If something in the game hits the trigger, then the Lua CollisionHook is called. But how can I send/call/invoke functionality in C++ from that part of the Lua script?

Link to comment
Share on other sites

You got 3 possibilities in my opinion to achieve that :

 

- use pure lua ( just google for "c++ lua call function" e.g.: http://gamedevgeek.com/tutorials/calling-c-functions-from-lua/ )

- use luabind (c++ interface which allows classes (c'tor, d'tor, inheritance etc.) but you still have to do the hard coding)

- tolua++ (which creates the code for you, if i understand it right, never used it)

Link to comment
Share on other sites

I wouldn't use LuaBind. I tried it and found it to be inappropriate. For example, the lua userdata object won't be persistent for the life of a C++ object. You'll get a different handle each time it's passed to Lua.

 

For simple global functions, the lua_register mechanism looks good.

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

Thanks for the answers. Right now I do the following:

  1. I create a pivot and make the triggerbox a child of it.
     
  2. During the loading of the map I check every entities name.
     
  3. If it is a certain name then a C++ object is created at its position while the Lua object is being removed. In this situtation I have a class with a Shape file.
  4. The new C++ object has Collision Hook.

 

This process is quite slow and looks a bit sluggish. Sometimes it is usefull for instance when you have a player start pivot and you want to create the player via C++. However with a CSG brush that I created in the editor this is not that easy. I have to make a class for that trigger, add a shape to it and make sure all properties from Lua are copied to the new C++ object. I might as well not use a csg brush at all and just a pivot since I have to create it by hand.

Link to comment
Share on other sites

Currently I have a trigger in my scene. The trigger has a script attached to it with the CollisionHook function

 

If something in the game hits the trigger, then the Lua CollisionHook is called. But how can I send/call/invoke functionality in C++ from that part of the Lua script?

 

Among other things, It is this awkward interface between C++ and LUA (or between any coding language -> scripting language for that matter) which makes me not want to touch scripting with a 10 foot pole.

STS - Scarlet Thread Studios

AKA: Engineer Ken

 

Fact: Game Development is hard... very bloody hard.. If you are not prepared to accept that.. Please give up now!

Link to comment
Share on other sites

You can do this two ways:

1. Create a C++ object, set the trigger entity's user data to this object, then set a collision callback for the entity.

2. If you want to keep things more script-oriented, exposed your C++ class to Lua with LuaBind, then in the script collision function do something like this:

function Script:Collision(entity,position,normal,speed)

local myobj = self.entity:GetUserData()

myobj:StartCallingMyOwnFunctions()

end

 

So basically you can create your own script API to call your own low-level game functions. Your processing-intensive code can go into C++ functions, and you can call everything from Lua. This is the way it's meant to be used by power users. biggrin.png

 

The ToLua binding process seems confusing at first, but once you have it set up, it can be totally automated. Here is BlitzMax code for a tool Chris wrote that scans our header files and creates the clean header file ToLua requires.

 

To expose a C++ class to Lua, add "//lua" after the class name, member, or function you want to expose. We just use a .bat file that runs our auto-generator and then runs ToLua++, so the entire process is automated:

'Open header file
Global cleanheaderfile = WriteFile("luacommands.pkg")
If Not cleanheaderfile RuntimeError "could not open file luacommands.pkg"

'Write necessary header functions
WriteLine cleanheaderfile,"$#include ~qLeadwerks.h~q"
WriteLine cleanheaderfile,"$using namespace Leadwerks;"
WriteLine cleanheaderfile,""

'Traverse directory and write all marked functions
TraverseFiles(CurrentDir())

'Assign classes
For Local class:TClass=EachIn TClass.list
If class.parentname
class.parent = TClass.Find(class.parentname)
'If class.parent Print class.parent.name
EndIf
Next

'Sort classes
'TClass.list.sort()

'Write the data
For class=EachIn TClass.list
class.Write(cleanheaderfile)
Next

WriteLine cleanheaderfile,"bool dofile(const std::string& path);"
WriteLine cleanheaderfile,"bool require(const std::string& path);"

CloseFile cleanheaderfile

Type TClass
Global map:TMap=New TMap
Global list:TList=New TList

Field written:Int
Field name:String
Field parent:TClass
Field parentname:String
Field members:TList=New TList

Function Create:TClass(name:String)
Local class:TClass=New TClass
class.name=name
list.AddLast(class)
map.Insert name.Trim(),class
Return class
EndFunction

Function Find:TClass(name:String)
Return TClass(map.valueforkey(name.Trim()))
EndFunction

Rem
Method ContainsParent:Int(class:TClass)
If parent
If parent=class
Return True
EndIf
Return parent.ContainsParent(class)
EndIf
Return False
EndMethod

Method Compare:Int(o:Object)
Local class:TClass=TClass(o)
If class.ContainsParent(Self) Return -1
If Self.ContainsParent(class) Return 1
If class.name>Self.name Return 1
If class.name<Self.name Return -1
Return 0
EndMethod
EndRem

Method Write(stream:TStream)
If Not written
Local baseclass:TClass=parent
While baseclass
baseclass.Write(stream)
baseclass=baseclass.parent
Wend
If parentname
stream.WriteLine "class "+name+" : public "+parentname
Else
stream.WriteLine "class "+name
EndIf
stream.WriteLine "{"
For Local s:String=EachIn members
stream.WriteLine " "+s
Next
stream.WriteLine "};"
stream.WriteLine ""
written=True
EndIf
EndMethod

EndType

'Helper Functions
Function FindLua(inputFile:String)
'Print "Analyzing ~q"+inputfile+"~q"

file= ReadFile(inputFile)
Local closureneeded = False
Local class:TClass

If Not file RuntimeError "could not open file"

While Not Eof(file)
line$ = ReadLine(file)
OriginalLine$ = line
'line = Lower(line)

Local sarr:String[]
line=line.Trim()
sarr=line.Split("//")
If sarr.length>1
Local tag$ = sarr[sarr.length-1]
If tag.ToLower()="lua"
line=sarr[0].Trim()
'If "class is in the string print the line then an open bracket else print the line tabbed twice
'DebugStop
If Instr(line, "class",1) <> 0
Local sarr2:String[]=line.split("")
class=TClass.Create(sarr2[1])
'class.name=sarr2[1]
If sarr2.length>4
class.parentname=sarr2[4].Trim()
EndIf
'Print class.name

'WriteLine cleanheaderfile,sarr[sarr.length-2].Trim()
'WriteLine cleanheaderfile,"{"
'closureneeded = True
Else
If class'Assert class
class.members.addlast(sarr[sarr.length-2].Trim())
Else
Print "Possible error in file ~q"+inputfile+"~q. No class found."
EndIf
'Print sarr[sarr.length-2].Trim()
'If closureneeded WriteLine cleanheaderfile,"~t" + sarr[sarr.length-2].Trim()
EndIf
EndIf
EndIf
Wend
'If closureneeded
'WriteLine cleanheaderfile,"};"
'WriteLine cleanheaderfile,""
'EndIf

'Extra global functions
CloseStream file
EndFunction

Function TraverseFiles(root:String)

dir = ReadDir(root)
If Not dir RuntimeError "failed to read current directory"

Repeat
currentFile$ = NextFile(dir)
If currentFile = "" Exit
If currentFile = "." Or currentFile = ".." Continue
If FileType(root + "\" +currentFile) = 2
'DebugStop
TraverseFiles(root + "\" + currentFile)
EndIf
If ExtractExt(currentFile) = "h"
'Print(root + "\" + currentFile)
FindLua(root + "\" +currentFile)
EndIf
Forever

CloseDir dir

EndFunction

 

 

Link to comment
Share on other sites

Sweet, thanks for post. I will try both of these solutions and see what works best for me. I think that many people will encounter this topic so this is tutorial gold.

You can do this two ways:1. Create a C++ object, set the trigger entity's user data to this object, then set a collision callback for the entity.
How can I retrieve the entity user data when the CSG brush is not an entity? At least, when I load the map with a callback, I can only get entities like pivots, models, lights etc, but not brushes.

 

void StoreWorldObjects(Entity* entity, Object* extra)
{
//Prints out Lights, Models, Pivots but not CSG brushes.
System::Print(entity->GetKeyValue("name"));
}

Link to comment
Share on other sites

For your #1 are we able to get CSG models in the scene in C++? Because I would think the ideal would be to be able to cycle through CSG brushes and find one by name (that we made a trigger) and be able to set it's entity data for collision callback, but I thought you mentioned that CSG brushes aren't treated like normal entities?

Link to comment
Share on other sites

CSG brushes get collapsed into merged geometry, unless their mass is non-zero or they have a script attached.

 

Exposing your own API through ToLua is the serious way to go about this. It's extremely powerful.

  • Upvote 1

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

I didn't know that. Just tried it out and adding mass or adding a custom script works. :)

 

Please consider adding this to the documentation for loading a map. I have to edit some tutorial information as well because I said it was not possible.

http://www.leadwerks.com/werkspace/page/documentation/_/command-reference/map/mapload-r510

 

 

I will deffinetly try out the ToLua tool. This knowledge has to get out to the people really. I can't be the only one trying to do this?

Link to comment
Share on other sites

So this is my current approach which I think is almost working.

 

1.Load map and see if trigger object exists. Make a new object of the Trigger1 class and pass information along.

void StoreWorldObjects(Entity* entity, Object* extra)
{
  if(entity->GetKeyValue("name") == "Trigger1")
  {
     trigger1= new Trigger(entity->GetUserData());
  }
}

 

Inside the trigger (which inherits Leadwerks::Object) I do the following:

void CollisionHook(Entity* entity0, Entity* entity1, float* position, float* normal, float speed)
{
   System::Print("-------------------------------");
}

Trigger::Trigger(void* userdata)
{
   this->SetUserData(userdata);
   this->AddHook(Entity::CollisionHook, CollisionHook);
}

 

The trigger object is created.

The Lua function can confirm collision

Problem: the collision callback in C++ is not called.

Link to comment
Share on other sites

Would be nice if trigger is set that it also doesn't get merged since that's a popular use for CSG too, and people would want to know about triggers in C++. If I understand now we'd have to attach a script to prevent this merging but if someone is dealing with all C++ the script would basically be empty and just used to prevent the merge. That seems silly. And we wouldn't give a mass to a trigger so that method won't work for C++ either.

 

 

Or better yet, give us an additional property that tells the engine not to merge said CSG and check it by default. This gives us way more control.

Link to comment
Share on other sites

@Aggror your code is a little fragmented but I know in LE2 the way you'd do this is:

 

 

1) Find your LE entity (like you are doing) and pass it to a C++ class (like you are doing)

2) In the ctor of this class you set the class instance (this) to the entities (the csg trigger) user data: entity->SetUserData((void*)this);

3) Then set the collision callback on the entity csgEntity->AddHook(...);

 

 

You don't HAVE to set the entity user data, but the reason to do so is so you can get the entity user data from inside your 1 global collision callback, cast to your object, and call it's methods. To make this flexible make a base class that all your objects will derive from that has an OnCollide() virtual method. Then your global collision callback can be very generic and you'll only need 1 because you can cast to the base class and call it's OnCollide() which if the object is really a child and overrides the OnCollide() method, that objects method will be called. Hope that makes sense smile.png

 

 

class GameObject
{
private:
  Entity* _entity;
public:
  GameObject(Entity* e) : _entity(e)
  {
     _entity->SetUserData(this);
     _entity->SetHook(...);
  }
  virtual void OnCollide(...) {}
};

class CustomObject : public GameObject
{
public:
  CustObject(Entity* e) : GameObject(e) {}
  virtual void OnCollide(...) {}
};

// find csg trigger
trigger = CustomObject(csgTriggerEntity);

// as long as all your objects that require collision derive from GameObject this will be the 1 and only global collision callback you'll need
void CollisionCallback(Entity* e1, Entity* e2, ...)
{
  GameObject* obj = (GameObject*)e1->GetEntityUserData();
  obj->OnCollide(e2, ...);
}

  • Upvote 1
Link to comment
Share on other sites

Sweet, thanks Rick. Great idea to make the virtual OnCollide function.

 

For the sake of this topic, this is what I have that works:

 

 

[/code]

//Inside LoadMap callback

if(entity->GetKeyValue("name") == "Trigger")
{
   trigger= new Trigger(entity);
}

 

 

//Trigger.h

class Trigger
{
public:
   Trigger(Leadwerks::Entity* entity);
   ~Trigger();

   Leadwerks::Entity* entity;
};

 

//Trigger.CPP

void CollisionHook(Entity* entity0, Entity* entity1, float* position, float* normal, float speed)
{
   System::Print("-------------------------------");
}

Trigger::Trigger(Entity* entity)
{
   this->entity = entity;
   this->entity->AddHook(Entity::CollisionHook, CollisionHook);

}

Trigger::~Trigger()
{
   this->entity->Release();
}

Link to comment
Share on other sites

What entities do you have to create in code besides the player and enemies? Why would you need a brush to mark their positions?

 

I have two major use cases for this. The kind of game which I am currently prototyping is a point and click isometric perspective. Therefore, I have a desire to know immediately, what the player has clicked, and is pointing at.

 

1) Any object that I want the player to interact with, for instance, a barrel, box or chest, needs to be an entity. I might want to show an animation of the chest opening. Perhaps I have some rubble I want to pick up, and remove from the map. Also there are NPC's, vendors things like that. Currently, I interact via a right click, pick, peruse the user data and get an appropriate right click menu based on type of entity.

 

2) I want to be able to determine what an entity is so I can determine, via a mouse click, the spot on the map the player is currently selecting is navigable. Currently this is very difficult to, since pretty much all CSG's are considered navigable, so the only way I know of to let a player know, what locations are considered navigable is to make everything a type of entity.

Link to comment
Share on other sites

Add this:

Trigger::Trigger(Entity* entity)
{
       this->entity = entity;
       this->entity->AddHook(Entity::CollisionHook, CollisionHook);
       this->entity->SetUserData(this);
}

void CollisionHook(Entity* entity0, Entity* entity1, float* position, float* normal, float speed)
{
       System::Print("-------------------------------");
       Trigger* trigger = (Trigger*)entity0->GetUserData();
}

 

You can also make the CollisionHook function static, so it gets contained within the class.

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

2) I want to be able to determine what an entity is so I can determine, via a mouse click, the spot on the map the player is currently selecting is navigable. Currently this is very difficult to, since pretty much all CSG's are considered navigable, so the only way I know of to let a player know, what locations are considered navigable is to make everything a type of entity.

Can't you use Entity::GoToPoint() function to check if point is navigable?

Link to comment
Share on other sites

Can't you use Entity::GoToPoint() function to check if point is navigable?

 

I am currently using the Entity::GoToPoint() function on left clicks. However, I was thinking about it doing picks on a mouse hover as well, and then show the player what actions are possible, but perhaps changing the mouse cursor. Another problem I've encountered with GoToPoint, is if the player clicks on some place, which the playing character cannot reach, I do not know how communicate to the player that his character cannot reach their. Instead they will continue to walk, effectively stuck at the point of closest approach to their unattainable destination. This typically happens if I clock on a CSG wall on accident, or something.

Link to comment
Share on other sites

steeleb, I was playing with something like that and what I did was attach a script to the csg floors that set a key to to say "walkable". Then when the mouse picks it would look for that value and only allow movement if it had the walkable key set to "true".

Link to comment
Share on other sites

Yeah, that's kind of what brought my attention to this thread. I can make all sorts of objects in a 3D tool, and attach various scripts to them, to determine how a player can interact with a particular object. But what I was unsure of, is if the base code for the game is in C++, how do I communicate the script information to the (CustomObject*)userdata of my entity. Currently, I give entities a particular name, and that name determines what their CustomObject will be, and allows me to check the data. I normally check this on map loads. If it's something like a building, or a wall, then I say not walk-able. And while I would like to take advantage of the Lua scripts in the editor, and set data that way.

 

Now that I think about it it would be nice if I could assign the (CustomObject*)userdata to an entity, in the editor, in much the same way the lua script data is assigned? Or perhaps use the lua-binding utilities to bind the lua script, to my C++ custom object. Seems a little daunting, however, since I know very little lua. smile.png

Edited by steeleb
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

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

 Share

×
×
  • Create New...