The Power of 10: Implement Nasa Rules to Your Roblox Studio Development Workflow

Unlock the secrets to better coding in Roblox Studio with insights inspired by NASA's 10 programming rules.

The Power of 10: Implement Nasa Rules to Your Roblox Studio Development Workflow


Introduction

In Roblox Studio, writing a clean, efficient, and maintainable code is crucial. NASA follows 10 programming rules they have developed, originally designed for high-stakes aerospace software, that can be adapted to enhance your coding practices in Roblox Studio using Luau scripting language. In this 10 steps long blog tutorial, you will find out how you can implement these rules in your Roblox projects, along with alternatives where necessary.

1. Avoid Complex Flow Constructs

Avoid looping functions at all cost unless there is no other alternative, as they can lead to stack overflow errors and make your code harder to manage. Instead, stick to simple control structures like for, while, and repeat for looping, which provide clearer logic and better performance in your scripts.

--Limit this:
function SpawnNpc(plr: Player)
    local Npc = game.ReplicatedStorage.NPC:Clone()
    Npc.Parent = workspace
    task.wait(1) --> correct way to use "wait" as it's not deprecated
    plr.leaderstats.EnemyCount.Value += 1
    SpawnNpc(plr) -->This run the function again
end

SpawnNpc(game.Players:GetPlayers()[1])
    
--Use this:
while true do
    local Npc = game.ReplicatedStorage.NPC:Clone()
    Npc.Parent = workspace
    task.wait(1) --> correct way to use "wait" as it's not deprecated
    plr.leaderstats.EnemyCount.Value += 1
end

2. Set a Loop Limit

When writing loops in Roblox Luau, it's important to ensure that they have defined boundaries. This means specifying the exact number of iterations or providing a clear condition for when the loop should stop. Loops with fixed bounds are easier to debug, and optimize compared to loops with non-limited runs.

You can achieve this by using built-in for and repeat until loops, which have defined limits by default. Occasionally, you can also use a while loop combined with a break statement to stop the loop when a certain condition is met. This approach ensures that your code remains predictable and manageable.

--> This will run 10 times.
for i = 1, 10 do
    print(i)
end

--> This will run continuously for 2 seconds.
local CurrentTime = os.clock()
local cooldown = 2

repeat 
    print(CurrentTime)
    task.wait() 
until os.clock() - CurrentTime >= cooldown

--> This will run continuously until the random number is 1.
local 
while true do
    local randomNumber = math.random(1,10)
    if randomNumber == 1 then
        break
    end
    task.wait()
end

In some cases, an endless loop may be necessary for certain logic, such as an NPC checking for players, as shown below. Notice that there is still a break statement within an if condition, meaning the loop can end. However, this will only happen when the NPC's health reaches zero. Without the break, the loop could lead to some issues, causing errors and affecting optimization.

Since the code is placed under the NPC model, it will reset and function correctly when the NPC respawns. With that in mind, you can also place the script in ServerScriptService and use functions that respond to NPC death events to manage the workflow more effectively. This approach allows for a more structured and efficient handling of NPC behavior without relying solely on endless loops.

while true do
        --> Check if the NPC's health is greater than 0
        if humanoid.Health <= 0 then
            print("NPC is dead. Stopping movement.")
            break -- Exit the loop if the NPC's health is 0
        end

        
        if targetPlayer and targetPlayer.Character then
            local targetPosition = targetPlayer.Character.HumanoidRootPart.Position
            
            --> Create a path to the target position
            local path = PathfindingService:CreatePath({
                AgentRadius = 2,
                AgentHeight = 5,
                AgentCanJump = true,
                AgentJumpHeight = 10,
                AgentMaxSlope = 45,
            })

            path:ComputeAsync(NPC.Position, targetPosition)
            path:MoveTo(NPC)

            --> Wait for the path to finish moving
            path:MoveToFinished:Wait()
        else
            print("Target player not found.")
        end
        task.wait() 
    end
end

3. Avoid Creating New Memory During the Run

Instead of dynamically creating objects during gameplay, predefine your objects and reuse them. For example, use existing local variables to manage frequently used items, which can improve performance and reduce memory usage.

--> Predefined variables
local _workspace = workspace 
local CoinPos = Vector.new(2,5,2)

--> Code ex
local function SpawnCoin(coin)
    coin.Position = CoinPos --> CoinPos variable is reused
    coin.Parent = _workspace --> _workspace variable is reused
    
SpawnCoin(_workspace.Coin:Clone())

To avoid dynamic memory allocation, create a pool of objects that you can reuse throughout your game. When an object is no longer needed, return it to the pool instead of destroying it. This reduces the overhead of constantly creating and destroying objects.

local objectPool = {} -- Initializes an empty table to act as the object pool

-- Function to get an object from the pool or create a new one if the pool is empty
function getObject()
    if #objectPool > 0 then
        return table.remove(objectPool) -- Returns an object from the pool if available
    else
        return game.ReplicatedStorage.ObjectTemplate:Clone() -- Creates a new object if the pool is empty
    end
end

-- Function to release an object back into the pool
function releaseObject(obj)
    obj:SetPrimaryPartCFrame(CFrame.new()) -- Resets the object's position
    table.insert(objectPool, obj) -- Inserts the object back into the pool
end

4. Shorten Functions

It is advisable to keep your functions concise. As a general rule, functions should be limited to a few lines of code. If a function is getting too long, consider breaking it into smaller helper functions. This improves readability and makes debugging way easier as you can identify where the issue located.

local function createLeaderstats(player)
    local leaderstats = Instance.new("Folder")
    leaderstats.Name = "leaderstats"
    leaderstats.Parent = player
    return leaderstats
end

local function createScore(leaderstats)
    local score = Instance.new("IntValue")
    score.Name = "Score"
    score.Value = 0
    score.Parent = leaderstats
end

local function createHealth(leaderstats)
    local health = Instance.new("IntValue")
    health.Name = "Health"
    health.Value = 100
    health.Parent = leaderstats
end

local function handlePlayerJoin(player)
    local leaderstats = createLeaderstats(player)
    createScore(leaderstats)
    createHealth(leaderstats)
    print(player.Name .. " has joined the game with 100 health.")
end

5. Use Assertions for Each Function

Each function should have at least two assertions to ensure it is working correctly. Assertions are a powerful tool for ensuring the correctness and stability of your code. Assertions allow you to specify conditions that must be true at certain points in your code. If an assertion fails, it errors and you can identify and fix issues more quickly.

local function calculateRectangleArea(width, height)
    assert(type(width) == "number", "Width must be a number")
    assert(type(height) == "number", "Height must be a number")
    assert(width > 0, "Width must be greater than 0")
    assert(height > 0, "Height must be greater than 0")
    
    return width * height
end
calculateRectangleArea(10, 20)

You can achieve the same using error() whenever a condition is met:

local function calculateRectangleArea(width, height)
    if type(width) ~= "number" then
        error("Width must be a number")
    
    elseif type(height) ~= "number" then
        error("Height must be a number")
    
    elseif width <= 0 then
        error("Width must be greater than 0")
    
    elseif height <= 0 then
        error("Height must be greater than 0")
    end
    
    return width * height
end

calculateRectangleArea(10, 20)

6. Limit the scope of your variables

Managing the scope of your variables is crucial for maintaining clean, efficient, and secure code. Use local variables within functions whenever possible to avoid unintended side effects. Limit the usage of global variables and functions to reduce naming conflicts, especially on clients as it may be also exploitable. You can replace global variables and functions with modules allowing you limit access to some scripts.

--// Module Script \\--
local Module = {}

Module.Number = 4

function Module.AddNumber()
    Module.Number = Module.Number + 1
end

return Module
-- // Main Server/Local Script \\--

local Module = require(path_to_module) --> access the module

Module.AddNumber() --> run the function from the module

-- use Module.Number to access the Number value from the module
print(Module.Number) --> Print the Number (5)

Keep your sensitive data and API keys server-sided to prevent leaks. If you want to retrieve information from HTTP requests, you can use Remote Functions on the server side to return it back to the client.

--// Server Sided Script \\--

local API_Key = "B34vaSW2QFZasGFf127"
local Url = "https://marketplace.roblox.com/api/v1/" .. API_Key .. "/" -- Use .. for string concatenation
local HttpService = game:GetService("HttpService")
local remoteFunction = game.ReplicatedStorage:WaitForChild("RemoteFunction")

local function GetData(plr, item)
    local response = HttpService:PostAsync(Url .. item) -- Use PostAsync to send a POST request
    return response -- Return the response to the client
end

remoteFunction.OnServerInvoke = GetData
--// Client Sided Script \\--

local remoteFunction = game.ReplicatedStorage:WaitForChild("RemoteFunction")

local function RequestData(item)
    local response = remoteFunction:InvokeServer(item) -- Invoke the server function and get the response
    print("Response from server: " .. response) -- Handle the response
end

RequestData("hat")

7. Check Returned Values

Always check the return values of functions and methods to ensure that your code is handling errors and unexpected situations correctly. Ignoring return values can lead to bugs and issues in your game.

local function GetCharacter(plr : Player)
    if plr.Character then
        return plr.Character
    end
    return plr.Character or plr.CharacterAdded:Wait()
end

local Character = GetCharacter(plr) --> Get the returned value

if Character then --> Check if the return value isn't nil
    print(Character) 
else
    warn("Player's character didn't load")
end

You can also use pcall functions as catch any error that may occur, returning the success status and the result of the function.

local success, response = pcall(function()
    return HttpService:GetAsync("https://api.example.com/data")
end)
if success then
    -- Process the response
    local data = HttpService:JSONDecode(response)
else
    -- Handle the error
    warn("Error fetching data:", response)
end

8. Limit Data Usage

Consider using tables to drive your program's behavior. By storing data in tables and using functions to process that data, you can write more efficient, readable, and maintainable code.

-- Instead of...
local function generateFunctions()
    local functions = {}
    for i = 1, 5 do
        local function func()
            return i * 2
        end
        table.insert(functions, func)
    end
    return functions
end

-- ...consider...
local data = {
    { x = 1 },
    { x = 2 },
    { x = 3 },
    { x = 4 },
    { x = 5 }
}

local function process(data)
    return data.x * 2
end

9. Limit the Use of Non-Function Specified Variables

Limit the use of variables that are not encapsulated within functions. This helps improve code organization, reduces the risk of naming conflicts, and enhances maintainability. By keeping variables local to functions whenever possible, you ensure that their scope is limited. This way, they do not unintentionally affect other parts of your code.

-- Instead of...
food = "chocolate"  -- Global variable

local function eat()
    print("Player ate " .. food)  -- Accessing global variable
end

eat()


-- ...consider...
local function eat(food)
    print("Player ate " .. food)
end

eat("chocolate") --> function variable (doesn't affect the rest of the code)

10. Compile with Warnings

Always compile the code with all warning messages turned on. Test your scripts regularly and pay attention to any warnings or errors. Use the output to identify issues and address them promptly. This practice helps maintain code quality and reduces bug risks.


Conclusion

Even though some of these rules may not seem directly relevant to NASA's, applying them to your Roblox development can significantly enhance your coding practices. By applying to these principles, you can create cleaner, more maintainable, and efficient code. These practices foster a better workflow, making it easier for you and your team to collaborate and improve your games.