Local Contexts
Overview
In this tutorial, you will be learning the benefits of using Local Context for your game to help you improve networking performance. You will be learning how to take an object that is usually networked and optimize it by handling the networking side of it yourself.
- Completion Time: ~30 minutes
- Knowledge Level: Good knowledge of Lua scripting, and Networking in Core.
- Skills you will learn:
- Local Context vs Static Context
- Custom networking with Damageable Objects
- Shared pickups with 0 networking
- Cross context calling
- Testing Local Context
Local Context vs Static Context
A Local Context is very similar to a Static Context. The main difference between them is that the Local Context allows for the objects inside that context to be modified, whereas a Static Context will not. So this means that the objects which are inside the Local Context are spawned on the server and the client, including Lua scripts, which is the same behavior as a static context.
When testing your game, it is recommended that all testing is done in Multiplayer Preview Mode because of the way local contexts work, and this is closest to a published game running on a live server.
Any networked object that is added to a local context will prompt you to disable networking. Local contexts do not allow networked children.
Networked Objects
For the tutorial, we will be focusing on Damageable Objects and Shared Pickups to show how you can do your networking to optimize networking performance and reduce networked object count which will lead to an improved player experience.
For another alternative to networking performance, consider taking a look at network dormancy. This method will give you control over when a networked object should be updated, which will make games more performant and utilize more networked behavior.
Shared Pickups
Games that have pickups usually are a first come first serve, meaning whoever gets to the pickup first is the only one that gets it because it is then destroyed, and other players can not pick it up.
Using Local Context, we can provide an optimized solution that uses 0 networked objects, and also create the pickups in a way that it is a shared item so that other players can also pick it up.
This solution requires a bit of thinking because a local context script will need to do cross-context calling to perform tasks such as spawning effects and giving the player a resource.
Create Folder Structure
- Create a group in the Hierarchy called
Gem Pickups
. - Inside the Gem Pickups group, create a Local Context called
Local Gems
. - Inside the Gem Pickups group, create a Client Context called
Client
.
Create Gem Pickup
Create a pickup that will be added to the Local Gems folder. Add as many as you want, and spread them out.
- Set the pickup Collision and Camera Collision to Force Off.
- Add a Trigger as a child and set Collision to Force On.
Create GemPickups Script
Create a script called GemPickups
and place it into the Local Gems folder. This script is responsible for making sure the player can't pick up a pickup multiple times. It will also handle cross-calling for the server to update the player's resource, and cross-calling to the client to handle turning off the visibility and playing the effects.
The reason for cross-context calling is that even with doing environment checks, it will still throw an error about accessing some API calls from the given context. So doing a cross-context call will help around this issue.
Add Variables
Add the following variable so you have a list of all the triggers. The players
table will hold the players that have accessed this pickup. This prevents them from entering the trigger and getting another gem resource.
local triggers = script.parent:FindDescendantsByType("Trigger")
local players = {}
Create OverlappedTrigger Function
Create a function called OverlappedTrigger
. This function will check if the player has already overlapped this trigger to prevent multiple pickups. The 2 Broadcast
calls are cross-context calls to hide the gem on the client, and add one resource to the player on the server. Notice that the addgem
broadcast is not inside an environment check. This is because this broadcast will be sent cross-context to both the client and the server.
local function OverlappedTrigger(trigger, obj)
if obj:IsA("Player") and (not players[obj] or not players[obj][trigger]) then
if(not players[obj]) then
players[obj] = {}
end
players[obj][trigger] = true
if Environment.IsClient() then
Events.Broadcast("hidegem", obj, trigger.parent)
end
Events.Broadcast("addgem", obj, trigger:GetWorldPosition())
end
end
Create OnPlayerLeft Function
Create a function called OnPlayerLeft
that will be called when the player leaves the game so the players
table can be cleaned up.
local function OnPlayerLeft(player)
if players[player] ~= nil then
players[player] = nil
end
end
Create Trigger Loop
Create a loop that will loop over all the triggers to connect the beginOverlapEvent
.
for index, trigger in ipairs(triggers) do
trigger.parent:RotateContinuous(Rotation.New(0, 0, 30 + math.random(1, 60)))
trigger.beginOverlapEvent:Connect(OverlappedTrigger)
end
Connect Player Left Event
Connect up the playerLeftEvent
that is fired when the player leaves the game.
Game.playerLeftEvent:Connect(OnPlayerLeft)
The GemsPickup Script
GemsPickup
local triggers = script.parent:FindDescendantsByType("Trigger")
local players = {}
local function OverlappedTrigger(trigger, obj)
if obj:IsA("Player") and (not players[obj] or not players[obj][trigger]) then
if(not players[obj]) then
players[obj] = {}
end
players[obj][trigger] = true
if Environment.IsClient() then
Events.Broadcast("hidegem", obj, trigger.parent)
end
Events.Broadcast("addgem", obj, trigger:GetWorldPosition())
end
end
local function OnPlayerLeft(player)
if players[player] ~= nil then
players[player] = nil
end
end
for index, trigger in ipairs(triggers) do
trigger.parent:RotateContinuous(Rotation.New(0, 0, 30 + math.random(1, 60)))
trigger.beginOverlapEvent:Connect(OverlappedTrigger)
end
Game.playerLeftEvent:Connect(OnPlayerLeft)
Create GemServer Script
Create a script called GemServer
. Place the script inside the Gems Pickup group in the Default Context. This script will listen for a specific broadcast event so it can update the player's gems resource amount. This is a cross-context script that will be called from the GemPickups script.
Connect up the broadcast event for addgem
.
Events.Connect("addgem", function(player)
player:AddResource("Gems", 1)
end)
Create GemClient Script
Create a script called GemClient
and place it into the Client folder. This script will be responsible for spawning the effect and audio when the player picks up the gem. It will also hide the gem from that local player. This is a cross-context script that is called from the GemPickup script.
Create a template for the effect that you want, and add it as a custom property called GemPickupEffects
.
local GEM_PICKUP_EFFECTS = script:GetCustomProperty("GemPickupEffects")
local localPlayer = Game.GetLocalPlayer()
Events.Connect("addgem", function(player, pos)
if player == localPlayer then
World.SpawnAsset(GEM_PICKUP_EFFECTS, { position = pos })
end
end)
Events.Connect("hidegem", function(player, gem)
if player == localPlayer then
gem.visibility = Visibility.FORCE_OFF
end
end)
Test the Game
Test the game in Multiplayer Preview Mode and make sure that the gem can be picked up. Make sure to test with multiple players.
Damageable Object
By default, Damageable Objects are networked when added to the Hierarchy. This is because the changes of the damageable object at runtime need to be replicated to the players so they know about the state of the object. When a player shoots a damageable object and kills it, this information needs to be received by the players. The more players in your game, the more networked objects are used.
If we take a look at a general setup of a damageable object, you will see that just for one of these objects, it costs 9 networked objects per player, and this is a basic dummy damageable object. 9 might not sound a lot, but when you try to support more players in your game, this number will grow very quickly.
To get the total amount of networked objects in your game, you can take the networked object count of an object, and times it by the total number of players supported for your game. So for example, in the image below the cost is 9 networked objects for one player. If you wanted to support 12 players, then 9 x 12
= 108. This is just for that single object.
Custom Networking
In this section, we will go over how you could optimize the damageable object so that each damageable object will have a 0 network count. This means that you will need to create your own networking solution to reduce the networking count. This section attempts to cover most edge cases, but for further improvement, this would be down to you and how your game objects should behave.
Create Local context
The Local Context will contain all the objects that would normally be networked. These objects (including scripts), will be spawned on the server and the client. The local context you will create will need to have networking enabled. The reason for this is that the custom networking method you will be using in this case is Dynamic Properties**. All health data for the damageable objects will be sent to the clients this way.
- Create a Local Context in the Hierarchy.
- Right-click on the Local Context and select Enable Networking.
- Add a string custom property called
HealthData
. - Enable Dynamic Property for the HealthData property.
Create Group Container
To keep things organized and easier to loop over, create a Group called Local Damageable Objects
. This group is where you will place all the damageable objects that you want to have 0 networking.
Create Damageable Objects
From Core Content, search for damageable crate
to find the Damageable Crate object and add it to the Local Damageable Objects group.
When you drop a networked object into a Local Context, it will not go directly into that folder. You will need to then drag the object into the Local Context, which will then prompt you to disable networking and distance the template if it applies.
Add Weapon
To do damage to the damageable crate, you will need a weapon.
From Core Content, search for Shotgun
to find the Advanced Shotgun and add it to the Hierarchy.
Test the Game
Local Preview Mode
Test the game in Local Preview Mode first to see if everything works as expected. You should see that the crate is destroyed and the effect is played.
Multiplayer Preview Mode
Test the game in Multiplayer Preview Mode. You should see that the crate doesn't appear to get destroyed. What is happening here, is that the crate on the server is destroyed, but the client has no way of knowing that. Remember that the Local Context spawns on the server and the client. There is no networking happening.
Monitor Health
In this section, you will be creating a script that will be monitoring the health of all the damageable crates and send this data to each client. Doing this will allow you to handle the client-side destroying of the crate and also play the effect.
The method you will be using to send the data is the dynamic property HealthData
that you created earlier. This property will contain a string made up of the health of each damageable crate. Because the local context children are in the same order for both the server and the client, we can use this as an advantage for minimizing the amount of data that is sent by the dynamic property.
Create MonitorHealth Script
Create a script called MonitorHealth
, and place it inside the Local Context which is networked. You will also need to add a reference to the Local Damageable Objects group so that the script can loop through the children.
Add Variables
Add the following variables so that you have a reference to the property and all the children that are a type of DamageableObject.
local LOCAL_DAMAGEABLE_OBJECTS = script:GetCustomProperty("LocalDamageableObjects"):WaitForObject()
local children = LOCAL_DAMAGEABLE_OBJECTS:FindDescendantsByType("DamageableObject")
Create UpdateHealthData Function
Create a function called UpdateHealthData
. This function is responsible for getting the hitPoints
for each damageable object and adding it as a string to the HealthData dynamic property.
local function UpdateHealthData(obj)
local data = {}
for i, c in ipairs(children) do
if Object.IsValid(c) then
table.insert(data, c.hitPoints)
end
end
script.parent:SetCustomProperty("HealthData", table.concat(data, "|"))
end
Create OnChildAdded Function
Create a function called OnChildAdded
. This function will update the children
table so that it contains a list of all the damageable objects. Because some damageable objects can spawn a networked template, you need to monitor when this happens so that the damagedEvent
is connected to the new child. Notice that an Environment
check is being done since it needs to be done on the server.
Damageable Objects with Networked Template Property Set
If the damageable object has been set up to spawn a networked object when it is destroyed, then the object that is spawned will not be killable on the client. The template will have to be non-networked for this to work.
local function OnChildAdded(parent, child)
children = LOCAL_DAMAGEABLE_OBJECTS:FindDescendantsByType("DamageableObject")
if(Environment.IsServer()) then
child.damagedEvent:Connect(UpdateHealthData)
end
end
Create OnPropertyChanged Function
Create a function called OnPropertyChanged
that will be called when the dynamic property HealthData changes. This will split the string and loop over the parts (health of the damageable object) and check if the health is 0
. If the health is 0
then we can kill the damageable crate by calling Die
which will also trigger the client template on the damageable crate.
local function OnPropertyChanged(obj, prop, destroy)
if prop == "HealthData" then
local objs = LOCAL_DAMAGEABLE_OBJECTS:FindDescendantsByType("DamageableObject")
local data = { CoreString.Split(script.parent:GetCustomProperty("HealthData"), "|") }
for index, health in ipairs(data) do
if Object.IsValid(objs[index]) and tonumber(health) == 0 then
if destroy then
objs[index]:Destroy()
else
objs[index]:Die()
end
end
end
end
end
Create Children Loop
Loop over all the children on the server and setup the damagedEvent
.
if Environment.IsServer() then
for i, damageable in ipairs(children) do
damageable.damagedEvent:Connect(UpdateHealthData)
end
end
Connect Property Changed Event
On the client connect up the customPropertyChangedEvent
and call OnPropertyChanged
to make sure existing crates are removed.
if Environment.IsClient() then
script.parent.customPropertyChangedEvent:Connect(OnPropertyChanged)
OnPropertyChanged(nil, "HealthData", true)
end
Connect Child Added Event
Connect up the childAddedEvent
that will be called anytime a new object is added to the group.
LOCAL_DAMAGEABLE_OBJECTS.childAddedEvent:Connect(OnChildAdded)
The MonitorHealth Script
MonitorHealth
local LOCAL_DAMAGEABLE_OBJECTS = script:GetCustomProperty("LocalDamageableObjects"):WaitForObject()
local children = LOCAL_DAMAGEABLE_OBJECTS:FindDescendantsByType("DamageableObject")
local function UpdateHealthData(obj)
local data = {}
for i, c in ipairs(children) do
if Object.IsValid(c) then
table.insert(data, c.hitPoints)
end
end
script.parent:SetCustomProperty("HealthData", table.concat(data, "|"))
end
local function OnChildAdded(parent, child)
children = LOCAL_DAMAGEABLE_OBJECTS:FindDescendantsByType("DamageableObject")
if(Environment.IsServer()) then
child.damagedEvent:Connect(UpdateHealthData)
end
end
local function OnPropertyChanged(obj, prop, destroy)
if prop == "HealthData" then
local objs = LOCAL_DAMAGEABLE_OBJECTS:FindDescendantsByType("DamageableObject")
local data = { CoreString.Split(script.parent:GetCustomProperty("HealthData"), "|") }
for index, health in ipairs(data) do
if Object.IsValid(objs[index]) and tonumber(health) == 0 then
if destroy then
objs[index]:Destroy()
else
objs[index]:Die()
end
end
end
end
end
if Environment.IsServer() then
for i, damageable in ipairs(children) do
damageable.damagedEvent:Connect(UpdateHealthData)
end
end
if Environment.IsClient() then
script.parent.customPropertyChangedEvent:Connect(OnPropertyChanged)
OnPropertyChanged(nil, "HealthData", true)
end
LOCAL_DAMAGEABLE_OBJECTS.childAddedEvent:Connect(OnChildAdded)
Test the Game
Test the game in multiplayer preview and check that it works.
Summary
Local Context allows you to improve networking performance for your game, which will improve the player experience. Make sure you test your game in a multiplayer setting when implementing your networking solution.
Learn More
Network Contexts | Networking in Core | Network Relevancy | Damageable Objects