Fighting Lag

This tutorial is a follow-up to the Experimental Mode tutorial. That tutorial explains a certain model of making games called the client-server model, which involves using RemoteFunctions and RemoteEvents to facilitate communication between the server and its clients. In order to have the context necessary to understand this tutorial, it is highly recommended that you read through the last one.

Networked games like your games on Roblox come with the frustrating, unavoidable problem of lag. If your game has Experimental Mode disabled, you will likely use a decent number of RemoteFunctions or RemoteEvents. If you use RemoteFunctions or RemoteEvents, there will be necessarily be lag in your game. Fortunately, there are a couple of strategies you can use to embrace this lag and gracefully integrate it into your game’s functionality.

The Source of Lag[edit]

Before learning some strategies that hide lag, it is important to have an accurate mental model of why lag must exist in games with remote calls.

Let’s say you are one of our users in the United Kingdom and you are playing a non-Experimental Mode Roblox game. In this game, a local script allows you to press the ‘F’ key to fire a cannon. The developer of this game has read the Experimental Mode tutorial and has decided that cannonballs are too important for the client to handle. Consequently, the local script makes a remote call to the server so the server can be the one that fires. When the client detects that the ‘F’ key has been pressed, it contacts the server, the server fires the cannon, and the server lets the client know what happened as a result. However, because your computer is in the United Kingdom and because Roblox servers are in the United States, this requires a signal to be passed across the entire Atlantic Ocean. Twice. This takes time, and this time comes in the form of an annoying lag between pressing the ‘F’ key and actually seeing the cannon fire.

“Laggy Cannons”[edit]

In order to make that last example a little less hypothetical, this tutorial is accompanied by a simple Roblox game called “Laggy Cannons”. You can play it online here, and you can also download the Studio version for yourself if you so choose.

In Laggy Cannons, there are four cannons aimed in the direction of four moving targets. Each cannon-target pair is affected by the same artificially exaggerated lag time, but each cannon-target pair addresses this lag in a slightly different way. Some pairs handle this lag better than others. The purple pair is better than the red, the red pair is better than the orange, and the orange pair is better than the blue.

LaggyCannons Gameplay.png

Before reading the rest of this tutorial, now would be a good time to go play Laggy Cannons and see if you can feel the differences between the four. To play Laggy Cannons, use the four teleporters near the spawn point to teleport to the cannon pads with corresponding colors. When standing on a cannon pad, you can press ‘F’ to fire a cannonball and try and hit that cannon’s moving target. Step onto the colored area of the cannon pad to return to the teleport pads and try another cannon. You can manipulate the number of seconds of artificial lag by clicking the ‘+’ and ‘-‘ buttons on the control pad near the spawn point. The rest of this tutorial goes into depth explaining how each color’s cannon or target behaves and why. We will also generalize the strategies that each pair uses so you can more easily apply them to your games.

The Blue Cannon[edit]

LaggyCannons BlueCannon.PNG

Let’s begin with the cannon that really doesn’t try at all: the blue cannon. If you press the ‘F’ key, you will immediately see and hear the explosion of the cannon firing, but you won’t see the cannonball for a good while. Here is the function that is called in a local script when you fire the blue cannon:

--In a local script
local function badCannonStrategy()
    Workspace.FireCannonEvent:FireServer(activeCannon)
    createExplosion(activeCannon)
end

The first line of this function uses a RemoteEvent called “FireCannonEvent” to ask the server to fire a global variable called “activeCannon”. The activeCannon here is, of course, the blue cannon. FireCannonEvent is a one way request to the server. After sending the server a message asking it to fire the blue cannon, this local script immediately goes on to show an on-screen explosion using the unshown helper function “createExplosion”. There is no effort here to work with the lag that is inherent to the RemoteEvent. The message is sent, the explosion is created, and then we wait for the server to fire the projectile.

Lesson 1: Be Careful with your Code[edit]

The badCannonStrategy() function looks innocent enough. It fires the cannon and creates an explosion, isn’t that what we want? The problem is that it ignores lag entirely, and this is very easy to do. If you want to make excellent games, you must notice that there will be lag here. Read on to learn what you can do about it.

The Orange Cannon[edit]

LaggyCannons OrangeCannon.PNG

The orange cannon slightly modifies the blue cannon’s strategy in order to synchronize the firing of the projectile with the explosion. The function that fires the orange cannon looks like this:

--In a local script
local function okCannonStrategy()
    local waitForResponse = Workspace.FireCannonFunction:InvokeServer(activeCannon)
    createExplosion(activeCannon)
end

You can see that the only change here is the first line of the function. Instead of using a RemoteEvent to fire the cannonball, the orange cannon uses a RemoteFunction called “FireCannonFunction”. You may remember from the tutorial on remote calls that a RemoteFunction differs from a RemoteEvent in that it has a return value. While a RemoteEvent is only a signal from the client to the server, a RemoteFunction is a signal from the client to the server and also a return value from the server to the client. Furthermore, because the client expects a return value, it will actually wait for this return value to come before it moves on in its local script. This is exactly the feature the orange cannon script takes advantage of. This slightly better cannon-firing function waits for the server to say it has fired a cannonball before it moves on to create the accompanying explosion.

Lesson 2: Synchronize the Experience[edit]

The blue cannon doesn’t make sense because you see the cannon explosion long before you see the cannonball. In the real world, explosions and cannonball firings occur simultaneously. In order for a game to be fun and immersive, it shouldn’t be violating the player’s expectations over silly things like this. Follow the example of the orange cannon and use RemoteFunctions to synchronize events across the client and server.

The Red Cannon[edit]

LaggyCannons RedCannon.png

While the orange cannon solves the problem of unsynchronized cannonballs and explosions, it actually exposes another problem with lag. When the player presses the ‘F’ key to fire, there is a significant delay before the game responds at all. This is called input lag, and it is one of the things that players hate the most. There is a simple trick you can use to remove input lag. You can’t speed up how long it takes for the cannon to fire. No matter what, that is going to take the round-trip time it takes for a message to go from client to server to client. You can, however, give the player some other immediate feedback that will make them feel that the game is responding to their commands. The immediate feedback the red cannon chooses to give is a sound. When the player presses ‘F’, the red cannon immediately begins to play the sound of a cannon fuse burning. This sound will play for as long as it takes for the server to respond to the RemoteFunction. Once the server responds, the red cannon turns off the burning fuse sound and creates an explosion that accompanies the projectile the server just fired.

--In a local script
local function bestCannonStrategy()
    Workspace.Sounds.BurningFuse:Play() 
    local waitForResponse = Workspace.FireCannonFunction:InvokeServer(activeCannon)
    Workspace.Sounds.BurningFuse:Stop()
    createExplosion(activeCannon)
end

If you go play with the red cannon, it should feel totally natural. There is still a delay between pressing the ‘F’ key and seeing the cannonball fire, but it is unnoticeable because the cannon incorporates this lag into the way that it works.

Lesson 3: No Input Lag![edit]

Players hate input lag. It is frustrating to feel a disconnect between yourself and the game. Though it may be necessary to have a laggy response to player input, you can always give them something small to indicate that their request has been heard. This can be a sound, a changing brick color, or really anything at all. Let the players know that they are being listened to and you will have happy players.

The Purple Target[edit]

LaggyCannons PurpleTarget.png

The purple cannon works in exactly the same way that the red cannon works. The difference between the purple platform and all of the other colors is the functionality of the moving target.

When you hit the blue, orange, or red target, you will notice that there is a delay between seeing the target get hit and the resulting “ding” sound and score update. As an example, the “ding” and score update for the blue target is executed by the following server function:

--Server script
Workspace.Blue.Target.Face.Touched:connect(function(projectile)
    wait(_G.artificialLag)    -- How Laggy Cannons creates artificial lag
 
    --Play target hit sound
    Workspace.Sounds.TargetHit:Play()
 
    --Update blue score
    blueScore = blueScore + 1
    Workspace.Blue.Scoreboard.SurfaceGui.ScoreLabel.Text = blueScore
end)

From the principles learned in the Experimental Mode tutorial, we know that something as important as the player’s score must be handled by the server. Therefore, there must be a delay when updating a target’s score. But what about the sound of hitting the target?

Although the server is ultimately the one that decides whether or not the target was hit, the client is perfectly capable of immediately playing the target hit sound itself. Worst case scenario, if the client and server have a rare disagreement over whether the target was hit, the player will experience a discrepancy between the “ding” sound and the score update. This might happen one in five thousand times, and it is a cost that is well worth giving the player immediate feedback. Therefore, we can actually split the previous server script into a server script and a local script:

--Server script
Workspace.Purple.Target.Face.Touched:connect(function(projectile)
    wait(_G.artificialLag)    -(How Laggy Cannons creates artificial lag)
 
    --Update purple score
    purpleScore = purpleScore + 1
    Workspace.Purple.Scoreboard.SurfaceGui.ScoreLabel.Text = purpleScore
end)
 
--Local script
Workspace.Purple.Target.Face.Touched:connect(function(projectile)
    --Play target hit sound
    Workspace.Sounds.TargetHit:Play()
end)

Lesson 4: Split your Scripts[edit]

Sometimes you have to do half the work on server and half the work on the client. Don’t be afraid to detect the same event twice. The local detection gives the player feedback, and the server detection handles important game logic.

Lag in the Real World[edit]

The default artificial lag for Laggy Cannons is one second. This is layered on top of whatever lag your machine naturally experiences when playing a Roblox game. Across our player base, we see players with lag ranging anywhere from 0.1 to 1.5 seconds. Average lag seems to hover around 0.3 seconds. Luckily, you are now equipped with the tools to handle this lag.

Testing Lag in Studio[edit]

If you want to simulate lag in Roblox Studio, you can go to File -> Settings -> Network -> IncomingReplicationLag and adjust the number of seconds of lag that Studio will simulate when running a test server. To learn how to run a test server, see the previous tutorial.