Is there a convenient way to prevent individual lines of code breaking a game without a silly amount of sanity checks

Hi,

I have a game that works on a round based script. When a player left at a specific time, I got an error along the lines of:

Attempted to index nil with 'HumanoidRootPart'

Referring to the following line of code:

v.Character.HumanoidRootPart.CFrame = mapSpawns[rand:NextInteger(1,#mapSpawns)].CFrame + Vector3.new(0,5,0)

In which the error caused the Game Logic script to break (Requiring me to restart the relevant server). Ive had multiple similar issues in the past and have just normally done a quick fix of a sanity check.

My confusion is however, surely there is another option than putting a sanity check (e.g if v then) after every single line that has the risk of blowing up the entire game code? Is there something that can be implemented that completely resets the game code if an uncommon glitch like this occurs (E.g. The code detects it has not progressed in a while implying it has errored, and to then reset the script fully as a result?) As the alternative is adding ‘if v then’ for example to every other line to give the code no ability to throw back an error. How do other games that run on a similar game-logic round based system deal with an issue like this?

Hope this makes sense
Thanks

Personally, I use Typescript to code my games which includes a lot of Dependencies to avoid that kind of stuff. Although, how the code is compiled and translated into lua is very long because it’s not written by a human, nevertheless it still doesn’t affect the game at all. So all I could tell you is that sanity checks, whether you have a few or a huge amount won’t affect the performance of the game.

2 Likes

The secret for me is a silly number of checks.

Actually, the issue in 90% of my cases is death, so I check for death with one function, over and over. That check becomes my debounce.

1 Like

Several options:

  • bite the bullet and just do the checks
  • pull the annoying checks into a function (but then, you’ll have to check that function…)
  • embrace the errors, ignore checks, and use pcall at whatever level you want to see if the whole chain of calls succeeded without explicit checks at every level.

The third one is similar to exception handling in java or C++.

The upside is that if you’re conservative about how often you call pcall you can write code with a lot less error handling.

The downside is that it doesn’t seem to be a very common way people write code in lua (although it is in other languages) and it may be slower than checks.

Basically, when you’re in a function and calling another function that might fail: if you are able to handle the error right there, use pcall and do a check. If you are not able to handle the error, just let it “bubble up” to whoever is calling you.

1 Like

I think it’s because Luau is sandboxed and doesn’t crash the entire application when it errors, unlike normal C++, Java or any programming language that’s not meant specifically for games. Also, just putting pcalls everywhere can be several times worse than just, fixing the error? I don’t think it’s a good idea to encourage hiding problems in your games behind a pcall-wall until the bugs pop up, leading to painful debugging because important errors just aren’t there.

1 Like

This seems like you can make Character a variable and do a guard statement instead.

for _, v in pairs(Players) do
    local Character = v.Character
    if not Character then
        continue
    end
    ...
end

You could also pack together statements with and or or so there’s only really one if, and/or use multi-line if-statements for your code to look cleaner

if  not x or --Checks and actual code are cleanly separated by the 'then' statement
    not y or
    not z or
    not w
then
    TacticalNuke()
end
1 Like

C++ and Java don’t crash the program when they throw, they go up the stack until they hit a try/catch block (their pcall equivallent).

Agreed! I don’t suggest putting pcalls everywhere. Only pcall if you can actually do something about the error right there. If you can’t, let it bubble up to a higher call level:

local function SomethingThatCanError(playerName)
  local player = game.Players[playerName] -- can be nil, ignoring that
  player.Character.Head.BrickColor = BrickColor.Random() -- can error, ignoring
end

local function HigherLevelThatCanError()
  SomethingThatCanError("Player1") -- this call can error but we would just quit the function if it does
  SomethingThatCanError("Player2") -- same. This won't run if the above errored, just like if we had used a guard statement
end

-- high level code catches low level errors
while task.wait(1) do
  local success, err = pcall(HigherLevelThatCanError)
  if not success then
    print("Error we caught:", err)
  end
end

This is a good point, and the lack of support from the debugger for this sort of thing is part of the reason it’s not commonly used. I agree with you that you could be hiding bugs this way, but I also think you can hide bugs with guard statements too:

local character = player.Chracter
if not character then return end

Because you mispelled Character, your function will always silently fail. At least with pcall you’ll be able to catch the exact error at some level, and it will give you more info than just “pass/fail”.

2 Likes

Solve the root (pun not intended) of the problem, rather than trying to fix the symptom.
Players in Roblox have an ability to reset their character at any time, they may die or simply have not loaded their character yet.
There is no way for the engine to guess what do you want to do in each of those cases. Should you ignore players without characters or wait for them to load? Should you punish players for resetting or not? Et cetera…
What I do is, I create a separate module that solely deals with keeping track of players HumanoidRootPart's

The module:

Roots = {}
game.Players.PlayerAdded:Connect(function(plr)
   plr.CharacterAdded:Connect(function(char)
      Roots[plr] = char.HumanoidRootPart
   end)
   --optional
   plr.CharacterRemoving:Connect(function(plr)
      Roots[plr] = nil  --putting nil removes an entry altogheter - this solution ignores players without a character
   end)
end)

return Roots

Main script:

local Roots = require(game.ServerScriptService.Modules.Roots) --example path to module
for _, root in pairs(Roots) do
   root.CFrame = mapSpawns[rand:NextInteger(1,#mapSpawns)].CFrame + Vector3.new(0,5,0)
end
1 Like

Thanks for the replies everyone, they have been helpful in giving me some insight to the issue. Considering the way the rest of my game is set up, it really seems that the best way is to do the pedantic sanity checks for all relevant lines, however grouping it all into a function to reduce the amount that I have to do this. Thanks!

1 Like