Lua scripting practices

For discussion of the code running behind the game

Moderator: Staff

User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Lua scripting practices

Postby Roots » Tue Jan 22, 2013 6:46 am

Now that we're getting further into Lua development, especially with map scripting, I've been thinking of design paradigms and other tricks to make this code easier to read, develop, and maintain. In this thread I want to share some of those ideas and see what others have to say.


When setting up maps, you typically have to create sprites, objects, dialogues, events, zones, and functions. These all tie together and need a way to reference one another. For example, I create an event that is triggered when the camera enters a zone, which then takes control of a sprite and moves him to a location before beginning a dialogue. It's hard to see how all the pieces fit together at a glance, especially because the way we identify each object/sprite/dialogue/event/whatever is by assigning it an ID value and then these ids are passed around everywhere. It can get quickly get confusing. Bertram made changes in VT so that ids are replaced with strings to make the ids a little more obvious, but I'm not sure I want us to go that route because it would require a lot of changes. Today, however, I thing I figured out a great alternative.

Lets say we're making 6 sprites on a map and we want to assign them ids: 1, 2, 10, 20, 21, 30. When we create sprite objects in Lua and pass them to the map, the binding code includes instructions that tells Lua "you are not responsible for managing this memory", meaning it won't try to free the pointers to the sprites when it cleans up. But it will still retain pointers to those sprites, and the same pointers are passed into the C++ map mode code. What you can do is create a table of pointers for different class objects that are passed to map mode, where each table uses a string identifier as the ID and the value is the pointer to the object representing the id. So for example:

Code: Select all

sprites = {};
sprites["claudius"] = ConstructSprite("claudius", 1, 20, 15);
ObjectSupervisor:AddObject(sprites["claudius"]);


Here we create a sprite, give them the ID# of 1 and set their initial x/y position, then pass the pointer to the sprite object to map mode so it has a copy to manage. Map mode doesn't know nor care about the "sprites{}" table. It only sees that it has an ID of 1 and processes it accordingly. Now we can reuse "sprites["claudius"]" whenever we want to refer to the Claudius sprite instead of having to pass the id "1" around everywhere, and try to remember that that id represents Claudius' sprite.

So what about a Lua call that requires a sprite ID? Simple:

Code: Select all

event = SpritePathMoveEvent(sprites["claudius"]:GetID(), 40, 30);


We just call the GetID member of the pointer. That's a little long though, so I think we can shorten it further by creating an alternate constructor for SpritePathMoveEvent that accepts a pointer to a sprite class object instead of just an ID. Then the call becomes:

Code: Select all

event = SpritePathMoveEvent(sprites["claudius"], 40, 30);


Pretty easy to understand, isn't it? This event moves the Claudius sprite to the x,y position specified.

I'm 95% sure that this is possible to do, and I've been experimenting with it as I'm doing heavy work on the river access cave map. The only way I could see it not working is if the sprites["claudius"] object does not get updated in Lua when it is updated in C++. (For example, because the player moved Claudius around). I'll have to look to see if this is the case, but I think the two should be pointing to the same memory and thus everything should be fine. I'll confirm if this is the case or not.


I think this could make map scripting a healthy degree easier without requiring any changes to existing code. If it proves effective, I think we should move toward making this a standard map design paradigm in the future, and we should have high-level tables to store all of our various class object pointers.
Image
User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Re: Lua scripting practices

Postby Roots » Tue Jan 22, 2013 4:32 pm

My experiments of the practice I described above so far are proving to be very successful. I'm fairly excited about this, as it makes it so much easier to understand how the scripts are put together. As an example, I added this line to the Load() function of the map:

Code: Select all

   EventManager:StartEvent(events["opening_dialogue"], 2500);


Now if I want to know exactly what event this does, I just need to search the script for the string "opening_dialogue" and boom, there it is. No more needing to remember an event ID like "10" and then digging through all the created events to find it.


I noticed that we had no way to start an event after a specified period of time, unless that event was chained off another event (ie, start event B 2000ms after event A ends). I needed this feature available to start an event shortly after a map is loaded, and I technically could have done it by starting an event immediately that does nothing and chain the real event off of that. But instead of that hack, I just added a couple of simple calls to the EventSupervisor class in map mode to achieve this. It will be added in my next commit.

Code: Select all

   /** \brief Begins an event after a specified wait period expires
   *** \param event_id The ID of the event to activate
   *** \param wait_time The number of milliseconds to wait before starting the event
   *** \note Passing a zero value for wait_time will result in a warning message and start the
   *** event immediately. If you wish to start the event immediately, use the version of StartEvent
   *** that does not require a wait_time to be specified.
   **/
   void StartEvent(uint32 event_id, uint32 wait_time);

   /** \brief Begins an event after a specified wait period expires
   *** \param event A pointer to the event to start
   *** \param wait_time The number of milliseconds to wait before starting the event
   *** \note Passing a zero value for wait_time will result in a warning message and start the
   *** event immediately. If you wish to start the event immediately, use the version of StartEvent
   *** that does not require a wait_time to be specified.
   **/
   void StartEvent(MapEvent* event, uint32 wait_time);



My next hurdle that I am finding is that it is rather messy to mix dialogue events with other events. What I mean is let's say I have an event chain like the following:
- Move 3 sprites to a relative destination
- Turn one sprite another direction
- Begin a dialogue
- Turn another sprite in the middle of the dialogue
- Move sprite to another position during the ongoing dialogue

Currently we support events occurring after a line or an option in a dialogue, but only one event per line/option is allowed and the event is executed immediately after the line/option ends. Not having support for multiple events and the timings for those events is an annoying limitation. You can get around it by creating a new dialogue for each line and setting up the other events between the lines, but this is rather annoying (and it means the dialogue box is going to continually appear and disappear, which may not be desired behavior).

I think we should change the way events are managed in dialogues to:
- support any number of events to occur for a given line or option
- allow the user to specify the timings of those events, including whether the time is relative to the start of the line/option or the end of it

I plan to change the API to support this sometime later this week.
Image
User avatar
gorzuate
Developer
Posts: 2575
Joined: Thu Jun 17, 2004 3:03 am
Location: Hermosa Beach, CA
Contact:

Re: Lua scripting practices

Postby gorzuate » Wed Jan 23, 2013 5:08 am

This is some good stuff here :approve:
User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Re: Lua scripting practices

Postby Roots » Wed Jan 23, 2013 9:05 am

Another thing I'm going to change is the way we call script functions. Right now you have to specify an integer that corresponds to the function that you want to call. There's really no reason to use integers for this I feel, and it just gets confusing because your functions are literally named "1", "2", etc. There's already a map_functions{} table required to be in the Lua file. I'm going to rename this table to just "functions" and then use strings as the keys to access each function. So for example, I could write a function called "PrepareFirstEnemyEncounter" and call it by that name. We would still require each function that is to be called via the event interface to be a member of the functions{} table, but that shouldn't be a big issue.
Image
User avatar
Bertram
Senior Member
Posts: 127
Joined: Fri Feb 26, 2010 10:08 am

Re: Lua scripting practices

Postby Bertram » Thu Jan 24, 2013 4:12 pm

Well, all that reminds me something ;)
User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Re: Lua scripting practices

Postby Roots » Fri Jan 25, 2013 7:54 am

Indeed. :) The reasons that IDs were used in the first place, BTW, was to make it easier to lookup values in containers. But for functions it makes absolutely no sense to use IDs, since functions only perform one lookup when the function is loaded, and a pointer is maintained to call it when we need to.


Anyway, here's another set of changes I'd like to make. If you look at a map script, you'll see a ton of calls like this:

Code: Select all

   DialogueManager:AddDialogue(dialogue);
   Map:AddZone(roam_zone);
   Map:AddGroundObject(sprite);
   EventManager:RegisterEvent(event);


What these calls do is take a class object that the script constructed and give it to the appropriate manager class. A common error is to construct an object and forget to register it, so then the map has no idea that this objects exists at all. Now as of yet, I haven't come across a situation where I need to construct one of these objects but not register/add it to the manager. So I'm going to look into seeing whether it's possible for us to eliminate these calls entirely (by having the class constructors register the objects automatically). The only reason it might not be possible is that the functions I listed above do a Luabind trick where they transfer "ownership" of the object to MapMode/C++ code, instead of leaving the memory managed by Lua and it's garbage collector. I'll need to see if I can tell constructors to do the same thing.

If I can do this, it should help clean up map script code greatly and make it both easier to read and less error-prone.
Image
User avatar
Bertram
Senior Member
Posts: 127
Joined: Fri Feb 26, 2010 10:08 am

Re: Lua scripting practices

Postby Bertram » Fri Jan 25, 2013 10:17 am

:approve: If you can achieve that, it would a fine improvement.

I tried a part of that as well, but at the time, I was still not understanding the luabind adopt policy well, and made a small mistake that made me drop the patch. (just beware about that!)

As for the DialogueManager one, you might have to remove the constraint to add a dialogue that isn't fully initialized yet.
And beware about adding ground objects, you might later want to add sky, or other objects types.

regards.
User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Re: Lua scripting practices

Postby Roots » Sat Jan 26, 2013 12:40 am

I'm sure there's a way to do it. If the adopt policy can't be applied to class constructors correctly, then I'll just write some methods that construct the object, register it, and return a pointer to it for Lua to use. I'll let you know what I find out.

Good call about the dialogue manager. I haven't dug around in that code in a long time so I couldn't recall exactly how it works.

And yeah, I realized that for map sprites it would be difficult to do the registration if you don't know what layer they belong in. I might require the layer to be passed to the constructor, or simply add a method that lets you set a sprite's layer after construction (with the understanding that all objects are initially placed in the ground layer).
Image
User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Re: Lua scripting practices

Postby Roots » Sat Jan 26, 2013 2:04 pm

I just realized that it's impossible to register events inside their constructor, since the event needs to be fully constructed before it can be used by EventSupervisor (or anything else for that matter). So this isn't even a Luabind issue. It's a C++ issue. Hence, we'll need an alternative way to construct classes.

I think the best solution is to add a static "Create()" method for each type of event class. This function will do the following:
1) Create an instance of the class object
2) Register the object with the EventSupervisor
3) Return a pointer to the newly created object

In Lua, here's what it would look like:

Code: Select all

-- Old way, calling the class constructor and then registering the event
event = hoa_map.PathMoveSpriteEvent(18, 22, march_distance - 0.25, 0);
EventManager:RegisterEvent(event);

-- New way, calling the create method with no registration line required
event = hoa_map.PathMoveSpriteEvent.Create(18, 22, march_distance - 0.25, 0);


This is assuming of course that I can bind static class methods to Lua. I think I can though (I checked the documentation). But even if that fails, I can just write non-class functions to do the same thing. But I'll try to get static methods working so that all functions related to a class are inside that class itself. I'm also planning to make the event constructors private so that it's clear that Create() is the only way to make an object of the class.


The other classes (dialogues, sprites, etc.) will need the same sort of thing, as they too can not be registered until after their constructor completes.
Image
User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Re: Lua scripting practices

Postby Roots » Sat Jan 26, 2013 2:36 pm

Yup, just tested with one of the event classes and got it working fine using a static method. I'm going to go ahead and do the following:

1) Write a static "Create" method for each non-abstract event class
2) Make all event class constructors private
3) Remove bindings of event class constructors so that Lua can only create events using the Create() method
4) Remove the "RegisterEvent" method from Luabind, as it shouldn't need to use it anymore
5) Update opening scene and cave map with the changes and remove all "RegisterEvent" calls in those files

Shouldn't take me too long.
Image
User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Re: Lua scripting practices

Postby Roots » Wed Jan 30, 2013 1:43 pm

Implemented as of commit #2084. Bertram, if you want to take a look at that changelog and incorporate the changes into VT, be my guest. It may take you a while to update all of your map scripts though. That's kind of why I wanted to work on this now so we don't have a bunch of extra work updating our map scripts later. :)


Next I'll be taking a look at dialogues and see if I can simplify their construction/use in map scripts.
Image
User avatar
Bertram
Senior Member
Posts: 127
Joined: Fri Feb 26, 2010 10:08 am

Re: Lua scripting practices

Postby Bertram » Wed Jan 30, 2013 11:18 pm

Hi Roots :)

I will. This one implementation looks fine to me. Unfortunately, it seems indeed luabind cannot give away an object at construction.

Did you make sure you made luabind lose the adoption by making c++ adopting the result of the Create calls?
I haven't seen that in the code and still the game isn't crashing, so I actually wondered.

Regards,
User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Re: Lua scripting practices

Postby Roots » Thu Jan 31, 2013 4:00 am

Yeah I was curious about that too. But I think that because Lua is calling a C++ function, and inside that C++ function an object is created, the object stays managed by C++ and not Lua. If Lua calls a constructor directly, on the other hand, then Lua is given ownership of that object. I'm pretty sure that's how it works. The game would crash otherwise because both Lua and C++ would be trying to free these event objects.
Image
User avatar
Bertram
Senior Member
Posts: 127
Joined: Fri Feb 26, 2010 10:08 am

Re: Lua scripting practices

Postby Bertram » Fri Feb 01, 2013 12:27 pm

Yeah that must be it. Right! My turn to apply this patch!

(I'll keep your authorship in the corresponding commit when I'll get around doing that.) :hack:
User avatar
Roots
Dictator
Posts: 8666
Joined: Wed Jun 16, 2004 6:07 pm
Location: Austin TX
Contact:

Re: Lua scripting practices

Postby Roots » Mon Feb 16, 2015 7:58 pm

I'm working on map scripts again and looking for further ways to make improvements to them. One thing in particular that I'm working on is developing a standard for the organization of map scripts. It makes it a lot easier to write scripts if they follow a similar formula. I'm also still in the process of removing our usage of ID numbers everywhere. I'm focusing on the river cave map once again, since that map makes the most use of various features (enemies, events, and so on) than any other. Ultimately once I have the standard figured out, I'll add it to the following wiki section.

http://www.allacrost.org/wiki/index.php ... ript_Files


Here's a preview of the things I've got going so far.

1) Common object tables

Code: Select all

-- Containers used to hold pointers to various class objects.
-- A unique string key is used for each object entered into these tables.
zones = {};
objects = {};
sprites = {};
dialogues = {};
events = {};


These tables are global to every map file and are located near the top. They hold pointers to the various map class objects we create, and a string identifier is used as the key. Not every object you create will go into these containers, however. For example, enemy sprites do not get placed in the sprites container, because those are managed by the zones that the enemies roam in.

2) Class object creation methods

Code: Select all

   CreateZones();
   CreateObjects();
   CreateSprites();
   CreateEnemies();
   CreateDialogues();
   CreateEvents();


These functions in the Lua file are helpers to the required Load() function and create all of the corresponding class objects. They also populate the tables I mentioned previously. The order that these functions are called is very important, because some of the objects require the use of other object types in their construction. For example, a dialogue sometimes needs to add itself to a sprite, so the sprite must first exist. Enemies need to be added to the zones that they will roam around in, etc.

3) Debug features

This one I haven't made much progress on yet, but we need a way to make these maps easy to debug. I'm thinking of having a debug function that will configure the map to do things like enable infinite stamina, prevent non-event battles from occurring, change the player's position, and so on. I'll figure out something for this eventually, but it is something I think we want to keep in the map script permanently and not remove it once the map is "finished".
Image

Return to “Programming”

Who is online

Users browsing this forum: No registered users and 1 guest