In GDScript, the main way to do optimization is to optimize the approaches themselves, the algorithms and data structures you choose to use (dictionary vs array is most common).
Knowing which engine functions to call also helps. For example, you mentioned accessing other nodes: if you use
$path everytime for a node that you know is always present, sure it will have a cost (small), the engine has to decompose the path and compare node names until it finds the correct one, which can however be easily avoided by storing the node in a variable with
onready, with the added advantage of failing earlier if the path is wrong.
Another point is memory: allocating memory often has a high cost.
.instance() may allocate memory and run construction logic. In Godot, custom allocation strategies are in place under the hood to optimize this already, but constructing and destruction objects still has a "setting-up" cost when they are many. So if you plan on having hundreds of VFX and bullets in your game, investigate how much do they take on frame time and pool them if needed.
Lazy-loading can also impact performance: a typical bottleneck in Godot 3 are shaders. They are very big internally and take a long time to compile, and that can only happen when first needed, so the first time an object is visible or an effect plays, it causes shader loading and short freeze (See https://github.com/godotengine/godot/issues/13954). It's not necessarily related to scripts, but something to know about if you optimize your game.
The same applies to sounds: if you play a sound by
loading it on the spot, it might be loaded on that spot, and cause a freeze. The solution is to either set it in advance on the sample player, or preload it.
Use signals and input callbacks when suited. It can be tempting to put everything in
_process, but not all things are frequent enough to need constant polling.
Optimizing script bottlenecks can be adressed in two ways:
- Old school method, you can wrap the region you want to profile between
OS.get_ticks_usec() calls and calculate how many microseconds the function took to execute. Keeping everything under 16ms is usually the goal. Try on lower-end devices if you have margin.
- Use the profiler in the debugger tab. It has a listing of functions, but will only profile if you enable it while the game runs. I find it lacking in usability and features though, even the doc has almost no details on it https://docs.godotengine.org/en/3.0/tutorials/debug/overview_of_debugging_tools.html?highlight=profiling#profiler, which is why I often fallback on the old school method.