How do some games replicate body part rotation so smoothly?

  1. What do you want to achieve?
    I want to replicate the rotation of a clients head (it points towards the camera direction) to all the other clients smoothly, preferably without firing 2000 remotes per second.
    I’m rotating the head by changing the C0 of the neck motor6d which apparently doesn’t replicate automatically.

I’m referring to how I would replicate the neck motor6d smoothly to all clients

  1. What is the issue?
    My game currently replicates the neck by firing a remote 4x a second from each client with the pre-calculated CFrame of the neck. I used CFrame:Lerp() to make it seem smoother but it still looks jittery on other clients.

For example, Blood & Iron does this insultingly well and I truly have no idea how they do this. I’ve been brainstorming with a couple friends for a while and we came up with some solutions but most of them involved hundreds of remotes per second.


Watch the heads of the players and how smoothly they update for me. I would love an explanation as to how I would go about making functionality similar to this.

  1. What solutions have you tried so far?
    Uhh I’ve tried sending 4 remotes per second from each client, and it works efficiently but it’s not as smooth as some other games I’ve seen.
1 Like

I’m pretty sure the server just tells the client “the character moved to xyz” and then the client uses the r6 animation to move them there.

I think you are thinking that the server is sending a cframe for each body part movement or something.

2 Likes

Sorry I’ll rephrase the post. I mean how does the game replicate the rotation of the head so quickly? Roblox already replicates torso automatically.

Same thing, all the server needs to say is “character looked in this direction”, then the client can animate the character looking in that direction.

2 Likes

How do I tell the server that the character is looking in that direction though? The camera is only available on the client and I have to send a value with a remote event.

Try using Ray Casting. You can get the unit vector for direction.

Please read at least 20% of the topic or even the title before you reply… I want to replicate the neck motor6D to all clients smoothly, I already made the head point to the camera.

Personally I’m doing it 5 remotes per second transmitting only the x,y,z orientation of the C0 CFrame only three numbers however for multiple Motor6D joints then I tween in between the time spent which is 1 remote : 0.2 seconds any less remotes and I find the smoothness drops considerably please tell me if you have a better solution though,

here is my result:

	--Event is every 0.2 seconds
	--I require rate limiter and angles verification and a lot more
	local turretTweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Linear)
	self:ConnectClientEvent("ReplicateMotor6D", function(player, motor6D: Motor6D, x, y, z)
		if not motor6D then
			return
		end
		assert(motor6D:IsA("Motor6D"), "Motor6D Has to be inserted")
		local part0 = motor6D.Part0
		local networkOwner = part0:GetNetworkOwner()--shoddy replication security
		if networkOwner and player == networkOwner then
			local goalCFrame = CFrame.new(motor6D.C0.Position) * CFrame.fromOrientation(x, y, z)
			local goal = { C0 = goalCFrame }
			TweenService:Create(motor6D, turretTweenInfo, goal):Play()
		end
	end)
6 Likes

Thanks for the reply! I guess I was just paranoid about firing too many events. Just wondering, is the script you’re showing a localscript or is it running on the server?

Oh the script is the server script, it’s tweening on the server.

I need it in my scenario in order to change the C0 for the turrets hitboxes and parts on the server to replicate. Unfortunately, the cons is that it’ll produce server strain but hey it works for now.

Also haven’t tested it out for extremely laggy players I believe it’ll have an issue there.

1 Like

So if I were to fire 5 events per second from each client with their pre-calculated CFrames to the server, and the server updates the CFrame of the neck of lets say 30 clients, would that be too much strain on the server? I wouldn’t use any tweens or anything but I might use CFrame:Lerp to smooth it a bit.

IDK, thats up for testing, I haven’t actually done it in a real game and measured the bandwidth.

I recommend the control 7 stats viewer and the standard keeping the bandwidth within goal range 50kb/s for each player which my current method does even for three motor6ds.

I’m confused what you mean with CFrame:Lerping(), it might be better if you want to ensure the head turns at a constant speed, but my turrets are not constant speed atm.

local goalC0 --remote event updates this

while wait() do -- pretend this is runservice heartbeat on server
motor.C0 = motor.C0:Lerp(goalC0,alpha)
--do some Cframe math with alpha to lerp at constant speed towards goal location
end
2 Likes

for future reference, blood & iron just updates head movement at 30hz
obviously it’s not ideal to do that, there’s better ways such as lerping at maybe 8hz with minimal data being sent to the server, and only replicating to nearby clients, but that’s how he achieved the ‘smooth’ effect

in my game, i get the direction of which the player should look (both the x and y), i compress it enough to the point where combining them with bitwise functions makes the max value 10,000 (i cut off the decimals and round it to an integer, so for example the xLook = [0, 14] and the yLook = [0, 14]. 10000 because thats the max value my compression module allows), then i compress that into 4 bytes and send it to the server for replication. the other clients decode this data and determine where the main client’s looking (its lossful and inexact but it still looks pretty good)

it’s not an easy system to explain, but its very efficient for my desired playerbase of 200 players a server. 6hz and it only updates to players within a 120 stud radius. its looks pretty good too

i know nobody asked, but this is a pretty common question on here so i thought i should share how others implement it and how i implemented, to help both beginners and advanced people for the future.

7 Likes

Compressing into just 4 bytes is honestly amazing. This looks like a very efficient solution. Updating to players in 120 stud radius is a nice feature too which I might implement, though usually I work with smaller maps.

Just out of interest, what does your compression module look like, if you don’t mind showing?

1 Like

im not in studio rn, but heres how it works:

-- on server
createNumberInstanceFolder()
for i = 0, 10000 do
      createNumberInstance(i) -- creates an instance with the name the value of i
end

-- on client
getNumberInstance(504) -- gets the number instance 504. type(instance) == 'Instance'
socket:FireServer('Himalayan Salt', getNumberInstance(1653), getNumberInstance(0)) -- i forgot how to use remotes, this module i made is justt a wrapper for remotes.

-- on server
socket:BindEvents({
      ['Himalayan Salt'] = function(player, number1Enc, number2Enc)
            -- check if number1 and number2 are instances, descendants of the numberInstance folder
            local number1, number2 = numberFromNumberInstance(number1Enc), numberFromNumberInstance(number2Enc)
            -- typeof number1 and number2 are numbers. number1Enc and number2Enc are instances
            workspace:SetAttribute('n1', number1)
            workspace:SetAttribute('n2', number2)
      end
})

this is pretty much what i do, but not modularized.
instances are 4 bytes, numbers are 8 bytes

the entire system is:
to server: 4 byte headDirection Instance + 1 byte type + 9 byte overhead
to other clients: 4 byte headDirection Instance + 1 byte type + 4 byte player + 1 byte type + 9 byte overhead
and given that you create enough number instances, you can add the getplayers index which the player is in, into the 4 bytes headDirection Instance, thus saving you 5 bytes and becoming:

to other clients: 4 byte headDirection&Player Instance + 1 byte type + 9 byte overhead

but creating too many number instances will crash the server, so you’d have to create each 10000 after every wait(1) maybe, to be able to send larger values to the server. that would obviously boost how much memory the server uses- the instances i make have little to no properties but still a bunch of instances are being made


if people start using this idea in their devforum posts or games, dont forget me. i invented this :wink: :cool:

1 Like

i can make a community tutorial on this if you guys want, to go in depth. this stuff comes second nature to me but not to others, and i think it would be beneficial to introduce this style of thinking and optimization to other people whether again theyre beginners or advanced. :uhh:

1 Like

That would be great! I’m trying to achieve the same thing in my own game, but the system that I scripted makes a LOT of lag in a live server.

1 Like

you’re gonna use my implementation? happy i could help :happy1:

1 Like

on 6/04/23 i made a module which theoretically couldve optimized this down to a byte
taken directly from discord
__
byteCoding
compress an integer into a special cframe, which sends over remotes as a single byte, and then convert it back

example usage:

-- server
socket:BindEvents({
    recv = function(player, datum)
        local correspondingInteger = byteCoding:FromCFrame(datum) -- now you have a value between 1 and 24. in this case, it's '5'.
        -- ...
    end
})

-- client
socket:FireServer(byteCoding:FromIndex(5))
local module = {}

-- https://dom.rojo.space/binary.html#cframe
local unformattedSpecials = {
    [0x02]={0,0,0},
    [0x03]={90,0,0},
    [0x05]={0,180,180},
    [0x06]={-90,0,0},
    [0x07]={0,180,90},
    [0x09]={0,90,90},
    [0x0a]={0,0,90},
    [0x0c]={0,-90,90},
    [0x0d]={-90,-90,0},
    [0x0e]={0,-90,0},
    [0x10]={90,-90,0},
    [0x11]={0,90,180},
    
    [0x14]={0,180,0},
    [0x15]={-90,-180,0},
    [0x17]={0,0,180},
    [0x18]={90,180,0},
    [0x19]={0,0,-90},
    [0x1b]={0,-90,-90},
    [0x1c]={0,-180,-90},
    [0x1e]={0,90,-90},
    [0x1f]={90,90,0},
    [0x20]={0,90,0},
    [0x22]={-90,90,0},
    [0x23]={0,-90,180},
}

local formattedSpecials = {} -- {index, angles}

for key, v in unformattedSpecials do
    table.insert(formattedSpecials, {key, CFrame.new(Vector3.zero, Vector3.new(math.rad(v[1]), math.rad(v[2]), math.rad(v[3])))})
end

function module:FromByteIndex(bidx)
    for k, v in ipairs(formattedSpecials) do
        if (v[1] == bidx) then
            return k
        end
    end
end

function module:FromCFrame(cf)
    for k, v in ipairs(formattedSpecials) do
        if (v[2] == cf) then
            return k
        end
    end
end

function module:FromIndex(idx)
    return formattedSpecials[idx][2]
end

return module

if one were to give different look directions which are angled intelligently a different cframe angular value s.t. there are less look directions than the length of that cframe angles array formattedSpecials (1 <= n <= 24), then data could be send as 1 byte + 1 byte overhead + 9 byte overhead

however, this did NOT work. i may have misinterpreted special cframes. i didn’t do any research on them.

1 Like