Breaking into "Advanced Scripting"

Micro optimization is just as readable as your “bloatware” that you glaze for no reason.

Perfomance.
You keep pointing to clouds, claiming that 10-length-deep inheritance laggy bloatware is somehow more maintainable than a well-designed and well-optimized 10-liner.

You are so retro :money_mouth_face: have you joined 2020 :scream:

Readable code that runs slow isn’t readable when it’s running at 5 FPS.

When did I say this?

u still haven’t answered my question :face_with_raised_eyebrow:

I did answered already.

Every part of game is perfomance critical if it runs past initiation.

Let me be the devil’s advocate here for a moment and ask you: why? Or more specifically, what is it that you’re trying to do in game programming that you’re not able to do at your current skill level? Are you trying to solve a hard gameplay problem like writing sophisticated AI for NPCs? Are you looking just to write cleaner, more maintainable and less error prone code?

I’m a strong believer in letting necessity be your guide, so to me, asking “where can I use metamethods” is maybe the wrong way to approach things. It seems arbitrary to me to just want to use some language feature for the sake of it; like wanting to buy some expensive, exotic planishing hammer and asking “hey, what should I whack with this?” I feel that it’s usually more productive to identify what your weaknesses are, and what you’re currently struggling with in terms of not being able to realize the game of your dreams into code, and then ask “what techniques, patterns, algorithms, data structures, etc. should I learn to help me make this feature in my game?”

TLDR; find a real thing you want to make, but don’t know how, and ask people here what new things you should learn to achieve your goal.

Well, honestly what I want to do that I’m honestly not to able to do is write cleaner, more maintainable, and less error prone code. That is really something I do not think is my best area I do want to improve it something I was not able to ask for help on as I couldn’t really think to well at the time I posted this post :sweat_smile:

I do see what you mean with my arbitrary question it was a bit random but honestly I did want to know if it’s something I can use as I try to eventually get better at lua. But than again it really was pretty broad and I probably could’ve narrowed it down. And, well on what I’m struggling with there is a bit that I should probably try improve on i.e. algorithms (I haven’t really directly used any maybe indirectly however) but I do feel like it could improve my code and what not thanks for the comment :slight_smile:

Shortening variables to 2 characters or less is not advanced scripting and doesn’t offer any benefits at all. It makes your code less readable.

Coroutines and tasks are pretty much the same thing with different wrappers. They can be very useful and I use them quite a lot. For example, say I’m making a fireball ability. My character is doing the cast animation but gets stunned out of it, I can use the task library to achieve this functionality.

function ThrowFireball()
-- projectile code
end

function ActivateAbility()
   castDuration = task.delay(2, ThrowFireball)
end

function OnStun()
  if castDuration then
    task.cancel(castDuration)
  end
end

For context, if OnStun is ran, the task.delay never fires the ThrowFireball function because the thread has been cancelled.

Metatables and methods allow you to create class-like tables. You can achieve things like lazy values (dynamically updated values), or classes that inherit from a parent class using the __index metamethod. Once I created an ability system that compiled abilities from config files using metamethods. It was very over complicated but a very fun challenge nonetheless. It’s just about being creative, curious and tinkering with things. Although I find Metatables to be a very big distraction and a lot of the times impacts productivity negatively when overused. They’re not needed and are entirely user preference.

(lol this ended up longer than I meant it to, but I guess I had a lot to say on the topic)

While this is technically correct that there is a hit on performance, I think calling it an INSANE hit is overselling a bit.

I wrote a benchmark to measure this with these results over a million iterations

Benchmark Code
task.wait(5) --let's roblox calm down


local ITERATIONS = 1e6
local HEAVY = 500


------------------------------[[META WORK FUNCTIONS]]--------------------------------------------
local meta = {internalCount = 0}
meta.__index = meta

function meta:InternalAdd()
	self.internalCount += 1
end

function meta:HeavyInternal()
	for i=1, HEAVY do
		self.internalCount += 1
	end
end


------------------------------[[FUNCTIONAL WORK FUNCTIONS]]--------------------------------------------
function Add(t)
	t.internalCount += 1
end

function HeavyAdd(t)
	for i=1, HEAVY do
		t.internalCount += 1
	end
end


------------------------------[[DRY AS MUCH AS POSSIBLE WITHOUT SKEWING RESULTS]]--------------------------------------------

local function measureTime(str, func)
	local t = os.clock()
	func()
	local r = os.clock() - t
	print(str .. (r))
	
	task.wait(2) --let's let roblox do whatever it needs to before the next test
	
	return r
end

------------------------------[[LIGHT WORK BENCHMARKS]]--------------------------------------------

local obj = setmetatable({}, meta)
local metaA = measureTime("metatable counter: ", function()
	for i = 1, ITERATIONS do
		obj:InternalAdd()
	end
end)

local t = {internalCount = 0}
local funcA = measureTime("function counter: ", function()
	for i = 1, ITERATIONS do
		Add(t)
	end
end)

t = {internalCount = 0} --Just for completeness
local inline = measureTime("inline: ", function()
	for i = 1, ITERATIONS do
		t.internalCount += 1
	end
end)


------------------------------[[HEAVY WORK BENCHMARKS]]--------------------------------------------
local metaB = measureTime("meta heavy internal: ", function()
	for i = 1, ITERATIONS do
		obj:HeavyInternal()
	end
end)

t = {internalCount = 0}
local funcB = measureTime("heavy function: ", function()
	for i = 1, ITERATIONS do
		HeavyAdd(t)
	end
end)

--I don't bother with inline heavy because the result is basically just the same story as comparing function vs meta

------------------------------[[LET'S PUT IT INTO A MULTIPLIER]]--------------------------------------------
print("\n\n")
print(string.format("inlining is %.4fx faster than functions and %.4fx faster than meta", funcA / inline, metaA / inline))
print(string.format("light work functions are %.4fx faster", metaA / funcA))
print(string.format("heavy work functions are %.4fx faster", metaB / funcB))

RESULTS

metatable: 0.01828370000293944
function: 0.011764800001401454
inline: 0.0067952999961562455

metatable heavy: 3.3275229999999283
function heavy: 3.301007699992624

inlining is 1.7313x faster than functions and 2.6906x faster than metatables
light work functions are 1.5541x faster
heavy work functions are 1.0080x faster

So the end is the most important part. Worst case scenario for metatables, the direct function equivelant is 1.5541 times faster here. But when there is enough work inside the function to normalize the results, the speed gain drops quickly as you would expect (1.0080x faster with a 500 item loop inside). Most of the time spent in these lightweight functions, unsurprisingly, is the actual function call itself. Ultimately though I am claiming that the performance cost here is negligible except tight loops and systems that do a ton of work. Most of the time though when people are using an OOP style they are keeping those systems that do a ton of work optimized just encased in the object or as a component housed in the object. And when they don’t more often than not the best place to focus on optimizations are things like acceleration structures, not the way the functions are being called.

This is not true. Programming is for getting a computer to do a task within the budgets set by the project. For a video game the budgets tend to often be hitting tight timelines for getting frames out at the target framerate. While a higher target framerate is often good realistically anything 60 and above is fantastic and you only need 30fps for it to be playable. More is better though. The trouble is what limits framerates is often some bottleneck at very specific points and more often than not it’s something running at a time complexity of note. Best to spend your time on any bottlenecks preventing targets from being hit or working on new features or something else if the game runs at targets.

If you don’t have any cpu idle time at all and are not hitting perf targets then yes. If you are exceeding targets or have idle time then any extra headroom is usable space for unoptimal algorithms so long as they still are hitting target. Often the unoptimized approach is easier to write.

Micro optimizations can be very readable for sure, but not always. And they definitely can limit extensibility if they drive you to make choices that are not easy to extend or end up tightly coupled. Loose coupling often(not always) necessitates some level of separation that could be sped up were it not there for example which means it will be harder to add new features or swap out parts if you focus only on performance.

Inheritance is evil and should never be used. I’m kind of joking there, but I hate having anything inherit from anything else. I much prefer to keep my objects as encapsulated containers that hold things they are composed of and let each container have systems that manage how they run. My OOP style is very close to ECS in a lot of ways. I only care about OOP so far as encapsulation goes and polymorphism sometimes. OOP like syntax is useful because it cleanly and logically groups components while coming with built in decoupling since for anything using my object, it uses my interface. You can almost consider my OOP style as a facade over ECS (though my style often doesn’t directly fall under ECS). But that’s it, that’s why I’ll continue using it, and recommending it, despite the fact some things are technically faster. This is basically how I program in cpp too, that just has the benefit of doing this without the overhead.

2 Likes

It depends on the problem but there is a generalizable “rule” that can help determine what should be done in some cases. It’s basically just an extension of separation of concerns (the whole concept of making functions deal with only one thing). In general though this is an extension of that where the function or system you are calling this from may yield at some point or if the function you are calling may yield and you don’t want either to wait on the others yields.

Let us consider an example of when we would use coroutines. Let’s say you wanted to write something that schedules code in a video game. For simplicity we will only consider one point per frame where code runs.

while true do
    queryInputs()
    scheduleCode()
    physics()
    render()
end

We will consider this as our very, very basic game loop. We want to run user code in the schedule code spot. Well we can treat all user code as coroutines so that the different scripts don’t stop each other from running which makes a good start. So we will want to track all the coroutines we have which for the moment you can think of as individual scripts. Each time we call this, we will go through our list of coroutines we have and tell each of them to run. Since we do this in order, each coroutine will run until they call yield at which point control will be returned back to you as the task scheduler. If a function ever returns instead of yields, it is considered dead and you remove it from the list of active coroutines. You do this for all scripts. This is basically what coroutines are for. How does the task library fit into this though? Simple, the task library is the way for the user code you are scheduling to talk to the scheduler to handle when new code will run. task.spawn() will just directly spawn a new coroutine inside the scheduler. This is an oversimplification of what a task scheduler is like, but it should give you a good idea of the uses and differences of tasks and coroutines.

One niche example is I once had code that was connected to a remote event (someone elses code was connected) but I needed to intercept it and keep it running while they were passing messages back and forth. The trouble is when I connected, theirs would still continue running and the server would break if the clients side ever sent more than one reply. The way I got around this was connecting so I could listen to the message but not start actually responding until the next frame so I had time to clean up the other connection to the remote while still ensuring the remote always got a reply. This is a very niche example, but one use case I came across. The actual need for a frame delay though is actually slightly more common than you would think, even though it is rare.

But in general the reason you need to do this is about respecting a code order when you can’t know the specifics of the order the code will run in. Or if you just want to quickly let out a piece of code that has to run after what you are currently running like if you knew an answer in a for loop and didn’t want to bother transferring that local variable all the way to the end of the function to apply it.

One resource you have here on the forums is the code review section. If you’re comfortable with it, you could post up a section of code that you’re not happy with, or one that you feel there must be a more elegant way to solve, and get direct feedback on how others here would do it. I, for one, find it super useful when tackling a hard problem to see what existing solutions look like; not necessarily the exact problem, but related and similar problems that people have solved and shared code for in open source projects, on git, in uncopylocked demo places here on Roblox, etc.

Looking at other peoples’ code that is solving problems similar enough to what you’re doing to be directly relatable is often the best way to learn (both how to do something better, and in some cases, how not to do something :slight_smile:

1 Like

Yes, this 1000 times.

Writing optimally-efficient code is very often at odds with being optimally efficient at writing said code. Simply put, in real-world game development, code efficiency and coding efficiency are both important and should be balanced. The balance is going to be different for making a game as compared to making a game engine or a shared library, of course. But there is always some compromise.

Runtime efficiency of your code is just one of many considerations in the larger picture of game development. It’s doubly true on Roblox, since most games’ Lua code is not performance critical, and C++ engine code is doing the heavy lifting of rendering, animating, pathfinding, etc.

You can easily write naïve, woefully-inefficient Lua functions that you’ll never need to revisit and optimize. Trying to make all your code as runtime and memory efficient as possible is a fool’s errand. Implement → profile → optimize where it matters. That’s the usual process to balance code efficiency with coding efficiency.

I’m not saying you shouldn’t constantly strive to write better code and learn the best techniques, I’m saying that this level of perfection isn’t necessary to actually make a game. You can make a great game, even a top-grossing game, with just average programming skills and no particular expertise in Lua/Luau. This is not an opinion, it’s a fact that’s supported by hundreds of successful games on this platform whose code quality would make a computer science professor recoil in horror.

3 Likes

YES!! At the end of the day, your players do not care about how well you write your classes, how well you use mt’s, etc. All they care about is if the game is fun.

Now, writing libraries and frameworks is a quite a different story. How well and intuitive you use metatables, OOP etc. will probably matter. You’re trying to give people a reason to use your library. Is it optimized? Is it intuitive to use? Why should I use this over this?

This makes sense as well. No point in optimizing something that doesn’t need optimization.

that one file in undertale containing all of the dialogue in the entire game as a single switch statement

1 Like

Of course, I do try to post my code like a while ago I did make a post on some little AI system I was making but there wasn’t to many replies to it :sweat_smile: Of course, I still do try my best to not get demotivated by that and try to get some feedback from other sources. I will probably try to look into similar problems like you’ve mentioned and try to fix my problem or find solutions based on that I’m surprised that I haven’t came up with an idea like xD.

Thanks for the extended explanation of coroutines I will also probably read the docs (maybe a few lua books) and eventually I will understand it. I will probably look into the task.defer thingy (not really sure what it’s called honestly xD) but they do look pretty interesting and something I can use to optimize my project which I do hope will be able to run on phones :sweat_smile:

1 Like

I do have a question, is there any styling tips you guys have? I do want to make my code more readable, and I do want it to formatted well ofc (I am kind of a stickler on organization and formatting xD) if you guys can refer me to any forum posts, books, etc I’ll lake a look at it :slight_smile:

I usually start out my code like this:

-- SERVICES --

-- IMPORTS --

-- VARIABLES --

-- CONSTANTS --

-- PRIVATE FUNCTIONS --

-- CONSTRUCTOR [If doing OOP of some-sorts]

-- PUBLIC FUNCTIONS --

-- HANDLERS --

-- MAIN --
  • Services from game:GetService() goes into the SERVICES section,

  • ModuleScripts (custom libraries, util, shared code, etc.) go in the IMPORTS section,

  • Variables that change over time or hold temporary data - not to be confused with constants, go into the VARIABLES section,

  • Values that DON’T change, often written in SCREAMING_SNAKE_CASE, go into the CONSTANTS section,

  • Functions that are only used internally within this script/module go into the PRIVATE FUNCTIONS section,

  • If you’re doing Object-Oriented-Programming, CONSTRUCTOR section should be used for the constructor function.

  • Functions that other scripts or modules can call can go into the PUBLIC FUNCTIONS section. This acts as your “API” essentially

  • Event connections and callbacks go into the HANDLERS section. (.Touched, RemoteEvent.OnServerEvent, PlayerAdded, etc.)

  • MAIN acts as your scripts main entry point. Runs ONLY once after setup. This could be starting loops, initializing systems, or kicking off your logic.

TL:DR

  • Services → Roblox’s built-in tools
  • Imports → Your external/custom tools
  • Variables → Changeable values
  • Constants → Fixed values
  • Private Functions → Helpers (internal use only)
  • Constructor → OOP object builder/constructor
  • Public Functions → Script’s “public API”
  • Handlers → Reactions to events
  • Main → Script startup logic

Hope this helps! :smiley:

I do something like that which I do like ofc but I might try this as well…