PLUGIN: Lua for AGS

Started by Denzil Quixode, Fri 04/09/2009 19:30:22

Previous topic - Next topic

Denzil Quixode

Edit: I decided to update my original post for the new version rather than trying to have several threads, or post updated versions throughout this one. Sorry if this confuses anyone, it just seemed like the best option.


Hello!

This is my first plugin. It is quite experimental, but I'm fairly pleased with the way it's gone so far. I've developed it against version 3.1.2SP1 (build 3.1.2.82) and haven't had a chance to try it with other versions, so I'm not sure how well it works with them.

Download it here:


The purpose of the plugin is to allow you to write parts of your game's code in Lua, an alternate scripting language to AGS-Script. It is intended for people who are already fairly comfortable with scripting and are interested in trying something a bit different.

The files included in the package are five DLLs - AGS.Plugin.Lua.dll, AGSPlugin.SciLexer.dll, agslua.dll, lua51.dll and lua5.1.dll. Copy all of them into the AGS application home directory. Once you have these DLLs in place and you fire up AGS, there will be a new icon in the project tree called simply "Lua", and also a new entry in "Plugins" called "Lua Run-Time Component". Both of these are disabled by default, and should have no effect until you choose to activate them for a particular game. You need to have both of them active or inactive or you will probably get odd errors. (I'd recommend trying it first on a demo game rather than something important, just in case.)


To create your first Lua script, expand the "Lua" icon, right-click on the "Lua Scripts" icon inside it and select "New Lua script...". Choose a suitable name for it in the window that pops up, and click "Create".


Okay, you will now have to enter some Lua code. How to write Lua itself is not something I'll go into too deeply just yet, but to get you started:

  • Lua has strings, numbers and boolean values, just like in AGS-Script. (However, there is only one "kind" of number, rather than int, short, float etc.)
  • You do not need to declare the type of your variables - i.e. just 'a = 5' is fine, you do not need to tell Lua that 'a' is a number.
  • However, you do need to declare that a variable should be local to a function, by putting 'local' before it the first time you use it, e.g. 'local a = 5'. Otherwise, you are setting the value of a global variable called 'a' that other functions could overwrite.
  • In Lua, zero is not considered to be "false". For example, in Lua if you write "if (0) then ... end", the inside of the block is run - compare this to AGS-Script, Javascript and other "C"-based languages where "if (0) { ... }" will not run the statements inside the block. (That is a fakey example, a more realistic one is that you might have a function that returns 0 or 1 to indicate success or failure.) The only values considered "false" are the boolean value false, and nil (which is basically what Lua calls null).
  • You do not need to end your statements with a semicolon - but you can if you want;
  • Blocks start and end with words, instead of C-style { } curly braces:
  • function funcname(param1,param2,param3)  [...]  end
  • if [condition] then  [...]  elseif [condition] then  [...]  else  [...]  end
  • while [condition] do  [...]  end
  • If you want a series of if-else-if conditions, you will most likely want to use use elseif (all one word) rather than "else if", which is valid but not what you might expect.
The scripts are saved in a subfolder of your game project directory called "lscripts". You can right-click on the "Lua Scripts" icon and select "Refresh" to reflect changes to the lscripts directory made outside of the AGS Editor.

Once you have written a Lua script, you will want to run it as the game is starting up. If you don't do this, any functions you define inside the script will not be available later on. Open the GlobalScript.asc file and add Lua.RunScript("test.lua"); into the game_start() function (replace "test.lua" with the name of your script if you called it something different):

Code: Lua
function game_start()
{
  Lua.RunScript("test.lua")
  // (... the rest of your initialisation code ...)
}


Okay. Now to test calling your function. Go to a room script and in the handler function for some activity (looking at a hotspot or something), add:

Code: Lua
function hHotspot1_Look()
{
  LuaValue* result = Lua.Call("hello");
  player.Say(result.AsString);
}


What this does is to call the Lua global function "hello", convert the return value to a string, and pass it to player.Say(). It is the equivalent of player.Say(hello());, if hello() was an AGS function rather than a Lua one.

Okay! You are now ready to try compiling and running your game. Hopefully, your player character will say the string returned from the Lua function. (If not, please let me know!)

If you are wondering how using this plugin affects things:

  • A file called lscripts.dat will be created in your "Compiled" folder. This file contains all of the files from the lscripts directory. It is from this that Lua.RunScript() is actually loading them, not from the original source code files. This file is not a plain-text file and should provide enough protection to stop casual curious people from snooping in your scripts (it's not strongly encrypted or anything, but it doesn't make it easy).
  • Save games are bigger - by a few dozen kilobytes, even if you don't define your own functions. The more you add into the Lua "universe", the bigger this will get. But it should remain sensible - let me know if it seems to be making your save files really huge.
I have put up a page explaining more about what you can do with the plugin here: http://lua-for-ags.wikispaces.com/UsingLuaForAGS Link not working

Kweepa

Wow, nice work!
How does the speed compare with AGS scripting?
I can see this being pretty useful for data management.
Still waiting for Purity of the Surf II

Denzil Quixode

I have not yet had time to properly benchmark them against each other, but Lua is considered very fast for a dynamically-typed language. It probably is still not fast enough to do real-time graphics effects that need to do lots of pixel manipulation every frame or something, but still, it should be fast enough for you not to be able to tell the difference most of the time.

Denzil Quixode

#3
Edit: This post used to be some out-of-date information about calling Lua functions from AGS.
See this Wiki page for the most up-to-date info on that: http://lua-for-ags.wikispaces.com/LuaValueLists

Pumaman

This is pretty neat work!

QuoteI realise that what I haven't demonstrated yet is any compelling reason why you would want to have the option of writing part of your game in Lua.

Yes, I think this would be useful :) 
If it allows things like resizable collections to be easily supported then that would be quite cool.

Snarky

Quote from: Denzil Quixode on Fri 04/09/2009 19:30:22I realise that what I haven't demonstrated yet is any compelling reason why you would want to have the option of writing part of your game in Lua. In time I hope to be able to do this, but for now you may have to just trust me on that.

Grim Fandango was scripted in Lua. Therefore, obviously, working in Lua will enable you to make games as good as Grim Fandango.

Done and done!

Denzil Quixode

#6
Haha  ;D. It's true that Grim Fandango was scripted in Lua, and other games like Psychonauts, and I believe companies like Telltale and PopCap use it in the frameworks for their games. It's popular because it's very small and light, very cleanly coded so it compiles on almost anything, very permissively-licensed (you can use it freely in commercial and non-commercial things without paying for it or agreeing to release any of your own code) and very fast (again, for a dynamic scripting language, not compared to C++ or something).

Okay, this is the first in a short series of posts I'm gonna do about some of Lua's features. This is partly an introduction of how to use Lua, and partly explanation of what I think Lua is good at & valuable for. So, if you're interested in learning Lua, don't solely rely on these, as I might be skipping things that you'd like to know in favour of things I think are cool. All feedback is welcome! :)

Edit: Moved to http://lua-for-ags.wikispaces.com/DataDefinition

SSH

This is so great: it lets me make TWO terrible puns in one blog post!

http://ags-ssh.blogspot.com/2009/09/lua-of-temptress.html
12

Denzil Quixode

Hey, thanks, SSH! Much appreciated.

(Also, it's funny you should mention puns - Lua was created in Brazil, named after the Portuguese word they use for "moon". Apparently it was originally intended to replace a previous scripting language they were using called "Simple Object Language", or "SOL" - and "sol" is also the Portuguese word for "sun"...)

(...I know way too much about this.  :P)

Denzil Quixode

#9

Igor Hardy

Somehow I always thought Lua was something made internally at LucasArts specifically for Grim Fandango.

edmundito

#11
Actually, what happened was that the lead programmer for Grim Fandango found that Lua was just what they were looking for to replace the SCUMM language, and it's apparently recorded as the first game to use the language (for game scripting) (see: http://www.lua.org/history.html and word search for "grim fandango"). Now days it's pretty much becoming the adopted standard for professional game scripting* because it's easy to integrate into anything and it also it's lightweight enough to work with consoles like the Wii.

---

Very interesting work, Denzil! I always wondered myself what it would be like if AGS switched to a more standardized language, and something that's late-binding based. I have done Lua integration in the past, and there's a couple of things that bugged me overall that are huge "gotchas" when you learn:

1. Counting starts at 1, not 0. Oh my!
2. Unless you defined a variable local inside a function it will be global, like JavaScript.
3. There's no decent debugger implementation that can be plugged in. You have to write your own.

That being said, adding this sort of scripting could be the missing piece for a project that I was working on in AGS earlier... actually, can you distribute a game with open Lua scripts? As in I got my agsgame.exe file somewhere, and I let the player write their own script. Is that possible?

---

* Not to say that there are other scripting languages like UnrealScript, Lisp, or Python, but Lua is very widely adopted.... I mean, WoW uses it!
The Tween Module now supports AGS 3.6.0!

Denzil Quixode

#12
Quote from: Edmundito on Tue 08/09/2009 01:41:23I have done Lua integration in the past, and there's a couple of things that bugged me overall that are huge "gotchas" when you learn:

1. Counting starts at 1, not 0. Oh my!
I agree, it's a bummer. There's not a lot you can do except get used to it and warn people as quickly as possible. Hopefully it not a deal-breaker for too many people. (Some people try to hack Lua to go from 0, but I'm not going down that route.)
Quote2. Unless you defined a variable local inside a function it will be global, like JavaScript.
Yeah, I was annoyed about this too, but it turns out that making variables local by default is an even worse choice, because of the way Lua does variable-scoping (which is really good, and honestly not worth sacrificing to avoid having to write "local" all over the place, IMHO). Maybe in some future version they will be clever enough to find a solution for it, but it really is a more complicated situation than just a bad design choice.
Quote3. There's no decent debugger implementation that can be plugged in. You have to write your own.

That being said, adding this sort of scripting could be the missing piece for a project that I was working on in AGS earlier... actually, can you distribute a game with open Lua scripts? As in I got my agsgame.exe file somewhere, and I let the player write their own script. Is that possible?
Sure - The standard Lua functions loadstring() and loadfile() are available to compile script strings/files into new functions.

Joseph DiPerla

Awesome work DQ!!!!! Probably one of the top 5 most useful plugins.
Joseph DiPerla--- http://www.adventurestockpile.com
Play my Star Wars MMORPG: http://sw-bfs.com
See my Fiverr page for translation and other services: https://www.fiverr.com/josephdiperla
Google Plus Adventure Community: https://plus.google.com/communities/116504865864458899575

Denzil Quixode

#14
Thanks for the support  :) It's still quite early days though. What I really want to do is start making AGS functions available to the Lua scripts (I mean the in-built AGS functions, not the ones you define yourself in the global/room scripts - I don't think there is a way to call those from a plugin) and add objects to the Lua universe that represent AGS characters, inventory items etc. These objects would support the standard methods (for a character: Say, WalkTo, etc.) but also, allow you to add your own custom methods and fields.

For example, eventually you should be able to do this (Lua):

Code: ags
-- get an AGS character object from a special "ags" table managed by the plugin
local cEgo = ags.cEgo

-- add a new custom method to it (note the colon instead of a dot - this is Lua "method" syntax)
function cEgo:Greeting()
	-- "self" inside a Lua method is like "this" in AGS-Script, Javascript etc. - like an invisible parameter
	-- that refers to the parent object (in this case, cEgo, although we could reuse this function for different
	-- characters)
	self:Say("I feel... odd...")
	self:Say("Lua... is... controlling me...")
end


...and then, you would also be able to call these Lua methods from AGS-Script, in a similar way to calling a global function using Lua.Call:

Code: ags

function room_AfterFadeIn()
{
    cEgo.LuaMethod("Greeting", null, null);        // Call cEgo:Greeting() with no parameters or return values
}


(This is speculative, it isn't implemented yet, but I think it should all be possible.)

Joseph DiPerla

Lots of hard work ahead of you. But if you get LUA to work with AGS like that, you will have made AGS the most powerful Windows Engine on the market. Now all thats needed is a port of the Run-time plugin so that it would work on the Linux and MAC versions when/If they catch up.

---

ERRRR. The freeware/swapware market that is.
Joseph DiPerla--- http://www.adventurestockpile.com
Play my Star Wars MMORPG: http://sw-bfs.com
See my Fiverr page for translation and other services: https://www.fiverr.com/josephdiperla
Google Plus Adventure Community: https://plus.google.com/communities/116504865864458899575

Denzil Quixode

#16
I am hoping to get a new version out in the next couple of days. In the meantime I'm going to return to that question of why using Lua in AGS would ever be useful. One Lua feature that I haven't mentioned yet is coroutines. I'm not going to describe what they actually are here, or how to directly create and use them - instead, I'm going to try to outline something quite tricky to do in AGS that could be easier to do with Lua, and (for now) just say that coroutines are the magic that would let it work.




Say you're making a game where the PC is a member of crew on board a space ship. You want to give the impression in-game that the crew is a real community of people having independent lives. You don't want the NPCs to just stand/sit around, waiting for the PC to approach them before they do anything. They may move around, from room to room, start conversations of their own that you can eavesdrop on (or wander away from, if you're not interested). If you interrupt people in the middle of a conversation, they might carry it on where they left off, later. And so on.

An important part of being able to achieve that is being able to easily set up a chain of simple events that run like a cutscene, but unlike a cutscene, you don't want the game to block while it's going on - there might even be several of these things going on simultaneously in the same room, and the PC should be free to walk around and do stuff. Let's call them non-blocking cutscenes.

So, how would you implement that? Let's start with a fairly simple chain of events like: an NPC walks across the room to a particular point, pauses a second, runs an animation loop, walks across to another point, pauses again, faces the PC, says something, then walks out of the room. The best way I can see how to do it is to split this up into a sequence of steps, give them all a number, and do a big if-else-if block in the main script's repeatedly_execute() function, where each step waits for the previous one to finish before continuing. Something like this:

Code: ags
//(AGS Script)

// Counter to keep track of where we are in the sequence
int SEQUENCE_1_STEP = 0;
// Timer ID to use when waiting for NPC to finish pausing
#define PAUSE_TIMER  10
// Overlay for background speech
Overlay* bgSpeechOverlay;

function repeatedly_execute() {
	// Set SEQUENCE_1_STEP to 1 to kick things off
        if (SEQUENCE_1_STEP == 0) {
                // do nothing
        }
	else if (SEQUENCE_1_STEP == 1) {
		// Walk over to a point...
		cNPC.Walk(50, 100, eNoBlock);
		SEQUENCE_1_STEP = 2;
	}
	else if (SEQUENCE_1_STEP == 2) {
		if (!cNPC.Moving) {
			// ... pause ...
			SetTimer(PAUSE_TIMER, 80);
			SEQUENCE_1_STEP = 3;
		}
	}
	else if (SEQUENCE_1_STEP == 3) {
		if (IsTimerExpired(PAUSE_TIMER)) {
			// ... run an animation ...
			cNPC.Animate(3, 1, eOnce, eNoBlock);
			SEQUENCE_1_STEP = 4;
		}
	}
	else if (SEQUENCE_1_STEP == 4) {
		if (!cNPC.Animating) {
			// ... walk across to another point ...
			cNPC.Walk(240, 80);
			SEQUENCE_1_STEP = 5;
		}
	}
	else if (SEQUENCE_1_STEP == 5) {
		if (!cNPC.Walking) {
			// ... pause ...
			SetTimer(PAUSE_TIMER, 80);
			SEQUENCE_1_STEP = 6;
		}
	}
	else if (SEQUENCE_1_STEP == 6) {
		if (IsTimerExpired(PAUSE_TIMER)) {
			// ... face the PC ...
			cNPC.FaceCharacter(cEgo, eNoBlock);
			SEQUENCE_1_STEP = 7;
		}
	}
	else if (SEQUENCE_1_STEP == 7) {
		if (!cNPC.Animating) {
			// ... say something ...
			bgSpeechOverlay = cNPC.SayBackground("...are you responsible for this?");
			SEQUENCE_1_STEP = 8;
		}
	}
	else if (SEQUENCE_1_STEP == 8) {
		if (!bgSpeechOverlay.IsValid) {
			bgSpeechOverlay = null;
			// ... then walk out of the room.
			cNPC.Walk(0, 150, eNoBlock);
			SEQUENCE_1_STEP = 9;
		}
	}
	else if (SEQUENCE_1_STEP == 9) {
		if (!cNPC.Moving) {
			cNPC.ChangeRoom(10);
                        SEQUENCE_1_STEP = 0;
		}
	}
}


So that's the first one done. In about sixty lines of code, with an int and an Overlay* to keep track of. We've also used up one of only 20 available timers. It is starting to look daunting to have dozens of these throughout the game. Now, obviously, you can start trying to make it a bit more managable by moving this code out of repeatedly_execute() into a new script, dedicated to these non-blocking cutscene things, and have repeatedly_execute() just call a handler function for each one in turn. You can try to squish each if-else-if step to be on a single line, to try and make it easier to see what's going on throughout the sequence at a glance, but that will probably end up making it even less readable. In order to get around that limited number of timers, you could try using another int variable, that you decrement every turn during the right SEQUENCE_x_STEP until it reaches zero. But it's starting to get annoying just naming all these variables.

It's also no fun tinkering with the sequences, since adding or removing steps means changing all of the SEQUENCE_x_STEP numbers after that point, which takes ages and it's easy to make a mistake. Then there's one sequence where you'd like to repeat part of it in the middle three or four times before continuing, but there's no way of doing it that feels right. You could just copy and paste the steps, then fix up all the SEQUENCE_x_STEP numbers (time-consuming, since there's quite a lot of steps involved - and also, what if you decide later you want to change the number of loops, or alter the steps inside the loop...?). Or, you could create another decrementing int variable, one that counts down the number of loops and decides whether to send SEQUENCE_x_STEP "back in time" to the beginning of the loop or not (which would hurt code-readability even more, as you have to read it back carefully to see where the loop starts and finishes - also, if you want to run the whole sequence multiple times, it's easy to forget to reset this loop variable, so the looped part only plays once the next time...)




Okay, enough hypothetical lecturing, I'm sure you get the picture - this is a case where you'd probably end up compromising quite heavily, to avoid making too much work for yourself, and to keep the code from getting unmanageable. So how could Lua help?

(First of all, this isn't yet possible with the currently-released version, because you can't call AGS system functions from Lua. But you will be able to in the next one.)

I would write a Lua script for handling non-blocking cutscenes, that adds:

  • NonBlockingCutscene_Start(), a function that kicks off a new nonblocking cutscene
  • NonBlockingCutscene_Wait(), a special version of Wait() for non-blocking cutscenes (it only works inside one)
  • several Character extender methods for non-blocking-cutscene versions of standard Character methods - i.e. Character:NonBlockingCutscene_Walk(), Character:NonBlockingCutscene_Animate(), etc.
  • NonBlockingCutscenes_Update(), a function that keeps all currently-running cutscenes ticking over (to be called once every frame)
I won't give you the code for this script now, but it really isn't that long or complex - I will include something like it as an example script in a later version of the plugin.

Only one new line in repeatedly_execute() would be necessary:

Code: ags

//(AGS Script)
function repeatedly_execute() {
	Lua.Call("NonBlockingCutscenes_Update", null, null);
	// (... rest of repeatedly_execute ...)
}


...and the implementation of a single cutscene using this system would be something like:

Code: ags

--(Lua)
function ags.cNPC:MyCutscene()

	NonBlockingCutscene_Start(function()
		ags.cNPC:NonBlockingCutscene_Walk(50, 100)
		NonBlockingCutscene_Wait(80)
		ags.cNPC:NonBlockingCutscene_Animate(3, 1)
		ags.cNPC:NonBlockingCutscene_Walk(240, 80)
		NonBlockingCutscene_Wait(80)
		ags.cNPC:NonBlockingCutscene_FaceCharacter(ags.cEgo)
		ags.cNPC:NonBlockingCutscene_Say("...are you responsible for this?")
		ags.cNPC:NonBlockingCutscene_Walk(0, 150)
		ags.cNPC:ChangeRoom(10)
	end)

end


When you want to start this cutscene from AGS Script, you would call the "MyCutscene" Lua method on cNPC, like this:

Code: ags

//(AGS-Script)
cNPC.LuaMethod("MyCutscene", null, null);


Changing it to loop over part of the sequence a couple of times, using a for-loop:

Code: ags

--(Lua)
function ags.cNPC:MyCutscene()

	NonBlockingCutscene_Start(function()
		-- repeat this bit 3 times before continuing
		for i = 1, 3 do
			ags.cNPC:NonBlockingCutscene_Walk(50, 100)
			NonBlockingCutscene_Wait(80)
			ags.cNPC:NonBlockingCutscene_Animate(3, 1)
			ags.cNPC:NonBlockingCutscene_Walk(240, 80)
			NonBlockingCutscene_Wait(80)
		end
		ags.cNPC:NonBlockingCutscene_FaceCharacter(ags.cEgo)
		ags.cNPC:NonBlockingCutscene_Say("...are you responsible for this?")
		ags.cNPC:NonBlockingCutscene_Walk(0, 150)
		ags.cNPC:ChangeRoom(10)
	end)

end


Part of my goal for this plugin is to make it as unobtrusive as possible, so you can use it for just one section of the game (even just one NPC) and just ignore it the rest of the time, if you wish. I don't pretend that Lua will make it easier to write any part of a game, but I believe it does have the potential to make some very cool things much easier.

Denzil Quixode

#17
I just thought of another example. One thing that I think is quite common is you'll have a list of messages in response to some action, and you'd like to select one each time according to a pattern, like:

  • Pick each one in turn, until the last one, then just keep showing that one
  • Pick each one in turn, then return to the first when you go past the end
  • Pick one at random
This is how you might implement the first one in AGS:

Code: ags
//(AGS-Script)

int MESSAGE_INC = 0;
#define LAST_MESSAGE 5
String NextMessage() {
	if (MESSAGE_INC < LAST_MESSAGE) {
		MESSAGE_INC = MESSAGE_INC + 1;
	}
	if (MESSAGE_INC == 1) {
		return "I don't want to.";
	}
	if (MESSAGE_INC == 2) {
		return "I really don't want to.";
	}
	if (MESSAGE_INC == 3) {
		return "I really REALLY don't want to.";
	}
	if (MESSAGE_INC == 4) {
		return "Okay. Oh, wait, no.";
	}
	if (MESSAGE_INC == 5) {
		return "No.";
	}
}

It's easily possible, but you do have to deal with numbers all the time, which feels sort of unnecessary. If you want to add or remove one early on in the list, you have to go through fixing the rest of the numbers, and remembering to set LAST_MESSAGE as well.

The second pattern is really just a minor change to the first one, so I won't bother with that. What about random order?
Code: ags

//(AGS-Script)
String RandomMessage() {
	int rand = Random(4);
	if (rand == 0) {
		return "Boring.";
	}
	else if (rand == 1) {
		return "Irrelevant.";
	}
	else if (rand == 2) {
		return "Mind-numbing.";
	}
	else if (rand == 3) {
		return "Yawn.";
	}
        else if (rand == 4) {
                return "It's a picture of Gordon Brown.";
        }
}

Still dealing with a lot of numbers, although it's never a pain to add more in this case, since order doesn't matter.




With Lua, I would first write a script that adds "selector factories" - special reusable functions that do the grunt work of implementing these patterns for you - and then use them to create NextMessage and RandomMessage (which will be ordinary Lua functions that you can call with Lua.Call()):

Code: ags
--(Lua)

NextMessage = selector.oneshot({
	"I don't want to.";
	"I really don't want to.";
	"I really REALLY don't want to.";
	"Okay. Oh, wait, no.";
	"No.";
})

RandomMessage = selector.random({
	"Boring.";
	"Irrelevant.";
	"Mind-numbing.";
	"Yawn.";
        "It's a picture of Gordon Brown.";
})


With the message lists written like this, you can very easily add, remove, and reorder the items, and also, change one selector pattern to another. Again - nothing that is impossible to do in AGS-Script - but you probably wouldn't bother.

(Like the NonBlockingCutscene stuff in the previous example, selector.oneshot and selector.random are not standard existing functions, I will include a script that implements them in the next version.)

SSH

Of course with this example you could also use my MultiResponse module ;)
12

Denzil Quixode

Ah, oops  :-[. Sorry, SSH - I haven't actually tried a lot of modules or plugins myself before, so I'm not really aware what else is already out there. I should probably do that before I try to give more examples...

SMF spam blocked by CleanTalk