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:
//(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:
//(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:
--(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:
//(AGS-Script)
cNPC.LuaMethod("MyCutscene", null, null);
Changing it to loop over part of the sequence a couple of times, using a for-loop:
--(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.