├── README.md └── writeups ├── attributes-valueobjects-instances.md ├── data-saving-format.md ├── dont-make-an-fps-on-roblox.md ├── lets-talk-about-perf.md ├── performant-design.md ├── remote-events-indepth-information.md ├── selene-stylua-robloxlsp.md ├── thread-reuse.md ├── why-you-should-care-about-performance.md └── why-you-should-use-wally.md /README.md: -------------------------------------------------------------------------------- 1 | # All of my Roblox-related writeups in one place. 2 | 3 | The first writeups added are copy-pasted directly from the devforum, which is why you may see "why did I make this post", or references to devforum-like concepts. 4 | 5 | I hope that this information helps you, or you learn something new from this heap of my knowledge. I'm likely going to add to this frequently- check back later! 6 | -------------------------------------------------------------------------------- /writeups/attributes-valueobjects-instances.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | I'm making this post because I see a lot of games using things like the below code snippet in competitive fighting games. This, to be put quite simply, isn't good at all. I will explain why in this post. 3 | ```lua 4 | local Stunned = Instance.new("Folder") 5 | Stunned.Name = "Stunned" 6 | Stunned.Parent = Character 7 | ``` 8 | 9 | --- 10 | ## The demon that is StarterCharacterScripts 11 | 12 | The first common pattern I see is value objects / folders being in StarterCharacterScripts. It is EXTREMELY expensive to put things in StarterCharacterScripts- and if you have resetting enabled, exploiters can mass lag the server by constantly respawning with alts. Not only does respawning with >10 instances in StarterCharacterScripts lag the server, it also leaks memory out of the sides due to a Roblox bug where [character instances don't get cleaned up](https://devforum.roblox.com/t/every-time-your-character-dies-it-leaks-memory-up-to-500-mb-probably-infinite/1889683). Also, any time the character respawns and you have non-scripts in there, it'll cause significant lag spikes in your game. 13 | 14 | 15 | --- 16 | 17 | ## ValueObjects & Instances as Temporary State 18 | (by temporary state, I mean for things like character states such as Running, Crouching, Stunned) 19 | ValueObjects and Instances are overused. You should never be depending on ValueObjects or instances for short-term state- attributes are acceptable, but still not optimal. So first off, let's identify a few things: 20 | - The client never calculates other clients state, speed, or anything of the such. 21 | - The client, as such, doesn't need to know anyone else's state, speed, or anything apart from appearance, position, and things like that. 22 | - The client doesn't need to know the state, cooldowns, or saved data of other clients or mobs. 23 | 24 | 25 | Let's identify a few things with ValueObjects, instances, and attributes: 26 | - ValueObjects, instances, and attributes replicate to every single client assuming they are a descendant of ReplicatedStorage or Workspace. 27 | - Instances are costly to create / destroy rapidly, and it has a lot of wasted data. The same with attributes, just to a lesser extent. 28 | - Functions like :FindFirstChild() take time to execute, so relying on chains of :FindFirstChild() or .Value can result in animation or state weirdness. Keep in mind, Roblox does all inbound connections in one frame. The same is said about attributes, but to a lesser extent. 29 | - Creating ValueObjects on character spawn is costly and will cost a significant amount of resources. 30 | 31 | I think it's pretty easy to identify the issues with this. Any form of using attributes, instances, or valueobjects for temporary state (characters) results in a lot of wasted bandwith. Here, let's do some math! Assuming you're *creating instances*: setting the name (16chars, let's assume, so 22 bytes), class (2 byte cost..? no documentation.), parent (5 byte cost). These are reasonable guesses for 1 instance creation- now if we have 8 of these updates in a single frame, that's 6 (Overhead assumption)+2+22+5 = 35 bytes, for a single instance creation. 8*35=280 bytes, multiply that by 60 that's 16.8k, aka 16.8 kilobytes. Now here's the catch- that's multiplied by however many players there are. So if you have a 25 player server, those 8 state updates per frame result in **420,000** bytes per second. That's 420 kilobytes. Now, here's a shocker: speedtest.net reports your internet speed in *bits*. 8 bits is one byte, so if you wanna know how that scales with your internet: 3,360 kilobits, which is 3.36 megabits (3.36 Mbps) for 8 state updates per frame. Just 8. 32 | 33 | Mind blown. That's so much wasted data, that most likely **isn't even used by any of your scripts.** This doesn't even mention the issues with memory, creation time, update speed, :FindFirstChild(), all of that. I'm not going into detail about that. 34 | 35 | Keep in mind: **these issues still persist with attributes.** You do NOT need to replicate data that *has zero usage anywhere in your codebase (at least most likely)*. 36 | 37 | Main takeaway: Don't use things that replicate to every player when that's not needed at all. If really necessary, don't create instances- make a ValueObject on character spawn instead. 38 | 39 | --- 40 | 41 | # Why should I care about these things? 42 | Because if your game remotely relies on ping, **the server's FPS directly impacts ping**. If your server is taking 20 milliseconds for physics and .Heartbeat, you are going to have 20 milliseconds of added ping. 43 | 44 | You're also going to notice client frame time being higher- all these instance changes need to be applied and executed. Remember, **16ms per frame is 60fps**. That's an incredibly low budget- 14ms per frame is 70 fps, and 16ms is 60. That's 10 gained fps- for 2 milliseconds of saved time. 45 | 46 | Also keep in mind that *player to player interactions* are most of the time beneficial for a game- having a higher player cap can make your game more enjoyable. 47 | 48 | Remember that the above math I just showed you is a more conservative assumption **considering you're making an instance**. 49 | 50 | --- 51 | 52 | # What about readability and ease of access? 53 | It's possible to make readable and good code without needing to use things like instances. I *personally* suggest using OOP for character control- it's efficient, looks good, expandable. However, there's people who would tell you to use ECS too- it's your decision. I'm not going to say "this specific way is better than X", I'm not trying to advertise OOP, this is just a suggestion. It's your decision on how you structure your code, I'm just telling you that *this specific way* is very bad. You can use RemoteEvents for replicating state from the server to the client, and back, as well. Just don't use normal instances / value objects / attributes. -------------------------------------------------------------------------------- /writeups/data-saving-format.md: -------------------------------------------------------------------------------- 1 | Assuming the following is true: 2 | ``` 3 | Blank remote call: ~9 bytes 4 | 5 | string: length + 2 bytes 6 | 7 | boolean: 2 bytes 8 | number: 9 bytes (IEEE-754 signed doubles) 9 | 10 | table: 2 bytes -- (There is no overhead for table indexes) 11 | 12 | EnumItem: 4 bytes 13 | 14 | Instance: 4 bytes 15 | 16 | Vector3: 13 bytes 17 | 18 | CFrame (axis-aligned): 14 bytes 19 | CFrame (random rotation, non-axis aligned): 20 bytes 20 | ``` 21 | 22 | This means 9 bytes per "remote" call can be reduced using a specific data structure, like the following: 23 | ```lua 24 | -- ID being a 1-character length string 25 | -- ID2 being another 1-character length string 26 | -- Create IDs by keeping track of the number of IDs created, then using string.pack to convert it into "binary" (theres a useless byte in there unfortunately) 27 | { 28 | [ID] = { 29 | {A, B, C} -- An individual call: Remote:Fire({ A, B, C }) 30 | } -- Array, indexes are free with no cost. 31 | [ID2] = { 32 | {A, B, C} -- :Fire({A, B, C}) 33 | {D, E, F} -- :Fire({D, E, F}) 34 | } 35 | } -- Dictionary 36 | ``` 37 | 38 | Using this format results in the following benchmark: 39 | 40 | Roblox: 41 | 42 | ![image](https://user-images.githubusercontent.com/80861876/228321062-76bedd2a-12e4-4a71-b986-de123280fc80.png) 43 | 44 | 45 | The format: 46 | 47 | ![image](https://user-images.githubusercontent.com/80861876/228321084-5d9d1f88-1bea-4c4a-ae3b-941218d7170d.png) 48 | -------------------------------------------------------------------------------- /writeups/dont-make-an-fps-on-roblox.md: -------------------------------------------------------------------------------- 1 | Any competitive, or hardcore, shooter on Roblox will do very poorly. I don't think you should make one. Here's why: 2 | 3 | 1. Roblox has no reliability types on RemoteEvents- you cannot do 60hz character movement without *massively* cutting out your players (anyone on wifi, anyone with a slightly subpar connection) 4 | 2. Roblox's default humanoids move at 20hz- this is too low for competitive shooters. 5 | 6 | Alright, that can be a lot of information for someone who's not too familiar with networking. Let's dive into it: 7 | # Reliability Types 8 | When a packet gets sent from your computer to the Roblox server, it has two different *reliability types*: Reliable, and unreliable. Because packets aren't guaranteed to go through, it will try to re-send a packet if it doesn't go through. **All networking traffic is halted during this time**. And there's also unreliable: If it doesn't go through, nothing happens. 9 | 10 | So, this means that people on subpar connections will be hurt if you send a packet every frame. If you have 0.5% packet loss (this is fine to play almost every game, and most people will have something similar when playing on wireless), that means every 2 seconds someone will experience a ping spike when you fire a remote event every frame. That is not playable. 11 | 12 | # Why isn't 20hz movement acceptable? 13 | 14 | Because competitive shooters require *much* more precise movement refresh rates, as the timings are *a lot* more precise. You cannot have a competitive tactical shooter with 20hz movement. 15 | 16 | # Why make this thread? 17 | This information really needs to be spread out more- I see many trying to make competitive shooters on Roblox, it is just a massive time waste. I want to put this in an area where it will be seen by many people, so no one tries to make a mistake and do this. 18 | 19 | --- 20 | 21 | If you wish to see competitive shooters on Roblox, please show your support this feature request: 22 | https://devforum.roblox.com/t/reliability-types-for-remoteevent/308510 -------------------------------------------------------------------------------- /writeups/lets-talk-about-perf.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | I have seen a lot of toxicity and differing opinions on performance in the Roblox community, within all different places. Some people are biased against opting for performance, some people are biased for it, some people want only performance, etc. etc. I am going to try to make this to resolve some of this toxicity and arguing. While that's, kind of absurd to attempt, I really do not like the misconceptions being spread around performance. 3 | 4 | ## My background + my goal for this "article" 5 | I have been scripting on Roblox for 5 years now. I would say I'm fairly successful, and I know a fair thing or two about this platform. I have been on both sides of this argument; I have argued against trading things for performance, and I have argued for trading things for performance. I would say that back-end work is my specialty, with performance being something I excel at. I am trying to keep this as neutral as possible, because I do not want to argue. As such I won't be replying to anything even slightly inflammatory; I don't want to argue. 6 | 7 | ## Topic #1: "Premature optimization is the root of all evil" 8 | The full quote is "We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.". What this means is, if code could be 10 microseconds faster, but it only runs every second, you should not touch it. You should not touch things that will not ultimately impact any form of user experience. There is no reason to. 9 | 10 | I see this used as an argument against micro-optimizations for serious things that actually should be prematurely optimized. I understand that it's difficult to tell what should be optimized and what shouldn't be for most coders, but I think there is an easy way to tell if something should be optimized or not: Is this going to affect the user if it does impact performance? If so, how much will it impact performance? 11 | 12 | You should be prematurely optimizing things like character replication, head movement, things that can crash the client or overload bandwith, or something that can cause freezes! In fact most of the time, you will wish you prematurely optimized these things! I have had many times where something I put off optimizing became a problem during testing and I wish I didn't put it off. Usually the optimization is pretty simple, and there isn't much trade-off. This is why you *usually* never see triple A games having massive performance issues that crash clients on release! Because.. they prematurely optimize the 3% of cases, because usually that 3% is clear. 13 | 14 | You should always be careful and do any easy optimizations on code that runs per-frame. There is almost never a loss to doing that. Most dramatic optimizations I do on games I become a developer for, take me an hour maximum, and improve UX drastically. Just do it. 15 | 16 | ## Topic #2: Performance vs. logging/stack traces/etc. 17 | If you are discarding error/logging info for the sake of performance, you are doing something wrong. 18 | 19 | ## Topic #3: Time is not the only performance factor 20 | Time, memory, and bandwidth. Those are what you need to optimize. Just because your server runs at 20 frames per second for an hour, doesn't mean it will for 6 hours. I think every Roblox dev needs to focus more on memory leaks; I plan on making a whole separate article for memory managemenet later. 21 | 22 | Remember that you only need to stay below ~12 milliseconds per frame. Memory and bandwidth are also important performance factors, and usually most possible optimizations that save time aren't really going to help you. 23 | 24 | ## Topic #4: Speed over everything 25 | - Do not fork a library just to micro-optimize it and then call it better. 26 | - Do not remake a library just to make it micro-optimized. 27 | - Do not switch libraries for the sole purpose of a speed gain. 28 | 29 | I will maybe add more to this later. 30 | -------------------------------------------------------------------------------- /writeups/performant-design.md: -------------------------------------------------------------------------------- 1 | *this post is targeted towards intermediate/experienced scripters, not beginners* 2 | 3 | A big problem with the common Roblox's developer on performance is "you shouldn't optimize something unless you need to", and while this is somewhat okay for less ambitious games, you really should focus on designing your code to not require optimization in the first place. 4 | 5 | For example, let's design a small physics-networking system similar to Roblox's. No code yet, so let's just lay down some design: 6 | 7 | Step 1: Each physics object checks if players are near it 8 | Step 2: If there is a player nearby, transfer network ownership 9 | Step 3: Client receives that, and starts doing physics calculations for the object. 10 | Step 4: The client sends updates every frame. 11 | Step 5: The server runs checks to make sure the client is accurate- if it's inaccurate, send an update to the client. 12 | 13 | If we're trying to design an efficient system, there's a lot of issues with this model of network ownership. 14 | 15 | --- 16 | 17 | Step 1 has an issue: why is each physics object getting the closest players? Assuming there's more physics objects than players, it would make more sense for the server to check if there's players in a certain radius. 18 | 19 | **Takeaway:** Design your code to do less- if there's a route to overall do less, then take it. Sometimes you don't need to do certain operations when you can design your code to work less. 20 | 21 | --- 22 | 23 | Step 4 has an issue: why do we need to calculate it every frame? Shouldn't we just update it when it changes, instead of wasting bandwith on the same value? 24 | 25 | **Takeaway:** Try to take away redundant code. If something is the same, then don't update it. Don't put out needless updates with things you already know. 26 | 27 | --- 28 | Step 5 has an issue: Why are we sending an update to the client when the client is either 1. exploiting or 2. incorrect- in which case the calculations will instantly realize it's wrong, and adjust accordingly? 29 | 30 | **Takeaway:** You don't need the full context to do something. You can rely on systems you wrote to do the work for you- you don't need to be perfect with 100% accuracy every time. 31 | 32 | --- 33 | 34 | Overall, you shouldn't need to traditionally "optimize" your code. You should have *already done it* by thinking "hey, this probably isn't the best idea, I should try something more efficient" before you even write it. This means you don't need to gut out systems and you'll have less issues with performance later on, because you thought about performance before even writing the code. Just a simple "this is probably bad, I should reduce the load" is enough to save time later rewriting and optimizing code, with zero effort spent on *actually* optimizing it. -------------------------------------------------------------------------------- /writeups/remote-events-indepth-information.md: -------------------------------------------------------------------------------- 1 | *Updated 10/27/2022* 2 | 3 | I hate the lack of centralized knowledge when it comes to things like how Roblox does networking, so I am going to do my best to share my knowledge with others when it comes to this. 4 | 5 | **Prerequisite Knowledge** 6 | - Low-level networking knowledge (packets, ping) 7 | - networking reliability and ordering. 8 | - Roblox's RemoteEvents 9 | - Throughput, bandwidth. 10 | 11 | Just a bit of information to start off with: You can press Shift F3 and then Shift 1 to view more networking data. 12 | 13 | Roblox does networking per frame- this means that networking is not handled *instantly*, but rather when a packet arrives, it's deferred to the next frame. Most games use end-to-end latency when they show ping (csgo, Minecraft, etc.). It's **not** RTT (round trip time). 14 | 15 | Shift F3: 16 | 17 | ![image](https://user-images.githubusercontent.com/80861876/226212846-f609e6c8-4866-444c-9ab5-7fd8de62b6c3.png) 18 | 19 | The ping you see in this image (62.56ms) is after frame desync- it factors in your framerate, pretty much. And yes, this means when the client/server drops below 60fps, ping will increase. It also means that using an FPS unlocker will decrease ping. 20 | 21 | Shift F3 + Shift 1: 22 | 23 | ![image](https://user-images.githubusercontent.com/80861876/226212851-debdf7af-a973-4b5c-95af-c6acc9009bf8.png) 24 | 25 | The ping you see in this image (25ms) is without frame desync in mind. 26 | 27 | --- 28 | 29 | # **RemoteEvents** 30 | When you fire a RemoteEvent, the data you sent into the remote event is added to a queue. This queue gets emptied per frame, and all the data gets sent out. The Roblox client receives this information with a ~9-byte overhead. It is important to keep in mind that remote events are reliable, meaning that there are mechanisms in place to prevent packet loss. 31 | 32 | It is also important to keep in mind that RemoteEvents, alongside a lot of other things, are ordered, meaning extra steps are taken to make sure that *all important data* (property changes too) is ordered and fired in the same order they were fired on the server. 33 | 34 | Another important thing to keep in mind, *that Roblox will throttle networking if you fire too many remote events too fast*. I don't know the metrics, but I have had it happen to me. 35 | 36 | There's a queue for RemoteEvents when a RemoteEvent is fired, but there is no listener. This is a *small* queue, and it should not be relied upon as you can lose important data. It *does* drop remote events, and fast-firing events will devastate this queue. 37 | 38 | Sources: 39 | 40 | ![image](https://user-images.githubusercontent.com/80861876/226212865-16ae9193-1fab-450c-98c7-e452e242dcb5.png) 41 | 42 | *(quick note, while it does say it "doesn't happen every frame" this is not my experience. I've taken microprofiler logs a lot, and it has happened every frame. so please keep that in mind. However at the same time, I may be wrong. )* 43 | 44 | ![image](https://user-images.githubusercontent.com/80861876/226212868-099ebda0-fd2f-4d94-a6c8-225802e26213.png) 45 | 46 | --- 47 | 48 | # **Physics** 49 | Physics networking is unreliable and unordered. This means that it, under normal circumstances, has less latency than RemoteEvents. This also means that physics will not care about packet loss or the order of events that happen. It doesn't have to. Part positions can come out of order, and packet loss doesn't affect positioning much as the position is replicated per frame. 50 | 51 | Another thing to keep in mind is that **CFrames are expensive to set**. You should avoid CFraming a lot of parts at once. 52 | 53 | Physics data is also replicated: if you have a visual moving part on the client, not only will this be visually affected by unstable ping and whatnot, but it will also take up needless bandwidth. 54 | 55 | As a quick footnote for this category, humanoids are expensive to replicate/use. You should always try to reduce the number of humanoids in your projects. 56 | 57 | # **Instance Replication** 58 | **Properties are not replicated from client/server network ownership, only physics data**. This is extremely important to note. Only positional/directional data is transferred, 59 | 60 | I haven't found that much information on how instances are replicated, how much it impacts networking, and whatnot, but I would imagine that it takes a notable amount of data to replicate instances. So I would *not* recommend relying on Roblox instance replication for entire instances, as it could impact your game and make things worse on low-throughput players. I would recommend creating said parts on the client to save all the wasted data. For example: 61 | ``` 62 | -- Server 63 | EffectsRemote:FireAllClients(position, "fireballEffect") 64 | 65 | -- Client 66 | EffectsRemote.OnClientEvent:Connect(function(position, effect) 67 | local Clone = EffectsFolder[effect]:Clone() 68 | Clone.Position = position 69 | Clone.Parent = workspace 70 | end) 71 | ``` 72 | *purely an example, I would not recommend taking from this.* 73 | 74 | Another noteworthy behavior of instance replication is the fact that **only the most recent property value is replicated.** This basically means that if you change a property 3 times in one frame, only the last value will be replicated. 75 | 76 | --- 77 | 78 | A table that describes the amount of data it takes to replicate values (rough, potentially inaccurate): 79 | ``` 80 | Blank remote call: ~9 bytes 81 | 82 | string: length + 2 bytes 83 | 84 | boolean: 2 bytes 85 | number: 9 bytes (IEEE-754 signed doubles) 86 | 87 | table: 2 bytes -- (There is no overhead for table indexes) 88 | 89 | EnumItem: 4 bytes 90 | 91 | Instance: 4 bytes 92 | 93 | Vector3: 13 bytes 94 | 95 | CFrame (axis-aligned): 14 bytes 96 | CFrame (random rotation, non-axis aligned): 20 bytes 97 | ``` 98 | (by Tomarty: https://devforum.roblox.com/t/ore-one-remote-event/569721/33) 99 | 100 | Roblox uses RakNet (http://www.raknet.com/, https://github.com/facebookarchive/RakNet) to manage networking, which implements reliability and ordering. 101 | 102 | While your target for bandwidth should be completely relative to your player cap, I always try to get the client *receive* below 100 kbps and the client *send* below 20 kbps. Although, it completely depends on the game and the player cap for said game. 103 | 104 | Thanks for reading! I hope this has helped you in some way. 105 | -------------------------------------------------------------------------------- /writeups/selene-stylua-robloxlsp.md: -------------------------------------------------------------------------------- 1 | Odds are, if you have Rojo, you have at least 2 of these installed. I like using all of them, and I'll explain why they're really cool here, and how they fit into my workflow- because there's not much information on this tooling out there! 2 | 3 | # First extension: Selene 4 | [Selene by Kampfkarren](https://kampfkarren.github.io/selene/roblox.html) is an opinionated linter designed for Roblox- but can be used outside of Roblox. It's a great tool- it helps catch bugs, and keeps your code nice and idiomatic. 5 | 6 | One of the biggest parts about development in general is keeping your code clean and readable, which is exactly why selene does. Your code being idiomatic is important, because the majority of people will be able to understand and read it. Selene helps keep your code idiomatic- it catches things like multi-line statements, duplicate keys in tables, empty if statements, unused variables, etc. etc. One really cool thing Selene also does is automatically flag deprecated functions, which means you'll instantly know when something you use is deprecated. 7 | 8 | ![image|580x85](upload://l8IA6h53X5mnt8jLOfZe2BscLwe.png) 9 | 10 | 11 | # Second extension: StyLua 12 | [StyLua by JohnnyMorganz](https://marketplace.visualstudio.com/items?itemName=JohnnyMorganz.stylua) is Lua formatter that goes hand-in-hand with Selene because a lot of the formatting lints are fixed by StyLua. StyLua follows the [ Roblox style guide](https://roblox.github.io/lua-style-guide/)- which I recommend you follow. This means your code will always be formatted in a way that's readable, idiomatic, and follows a style guide that **Roblox themselves follows**. Since StyLua is an auto-formatter, it can be as simple as pressing Ctrl + S to auto-format your code, which is very helpful. 13 | ![image|549x73](upload://8QiJK9D77xmqoxbg9C9pRuvEUGX.png) 14 | ![image|690x290](upload://vcRvttC7spcgLwJovhO43KCJWyQ.png) 15 | *(my personal formatting settings)* 16 | 17 | # Third extension: Roblox LSP 18 | [Roblox LSP by Nightrains](https://devforum.roblox.com/t/roblox-lsp-full-intellisense-for-roblox-and-luau/717745) really is the glue here- it provides highlighting, typechecking, all that for Roblox. It even provides autocomplete inside vscode for things like folders in workspace, which is very nice. I don't have very much to say about it myself, but it's impressive. I use it *alongside* selene, with both Roblox LSP diagnostics, and selene's lints as well. It's what works for me. 19 | 20 | --- 21 | 22 | While a lot of what I've said is personalized towards me, I encourage you to find your own balance- don't shy away from Rojo because it feels awkward for you, because there's a lot you can personalize. Look at the documentation for selene, Roblox LSP, look at the settings, and try figuring it out! You can go to an extension's settings through here: 23 | ![image|468x369](upload://oPf0MOtBbb2wVkqEMNVsqGlzLjP.png) -------------------------------------------------------------------------------- /writeups/thread-reuse.md: -------------------------------------------------------------------------------- 1 | ```lua 2 | local freeThread 3 | 4 | local function functionPasser(fn, ...) 5 | fn(...) 6 | end 7 | 8 | local function yielder() 9 | while true do 10 | functionPasser(coroutine.yield()) 11 | end 12 | end 13 | 14 | local function SpawnWithReuse(fn, ...) 15 | if not freeThread then 16 | freeThread = coroutine.create(yielder) 17 | coroutine.resume(freeThread) 18 | end 19 | local acquiredThread = freeThread 20 | freeThread = nil 21 | task.spawn(acquiredThread, fn, ...) 22 | freeThread = acquiredThread 23 | end 24 | ``` 25 | *(putting credit where it's due, I learned this pattern from @Stravant's GoodSignal)* 26 | 27 | --- 28 | ## What does this do? 29 | 30 | This snippet of code allows us to *re-use* threads instead of constantly creating new ones. This helps with performance. While it may look intimidating, it's not that complex. 31 | 32 | --- 33 | ## How does this work? 34 | 35 | These lines of code may seem complex and complicated, but I can assure you this process is very simple. 36 | 37 | All we really need to know are a few things: 38 | - Threads can only contain one function 39 | -`` task.spawn`` can **resume** a thread, aka make it stop yielding 40 | - We can pass arguments into ``coroutine.yield()`` through ``task.spawn(function, argsPassedIntoYield)`` 41 | 42 | So let's go over this process real quickly: 43 | 1. When we want to spawn a thread, detect if there's already a ``freeThread`` 44 | 2. If not, create a thread with the function ``yielder``. 45 | 3. Resume the thread we just created- this runs the ``yielder`` function. 46 | 4. After that, grab the ``freeThread`` reference, and set the freeThread variable to nil. We keep the value this way, but any reference to it is cleared, so nothing else can detect it or interfere. 47 | 5. Resume the thread **again**- except this time, it's yielding. Remember how I said we can pass arguments into ``coroutine.yield``? We pass the function through. That runs a function which.. literally just runs the function you put into it. This means that ``yielder``: 48 | 1. Waits until a function gets passed through 49 | 2. When a function does pass through, run it. 50 | 3. Continues waiting 51 | 6. After we run ``yielder``, put freeThread back, because it's done- it's not running any code. 52 | 53 | --- 54 | 55 | ## Why does this work? 56 | 57 | Because in this scenario, we are only creating a thread if we cannot access the existing thread. This means we don't need to consistently spawn threads- which increases the speed ever so slightly. However, this is important at an incredibly large scale when you're spawning threads up to hundreds of times a frame, resuming a thread instead of creating a thread can save a lot of time. This optimization method particularly helped my project BridgeNet- thread reuse is not implemented yet, but it helped speed it up *by over double*. 58 | 59 | Hope this explanation helped! 60 | 61 | note: I'm aware the example is bugged, I don't care enough to fix it. -------------------------------------------------------------------------------- /writeups/why-you-should-care-about-performance.md: -------------------------------------------------------------------------------- 1 | "Performance" means a lot. It can mean call time optimization, it can mean low memory usage. It could mean lowering bandwith usage, or maybe all of the above. Performance could be about something per-frame, or something that gets called once. It could be about reducing start-up times, or reducing load times. Or it could be all of those things! 2 | 3 | I'm going to attempt to explain when performance matters and why it matters. There's a lot of misinterpretations and people saying "don't use xx because it is 100 microseconds slower". Microseconds can matter when you're dealing with things like frame times, but does it really matter when you're running the code once per second? 4 | 5 | # Frame Time / Frame Budget 6 | So first off: let's define what fps is. A frame, or a "tick", or how long it takes to run things like rendering and server logic. For example, it takes 16 milliseconds to render a single frame and display it on your screen. This means you have 60 frames per second- which is what Roblox runs at. 7 | 8 | One important thing to note is that **16 milliseconds is a lot of time**. 1 millisecond is 1000 microseconds- a difference of 80 microseconds is pointless. It's a micro-optimization. One common example of needless performance worries is with object-oriented programming- the difference between regular function calling, and method calling, is negligible (within ~80 microseconds), and most of the time you won't be calling something per frame with object-oriented anyways. However something that saves ~800 microseconds is worth it- that's almost 1/16th your frame budget. 9 | 10 | ![image|686x62](upload://akcwuJw7LganqDr6iYoFaDhLj7t.png) 11 | (Benchmarking method vs. passing self) 12 | 13 | **One important thing to note is almost no optimization that sacrifices readability for speed is not worth it 90% of the time.** 14 | Making your code unreadable for a ~150 microsecond gain is not worth it. 15 | 16 | **Ping is directly impacted by frame time.** Due to the way Roblox networking works (packets are sent at the end of the frame, packets are received at the start of the frame), your frame time directly impacts ping. Going under 60fps will increase your user's ping. 17 | 18 | It's important to note that the more remotes you call, the longer it'll take to send those out. This literally increases frame time- which increases ping. Connections are also atrociously inefficient. 19 | 20 | ### The Microprofiler 21 | Since Roblox's fps-cap is at 60 (for both the client and server), that means each frame should take under 16 milliseconds- since 1/60 is 0.016, which is 16 milliseconds. So this is the *maximum* amount of time it takes before user experience starts dropping. The microprofiler lets us see what exactly is eating up the frame budget, and it's a very useful tool for debugging performance issues. 22 | 23 | ![image|690x278](upload://9QoOukVXZ71V0eeIEs9s3MvGFiO.jpeg) 24 | (A screenshot of the client-sided microprofiler) 25 | 26 | # Networking 27 | Networking is arguably the most important area to optimize. Optimizing your networking can significantly improve user experience by the frame time on the server (which subsequently reduces ping), ping spikes, and in some extreme cases prevent disconnecting from the server. In other extreme cases, not optimizing bandwith can result in long freezes on the server. However I've already talked about this, and so have other developers- I'll just link the posts below. 28 | 29 | https://devforum.roblox.com/t/in-depth-explanation-of-robloxs-remoteevents-instance-replication-and-physics-replication-w-sources/1847340 30 | 31 | https://devforum.roblox.com/t/network-optimization-best-practices-how-to-keep-your-games-ping-low/1913475 32 | 33 | # Client FPS 34 | This is arguably one of the most important factors of optimization. Your client's FPS should be top priority, as a sub-60 FPS experience may significantly diminish the quality of time spent on your game. There's a lot of *great* ways to optimize the client though: 35 | - Implement a chunkloading system 36 | - Turn off Shadows and CastShadow on things that don't need them 37 | - Delete textures the client will never see 38 | - Don't run heavy code on the client- optimize all your code on the client. It directly impacts FPS. 39 | - Don't overlap textures- this significantly increases render time. 40 | - Transparent blocks / textures are substantially worse for performance. 41 | 42 | # Conclusion 43 | You should focus on optimizing client fps, server fps and bandwith- you shouldn't be focused on things like reducing call time for functions that get called once per second. **Your frame time is what matters.** 44 | 45 | I don't recommend using attributes, valueobjects and instances for things like state. This isn't good, but I have an entire post about that. 46 | https://devforum.roblox.com/t/lets-talk-about-attributes-valueobjects-and-instances/1918761 -------------------------------------------------------------------------------- /writeups/why-you-should-use-wally.md: -------------------------------------------------------------------------------- 1 | For some context: Wally is a package manager for Roblox. Link is here: https://wally.run/ 2 | 3 | So firstly, lets talk about what a package manager does, as I'm sure a lot of people don't know what a package manager does- and that's okay! I'll explain it right now. Let's take a few projects: [TopbarPlus by ForeverHD](https://devforum.roblox.com/t/zoneplus-v320-construct-dynamic-zones-and-effectively-determine-players-and-parts-within-their-boundaries/1017701), [ZonePlus by ForeverHD](https://devforum.roblox.com/t/zoneplus-v320-construct-dynamic-zones-and-effectively-determine-players-and-parts-within-their-boundaries/1017701), [ClientCast by Pyseph](https://devforum.roblox.com/t/clientcast-a-client-based-idiosyncratic-hitbox-system/895217/252), [PacketProfiler by Pyseph](https://devforum.roblox.com/t/packet-profiler-accurately-measure-remote-packet-bandwidth/1890924) and [BridgeNet by ffrostflame](https://devforum.roblox.com/t/bridgenet-insanely-optimized-easy-to-use-networking-library-full-of-utilities-now-with-roblox-ts-v199-beta/1909935). Every single one of these projects- which are **resources** made by the community, have one thing in common: they all use external dependencies (relies on other resources, created by other scripters). 4 | 5 | TopbarPlus relies on [Maid by Quenty](https://devforum.roblox.com/t/how-to-use-a-maid-class-on-roblox-to-manage-state/340061) 6 | ZonePlus relies on [Janitor by HowManySmall](https://github.com/howmanysmall/Janitor) 7 | BridgeNet relies on [Promise by evaera](https://github.com/evaera/roblox-lua-promise), and [GoodSignal by Stravant](https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063) 8 | ClientCast relies on [GoodSignal by Stravant](https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063) 9 | PacketProfiler relies on [Roact by Roblox](https://github.com/Roblox/roact/), and [GoodSignal by Stravant](https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063) 10 | *Fun fact: in that list alone, three of those projects use Wally* 11 | 12 | A package is essentially an open-source resource for others to use. A package manager manages your packages and dependencies (some packages rely on other packages!). What exactly does that mean? 13 | 14 | The first thing a package manager does is make things incredibly easy to install. You don't need to manually download anything, manually update anything by dragging files, moving stuff around. You just edit a file a little bit, and the package manager updates it for you. 15 | 16 | The second thing a package manager does is manage dependencies. As I mentioned, a lot of packages use at least one dependency- so by using a package manager, it automatically updates **those dependencies for you as well**. Awesome! Never worry about drag-n-drop file nightmares again. 17 | 18 | So onto the third thing a project manager does, and this gets a bit in-depth, so I'll start this simple. One thing you might've noticed with the list of packages I gave, is that ClientCast, PacketProfiler, and BridgeNet both rely on GoodSignal. So what does this mean if you're using BridgeNet, ClientCast, and PacketProfiler? 19 | 20 | It means you have **three** duplicate scripts in your game which does literally the same thing, with slightly less performance because GoodSignal benefits from coroutine re-usage. Same thing issue with Promise, same issue with Janitor. Package managers alleviate this issue by having **one** main package that every dependency points to. 21 | 22 | Oh, yeah, and what happens when both you and a project use GoodSignal? If you use GoodSignal too, then you have FOUR duplicate scripts in your game by using these packages! And there's *no way* that's a reason to not use those packages. 23 | 24 | Wally, as a package manager, fixes this. On the surface, without looking inside, it looks like these are duplicates- but they're not. Every ModuleScript you see inside of a package- for example, ``ffrostflame_bridgenet@.0.0-rc2/Promise``, is actually a 1-line script that requires the real promise. Same thing with all the packages in the top Packages folder! 25 | ![image|387x307](upload://u0WIkMtpMa0y9HKEb49Oo9kxmj7.png) 26 | 27 | (promise in ffrostflame_bridgenet@2.0.0-rc2) 28 | ```lua 29 | return require(script.Parent.Parent["evaera_promise@4.0.0"]["promise"]) 30 | ``` 31 | 32 | So using Wally, to get a package, you would just do ``require(ReplicatedStorage.Packages.GoodSignal)``! Very easy, you don't need to change parents or anything like that. 33 | 34 | But there's a problem with this. Sometimes packages aren't on Wally, and sometimes dependencies of projects on Wally are just files within the projects source code- with the issues as mentioned :thinking:. So, with this, we can reasonably come to the conclusion that the *more developers that use Wally*, the better, and richer, Wally's database of packages will be. 35 | 36 | In fact, one of my main pain-points when developing my package BridgeNet was adding support for non-wally users. It's why the Roblox marketplace version is ever so slightly out of date! Every single time I finish a new version, I need to edit it to not use Wally, and then upload it to a few different places. It's annoying to do. [Knit, a popular framework made by sleitnick](https://github.com/Sleitnick/Knit), has a complex process for uploading non-wally versions. [Look at this script that sleitnick made to do it!](https://github.com/Sleitnick/Knit/blob/main/.github/workflows/release.yaml#L32) It's needlessly complex, as he should be able to just upload it to wally without worry. 37 | 38 | That's why I'm asking *you* to use Wally, for the benefit of both yourself, other developers, and for the sake of package creators. Wally is maintained by some *really* great people, and it's criminally underused. I'm extremely grateful to Wally's developers. All your favorite packages **are most likely on wally**. And if they aren't, you can upload them yourself! If you're worried about using others uploaded projects- **any downloaded packages are visible to you as a developer as well.** It's a public registry! Anyone can submit a project for usage. 39 | ![image|630x159](upload://aLWAirZA7Ng0a59WoESPXF6cMaV.png) 40 | ![image|323x182](upload://Aatq1LbnapaiPzNQY6OJUwDxH2k.png) 41 | *screenshots of wally users uploading projects not on wally, to wally* 42 | 43 | Updating your packages is as *simple* as running a single command: ``wally install`` 44 | ![image|627x145](upload://cxULaj054c4CCqXMywzcsu7lPjF.png) 45 | 46 | Downloading packages is as simple as dropping a few quick lines into a file. 47 | ![image|690x139](upload://l9OU9RStCHPbwQnkL4glWwpiQhf.png) --------------------------------------------------------------------------------