Different behavior in Editor vs Build (Physics) [1.0.0-re.65]

Hey Folks,

I initially found this issue trying to network the com.unity.charactercontroller package but in the process of bisecting the issue, can also see the problem in just a vanilla physics setup.

I have a cube that I spawn per player, and simple WASD controls that apply angular impulses to the cube.

[UpdateInGroup(typeof(GhostInputSystemGroup))]
// Input collecting
public partial class ThirdPersonPlayerInputsSystem_Dupe : SystemBase
{
    protected override void OnCreate()
    {
        RequireForUpdate<FixedTickSystem.Singleton>();
        RequireForUpdate(SystemAPI.QueryBuilder().WithAll<ThirdPersonPlayerInputs>().Build());
    }

    protected override void OnUpdate()
    {
        uint fixedTick = SystemAPI.GetSingleton<FixedTickSystem.Singleton>().Tick;

        foreach (var playerInputs in SystemAPI.Query<RefRW<ThirdPersonPlayerInputs>>())
        {
            playerInputs.ValueRW.MoveInput = new float2();
            playerInputs.ValueRW.MoveInput.y += Input.GetKey(KeyCode.W) ? 1f : 0f;
            playerInputs.ValueRW.MoveInput.y += Input.GetKey(KeyCode.S) ? -1f : 0f;
            playerInputs.ValueRW.MoveInput.x += Input.GetKey(KeyCode.D) ? 1f : 0f;
            playerInputs.ValueRW.MoveInput.x += Input.GetKey(KeyCode.A) ? -1f : 0f;
        }
    }
}

//Apply angular impulse
[UpdateInGroup(typeof(PredictedFixedStepSimulationSystemGroup), OrderFirst = true)]
public partial struct ThirdPersonPlayerFixedStepControlSystem_Dupe : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<FixedTickSystem.Singleton>();
        state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll<ThirdPersonPlayerInputs, PhysicsVelocity, PhysicsMass>().Build());
    }
    public void OnDestroy(ref SystemState state)
    { }
    public void OnUpdate(ref SystemState state)
    {
        foreach (var (playerInputs, pv, pm) in SystemAPI.Query<RefRW<ThirdPersonPlayerInputs>, RefRW<PhysicsVelocity>, PhysicsMass>().WithAll<Simulate>())
        {
            pv.ValueRW.ApplyAngularImpulse(pm, playerInputs.ValueRW.MoveInput.y * new float3(0, 0, 0.1f) + playerInputs.ValueRW.MoveInput.x * new float3(0.1f, 0, 0));
        }
    }
}

What I’m seeing is that in editor, the impulses are reasonable and the movement is smooth as expected. But in the build (Dedicated linux server + WebGL w/ Custom WebRTC transport) the cube has erratic behavior. Almost like there are 10x the number impulses applied. The same behavior existed on the character controller, in the build the character moved extremely fast.

I saw in another thread there are potentially some bugs server-side with tick rate? Any ideas?

The only other thing I can think of is my custom transport but I would expect many other things to be broken if I messed something up at the transport layer.

I’ve debugged this further. It doesn’t seem to be physics related. I’ve made the body static and moved the LocalTransform position directly instead of applying an impulse.

I’ve also removed input and just move the cube on every tick.

I log out the positions on the server and the values server-side seem to be correct!

So this seems to be a client side issue. Client side, the cube moves extremely fast (~10x the speed it should) and interestingly, client-side the cube does not honor the server side values at all. I did a test where the server would move the cube along the z-axis and the client along the x-axis. And the cube only moves along the x-axis (and at 10x the speed).

It almost feels like every predictive tick (even replayed ticks) are being applied to the cube instead of just the current one. Server values seem to be ignored completely. Even if I don’t modify the cube at all in the PredictedFixedStepSimulationSystemGroup client-side but do modify it server-side, the cube does not move at all. In the editor it jitters (as expected).

Edit: Adding context, this is definitely a regression from previous versions where I was able to sync remote player positions. Now all remote players don’t move at all (server snapshots are not being applied). I’d like to stay on pre.65 for separate reasons.

Edit 2: I’ve added SnapshotMetrics to the client build. The tick is being properly synced.

First question: Did you check that your client build is client only? (and not for some reason client/server). Just to be sure the client is connecting to right server.

Second: if you connect from the editor to the server instance, is the behaviour the same?

We didn’t change anything in 0.65 that can affect ghost synching that much. There is a bug with pre-spawned ghost (that has been fixed in another thread) that was preventing them to be synced correctly.
Is this player using pre-spawned ghost for some reason?

You said tick are synched properly. That just imply snapshots are received. There are ghosts inside that snapshot? You can also attach the NetDbg to the client instance to check what data is inside (without need to modify the code). Not the data itself but at least we see what the client is receiving.

If the snapshot is received, we can check what it is the latest applied tick to the player ghost. You need to add some extra logs though.
You can check what has been the last received snapshot for you player predicted ghost via
**PredictedGhost.AppliedTick or also checking the GetLatestTick() on the SnapshotBufferData (**although the method is internal I think).

I would suggest to open a case in any way, so we can check directly with your project. Or share the project itself if you are blocked.

My project is a bit convoluted on the networking side so it’s not the easiest to get into a shareable state.

client (webgl only right now) --- webrtc --> webrtc SFU -- webrtc --> tcp proxy service --> server (headless linux build)

But I confirm now that there is some weirdness going on with the network drivers because the client tries to open the default websocket connectoni that netcode uses.

My network driver constructor looks like this:

namespace WebTick.Transport
{
    public class LiveKitDriverConstructor : INetworkStreamDriverConstructor
    {
        private WebSocketManager webSocketManager;
        private LiveKitManager liveKitManager;

        private LiveKitDriverConstructor() { }

        public LiveKitDriverConstructor(WebSocketManager wsManager, LiveKitManager liveKitManager) {
            this.webSocketManager = wsManager;
            this.liveKitManager = liveKitManager;
        }

        public void CreateClientDriver(World world, ref NetworkDriverStore driver, NetDebug netDebug)
        {
            var driverInstance = DefaultDriverBuilder.CreateClientNetworkDriver(new LiveKitClientNetworkInterface(liveKitManager));
            driver.RegisterDriver(TransportType.Socket, driverInstance);
        }

        public void CreateServerDriver(World world, ref NetworkDriverStore driver, NetDebug netDebug)
        {
            if (webSocketManager != null)
            {
                var driverInstance = DefaultDriverBuilder.CreateServerNetworkDriver(new LiveKitServerNetworkInterface(webSocketManager));
                driver.RegisterDriver(TransportType.Socket, driverInstance);
                return;
            }

            throw new System.Exception("No websocket manager");
        }
    }
}

And i set it like this:

NetworkStreamReceiveSystem.DriverConstructor = new Transport.LiveKitDriverConstructor(wsManager, liveKitManager);

Then I create the worlds like this

var clientWorld = ClientServerBootstrap.CreateClientWorld("Client");

Is there anything else needed or anything that changed on the transport side of things that would be relevant?
var serverWorld = ClientServerBootstrap.CreateServerWorld("Server");

No the code looks legit. The only question is when you register the

NetworkStreamReceiveSystem.DriverConstructor = new Transport.LiveKitDriverConstructor(wsManager, liveKitManager);

Is it in a custom bootstrap, or before creating the world itself? Can you check that before the world is created, your driver constructor is the one in use?

Yeah it’s happening in the right place. The websocket weirdness seems to have gone away. I think when removing NETCODE_DEBUG define and adding UNITY_CLIENT define but I can’t confirm what did it.

The same erroneous behavior is still happening though. Going to tinker with it some more and then, if I can’t figure it out, see if I can boil it down to a testable case.

Would you be comfortable running non unity code if I create a smaller hello cube project demonstrating the issue?

  • unity webgl build
  • unity dedicated linux build (I run in wsl)
  • open source webrtc SFU
  • thin tcp proxy server

Edit: I’ve been tinkering some more.
I confirm that applied tick on the predicted ghost is being changed to the server tick.

I also set it to Predicted (instead of OwnerPredicted) and in this case a second player ghost does move but it moves very quickly (just like the client). Interpolated ghosts do not move at all and I don’t have any variants that would be disabling position updates. I don’t have any variants at all actually.

Something else to note is that I am not able to use any of the baking workflow because I’m targeting WebGL so I create my ghost prefab in code.

What doesn’t make sense to me is why the cube is moving so fast.

I’ve host the game and server so you can see for yourself: https://webtick-metaverse-frontend-pvun.zeet-chefstudios.zeet.app/

You can open a second tab to spawn a second cube (also predicted right now). Arrow keys to move.

Input system looks like this from hello cube example:

[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
public partial struct CubeMovementSystem : ISystem
{

    public void OnUpdate(ref SystemState state)
    {
        var speed = SystemAPI.Time.DeltaTime * 4;
        foreach (var (input, trans) in SystemAPI.Query<RefRO<CharacterInputs>, RefRW<LocalTransform>>().WithAll<Simulate>())
        {
            var moveInput = new float2(input.ValueRO.horizontal, input.ValueRO.veritical);
            moveInput = math.normalizesafe(moveInput) * speed;
            trans.ValueRW.Position += new float3(moveInput.x, 0, moveInput.y);
        }
    }
}

Cube is moving about 10x as fast as it should. On the server, it seems to me moving at the correct speed based on logs.

I confirm that this is something wrong with how implemented my WebRTC transport. Haven’t figured it out yet but I was able to get hello cube working with a very thin custom websocket transport + webgl client + linux server.

You can close this while I go to my room and think about what I’ve done

The code is legit. If you need help you can pass any code want and we can see if there is a gotcha somewhere.

The https://webtick-metaverse-frontend-pvun.zeet-chefstudios.zeet.app/ does not point to the cube scene but some other demo. Are you sure you are passing the correct address? Also in this case, things are replicated (but I think it was using normcore)

There is a problem in general with creating the ghost prefab via code that has been addressed for the final 1.0 release but that the current 1.0-pre.44 and 1.0-pre.65 still have IIRC.

Just to be sure, you need to create the prefabs in the first OnUpdate of your system (i.e in the InitalisationSystemGroup). Don’t do that inside the OnCreate, to avoid some component are erroneously mapped to the DontSerialiseVariant (and so nothing will move for example).

Ahh apologies - yes we had to swap out our demo for NormCore to move forward.

I was able to get things working using the WebSocketNetworkInterface (although I had to modify the webgl variant of it) so I think I just messed something up with my WebRTC transport. I’ll keep cranking on it and thanks for the help!

All right. Sound great. Let us know if you any extra tip. Also because you are using a custom WebRTC transport, any feedback in relation to that (transport related) is very nice to have!
Transport team would be very interested in any feedback in that regards.

I’ll be open sourcing it! Of course, WebRTC requires the developer to come up with their own signaling so it will have some opinion towards the SFU I’m using (LiveKit)

So far though it’s been pretty smooth! Well apart from this of course.