Network Synchronized Scenes

(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.

6 Likes

With Synchronized Camera

In oneshot scenes, adding a camera is extremely simple.

Just use NetworkAddSynchronisedSceneCamera

Here’s a basic example

    local dict = "anim@scripted@heist@ig25_beach@male@"
    RequestAnimDict(dict)
    repeat Wait(0) until HasAnimDictLoaded(dict)


    local playerPed = PlayerPedId()
    local playerPos = GetEntityCoords(PlayerPedId())
    local playerHead = GetEntityHeading(PlayerPedId())

    local scene = NetworkCreateSynchronisedScene(playerPos.x, playerPos.y, playerPos.z, 0.0, 0.0, playerHead, 2, false, false, 8.0, 1000.0, 1.0)
    NetworkAddPedToSynchronisedScene(PlayerPedId(), scene, dict, "action", 1000.0, 8.0, 0, 0, 1000.0, 8192)
    NetworkAddSynchronisedSceneCamera(scene, dict, "action_camera")
    
    NetworkStartSynchronisedScene(scene)

Doing a graceful exit is a bit more involved:

You must instead of using the synced scene camera native, CreateCam, PlayCamAnim, RenderScriptCams, and then when the scene is done do StopRenderingScriptCamsUsingCatchUp and then finally after that, destroy the camera.

This has the result of the gameplay camera will assume the position of where the animated camera last was, and as the player moves around the camera will gradually return to normal. You can see an example here:

The code for which, using the same scene (not from the video, but the previous example) is:

	local dict = "anim@scripted@heist@ig25_beach@male@"
    RequestAnimDict(dict)
    repeat Wait(0) until HasAnimDictLoaded(dict)


    local playerPed = PlayerPedId()
    local playerPos = GetEntityCoords(PlayerPedId())
    local playerHead = GetEntityHeading(PlayerPedId())

    local scene = NetworkCreateSynchronisedScene(playerPos.x, playerPos.y, playerPos.z, 0.0, 0.0, playerHead, 2, false, false, 8.0, 1000.0, 1.0)
    NetworkAddPedToSynchronisedScene(PlayerPedId(), scene, dict, "action", 1000.0, 8.0, 0, 0, 1000.0, 8192)
    local cam = CreateCam("DEFAULT_ANIMATED_CAMERA", true)
	PlayCamAnim(cam, "action_camera", dict, playerPos.x, playerPos.y, playerPos.z, 0.0, 0.0, playerHead, 0, 2) -- If your scene is looping, change 0 to a 1 in 2nd to last param
	RenderScriptCams(true, false, 0, true, false)
    
    NetworkStartSynchronisedScene(scene)
	
	local localId = NetworkGetLocalSceneFromNetworkId(scene)
	while localId == -1 do
		Wait(0)
		localId = NetworkGetLocalSceneFromNetworkId(scene)
	end
	
	repeat Wait(0) until GetSynchronizedScenePhase(localId) > 0.9
	
    StopRenderingScriptCamsUsingCatchUp(false, 4.0, 3)
    DestroyCam(cam, false)
9 Likes

In the NetworkAddPedToSynchronisedScene native’s above, i used a value of 1000.0. This value is used for the interpolation of the player position into the scene.
With a value of 1000, it instantly snaps to the correct position/rotation, which in conjunction with lower 8.0 interpolation values (like 2.0) can be odd as the player animation itself is gradual but the location is not.
Lowering the 1000.0 to a value like 2.0 will make the ped slide into the appropriate position for the animation which can be preferable for smoothness

4 Likes