Jump to content
WoWBB.org

LUA Tutorial


Madness

Recommended Posts

Introduction

First and foremost, why use a scripting language? Simply put, most of the game logic can be scripted for functionality, rather than coding it as part of the engine. As an example, think about loading or initializing a level. When the level is first loaded, maybe you want a cut scene to play. Or maybe you want to show some game credits. With a scripting system, you could get individual game entities to do specific tasks. Also, think in terms of AI. Non-Player Characters need to know what to do. Coding each NPC by hand, in the game engine could be a daunting task. When you wanted to change a behavior of an NPC, you would have to re-compile your system. With a scripting system, you could interactively change the behavior and save that state out.

Let's start off really simple. The first thing that we need to build, just to show off how to use lua, is a simple interpreter. What this will involve is:

1. Getting the lua interpreter code.
2. Setting up your dev environment.
3. Building an interpreter from scratch.

State of the union

The first thing we have to be aware of is that lua is essentially a state machine. Additionally, lua is a virtual machine. We'll come back to that in a bit (hopefully). Anyway, once you have initialized your programs interface to lua, you can send lua commands through a lua function called lua_dostring. But I'm really starting to get ahead of myself. Let's back this up a bit and start from the top of the interpreter.

First and foremost, pretty much every function in lua deals with a lua state. This essentially defines the current state of the lua interpreter; it keeps track of the functions, global variables and additional interpreter related information in this structure. You create a lua state with a call to lua_open. This function looks like this:

Dear guest, please login or register to see this content.


If you want, you can think of lua_State as a handle to the current instance of the lua interpreter. It's a fair analogy.

So, it we get a non-null lua_State we know that lua managed to initialize correctly. That's good, and it means we can now do stuff with lua. But we can only do a small set of things with lua by default. More on this shortly.

Let's get some code in place that we can use as a starting point. lua_open() is defined in lua.h (you can find that in lua's include directory). So, we should be able to compile the following code snippet as a win32 console application:

Dear guest, please login or register to see this content.


Unfortunately, this won't work. More to the point, it won't link. It's a pretty simple problem, but one of those niggling little issues you get when you're dealing with Open Source projects. Essentially, lua was written to be ANSI C compliant. Please note: I said ANSI C. Notice that all the files in the lua library all end with the "c" extension. This essentially means that the way the compiler mangles the names of all the functions in lua is based on the C calling convention. This is different that how C++ handles function naming. It's not terribly hard to fix, it just is annoying more than anything else. To fix it, simply wrap the #include <lua.h> and any other include statement that references lua library functionality inside of:

Dear guest, please login or register to see this content.


So, what we end up with is:

Dear guest, please login or register to see this content.


Easily, this is a candidate for inclusion in its own header file. Compilation at this stage will produce an exe that runs. It does nothing, but it runs.

So, at this point, you're probably thinking that if we have an 'open' statement, we are going to need a close statement. And you'd be correct. lua_close() essentially closes a state that was opened with lua_open(). Very straight forward. The format of lua_close() looks like this:

Dear guest, please login or register to see this content.


We can finish off our code above by adding this to the application:

Dear guest, please login or register to see this content.


And voila, we have a completely useless bit of lua. But, we have, in its infancy, a completely embedded scripting system in our app.

Doing something useful with lua.

Enough setup, lets get something functional. Currently we have enough code under our belt to create an interpreter. I'm going to focus the next section on building a very limited interpreter. No fancy editor, no inline debugging. Just a console that allows us to type in lua commands and have the interpreter performs actions. This is going to require us to know one more lua based function: lua_dostring(). Essentially, what this function does is perform actions in lua.

Dear guest, please login or register to see this content.


That's really, really … useless. So how do we go about making it less useless? Well, if you've gone out and downloaded lua, or looked at some lua samples, you'll see that lua has a function called print. So, let's add this into the code and give it a whirl. Here's the code that we would use.

Dear guest, please login or register to see this content.


Compiling has zero problems. Running it, however is another story all together:

So what's the deal? Did I lie? I mean, first and foremost, it's telling me that 'print' is a nil (read: null) value. So is there actually a function called print()? Well, there is, but it's not defined by the standard lua environment. It's in a library that we are going to have to link into our application. This mechanism is essentially how we are going to extend lua for our own selfish goals. Anyway, to get print(), and some other relevant functions to work, we are going to need more functions linked into the system. We can do that as follows:

Dear guest, please login or register to see this content.


Running the code now produces a real result.
I can also clean up the output by changing the instructions sent to lua:

Dear guest, please login or register to see this content.


Simply put, this outputs the summation of 1+1. So what I've done is create a simple, yet effective illustration of getting a scripting language into your app. This final source can be found in the included workspace as a project named SimpleInterpreter.

It's a relatively simple matter to take this rudimentary example and build a simple interpreter with it. All that we need to do that this point is add some text capturing facilities. I've done this in the project FunctionalInterpreter. This is a rudimentary interpreter, and is nothing other than a simple extension of the project SimpleInterpreter. I won't go over the code here. It's simply an extension of what I've shown so far.

Getting data from a file.

So far, we've been entering in all the lua code by hand. This is great, if you want to type your script over and over again. This is novel for about 5 seconds. So, how do we go about getting data from a file? Well, two solutions present themselves. So the question remains … how can we persist this data?

It's really very simple, much like everything we've seen so far in lua. Lua has a function, lua_dofile() that essentially processes the contents of a file. Wow, that makes life really interesting. So, if I was to add that to the current source code, I could get my application to execute a script whenever it runs. Essentially,

Dear guest, please login or register to see this content.


Which moves us to the next bit … what does "startup.lua" look like? Well, it looks like this:

Dear guest, please login or register to see this content.


As you can see, this is staying relatively simple. But, if you look, you can see a couple of problems with this analogy.

First and foremost, we now are exposing all of our source code for anything that we are building. This may not be a smart idea, since it's essentially exposing the functionality of the game (ie: the game logic) to everyone in a readily available format. Again, this might not be desirable. So, how to correct this deficiency? We compile our scripts.

You should be scratching your heads at this point. "Ash", you say, "wasn't the whole point of this exercise to avoid having to compile anything"? Well, yes, it is. But this compilation doesn't have to be done during the development process. You can leave your code in source format and compile it when you need to. In a second, you are going to see that in our application, there will be zero repercussions in doing this.

In the source workspace that I've included, I've also included to additional projects: lua and luac.

Lua is the interpreter that comes with the tarball distributed by the lua organization. Luac is a bytecode compiler. This essentially compiles the lua source into a format that is not human readable. It's also a bit faster. Not lots, but a little.

So, I'm going to take that script I just created, compile it into a bytecode compiled script, and use it in the exact same application, with the only change being the name of the file it loads.

OK. So, if we want to, we should be able to compile the last file we had, "startup.lua" into "startup.lub" using the following:

Dear guest, please login or register to see this content.


And at the very bottom, you see startup.lua and startup.lub. Open both up in a text editor, and you'll see that the lua file is textual and the lub file is binary.

I'd like to take a second and point out that the extensions given to these files is purely up to the developer. Lua itself doesn't care what you call them.

Integrating with your code

At this point, you see that it's not hard to put lua into your application. But what use is it at this point in time to you if you can't get it to do anything with your application. Think about it. At this point, we have a scripting system, but we have no real integration with your application. I can't use what I've built so far to actually do something, like getting an enemy AI to search for your player in game. What we are going to need to do now is expose functions, as well as methods to lua. So this is our next step. To do this, we're going to create a couple of simple functions that we can access via lua.

So, what I'm building is a simple Manager pattern for NPC characters. It's a singleton object that allows the creation, management, and deletion of NPC's that we can have in a game. I want to restate that this manager I've created is purely for educational purposes only. It in no way reflects what I would use for an NPC manager in real life.

Anyway, back to the project. I've created Binding01 as an example of what you can do when you integrate lua and your existing code base. Here's an overview of what's going on.

I've created a singleton object called NPCManager. It essentially creates and manages NPCObjects. It uses an STL list to keep track of them (again, this is a simple example). Currently, NPCObjects only contains a 'name' object. This object is never directly exposed to lua. Any time that we want access to one of these objects, we do it through the name of the object. This is crude and in no way is indicative of what can be done, but it's a straightforward example.

So, NPCManager creates and manages NPCObjects. NPCManager is a singleton, so it exists from the first time it is used until the end of its lifecycle. So, the next question is, how do we expose the NPCManager's functionality to lua? We do this through a glue function. So, what does a glue function do? Essentially, it's a communication function between lua and your code. So why do we need this communication? It's all because lua doesn't have the same variable types as C/C++.

Lua specifics

Life would be really good if lua data types and C/C++ data types matched. But if that was the case, then we'd be ending up exposing the same kind of troubles that we have with C/C++ (Memory allocation, type checking issues … lots more, but you get the drift). To help with this, lua is what's considered a dynamically typed language. In essence, lua doesn't have any variable types. There's no int, float, char or similar variable declaration in lua. The weird thing is, even though we've just said that there aren't any types in lua, there actually are.

Say what?

Well, by dynamically typed, what we mean is that the variable doesn't have any type definition, but the value does.

Say WHAT?

OK, this can be a little difficult to deal with, unless you've done any COM programming, or have worked under the hood with Visual Basic. If you have had this experience, then this is just a review for you; otherwise, we're off to a fantasy land of variant data types.

A Variant is essentially a 'catch all' data type. By definition, any variable created as a variant is of a variant data type … wait … yeah, that makes sense … kinda. Here, let's try this. Say I have a new typedef (in pseudocode):

Dear guest, please login or register to see this content.


What we have here is a new type called variant. Based on the value in type, we would use either the Integer, Float or String fields to get at data. Every variable that we create based on Variant is a variant, but what it contains is defined by its value. It's that simple. And, it can be very powerful, as you'll see shortly. Please note that lua doesn't use this kind of structure for it's internal data type. It's much more complex than this. This was purely an illustration.

So, all that lua understands is this one variant data type. The data that it can hold in this variant type is: nil, number, string, function, userdata, and table. So from this, we can see that a variable in lua can be numerical, character, or a function. The other two, userdata and table, I'll explain in a later document.

So, if we can put data into this variant in numerical or character, hopefully there is some way, on the C++ side, to be able to get that data out. And there is. There are several functions available to convert and manipulate this data. Some of them are:

Dear guest, please login or register to see this content.


There are more functions for accessing arguments passed through by lua. So what do they do? Well, as they read, they convert data from one format in lua to something in C++. But what's up with that index element. What about if we want to pass in a couple of arguments? I mean, that's completely possible. So how do we handle that? The answer is: the lua stack.

Lua's Stack.

Data gets passed from lua to C/C++ and back again through lua's stack. It's essentially a communication channel. So, when I call a function, the function is placed on the stack, the first argument is then placed on the stack, and so on. This isn't a traditional stack, since normally, the only operations that are available on a stack are push and pop. These functions that I've described all access specific elements on the stack. So, like I was saying before, it's not really a stack, but we'll consider it a stack for now. But each operation accesses an element in this stack by an index. This index value can be either positive, or negative. Here's what the lua doc's have to say about that:
A positive index represents an absolute stack position (starting at 1, not 0 as in C); a negative index represents an offset from the top of the stack. More specifically, if the stack has n elements, index 1 represents the first element (that is, the first element pushed onto the stack), index n represents the last element; index -1 also represents the last element (that is, the element at the top), and index -n represents the first element. We say that an index is valid if it lays between 1 and the stack top (that is, (1 <= abs(index) <= top)).

Just as a side note, there are several additional functions that are grouped in with the lua_toXXX set of functions used for accessing the lua stack. Check the docs that come with lua for a reference. In a later tutorial, I'll dig into them. Anyway, where was I? Oh yeah, glue functions …

If you look at my glue function l_addNPC:

Dear guest, please login or register to see this content.


So, essentially what I do is call the NPCManager's AddNPC method. As an argument, I grab the string that is currently the last item on lua's stack. Since AddNPC is a void method, I'm assuming that all goes well, and push a number, back onto lua's stack, essentially the return value. A similar process is done with DeleteNPC.

OK, I now have the glue functions. Now how to let lua know that these functions exist? And how do we let lua know how many arguments that these functions take. Well, your in for a bit of a surprise. You can, with lua, send as many arguments as you want to into a function in lua. As well, any function in lua can return more than one result. That's right. Any lua or lua accessible function can return multiple values. That's called a Tuple. It's cool, but it can also be a bit unsettling at first. So, what about getting the binding between lua and C/C++? Well, it's not that hard. Essentially what we have to do is register the function to lua. The lua function lua_register provides that functionality for us.

Dear guest, please login or register to see this content.


where

Dear guest, please login or register to see this content.


Surprisingly enough, lua_register() isn't a function, it's a macro. Here's what the macro expands to:

Dear guest, please login or register to see this content.


lua_pushcfunction pushes a C function into lua_State. As well, the name of this function (n) is now added to the 'global' function name space. Now, anytime in lua we use that name, it will be associated with that function. It's a one to one relationship.

So, with the lua_register macro, we have now exposed two lua functions called addNPC and deleteNPC. I can now use them in any lua script, so long as they are registered when I initialize lua. So, building upon the previous example, if you examine main.cpp, it's been modified as such:

Dear guest, please login or register to see this content.


I've highlighted the changes in the code in blue to identify the changes between the two versions of this code.

Next up, what would I do in lua? Well, it's surprisingly simple:

Dear guest, please login or register to see this content.


I can add and delete NPC's into the system. I then display the results as part of the engine, not the lua script.

I can also change the functionality of this application simply by changing the script. I don't have to recompile a line of the engine. If I want to use the luac compiler, I can. I've included the compiled version of 'startup.lua' called 'startup.lub'. Change main.cpp to load this file, and you're cooking with gas.


WoW Addon tutorial

This is where your addon really comes to life and ties in with the XML file. In the XML file tutorial we touched on the scripts tag and we sent everything to a function to deal with the events.

So what is the LUA file and what is it for you ask? The LUA file is where the meat of the code lives and hold most of your functions and what not for your addon. When a event happens we usually call a function to determine what has happened and then call the appropriate function within the LUA file.

First of all you need to register your addon with specific events. We normally do this using an OnLoad function where we setup the addon. Here is the HonorSpy OnLoad function:

Dear guest, please login or register to see this content.


So from the top down you can see we are registering our slash command which is the command line in game. So if in game we typed /hs it will call the function we set it to which is HSCmdLine.
Following the slash command setup you can see we are registering the events. This is basically telling wow that when these events happen let our addon know (these events will all be sent to our HS_OnEvent function).
Finally we put a simple message out saying the addon is loaded.

Ok so our addon will be told about when things happen. We now need our addon to figure whats happened and to do something. Take a look at the HS_OnEvent function:

Dear guest, please login or register to see this content.


We use a simple if/elseif setup to compare the “event” variable to the events we know we are looking for. When we find the event and match it you can run your code accordingly. In this case we call another function depending on which event has fired.

Lets just look at the HSLogin() function and show you what happens there:

Dear guest, please login or register to see this content.


Very simple we just get the realm and player name and put them together ready to store and compare against our saved data.

The HS_Update function is really the meat of the code which is as follows:

Dear guest, please login or register to see this content.


Ok so now things may seem to get a little more complecated and yes they do. Here we use variables for displaying and calculating the data we need. But the main part here is this line “for key,value in pairs(HonorSpy) do HScheck(key,value) end” this again calls another function for every name it finds in our saved variable HonorSpy so for every character name in its list, it calls the function HScheck passing on 2 variables for it to use. The first being the Key (name of the saved variable) and then the Value (do I need to explain this one?).

Rather than bombard you with more code right now i’ll just explain what happens. It reads through our list of characters and tries to match up our current character with the data we saved from before. If it finds a match it will update the data. If it doesn’t find a match it creates a new key in our saved variable to store the new data. After the matching it creates a list of all the data and totals up the values for displaying.

Here are the two functions should you want to look through:

Dear guest, please login or register to see this content.


Now for the final part. The slash command. This part brings up our window to show the data and after running the code to update the data it sets the text of our labels we created in our XML frame:

Dear guest, please login or register to see this content.


So that's it for now.
If you really read whole topic you are really interested in learning lua. GL with it.
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.

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.

Loading...
×
×
  • Create New...