설명
The session will be led by myself and facilitated by Paul Kind, who has been through the entire process of leaning scripting from scratch in Stingray (3ds Max Interactive).
This class is aimed at introducing scripting in Stingray (3ds Max Interactive)as a means to add interactions in your Immersive Experience and improve productivity. There is no pre-required knowledge of scripting, though a familiarity with Stingray (3ds Max Interactive) is required.
We start with the very basics, explaining concepts like variables, tables, and API calls, and move towards how scripting is integrated into Stingray (3ds Max Interactive) to create simple interactions via custom Flow nodes.
주요 학습
- Learn how to write your first line of script in Stingray
- Learn how to create a first custom flow node
- Understand core scripting concepts through a concrete example
- Discover a good organization structure to scale into larger projects
발표자
- David MenardDavid is a Software Engineer by training, with Master’s in Virtual Reality, and an MBA from HEC Montreal. After spending a few years working in the video games industry, he joined Autodesk to kick-start the effort around the ambitious project that is LIVE and Stingray, focusing on Virtual Reality. His deep experience in Virtual Reality and real-time rendering technologies has served him well as a Product Owner, enabling LIVE to become the One-Click solution to VR that it is today.
- PKPaul Kind3D Artist and Game Developer who enjoys helping people come to grips with Stingray, Maya and Related Tools.
DAVID MENARD: First question to all of you here. Who has never, ever written a line of code in their life? How many of you? OK. That's about five. And this is perfect. I'm going to just put this a bit lower. The class is really designed for a basic introduction for anyone who's never, ever scripted in their life. For everyone else, I hope you're going to find something here. Don't hesitate to ask questions along the way. I purposefully shortened the material of the class, so that we have enough time to chat, to ask questions. And hopefully the pace, I'll try and keep it very constant.
So first things first. Before I even introduce myself, everyone just open 3dsMax Interactive or Stingray. You have Stingray? Click on Templates, and just double click on the Basic template. Create one, and we're going to let that compile while we do the rest. And I think everyone here is on trial. So you can just close that window, and it's going to compile.
All right. So my name's David Menard. I work for Autodesk. I'm Product Owner for Revit LIVE. I joined Autodesk three years ago as a programmer. I went to school in programming four years, got a Bachelor of Engineering. This means I kind of have an unfair advantage, as well, here. Because obviously, I've spent four years studying to program. And Lua is all about programming, which is why I invited Paul with me-- where's Paul?
PAUL KIND: I'm over here.
DAVID MENARD: All right, Paul. Come up here. And Paul is kind of my success story. He's an artist-- well, I'll let Paul introduce himself. He's my success story. Win!
PAUL KIND: Hello? Plug in? Remote?
DAVID MENARD: Yeah.
PAUL KIND: On. So I guess you guys might be wondering about Lua a little bit. And first I'm just going to start with who I am. So I'm an artist. I started with pretty much being an artist-- a 3D animator mostly, or 3D modeling. Basically game art. And that's where I started with.
And I started using Stingray quite a bit. And I was actually on the Stingray team doing all the content for Stingray. And after very short order, I started working with Flow. And Flow took me to a really good place. And then I started to realize that I needed to do more than just what Flow can do-- not that Flow can't do it. But there's some times when you just want to make everything a little easier for yourself.
And luckily, with Lua, you can do that very easily. And you can create your own Flow nodes. And I'm pretty sure that's what we're going to be learning here today is how to create your own Flow nodes. And once you learn that bit, you can start really accelerating what you do in Flow.
So yeah, so I started in with Flow. But as an artist, I had no programming background-- or very, very little. I think I used Lingo, which was in Director from years prior. So yeah, if I was able to pick it up as an artist, I'm pretty sure that anybody can. It's not super complicated. And I would just say, take it one step at a time, learn slowly, and you guys are going to do great. But yeah, it's super awesome.
So some of the things that I've done since then-- because now, it's like an addiction. Once you start making Flow nodes, you're like, oh, here, let's make more. So I have a whole library. It's available online, I think. Oh, we're not showing it. But if anybody is interested in downloading a library of already pre-built Flow nodes, just let me know, and I will give you the link to it. Or maybe David can put it up on screen.
DAVID MENARD: Yep. Keep talking.
PAUL KIND: OK. Yeah. So that's a repository on GitHub. You can download the whole package. And once you have the package, it gives you a whole bunch of new Flow nodes. And you can use that to figure out how to write your own Flow nodes. And I was actually super surprised, because I had never actually thought anyone was even touching them. And I've talked to so many people here at AU that have been like, oh, my god, I found your Flow nodes. They're amazing!
So yeah, and it's super easy. It's really something anybody can pick up. And I really hope you guys do great with this class. And I hope that Lua works out well for you.
DAVID MENARD: So by the way, the link is available in one of our downloads on the AU class. So let's talk a bit about how this is going to work. By the way, those who just joined us, just make sure you open Stingray, and you start a basic project, so that it compiles while we talk.
So how it's going to work. First of all, I'm not going to present any videos. You know that usually, when you have hands-on labs or anything else, you prepare videos so that nothing goes wrong. The reason there is no videos, and the reason we didn't rehearse that much this class, is that when you're programming, you're going to make mistakes. It's completely inevitable that you're going to make mistakes.
So today when I write the lab, or when I do it with you guys, I want to make mistakes. And if I'm not making mistakes, I'm going to make intentional mistakes, to show you how to find your mistakes, and how to debug those mistakes. And I've found that that's the hardest thing to be able to teach, when you're teaching programming, and when you're teaching new programmers. It's not writing the lines of codes. It's really finding your own mistakes, and finding what's wrong with it. So making mistakes is part of the lab.
Next, we're going to actually follow the handout pretty line by line. So the handout is very detailed. We're not going to get to the end of the handout. We're only going to get about halfway through. If we have enough time, we might answer some questions about the rest. But by the end of the class, you should be able to do pretty much what Paul described-- writing Flow nodes, writing your own code, writing your own Lua functions inside of that. And writing really your own behavior inside of a Stingray or Max Interactive.
So before we get going, I just want to introduce our lab assistants. Guys, just come up here. And they can introduce themselves.
IGOR: Hello, everyone. My name is Igor. I am CTO of IM Designs. And I'm a nerd, and I like real-time rendering technologies. That's what we do.
RAVI: My name is Ravi Ray Wood. I'm an Architect and BIM Manager with Charles Hilton Architects out of Greenwich, Connecticut. And we do luxury houses of like $100 million single houses. So thank you.
DAVID MENARD: So thank you. Excellent. So these two guys are like Paul. They're self-taught as well. They started Lua with Max Interactive. So if you have any problems during the lab-- if you're stuck somewhere, if you have questions that are a bit more personal, just raise your hand really high, and yell out one of their names. Or yell out Lua or something. And they'll come and see you.
And to our lab assistants-- if you see that there are too many questions at the same time, just tell me. Raise your hands, signal me, and I'll take a break. And maybe I'll go and try and help. All right? So we'll try and keep this as casual as we can. It's late in the AU, and we're all kind of tired. Hopefully everything goes well.
All right. So let's get started. The first thing I want to mention is, throughout this entire lab, we're going to be building to something that's available in the online assets. Inside Scripts here, there's one thing called the Stingray App Kit Extensions. If you get to the end of the handout, you're going to end up with this. So this is pretty much a great example of where we want to get to. But we're not going to get to it by the end of the class. We're just going to get about halfway through.
All right, let's get right into it. The first thing every single programming class gives you, or has you do, is the Hello World. So most of you are familiar with the Stingray project structure. There's this little folder called Scripts here. And in there, there's the Lua folder. There's a file called project.lua right here. If you double click on it, you're going to open this. And this is pretty much the entry point to every single project.
Some of you have probably tried changing the default project. This is the default level that you're going to load. This is where you do it. But really, the first thing you want to do is at line 29-- or OK, the lines changed. But right here. We're going to write, print hello world, like this. And this-- congratulations. You've written your first line of code. So I think we've had five people who never wrote first line of code. Well, there you have it.
So this is pretty simple. Obviously, now, whenever we click on Play, if we go in the log console-- and nothing works, of course. See? First mistake, and this was already unintentional. I put a little dash here. I'm just going to delete that. That's not compiling. So saving this again. And when you press on Play, basically when the project plays, you're going to see hello world in the console. Now it's just compiling again. So I'm going to wait just a second.
While we wait, I'm going to give you a little pro tip. If you go in the window up here, there's a window called the external console. I almost exclusively use that to program in Stingray. The reason is that it has a debugger that I feel is a bit better than the default one. So in here, if you click on Lua, and open debugger, you're going to get a window in which you can open any project file, by just clicking Open. I'm just going to Open Project without Lua here.
See this, Create Lua Project? Open this, and we're good to go. It's the same file. I can save from here. And we'll get into what the debugger does. By the way, did anyone else have this wonky character here? Or was it just me? It was just me? Everyone else compiled properly? I'm asking you.
[INTERPOSING VOICES]
OK. Very. Fair enough. And I saw [? Takahashi ?] just walked in. Where is [? Takahashi? ?] OK. We have another lab assistant, [? Takahashi, ?] right here. He's going to help us through the lab, as well. Thank you.
AUDIENCE: Does anyone have a connector for the external [INAUDIBLE]?
DAVID MENARD: Hm?
AUDIENCE: Connector for the standard [INAUDIBLE]?
DAVID MENARD: I didn't get that. All right, so this is almost compiling. By the way, every time you change something in the project, it has to recompile. The reason it didn't compile here for the first time when I loaded it is because I had a programming error. Obviously, it'll block the compiler at this point. So we just have to wait it out. Hopefully, everyone else is fine. And we can keep going.
So in Lua, when you write a script, every single command that you write is going to be executed in a very linear fashion, right? You write print hello world, you write print hello world number two right after that. They're going to get executed sequentially. And there's really no magic to it.
If something is happening in your experience-- in your game or anything that you write, basically, I am afraid to say it, but it's your fault. The computer won't do anything magical. It'll just follow your instructions by the book. And I mean very, very literally. And it'll surprise you sometimes how by the book it'll do things. I'm getting some echo now.
So if I write, for example, print hello world, a second thing here-- second thing-- it'll just print them in the order. And caps are important. You see I put a capital P here. That would generate an error. I'm just going to change it to a small p. All right. I've finished compiling, I can finally run my game. And in the log console, we're going to see the print hello world.
Oh. That's why. I was compiling for the Hololens. Of course.
AUDIENCE: Under the compiler, how did you get this [INAUDIBLE]?
DAVID MENARD: Simply click on Open, or Control-o. All right. So see? Hello World is being printed. Perfect. That's exactly what I wanted. All right.
So obviously, Hello World is really genius. It's fun. We've written our first line of Lua. Yea. But now we want to actually do something that's interesting with the engine. Like the way you interact with the engine is not going to be by printing stuff. You want to be able to call the application programming interface, or API. So the engine API is just a way to interact, to call things that the engine can do, such as spawning units, moving units, detecting collisions-- that kind of thing.
And your best friend when writing Lua scripts is the documentation. So under Help here, I want everyone to click on Documentation. Here. It's going to give you this page. And right here on the left, you have Lua API Reference. Your best friend is exactly this. You click there, and this page will detail everything you can do to interact with the engine.
For our purpose, we're going to start with spawning a unit. That is done under-- actually, I can search here. Spawn unit. And you're going to see that I have World dot Spawn Unit. And we're going to use exactly this function. When you call these-- these are called functions. It's just ways you can interact with the engine. And you'll get used to this. We're going to come back to functions, and how to write your own.
So the documentation says, I need to call Spawn Unit, and give a few arguments. These things are information arguments-- are information I need to give to the function, so that it knows what to do when spawning a new unit. Obviously, I need to give it a world, because this is under World here. Self is the world. I need to give it a unit name, so which unit to spawn. I need to give it a position, and then orientation.
So let's get back to Stingray, or Max Interactive. By the way, I'm just going to say Stingray from now on. It's shorter. That's the reason. OK? And I'm going to go in my script, and at the same place that I printed this, my hello world, I'm going to spawn a unit.
So the way you interact with the engine, or the API, the application programming interface, you always start with Stingray dot. So in our case, I was looking at the World API. So I'm going to go Stingray dot World dot Spawn Unit. So if I look at spawn unit, it says, Self. Self means just the world. So I'm going to give it a world. This-- we'll get back to what the app kit is, by the way. It's just app kit dot managed world, dot world.
Then I'm going to give it a unit name. The unit name in this case is simply the path. And the relative path of the unit in your project. So I'm going to try and spawn my cube. My cube is under Content. And let's look at this. Content, Models, Props, and [INAUDIBLE] box. I'm going to copy this, because I'm never going to remember what it is. And I'm going to give it exactly this-- content, slash, models, slash, props, slash-- paste whatever I copied. Then I'm going to go back to my API, just to look at what I need to give it. Position orientation.
And you see this little interrogation mark? The question mark here just says it's optional. So I don't actually need to give it a position or an orientation. If I don't, and if I read this properly, it's just going to spawn it at 0,0,0. Right? So I'm going to do that, and hope everything works well.
So this is my preview. I moved this box, just to make sure that it's not in the way. Because if I spawn a box inside the box, I'll just think nothing happened. So I'm gonna click on Start, and-- was that my new box? That is my new box, spawned at 0,0,0. And you see, this is my other box. So there we go. We spawned a first box. We interacted with our engine for the first time.
Now obviously, there is a Flow node that does this. This is not too interesting. But it is the first time we called an API, right? So let's say I don't want to spawn it at 0,0,0. If I go back to my documentation here, it says that the third argument I give it could be the position. Let's do that. Let's give it a position.
So I'm going to do Stingray, Stingray dot vector 3. And I'm going to give it a position. Let's say 10, 10, 0. Stingray, if you remember correctly, is z up, right? So the up vector is in the z direction, or zed direction, if you're Canadian, like me. It's important. It's an internal fight.
So next, when I plus on Play, the first thing that's going to be spawned is my cube, and at a different position. So yea, now I have full control over this. So anyone has a question up to now?
AUDIENCE: Can you put the script back up?
DAVID MENARD: Oh-- yeah. I'll put the script back up. There we go. I'm going to try and make this a bit bigger, too. And if your line is too long, you can simply index it. So you can write the same line on different lines. It won't do anything.
AUDIENCE: Is that the product of [INAUDIBLE]?
DAVID MENARD: Yes. So from I had the question earlier, how to open this file. When you're in the external console, you have to go into Lua open debugger. Once you're in the debugger, you simply click Open, and start writing stuff here to search for it. It's Script, Lua, Project. So I'm going to wait a second, just for everyone to make sure you catch up.
So let's go back to our documentation for a second. This here-- the Spawn Unit function-- you'll notice at the end, it says, column Stingray.unit. This means it returns something. So when you spawn the unit, it will return a reference to that unit, so that you can use it later on. And we're going to store that in something called a variable that we can use later on.
And a variable is really anything you can dream of. You call it what you want. You store whatever you want. And it's just something to help you store information and use it later on. So the way you declare a variable, you simply name it.
In our case, we're going to use the Local keyword. That just means that the variable is only available inside this function. I'm going to call it, let's say my box is actually a chair, because we like chairs. So now, the unit that I spawned-- the box-- I called it a chair, because I think I'm going to use it as a chair later on. I can use this to later reference it, and maybe change its position. And let's actually do that.
So how will I change its position? Well, now I have to go back to the documentation, because obviously, I don't know how to change a position of a unit. I think it's probably going to be inside Unit. So I'm going to open the Units documentation here, and probably going to search for something like set position. No. I'm going to search for position then. There you go-- local position or set local position. Perfect.
So I'm going to use this function on my unit to be able to set its position some other time. And there is an i integer here, the index of the node. So my box, I only have one node in it. I think I only have one node in it. So let's try 1. So here, I'm going to do same thing-- Stingray.units.set_local_position. I'm going to give it my chair. Right? That's our first argument. If you look at the documentation, Self is always the kind of upstream API. So this chair.
The index-- I said I'm going to try 1, because a node is always 1, in my case. And I'm going to give it a new position-- Stingray.Vector3. So do I have to explain what a Vector3 is? Or is everyone good? Does anyone not know what a Vector3 is? OK. Vector3. Perfect.
A Vector3 is basically just three numbers in a row that you can use. Usually, they're used for positions. You can use them for rotations, as well. So in a game engine or a 3D world, you have three axes. Right? x, y, z. Well, Vector3s will describe positions on those three axes. Excellent.
So a Vector3. I'm going to give it a new position-- let's say 20,20,20. So my cube is going to end up in the air. So now, if I press on Play-- where is my cube? It's up here. There you go. So that worked.
All right. So we've seen with the API-- the application programming interface-- of the engine that we've been calling these things called functions. Right? Spawn Unit is a function. Set Local Position was a function. Now, let's build our own function. Functions essentially are just a set number of instructions that will get executed one after the other. And once you regroup those instructions together, you can reuse them as many times as you want.
So let's say you want to set the position of a unit-- its position, its rotation, and its color. You can put that in a function, give it three arguments-- position, rotation, color. And then, just call that on a single unit many, many times. So it's really useful for organizing your code. And you'll see that they are crucial later on to creating your own Flow nodes.
So let's just practice here and create a normal function that will do precisely exactly what we did. So in the same file, we're still in project.lua here. I'm going to call this function-- this is how you declare a function. Who knew? Function Spawn_A_Chair. You don't have to put underscores here, by the way. This is just kind of a programming style. A lot of people will do something like this. So I'll just do this.
Then the function here, you can give it as many arguments as you want. So I'm going to give it the unit to spawn, and I guess a position. This seems fair. And to end the function-- to tell Stingray where the function ends, you just type end. So now, inside of this-- inside Spawn_A_Chair-- there's a set number of instructions that I can reuse. Right now there is nothing, but I can still call my function. So I can do something like this.
Spawn a chair. Give it two arguments. So the first one, I said unit to spawn. I'm going to say this one. I'm just going to copy paste this, because I don't want to type all of this. And I'm going to give it a position, as well. Stingray.Vector3 just at 0,0,0. OK?
So this is how you call a function. You just open and close the parentheses. Is this OK, by the way? Who's following me? OK. About half the class is following me. So we'll take a tiny one-minute break, just to make sure that people can catch on.
Who has abandoned? Anyone abandon? OK. Do you want to try to catch up? Or are you just trying to follow?
AUDIENCE: [INAUDIBLE]
DAVID MENARD: OK. Because this happens a lot. Especially if you make a mistake at the beginning, it's hard to catch up.
AUDIENCE: Yeah, you just want to wait [INAUDIBLE] You're way too fast.
DAVID MENARD: Oh, OK. I'll slow down then.
AUDIENCE: Quick side question.
DAVID MENARD: Yeah.
AUDIENCE: Is Lua the only scripting language that Stingray uses?
DAVID MENARD: Yeah. So the question is, is Lua the only scripting language that Stingray uses? The answer is yes.
AUDIENCE: [INAUDIBLE]
DAVID MENARD: No. There is a C API, so C, C++ API, if you want to go that way. There are bindings between C and Lua, so you could implement your own scripting language. I don't think this is a subject we want to go into. [CHUCKLES]
AUDIENCE: But we can use C, C++.
DAVID MENARD: Yes. OK, so should I go back a bit? I just want to get some feedback.
AUDIENCE: Yes.
DAVID MENARD: Yes? On going back?
AUDIENCE: Yes.
DAVID MENARD: Back to where?
AUDIENCE: [INAUDIBLE]
DAVID MENARD: OK.
AUDIENCE: What [INAUDIBLE]?
DAVID MENARD: To spawning a chair, I guess. So if I go back here-- so remember, right. So if I explain variables again. So when you use the function-- the spawn_unit function-- I went to the API. There you go. And I'm just going to spawn, unit. So I obviously looked at the API and made sure that everything was here properly I think I should leave my code up a bit more, right? Does that make sense? OK.
AUDIENCE: David?
DAVID MENARD: Yep?
AUDIENCE: [INAUDIBLE] many [INAUDIBLE] algorithm for the [INAUDIBLE] people who have bad eyesight. [INAUDIBLE]
DAVID MENARD: There we go. So I'm going to delete my function. And let's make sure that everyone's following here.
[INTERPOSING VOICES]
AUDIENCE: Why would we be getting a nil value for this Flow?
DAVID MENARD: Hm?
AUDIENCE: You're getting nil value in manage world.
DAVID MENARD: Manage world dot world?
AUDIENCE: Yep. Manage world dot world. [INAUDIBLE] Thank you.
DAVID MENARD: Yep. This?
AUDIENCE: Yep. We're getting nil value.
DAVID MENARD: OK. The question is that we're getting a nil value here. So this is causing an error. It's probably-- are you in the right project file? I'm just going to go back, and go and help.
You're outside the project. You're outside the function. Put this in here. OK.
So we had an error over there. I think it's a good example. The error was that we wrote the Hello World and all our functions up here. So if you're not in this function, the project dot on level pre_flow, these are not going to be available to you. So make sure you just write it under this line here.
All right. So Dave Tyner's here as well to help. So don't hesitate to say hi to Dave Tyner. You saw him on the main screen yesterday. Now he's going to get red. [CHUCKLES]
All right. Let's keep going. Let's rewrite our function. Right? How we write a function? We just simply write function over here, and call it spawn_a_chair. And then write end two lines later, just to make sure that Stingray knows where we're ending. And what I'm going to do, at this point, I'm going to just cut these two lines that I had under on project load pre_flow, and I'm going to pass them up here.
And then, what I need to do to call this function-- I just need to open and close a parentheses. I'm just going to copy paste this, because it's safer. That's spawn-- yeah. And so this will actually call this function. So if I press on Play at this point, it's going do the exact same thing as it did before. It's going to spawn my unit. Actually, it's going to print "Hello World" first-- right here. It's going to print this second thing. Then it's going to spawn a chair.
And by spawning a chair, it's just going to go up in here, to line 30, and execute these two lines of code. So it's going to spawn the unit and set its position. I'm going to show you what that does. There you go. So the unit is spawned, and it sets its position. I'm going to come back to the code, give you a chance.
Is this pace a bit better, by the way, those who were still following? OK. Thanks.
AUDIENCE: So question here?
DAVID MENARD: Mm-hmm.
AUDIENCE: Now you're writing the function above the function?
DAVID MENARD: Yep. Yeah. We can talk a bit more about that later. There's something called scope that might be important. But it's not important here. Just make sure that if you-- yeah. You can write it above, and it's available down there.
OK, so I think this is a good opportunity to show you the debugger. Remember I said that we're going to make mistakes? Unfortunately, I didn't make too many mistakes, except just at the start. Let's say instead of spawn the chair here, I would have made a typo. Called him spawn_a_shair.
Now when I click on Play, I'm going to get some errors. You're going to see in the log console, it's going to say, "attempt to call global"-- can I? Hm. It's going to say up here, "attempt to call global spawn_a_shair a nil value." Usually when you see a nil value-- nil in programming, or in Lua, means nothing. It doesn't mean zero. It means nothing.
So this basically says, I tried to call spawn_a_shair. But it's nothing. It means it doesn't exist. Obviously, because I called my function spawn_a_chair. So this is a mistake. So I'm just going to go ahead and look. If you actually look here, it's going to tell you at what line you made the mistake.
So the debugger is friendly sometimes. In this case, it's very friendly. It's going to say line 42, I made a mistake. So right here, spawn_a_shair, I made a mistake. It didn't tell me exactly how to fix it. So I have to figure that out. But if I inspect the code a bit, I can see that I made a typo. So spawn_a_shair.
Next, I want to know what's happening in my code. Let's say it doesn't behave the way I want. Let's say here it spawns at 10, 10, 20, and I don't know why. These little dots that I put on the side here-- they're called breakpoints. For this to work, make sure that you have the proper tab open here-- the one that's printing most of the text. So this one-- before opening the debugger.
AUDIENCE: Question. If [INAUDIBLE] debugger?
DAVID MENARD: In the external console, you mean?
AUDIENCE: Right.
DAVID MENARD: Yes. It's only in there that you can put breakpoints.
AUDIENCE: OK.
DAVID MENARD: You can put some as well in the script editor. So if I open Lua here, and double click on this, you can put breakpoints on somewhere here. Or maybe they removed that. But to be honest, the external debugger is a lot better. So I would just recommend you use that.
AUDIENCE: Yeah, how do you get [INAUDIBLE]?
DAVID MENARD: To this?
AUDIENCE: Yeah. Just [INAUDIBLE].
DAVID MENARD: OK. Under Window, there is a external console. This is it.
AUDIENCE: How do you open the player [INAUDIBLE]
DAVID MENARD: So what was the question?
AUDIENCE: In that console, I don't see anything that says, open.
DAVID MENARD: OK. In this? It's under Lua-- Lua Debugger.
AUDIENCE: Thank you.
DAVID MENARD: And you'll see this is a console. It's basically writing the same stuff that you have in here. And the debugger lets you inspect your code. All right.
So once I have this, if I put a breakpoint, you're going to see that, whenever I hit Play in my game-- now it's all black. The reason it's all black is because my breakpoint was hit, and no code is being executed at all. OK? So it just stopped right here. And it didn't even execute this line of code. You can see in the console, there's no Hello World. In my external console, there's still no Hello World. So nothing has been executed.
And from here, you can step line by line to see exactly what the code does. So if I press Step Forward or Step Over-- the shortcut is also F10-- you can simply see, line by line, exactly what the code does. Hello World. Then when I press F10 once again, or step over, it just went, Second Thing. Then when I get to a function, I can either step over-- which will go to the next line of code right here-- or I can step into. And that's going to go inside the function.
Step into. Then you're going to see, I get to the line where I spawn my unit. And even if I press F10 here, now that this line has been executed, there's a chair in my variable called chair. You can even see it down here. There's a unit in it. But you still can't see anything.
The reason you can't see anything is because this is an interactive engine. It needs to update 30 times a second, or 90 times a second if you're in VR, or enough times a second so that it's interactive. If I paused it here, the world has a unit in it, but it hasn't been drawn yet, not even once. So for me to draw it once, I actually have to let it go.
So from here, I debugged what I needed. So I can press F5. F5 will simply tell the debugger, keep going until you hit a next breakpoint. And I don't have another breakpoint. So at this point, everything's fine. And I can stay interactive again.
AUDIENCE: Sorry. Do you have a breakpoint [INAUDIBLE] it's not stopping.
DAVID MENARD: OK, when it's not stopping, it's probably because a debugger was not opened with the right console window. Let me show you. Then I'll show you on screen after. So close this.
Has anyone been able to hit a breakpoint, by the way? I'm thinking there might be something with the VMs. Yeah?
AUDIENCE: I had to re-run it twice. [INAUDIBLE] that it finally went through.
DAVID MENARD: OK. That's kind of weird.
AUDIENCE: [INAUDIBLE]
DAVID MENARD: [INAUDIBLE]. So usually, with these, you just want to make sure you have the tab that's printing text whenever you press on Play. So let's say I press on Play. You see, there's text that's been printed here. I want to make sure I select this tab before launching my Lua debugger. And there's a good reason for this.
The reason for this is that I can debug more than one engine at the same time. I can actually debug a Hololens, if I have one connected, an Android device, iOS device. I can even debug the code that's running in the viewport here if I want to. But you need to have the right tab selected. If it's not working properly for you, try running the game instead of playing the level. The game will actually spawn another tab, and then you can hit the debugger, and it should still hit the same breakpoint. Let me know if that works. Cool. So that worked.
I'm going to repeat this. If you're having trouble attaching the debugger or hitting your breakpoints, just click on Run Game instead of Test Level. Run Game is going to start a new tab up here. You can see that's the one that is going to print text whenever I launch the game. And from there, I can open my debugger. And then my breakpoints will be hit.
All right. Are we good to keep going? Anyone want me to repeat something? Questions?
AUDIENCE: Can you scroll up a bit?
DAVID MENARD: Yep.
All right. Next we're going to jump. We have functions pretty down right now. We know how to call the engine API. Next, we're going to create our first Flow node. These are super useful, because they can encompass sets of functions that either don't exist in the current Flow graph, or a set of functionality or behaviors that you want to encompass or keep organized in your project.
A great example, as Paul mentioned earlier, he made a little laser teleport-- that's one Flow node. He wrote the code for that in about 20 lines, put that in a node, and now he can use it in the Flow graph as he wants. So writing Flow nodes is very similar to the same thing. We're going to have to create a function. But first, we're going to have to define our Flow node-- what it's going to look like, how it's going to appear in the Flow graph.
So once again, I'm going to go into my documentation. If I go back to the Help Home, and I search for custom flow-- let's search for custom flow. There's going to be a page somewhere here-- Create custom Flow nodes in Lua. This page describes really well the procedure to create custom Flow nodes. It will also describe some more advanced concepts for creating Flow nodes. I usually always have this page open when I create my own Flow nodes, because I just keep forgetting the concepts.
But for now, we're going to keep it simple. And we're going to open a file in the Scripts folder. In the Scripts folder here, there's a file called Global. And if I open this folder in Explorer, this is actually a dot script flow nodes. So guess what? All your Flow nodes will be described in here.
You can open it in the editor of your choice or the Stingray editor. I'm going to open it in the Stingray editor. Hopefully I can zoom in here. I can't. So I'm going to open it in another editor, and hopefully I can zoom in here.
So when you open a template project, there's usually at least one template node. Under nodes, you can see the bracket opens here and closes here. There's another bracket that opens here, closes here. So everything inside of this here is one node. So if I go in my Level Flow, and I look at this Example Print node, the category is "Project," and the name is "Example Print."
So if I go in Project in here, there is an Example Print node. And this is actually a custom node for this specific project. So we're going to do something very similar to this node right here. So first thing I'm going to do is, I'm going to copy paste this, because learning by example is always great. And I'm just going to copy paste the exact same thing right under it. So [CLICK] right there.
I'm going to call this "Open URL In Browser." And guess what this Flow node, what we're going to make it do. It's just going to pop any URL you give it into a browser page. Something that doesn't exist in Flow today, something that the engine provides through its API. But we want to expose it to Flow, so that we can add custom events to it.
The args part of your Flow node is what the Flow node will take in. Remember earlier, we had functions, and the functions took different arguments? The spawn unit function, for example, took-- what was it? A world? A position and a rotation. This is the same thing. This Flow node here-- we will tell it that it needs to take a URL, and that is a string. A string is just a series of characters, if you're wondering.
So ironically, here, right here, "string" is a string. The Category is where the Flow node will appear when I right click here. So if the Category is Animation, it will appear under Animation. If it's under Project, it will appear under Project. You can see that mine just appeared here, "Open URL In Browser."
You can create your own category, too. So I'm going to call this "DavidNodes."
AUDIENCE: Hey, David?
DAVID MENARD: Yep?
AUDIENCE: Why did you capitalize URL instead of [INAUDIBLE]?
DAVID MENARD: There is no good reason for that.
AUDIENCE: All right. But it matters?
DAVID MENARD: Yes, it matters. Capitalization matters. So the question was, why did I capitalize URL? I had no good reason to do that. It's just a personal choice. It does matter, though. If I capitalize it there, when I use it later on, it has to be capitalized. So capitalization matters.
AUDIENCE: So you left it in lower case when you type it in, because you have to take lower case.
DAVID MENARD: When you use it in a function, in the code, later on? Yes. Not when you type in the URL, though. That doesn't matter. Yes. Exactly. And I'll show it to you later. Here. I'll actually do this. So that it's stupid, and looks bad, and is prone to errors. This will make you go nuts.
OK. The last thing is the function here. And this is the most important one. Whenever you call this Flow node, this is the code that's going to be executed for this Flow node. Now here, it says, "ProjectFlowCallbacks.example." We're going to change this to "ProjectFlowCallbacks.open_url_in_browser."
And I'm going to stop there for now and go to my Level Flow. Is everyone good with this? So if I go to my Level Flow, what I want to do is, I want to call this whenever I press Space bar. So I'm going to first create a Flow node for keyboard button.
To search your Flow nodes like this, by the way-- my favorite function ever-- you just press on Tab. Press on Tab, then you can write pretty much anything, and it's going to find it. So if I want to spawn unit on position, I can look for stuff.
AUDIENCE: Does that work in the Unit Flow now?
DAVID MENARD: No, not yet.
AUDIENCE: No?
DAVID MENARD: No. I know. I'm so sorry. [CHUCKLES] All right, so the button name here, I'm going to just search for space. Going to Save. And now I'm going to write my custom node. I put it under David's Nodes, right? Yes. "Open URL In Browser." So whenever I press Spacebar, I want to open this URL. I'm going to say, www.Google.com.
And I'm going to press Play with this. Leave it there for a second.
AUDIENCE: Would this be a reason why a node wouldn't show up?
AUDIENCE: Yeah. Not getting an error.
[INTERPOSING VOICES]
AUDIENCE: I've got the casual read shows that open URLs, there's an option to add. And then, when I click that, there's nothing.
DAVID MENARD: No reason for that.
AUDIENCE: There it's copied and pasted.
DAVID MENARD: We're getting [INAUDIBLE]
OK, a lot of people are having problems with the Flow node not appearing when you click on it.
AUDIENCE: [INAUDIBLE] keyboard [INAUDIBLE].
DAVID MENARD: I'm going to answer that question in just a second. A lot of people are getting problems that, when you click on the node here, it just doesn't appear. It doesn't show up. I think it's a problem with the VMs. For some reason, the connection is not happening. Just restart the editor, and it seems to work after that. Are you getting the same thing, Dave?
AUDIENCE: Yeah, we restarted it.
DAVID MENARD: OK. And it seems to work fine.
AUDIENCE: Show it works on the [INAUDIBLE].
DAVID MENARD: The next question was, how do you get the Keyboard Button node? You can either search for it, under Input-- somewhere in here-- Keyboard Button. Or my preferred method is you press on Tab, and then you can search for nodes. So I simply search for keyboard, and Keyboard Button shows up. And from here you can select your button. We have another question over here? Or same thing?
Excellent error. Perfect.
AUDIENCE: Got a good one, then.
DAVID MENARD: We had an error that said, "Script Flow node definition not found." Is because the name in the definition of the Flow node was not found. It was capitalized. I'll show it to you. Just quickly, in the definition of the Flow node, we had someone go like this. And I don't want to point fingers, because I've done it many, many times.
This, whenever you go in the log console, you'll see that it gives you errors. Or even here. So it's giving me, "Found invalid script Flow node definition." That's because it didn't recognize name. So a property name must be a string. So here, we just need to make sure it's lowercase, not uppercase. All right? So does anyone not have this at this point? OK. We'll wait for a second.
Next thing we want to do is, we want to go find wherever this function is defined. Right? If we click on Play at this point-- I'll click on Play. And I press on Space. This is going to generate me an error. Right? There we go. And the error is called "Function not found. ProjectFlowCallback.open_url_in_browser." This is a very friendly error. It basically tells you exactly what's going on. Function not found. It can't find this function. It's because either it doesn't exist, like in our case, either it has a typo in it, or either there's some other thing that's going wrong. But usually the two main reasons this happened is it doesn't exist or there's a typo. Usually when this happens, you have a typo.
All right? So we're going to go create this function in our scripts. So if I go in Scripts in Lua, there's a script called Flow callbacks here. I'm going to open that in my debugger. So again, just checking in. This is a better pace than at the beginning? Most people are following? Thanks. Excellent.
All right. So once this is done, we can see we have the ProjectFlowCallbacks.example function that's defined here. And this is a function that the example Flow node was calling. Now we want to define another one. ProjectFlowCallbacks.open_url_in_browser. So what I'm going to do-- simple enough-- I'm going to create a function called Project, Flow, Callbacks. And if you want to avoid retyping, just copy paste. Again, just avoids errors. Dot open_url_in_browser.
So one thing to note about Flow nodes, or Flow node function callbacks. These functions that you're defining will always, always take one argument right here. And I call it t. It's called t by default. I don't know if, Dave, you have other experience than t?
AUDIENCE: [INAUDIBLE]
DAVID MENARD: Just a long thing to be annoying. OK, call it t. t is for table. [LAUGHS] So we didn't talk about tables just yet. I skipped that part earlier. We're going to get back to it. But t contains every other argument that we sent into our Flow node. So here, we simply defined one argument called url. So to access that, I can do t.url.
And remember, I did something very stupid here, and I capped the R, just to be annoying. So if I don't do this, it's going to fail. So I'm just going to change that. Because it's really bugging me. There you go. No. I pressed on Control-S to save, and now it's telling me I have a compile error. So it says, "The following resource failed to compile." This one. "Error compiling line 13 near end." This error happened because this is not a valid Lua line. So as soon as you write something that's not valid, and save, it's going to try and compile for you. If it's not valid, it's going to fail right away.
So I have to do something with the URL, right? But before doing anything, I'm going to print. I'm going to print the URL-- t.url. Just to make sure it works. Because you want to take tiny, tiny steps when scripting, and execute as much as you can that way. You catch errors early on.
And you'll see, eventually, you get the right reflexes writing your nodes, writing your scripts, when it comes to API calls. But you never, ever get the right reflexes when it comes to human logic. You're always going to make errors. And it's always going to be stupid errors. And we've all been there. So the earlier you can catch them, the better. That's why you need to execute your scripts as often as you can.
So now, I'm going to go back. I'm going to press on Play I'm going to look in my console, just to make sure that stuff is happening. The Hello World and the Second Thing are still being printed. I'm going to press on Space, and it's printing nil. That is because I didn't not give it a URL. So I'm going to give it something.
So already, I caught this error just because I executed it. If you remember, early on, I actually typed in Google.com. You remember me doing that? The reason it disappeared is because I changed the name of my variable. I changed it from a higher cap to a lower cap. So it just erased it, because it didn't recognize it. So that's what happened here. See? Mistakes. All the time.
I'm just going to re-execute this to make sure I have everything in place. I'm going to press Space bar a few times. And there you go. It's printing Google.com. So now I know I'm all set to keep going here. And I'm just going to replace this script, to make sure that everyone is fine with this. Any questions while we wait? Yes.
OK. So we're going to keep going, and we're going to make sure that this actually opens a URL in the browser. Lucky for us, if I go back to my documentation, in my Lua API Reference, once again, I will search for open-- there you go. Application open_url_in_browser. I'm going to click on this, and I'm going to look at what it does.
So we're under Application. So we're going to start with Stingray, dot, application. And the function is called open_url_in_browser. So I'm going to write Stingray, dot, application, dot open_url_in_browser. And it takes in one URL, and it returns nothing. This function does not return any values. So I'm going to do exactly that. Instead of-- I'm going to write here, stingray.application.open_url_in_browser.
And I'm going to give it my URL-- t.url. So fingers crossed I did everything correctly. Going to leave this here for a sec. Let's press on Play. And if I press on Space, I got an error. Perfect. What's my error? So this is a long error. When you have a very long error like this, you want to always look at the start-- the top one. So it printed my URL correctly. Then it says, URL must be HTTP or HTTPS protocol. So my guess from this is that I need to add HTTP in front of this. Something like this.
So everyone here is probably going to get the same error if they went with Google.com. So make sure you write HTTP, colon, slash, slash. I like to clear this just before executing-- keep it clean. You can press on Play, and now, when I press on Space bar, it actually opens the URL in the browser.
AUDIENCE: Woo!
DAVID MENARD: We got it! We got a woo. [CHUCKLES] OK. We're going to take--
AUDIENCE: Dave?
DAVID MENARD: Yeah?
AUDIENCE: So in here, is it just a coincidence that you named the function in the ProjectFlowCallbacks open_url_in_browser and also, the API call to do that is the same name?
DAVID MENARD: Yes. It's a pure coincidence.
AUDIENCE: OK.
DAVID MENARD: Just to illustrate this, I'll just do this. OK? And this will still work. Remember, if I change it here, though, I have to change the function that it calls in my definition. Right? So open_browser. So this was a pure coincidence.
AUDIENCE: OK. And a more question, David. How does anybody know to look-- how does it know that it's going to be this-- how does it know that your function is going to be in your project program callbacks? Is there another spot that it doesn't have to look here, [INAUDIBLE]?
DAVID MENARD: So the question was, how does Stingray know to look in the Project Flow Callbacks table. Right? So the Project Flow Callback table-- OK, let's do tables first, before answering that question. To answer the question really rapidly, the reason it knows is because it's looking in the global namespace. And this happens to be in the global namespace. Yes.
AUDIENCE: [INAUDIBLE]
DAVID MENARD: Oh. Where are my lab assistants? They left? [INAUDIBLE] they left. [CHUCKLES]
So we're going to explain tables a tiny bit before moving on. So tables are just variables. They're containers that can contain more than one object. When you declare a variable, you can only put one thing in it. A table can contain a series of things. So it's very useful if you want to declare 100 units, and spawn 100 units, to put those in a table, so that you can iterate-- so go through them one by one-- and do something on them one by one.
Tables are a series of key and value pairs. So the way you look up something in a table is through a key. The way you insert stuff is with a value. We'll do something concrete here. So remember how you declare a variable? Local. And I'll declare a table. You just open the curly brackets and close it. And this is an empty table. And I'm going to call it my_table, just to make sure that there's no confusion here.
Then I want to put stuff in my table. So we had this function that we created earlier called spawn_a_chair. It didn't return anything. But here we actually created a chair. So we're going to return the chair, because the functions can return something. And the way you do this is simply return chair. So now, whenever we call this function, what it returns we can store it in a variable.
So let's do that. Local chair1 equals spawn_a_chair. I'm gonna call chair2. And we only have 15 minutes left, right? So now in my table at index 1-- and this 1 here is actually my key. So when I want to look up something in my table later, I can use 1. This can be anything. It could be a string if you want. But at the index 1, I'm going to put chair1. And my_table, at the index 2, I'm going to put chair2.
So now notice, I put a breakpoint here, just because I want to see what's going to be in my table. You notice there's also these green lines here. These are called comments. It means these things are not being executed. Comments are great to leave notes to yourself for the next day, because we're all human. We all forget what we did the day before. Yesterday was a prime example.
So comments are really great to either explain an obscure part of logic or to leave reminders of stuff that you did. Multi-line comments-- you can add comments on more than one line-- are done like this. And then you close it like this. So at this point, my_table-- and I like doing this for myself. I like drawing what my table will actually contain. And this is not actual code. It's just to give me a sense of what I put in my table.
There's going to be a chair1, and there's going to be a chair2. So I expect my table to look like this after this series is executed. And you don't need to follow along with this, because this doesn't actually do anything. I just want to explain the concept a bit.
So when I press on Play, I think I had the right debugger opened. Apparently not. I'm going to close this. Going to make sure I have the right tab opened. Then open my profiler. And it did not. Apparently it was not the right tab because I had two running. So notice, I put a breakpoint on an empty line. That might be why it was not reaching this. That's always a bad habit. Can we try again?
There we go. It hit my breakpoint. All right. So here, if I execute, I jump to the next line. Down here-- I wonder if I can zoom-- OK. Down here-- I hope it's big enough for you guys-- I can see I can expect what's in my table. So this is really useful, especially when you have a lot of data that you want to put in tables. You can actually see what you have in there. So if you want to put values, you can inspect the indices, you can check that the values are correct. You can check everything that's in there.
And the tables-- this is actually the exact same structure that was used when we declared our function here-- the ProjectFlowCallbacks. So this is a table, and it contains the URL. So I may put a breakpoint here, and press on F5. So if I press on Space bar, you can see, I can actually debug my Flow callbacks. I just put a breakpoint, pressed on Space bar, and the t-- you can see what's contained in here, the URL. So I can make sure the URL is correct if there's a problem there.
If there's more than one thing in t-- say I declare. In the arguments, you can put anything here. You can put a value and you'd call it a float. Well then, your table would contain a URL and a value. It's that simple. And then your Level Flow would take a second in thing. Where's my? Mm. I changed the name.
So we only have about 10 minutes left. So I want to open the floor to questions. At this point, we've written our own Flow nodes, right, to open the URL in the browser. You can write a Flow node maybe to change the position of a unit. We've changed it in the project.lua. You can say that, whenever I press on Left, you change the position of a unit. You can add to these one after the other to build up your Flow node library, and really build up the kind of thing that Stingray can do.
The handout goes through this line by line, and explains every single concept with a small paragraph. We're about halfway through the handout at this point. Like I mentioned, I did not expect that we were going to go through the entire handout. That would be very ambitious. The concepts after this point start being advanced. OK? We're looking at classes, core routines, all that stuff.
But most of what you can do in Stingray-- and most of what you will want to do in Stingray-- can be achieved with this. As long as you find the right function in the Stingray API, you'll be able to interact with the engine in the way that you want.
AUDIENCE: How do we know what the engine can do, so that we know [INAUDIBLE]?
DAVID MENARD: Ah, that's an excellent question. And there's no good answer. OK, the question was, how do we know what the engine can do, so that we know how to look for it? Most of what you can think of, someone has implemented it in the engine. So it's just a matter of searching for it. You try keywords.
It's very similar in all engines. Some engines have deeper APIs than others. It's a matter of searching the documentation. And you'll see that eventually you find some keywords that come back frequently. So if I'm looking for stuff related to positions-- local, world-- you type in position, and you get a list of 50 things that it can do related to positions. You just scan through them. And eventually, you kind of get an idea.
I started with Unity, and lots and lots of stuff that you can do with Unity. It's the same process.
AUDIENCE: Are you able to expose your function to the inspector after the UI level? So if I was working with the team, and I was writing some scripts for that, would they be able to change the color by exposing my function? Or is that something that you have to commit the Flow?
DAVID MENARD: So the question is-- let me make sure I understand correctly-- are you able to expose the function to other programmers so that they can use it?
AUDIENCE: No. Sorry. [INAUDIBLE]
DAVID MENARD: Oh, to the Property Editor.
AUDIENCE: Right.
DAVID MENARD: Usually you want to go through Flow in that case. It's something that will come in. You can also use-- we used Level Flow at this point. There's also Unit Flow. So Flow nodes that are specific to one unit, and Flow graphs that are specific to one unit, you can expose those kinds of parameters in the unit. The unit in Stingray is kind of this same thing as a game object in Unity. So there is external in events. There's something like that. Maybe Paul knows. Go see Paul. I'm sure he knows.
AUDIENCE: Thanks.
DAVID MENARD: Yes?
AUDIENCE: So the table could just be declared in [INAUDIBLE] that why [INAUDIBLE]
DAVID MENARD: You mean the ProjectFlowCallbacks?
AUDIENCE: Right.
DAVID MENARD: Yes.
AUDIENCE: And then it says [INAUDIBLE].
DAVID MENARD: The first line is actually a nice little trick that Lua lets you do. Lua, by the way-- super lightweight, super powerful, super fast. It lets you do some neat tricks like this. That first line means, declare a variable called ProjectFlowCallbacks. And if it already exists, just set it to ProjectFlowCallbacks-- the one that already exists. Or if it doesn't exist, just empty table.
So the first time this script is ran, because all scripts are ran-- you run scripts-- it's going to declare a new variable. If you happened by mistake to run this a second time, it's just not going to do anything. So that's a nifty trick. You'll see this happen a few times in Lua. And you get used to these tricks.
AUDIENCE: OK.
AUDIENCE: Another question is--
DAVID MENARD: Yeah?
AUDIENCE: Just want to make sure I'm [INAUDIBLE]. Another question-- is there a central location where people have been placing their available Flow nodes, that we're able to have access to things?
DAVID MENARD: OK, so the question is, is there a central repository or something where people can share their Flow nodes? There's no centralized place where people share their Flow nodes. You'll find some online sometimes. I've placed some in the online assets, just because I have access to it. Paul has placed some on GitHub-- the link that he showed earlier. He has a really nice repository of Flow nodes. So I would encourage you to look at them. They're also great examples on what you can do in Flow.
I also mentioned that, if you go through the entire handout, you'll end up with the Stingray app kit extension. The concepts that are in the handout after what we've done today, if you go through them and you understand them, then you're pretty much good for life with Lua. You go through what classes are, project organization, and co-routines-- things that exist in Unity and in Lua, and how to automate those things. And it goes pretty deep. If you understand that, you're really good. You're good to go. But most people stop here, and they're very happy.
So I think we're out of time. If there's any-- two minutes. If there's any more questions, I can keep going.
AUDIENCE: Are there any classes online that you can take?
DAVID MENARD: Online classes for Lua. Lots of documentation for Lua-- official language documentation-- that you can find online just by googling Lua. The official programming language is not tied to Stingray. Obviously, there's a lot of game engines that use Lua as a scripting language. So whenever you're looking for things like FOR loops or how to declare stuff, Google is your best friend with Lua.
There are some Lynda.com resources for Lua's classes, that kind of thing. You're going to find them pretty easily. When it comes to Stingray-specific Lua classes, this is pretty much, as far as I know, the only thing that exists. But the concepts here-- it doesn't get much deeper than this. Once you understand this kind of stuff, you're going to get going pretty fast. And then it's just a matter of getting used to it.
I can stay here all day if you have-- or all night If you have more questions. Yes?
AUDIENCE: [INAUDIBLE]
DAVID MENARD: OK?
AUDIENCE: How do you put breaks in so I can see what values are being fed through?
DAVID MENARD: OK. The question is that you're having trouble debugging Flow. How do you put breakpoints and see what's happening? Custom Flow nodes-- breakpoints in your custom Flow nodes are the best way to go.
AUDIENCE: Well, I'm not even doing the custom ones. The stock--
DAVID MENARD: The standard nodes? I don't know if you have tips. I usually put a lot of debug prints in there. You can add some custom nodes just to feed the inputs and inspect them there.