(Network) Synchronized Scenes are a concept in GTA 5 scripting that facilitates playing multiple animations on ped, objects, or other entities together and offset around one origin.
Important warning
Network synchronized scenes do not work reliably in FiveM servers. With 2 or more clients in an area the scenes are prone to breaking for viewers. Local synchronized scenes work fine.
For more information see the GitHub issue on the subject: Network synchronized scenes will fail for remote clients with >2 clients present. · Issue #2910 · citizenfx/fivem · GitHub
Synchronized scenes exist in both online and offline capacities, with the online (“Networked”) counterpart having a local, offline counterpart on clients.
In order to make proper use of synchronized scenes the animations MUST be setup for this. “Being setup for this” refers to that they have properly set Initial Anim Offset Position/Rotations (Which can be obtained in scripts with GetAnimInitialOffsetPosition & GetAnimInitialOffsetRotation)
If your (or, realistically rockstars) animations are NOT setup for this, you will most likely have all objects piled up on each other and not behaving appropriately.
While “scene” might imply more than just one thing, there is no reason that a synchronized scene could not have just 1 animation, and use it to offset it appropriately.
An Example of a Synchronized Scene
Lets explain with an example in FiveM, this will be using the network equivalant of synchronized scenes to demonstrate how the local kinds work, but creating local synchronized scenes is largely identical (minus for being able to add camera animations to scenes with one native instead of needing to make a camera)
The animation dictionary that we will be using is anim_heist@hs3f@ig1_hack_keypad@male@
Creating the scene
First we need to define the scene itself, this is a bit of a loaded native with a lot of parameters.
local retval --[[ integer ]] =
NetworkCreateSynchronisedScene(
x --[[ number ]],
y --[[ number ]],
z --[[ number ]],
xRot --[[ number ]],
yRot --[[ number ]],
zRot --[[ number ]],
rotationOrder --[[ integer ]],
holdLastFrame --[[ boolean ]],
looped --[[ boolean ]],
p9 --[[ number ]],
animTime --[[ number ]],
animSpeed --[[ number ]]
)
the XYZ, and XYZ rotations are that, they define the origin of the scene which is what all of the animations will be offset from. There is no hard set standard for what an origin may be in order to line things up correctly, typically though its origin will be the position of the “main” object (i.e, an interactable object, or a vehicle)
Rotation order is rotation order, 2 is typically what you want
holdLastFrame - This may sound odd, but its extremely helpful in maintaining fluidity when using multiple synchronized scenes in a row as it will hold the last frame while you trigger the next (which an example of will be shown)
hooped - Looped, does what you think
p9 is typically 1.0,
animTime is typically 0.0
animSpeed is typically 1.0
Actually doing stuff
Now lets actually start making our scene. This animation dictionary has a lot of variations, but we’re just going to focus on three animation sets:
action_var_01
- The entry animation
hack_loop_var_01
- The looping animation that plays when you are hacking
success_react_exit_var_01
- Positive reaction and removal of usb stick, exiting the scenario.
First we need our primary object, which is also what the scene is anchored on which in this case is the fingerprint scanner. So lets create it real quick (and request our animation dictionary while we’re at it)
local scannerModel = GetHashKey("ch_prop_fingerprint_scanner_01d")
RequestModel(scannerModel)
RequestAnimDict("anim_heist@hs3f@ig1_hack_keypad@male@")
repeat Wait(0) until HasModelLoaded(scannerModel) and HasAnimDictLoaded("anim_heist@hs3f@ig1_hack_keypad@male@")
-- Spawn it a bit in the air just to avoid clipping with anything
local spawnPos = GetOffsetFromEntityInWorldCoords(PlayerPedId(), 0.0, 1.0, 1.0)
local scanner = CreateObject(scannerModel, spawnPos, true, false, false)
SetEntityHeading(scanner, GetEntityHeading(PlayerPedId()))
Now we have our all important origin prop, this doesn’t actually get animated but its still used as the origin because all the animated things need to align with it.
So lets create our scene
local scene = NetworkCreateSynchronisedScene(
GetEntityCoords(scanner), -- FiveM unwraps this to X, Y, Z
GetEntityRotation(scanner), -- ^
2, -- Rotation Order
true, -- Hold last frame
false, -- Do not loop
1.0, -- p9
0.0, -- Starting phase (0.0 meaning we play it from the beginning)
1.0 -- animSpeed
)
We grab the final position and rotation of the scanner and set our scenes origin as that, we also set the hold frame because we want to transition from action_var_01 to hack_loop smoothly
Creating our animated props
We need our usb stick and phone! Lets create those real quick (The final code will have this code above the scene just for clarity)
local phoneModel = GetHashKey("ch_prop_ch_phone_ing_01a")
local usbModel = GetHashKey("ch_prop_ch_usb_drive01x")
RequestModel(phoneModel) RequestModel(usbModel)
repeat Wait(0) until HasModelLoaded(phoneModel) and HasModelLoaded(usbModel)
-- Just create these inside the player because they will be immediately attached to the scene.
local phone = CreateObject(phoneModel, GetEntityCoords(PlayerPedId()), true, false, false)
local usb = CreateObject(usbModel, GetEntityCoords(PlayerPedId()), true, false, false)
Actually animating things
Now lets start adding things to our scene, the relevant natives for this are:
NetworkAddPedToSynchronisedScene
NetworkAddEntityToSynchronisedScene
NetworkAddSynchronisedSceneCamera
We don’t have any camera animations so we’ll ignore that for now
-- NETWORK_ADD_PED_TO_SYNCHRONISED_SCENE
NetworkAddPedToSynchronisedScene(
ped --[[ Ped ]],
netScene --[[ integer ]],
animDict --[[ string ]],
animnName --[[ string ]],
blendInSpeed --[[ number ]],
blendOutSpeed --[[ number ]],
duration --[[ integer ]],
flag --[[ integer ]],
playbackRate --[[ number ]],
p9 --[[ Any ]]
)
Lets add our player and our props.
local dict = "anim_heist@hs3f@ig1_hack_keypad@male@" -- For readability
NetworkAddPedToSynchronisedScene(PlayerPedId(), scene, dict, "action_var_01", 8.0, 8.0, 0, 0, 1000.0, 0)
NetworkAddEntityToSynchronisedScene(usb, scene, dict, "action_var_01_ch_prop_ch_usb_drive01x", 8.0, 8.0, 0)
NetworkAddEntityToSynchronisedScene(phone, scene, dict, "action_var_01_prop_phone_ing", 8.0, 8.0, 0)
8.0 and 8.0 are the blend in/out values, the higher they are the quicker the player will interpolate into/out of the animation
Flags are flags, but i won’t go into details on those for various spoiled teapot reasons.
and start it with
NetworkStartSynchronisedScene(scene)
And tada, we have an animation playing!
Now this is just one part of the animation puzzle, we got the entry animation but now we want it to loop… So how do we do that?
Part 2
This is where some offline/local synced scene natives come in handy, namely GetSynchronizedScenePhase
This is a native that is made for the singleplayer, local only animations but theres no equivalant for getting the phase from a network scene so we must use the local scene.
So we need to get the local handle equivalant for the scene, this can be done with NetworkGetLocalSceneFromNetworkId
A annoying quirk of this native though is that this native will return -1 (i.e, not found) if the scene was started in the same tick, so you need to wait atleast one tick.
Wait(0)
local localScene = NetworkGetLocalSceneFromNetworkId(scene)
(You can alternatively loop on this native every frame until it becomes non -1, which would likely make the most sense when doing this on another players client)
Now that we have the local id, lets keep an eye on our scene until its just almost done
repeat Wait(0) until GetSynchronizedScenePhase(localScene) > 0.99
The “phase” is the progress, from 0.0 to 1.0, since we have holdLastFrame enabled it will hold at the end (1.0) so this function is safe to run without worrying about it never being >0.99 because it exited (although if we didn’t have hold last frame, we may have trouble)
So now we’ve waited until the scene is basically done… What now?
Well we basically just repeat what we did before but with the loop animations!
Creating a new scene and starting it (albeit this time with the loop bool set to true)
scene = NetworkCreateSynchronisedScene( -- Repeated code...
GetEntityCoords(scanner), -- FiveM unwraps this to X, Y, Z
GetEntityRotation(scanner), -- ^
2, -- Rotation Order
true, -- Hold last frame
false, -- Do not loop
1.0, -- p9
0.0, -- Starting phase (0.0 meaning we play it from the beginning)
1.0 -- animSpeed
)
NetworkAddPedToSynchronisedScene(PlayerPedId(), scene, dict, "hack_loop_var_01", 8.0, 8.0, 0, 0, 1000.0, 0)
NetworkAddEntityToSynchronisedScene(usb, scene, dict, "hack_loop_var_01_ch_prop_ch_usb_drive01x", 8.0, 8.0, 0)
NetworkAddEntityToSynchronisedScene(phone, scene, dict, "hack_loop_var_01_prop_phone_ing", 8.0, 8.0, 0)
NetworkStartSynchronisedScene(scene)
Wait(5000) -- Simulated "hacking" part
Now we need to exit after our very accurate hard-coded-5000ms-hacking-part
This is, once again, quite easy. Only difference this time is we are not holding the last frame or looping and will wait until the scene is dune using the local synchronized scene native of IsSynchronizedSceneRunning
scene = NetworkCreateSynchronisedScene(
GetEntityCoords(scanner), GetEntityRotation(scanner),
2, false, false, 1.0, 0.0, 1.0
)
--success_react_exit_var_01
NetworkAddPedToSynchronisedScene(PlayerPedId(), scene, dict, "success_react_exit_var_01", 8.0, 8.0, 0, 0, 1000.0, 0)
NetworkAddEntityToSynchronisedScene(usb, scene, dict, "success_react_exit_var_01_ch_prop_ch_usb_drive01x", 8.0, 8.0, 0)
NetworkAddEntityToSynchronisedScene(phone, scene, dict, "success_react_exit_var_01_prop_phone_ing", 8.0, 8.0, 0)
NetworkStartSynchronisedScene(scene)
Wait(0)
localScene = NetworkGetLocalSceneFromNetworkId(scene)
repeat Wait(0) until not IsSynchronizedSceneRunning(localScene)
-- Clean up
DeleteObject(phone)
DeleteObject(usb)
The final result:
The complete code for this is available: syncedscenetutpart1.lua · GitHub
There is more that can be done, which ill explain in the future in the comments of this post.
You can of course wrap this kind of code in logic to put say, an actual hacking minigame in.