Most people don't really understand how the time component of programming in DM actually works. They just throw code at the scheduler and don't really understand what the scheduler actually does, leading to messy code, horrible practices, and in general, very confused spaghetti code.
Let's meet the scheduler.
The best way to describe the scheduler is via an analogy. Think of the scheduler as a powerful bureaucrat sitting in a dingy downstairs office. he's a bit cranky, and can only do one thing at a time. If you overload him, or don't keep the work you give him organized, he gets really confused and starts to complain.
procs and by extension verbs all have to operate one at a time. The scheduler processes each submitted proc or verb call one at a time, in the order they were received unless it receives other instructions on when to do things.
A proc can be either blocking, or non-blocking. A blocking proc is sort of like an "immediate" task that the scheduler needs to do before it can do anything else. A non-blocking proc is something the scheduler is told to do, but given some leeway as to when it can do it.
Each tick is a block of time in which procs are sent to the scheduler to complete. If the scheduler has too much work to do in the time alloted to a single tick, time stops. Your CPU reaches 100% or more, and the scheduler has to work overtime to get all of its work done. This is called CPU over-run.
By default, a single tick is 1/FPS seconds. An FPS of 30 results in 0.03333333 (repeating) seconds per tick, or 33.33333 milliseconds per tick. An FPS of 40 results in 0.025 seconds per tick, or 25 milliseconds per tick. And an FPS of 60 results in 0.01666666667 seconds per tick, or roughly 16.666667 milliseconds per tick.
The higher your FPS, the less time you have alloted per tick to do work. Now, the interesting thing about this, is that even though the window to get things done per tick is smaller, the higher your FPS, the more important it is to understand how your code is scheduled. This is because it becomes increasingly important to distribute The work you are doing over multiple ticks.
When an instruction is a blocking instruction, it will prevent any other function from taking priority within the scheduler, and thus will cause code to logjam up in a queue. A queue is a data structure where the first thing to enter it exits before any subsequent entries. This means that instructions queue up in a line.
Procs can be thought of as a line of instructions waiting for the previous instruction to finish.
Taking frequent breaks
It's important to note that work doesn't always have to occur all at once. You can instruct the scheduler to come back to that work later, and there are three main ways to do this:
1) the sleep instruction
2) the spawn block
3) the waitfor setting
By default, waitfor is a proc setting that is set to on. When something is set to waitfor = 1 (which is by default, true), the proc that it's set on, will cause the current proc executing in the stack to wait until the new proc is finished before continuing. The scheduler in essence puts the current proc back in the front of the line for processing, and then switches to processing the new proc as though it were in front of the current proc.
When waitfor is 0, on the other hand, it does something just a little bit different. It puts the new proc in the front of the line, and continues in the current proc as though the new proc hadn't happened yet. The new proc, is scheduled to happen the moment the current proc yields (either by ending, or sleeping).
Sleep Instructs a function to go back into the line. If you tell a function to sleep(10), it will sleep 10 tenths of a second (or one second), before continuing to be worked on. It puts it line according to the time schedule we have instructed the scheduler to work with.
There is also a special case use for sleep(), which is important to note. The current frame may have other pending tasks queued up. If you want to delay something until the currently queued tasks are done processing, you can use sleep(0), or just sleep() to delay the current proc until after tasks scheduled in the current frame.
Another interesting use of sleep(), is calling sleep() with a negative value as the argument. This will cause BYOND to check if there are other functions in the queue that should already have already been finished. If the scheduler is in overrun according to this check, it will suspend processing of the current function for the shortest time possible (which is the tick_lag value) to allow other events to finish their work. You can use this quirk to make heavy-duty loops not freeze BYOND completely while they do their work.
Spawn is a bit more subtle, and should be used with care. Spawn defines something called an inline function, and delays it for a specified duration.
All of the code that gets tabbed under a spawn() block will be treated like an unnamed inline function and be executed at a later, specified time. As such, the scheduler will put it in line based on the value you feed to spawn as the argument.
There is also some interesting special case behavior here much like sleep(). If you feed spawn() 0, the inline function will execute right after the current frame's scheduled procs sitting in the queue.
If you feed spawn() a negative value, on the other hand, the inline function is executed immediately before continuing in the current proc. This will treat the inline function like a blocking proc, which is honestly not something that there is much actual use for. Often, it's better to not use this quirk, and I can't think of any reasons to actually use it.
Now, with spawn, there are a number of reasons that it's good to avoid overuse of spawn() where it's not necessary. When you call spawn(), the scheduler has to search the queue for where to insert the handle to the inline function. This takes a bit of CPU time that's not profiled. If you are spawning a lot of stuff over and over again, it will cause some mysterious unprofiled CPU usage as it searches the list of pending procs for the proper position to insert the function handle.
Also, spawned() inline functions don't have a name, which makes them difficult to track using BYOND's profiler. This makes optimizing and bugtracking a lot harder at the end of the day, and can help to obscure problem code from being obvious. As such, i recommend usage of the waitfor setting, and avoiding things like nested spawns.
projectile.loc = null
set waitfor = 0
projectile.loc = null
In summary, I find it best to avoid spawn() in almost all cases. There are very few cases where delayed inline functions are necessarily a better approach than using a simple waitfor setting. It's also important to note that if you are doing things like setting spawn() at the beginning of New() or Login() functions, odds are you are doing things wrong in the first place.