Learning MORE Lua - An intermediate guide to scripting
Introduction
Welcome to Part 2 of the Learning Lua series. This guide is designed to teach you some good coding practices and introduce you to various techniques that are available. This guide also has a companion workshop table which can be found here. You will be told when the guide starts to reference that table.
There is more than one good way to accomplish a given idea usually, so keep an open mind and don't be afraid to try to combine ideas. The more you learn about scripting, the more flexibility you will have when trying to accomplish various tasks.
This is the second part of the Learning Lua series. Check out part one if you are just getting started.
Before the First Keystroke
Instructions on installation and setup of Atom[berserk-games.com]
Next you should bookmark the documentation knowledge base[berserk-games.com]. You will be referencing this site often once you start to write your own scripts. Its where you go to find Tabletop Simulator specific functions and how they work. You will most often use the API and Objects pages, at least in my experience.
Creating/Calling New Functions
A common practice in coding is to "call" another function. Some of these functions are part of the API. For example, using print("Word") is calling the function to put that string into the host's chat log. However we are not only limited to developer created functios, we can create out own. This can either be to continue a process elsewhere or to create your own function to create/modify/find information.
Calling another function you create is as easy as making up a name for the function.You can send parameters to your new function too.
You can also use functions to separate out work. There are multiple reasons you may choose to do this. You can use it on code that repeats or you can use it to make your code easier to read, avoiding large, bloated functions that try to do everything on their own. Plus, these functions can "return" information.
Return will automatically end your function as soon as it runs, so it should always be the last step in a line of logic.
Then you have larger functions, it can be hard to track what is being done at each step, leading to confusion. If this is a problem you are having, breaking one large function down into smaller functions can help you find problems.
Local/Global Variables
But for most applications, using a local variable is preferred. They are created by placing the word "local" before your variable name when the variable is being created. Local variables only exist within the function they are created. When the function ends, a local variable is forgotten. If you only use Global variables, in longer scripts you will find yourself thinking of more and more complicated variable names, accidentally overwriting variables, accidentally using old variables, etc. So below is an example of how this local variable would be different.
But as you move your code into different functions, they will need a way to communicate and share these local variables sometimes. If that is the case, you can share a local variable with another function by sending it as a parameter.
I personally avoid general variables as much as possible. I only ever use them if I need data to persist. For example, if you reference the example table, the "Save/Load" memory example has a Global variable which tracks the value of a counter.
Coroutines
When we go to use a coroutine, rather than just calling a function's name, we use startLuaCorotuine to trigger it instead. All coroutines should end with return 1 in Tabletop Simulator, to properly terminate them when they are over. When using startLuaCoroutine, the first parameter is a reference to where the coroutine script can be found (in this example, Global script). The second parameter is the function name.
That example of a coroutine did not pause at all. If we want to add a pause, we run the code coroutine.yield(0). This will "yield", making the coroutine wait until the next frame before it continues. So in this next example, we will yield for 200 frames.
---BEGINNING OF EXAMPLES---
From this point forward, you should have a pretty good grasp of the basic concepts of making a good script. Now you just need to learn to put those concepts into practice. In my opinion, the best way to manage that is learning by example. If you have not already, subscribe to the WORKSHOP COMPANION TABLE for this guide.
What follows will be short, simple descriptions of the concepts behind each example and what they do. On the example table, each token has scripting saved onto it, with detailed comments, explaining the inner workings of these tools. They also have information in their names/descriptions on what they are supposed to do.
At this point in your learning, all the basics are covered, so all that is left is to learn from specific examples and, occasionally, googling for specific Lua functions (ex. a rounding function). Happy tinkering.
Example: takeObject()
The first is the concept of giving a function parameters to tell it what you want it to do. You create a table which contains certain named elements (ex: position, rotation, etc) and give it to takeObject. (ex. takeObject(parameters) ). The Knowledge Base outlines what parameters functions can accept.
The second is the "callback" portion of the function. Some functions allow for a "callback", which means after the object from the function loads, it can call on another function to act. This is important, because when you use takeObject, you can't immediately take certain actions because the object doesn't physically exist on the table yet. So you can't lock it, set its tint, etc. So a callback will allow you to perform those actions after it has loaded into the world. Understanding callbacks is vital to manipulating objects after using takeObject.
Example: spawnObject()
Example: Timer
The timer function allows you to trigger a function after X amount of time. The timers require a UNIQUE identifier. The name you use cannot be shared by any variable in ANY code on the table. Because of this restriction, it is a common practice to use an object's GUID when creating a timer, because that GUID should be unique.
Currently in Tabletop Simulator timers will continue to run if the code is stopped/deleted. So if you have a recurring timer running, it will CONTINUE to run, and throw errors, if you end that instance of the code. Hitting undo or loading away from the table will cause these timer errors too, if the timer was running when the action was taken. This is a bug in TTS. In the Knowledge Base, you may see the option to repeat a timer automatically. I recommend not using it until this bug is rectified in the future.
Example: Locating Objects
You can use these same methods with many of the "Member Variables" listed in the Knowledge Base. What you will use is 100% dependent on the situation you are using it in, so be flexibile.
Example: onSave()
This is where onSave comes into play. It allows you to put anything you would like the script to remember between loads into a table. Then, when you load the table next, it will pull that information back out during onLoad() and let you access it. This is a fairly simple function that is vital to some scripts.
A piece of personal advice, I recommend always leaving onSave() for last when scripting. The reason is that sometimes some information can be saved due to a bug in your script and saving over it is required to fix it, which can be tedious to keep track of. I typically set my script up to use onSave but then don't actually activate it until I feel like I have everything else working how I intend it to.
Example: math.random()
But getting truly random numbers in scripting can be a bit tricky. The way they are generated is with a math formula which uses a "seed" number as its basis. You can set this seed value with math.randomseed(numberhere). If you used the number 1 for numberhere and then did math.random(), it would give you a result. If you set the randomseed to number here again and did another math.random, you'll end up with the same exact result. Because it is just based on that formula.
I do not have a perfect to avoid this problem so that different players all get different random numbers in different orders, but in my example I have included some tricks I often use. One of them involves using os.time(), which is the current time on the host PC, as the seed. Then occasionally changing seeds. You just need to be careful with this method because it only uses whole seconds. Good luck.
Conclusion
Once you are familiar with the concepts described in this guide, the best way to continue to learn is by doing. Make sometime, check out how other people managed the same thing, play with ideas.
The third part in the Learning Lua series (Learning Lua Functions) isn't a tutorial. It is just a collection of useful functions to help you accomplish your goals. It also highlights the usefulness of using separate functions to return specific results.
I hope you have gotten enough out of these guides to continue on your own. If you're like me, the satisfaction of making some neat little bit of script that does something interesting will keep you learning. Who knows, maybe one day we'll actually be good at scripting. haha
Thanks for reading. If you spotted any bugs, please leave a comment to let me know.
This guide was created by its original author on the Steam Community. Are you the author and want it removed? Request removal.