Photon Fusion multiplayer, client side input issue

I’m trying to make a Multiplayer Pong game with Photon Fusion network. I’ve followed the photon fusion tutorial from the documentation and setup my project.

I’m able to spawn the Host and control it normally. But when the client joins the session and tries to move, the movement is only reflected in the host device, but not in itself (client device). The collisions seem to be working and the ball is hit back.

The host is able to see all the gameplay, but the client can only see the host move.

Sharing my code for Network Runner Handler below,

public class NetworkRunnerHandler : MonoBehaviour, INetworkRunnerCallbacks
{
    public int currentPlayer;
    public int maxPlayers = 2;

    public NetworkPrefabRef playerPrefab;
    NetworkRunner networkRunner;
    public Dictionary<PlayerRef, NetworkObject> spawnedPlayers = new Dictionary<PlayerRef, NetworkObject>();

    async void GameStart(GameMode gameMode, string sessionName)
    {
        networkRunner = gameObject.AddComponent<NetworkRunner>();
        gameObject.AddComponent<RunnerSimulatePhysics2D>();
        // networkRunner.ProvideInput = true;

        networkRunner.ProvideInput = gameMode != GameMode.Server;
        networkRunner.AddCallbacks(this);

        var scene = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex);
        var sceneInfo = new NetworkSceneInfo(); 

        if(scene.IsValid)
            sceneInfo.AddSceneRef(scene, LoadSceneMode.Additive);

        await networkRunner.StartGame( new StartGameArgs()
        {
            GameMode = gameMode,
            SessionName = sessionName,
            Scene = scene,
            SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
        });
    }

    public void StartGame_UI(int state) 
    {
        if(state == 0)
            GameStart(GameMode.Host, "Feda");
        else
            GameStart(GameMode.Client, "Feda");
    }

    public void OnConnectedToServer(NetworkRunner runner){}

    public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason){}

    public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token)
    {
        if (currentPlayer >= maxPlayers)
        {
            request.Refuse();
        }
        else
        {
            request.Accept();
        }
    }

    public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data){}

    public void OnDisconnectedFromServer(NetworkRunner runner, NetDisconnectReason reason){}

    public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken){}

    private bool _mouseButton0;
    private void Update()
    {
        _mouseButton0 = _mouseButton0 | Input.GetMouseButton(0);
    }

    public void OnInput(NetworkRunner runner, NetworkInput input)
    {
        var data = new NetworkInputData();

        if (Input.GetMouseButton(0)) // Left mouse button
        {
            data.isMousePressed = true;
            data.mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        }

        if(Input.GetKey(KeyCode.W))
            data.direction += Vector2.up;

        if(Input.GetKey(KeyCode.S))
            data.direction += Vector2.down;

        if(Input.GetKey(KeyCode.A))
            data.direction += Vector2.left;

        if(Input.GetKey(KeyCode.D))
            data.direction += Vector2.right;

        data.buttons.Set( NetworkInputData.MOUSEBUTTON0, _mouseButton0);
        _mouseButton0 = false;
            
        input.Set(data);
    }

    public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input){}

    public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player){}

    public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player){}

    public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
    {
        if(networkRunner.IsServer)
        {
            NetworkObject networkObject = networkRunner.Spawn(playerPrefab, spawnPos(currentPlayer), Quaternion.identity, player);

            spawnedPlayers.Add(player, networkObject);

            currentPlayer++;
        }
    }

    public Vector3 spawnPos(int x)
    {
        if(x == 0)
            return new Vector3(0, -4f, 0);
        else
            return new Vector3(0, 4f, 0);
    }

    public Vector3 spawnRot(int x)
    {
        if(x == 0)
            return Vector3.zero;
        else
            return new Vector3(0, 0, 180);
    }

    public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
    {
        if(spawnedPlayers.TryGetValue(player, out NetworkObject networkObject))
        {
            networkRunner.Despawn(networkObject);
            spawnedPlayers.Remove(player);
        }
    }

    public void OnReliableDataProgress(NetworkRunner runner, PlayerRef player, ReliableKey key, float progress){}

    public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key, ArraySegment<byte> data){}

    public void OnSceneLoadDone(NetworkRunner runner){}

    public void OnSceneLoadStart(NetworkRunner runner){}

    public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList){}

    public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason){}

    public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message){}

}

Code for PlayerController which is attached to the prefab of the player being spawned

public class PlayerController : NetworkBehaviour
{
    [SerializeField] private float moveSpeed;
    [SerializeField] private Rigidbody2D rb;

    public override void FixedUpdateNetwork()
    {
        if(GetInput(out NetworkInputData data))
        {
            // rb.velocity = data.direction * moveSpeed * Runner.DeltaTime;

            if(data.isMousePressed)
            {
                Vector2 targetPosition = data.mousePos;
                Vector2 newPosition = Vector2.MoveTowards(rb.position, targetPosition, moveSpeed * Runner.DeltaTime);
                rb.MovePosition(newPosition);
                transform.position = newPosition;
            }
        }
    }

    public override void Spawned()
    {
        if (!Object.HasInputAuthority)
        {
            // Disable Rigidbody2D on non-authoritative clients to avoid conflicts
            rb.isKinematic = true;
        }
    }

    public override void Render()
    {
        if (!Object.HasInputAuthority)
        {
            rb.position = transform.position;
        }
    }
}

Network Input Struct

public struct NetworkInputData : INetworkInput
{
    public const byte MOUSEBUTTON0 = 1;

    public Vector2 direction;
    public Vector2 mousePos;
    public bool isMousePressed;

    public NetworkButtons buttons;
}

Please let me know where am I doing this wrong?

Hi,

There’s nothing wrong with your setup, which leads me to believe you are facing an issue caused by the interpolation target of the NetworkRigidbody2D component. Could you please try to remove the InterpolationTarget reference on the inspector? Just set it to none. Also if it makes sense for you game to have client side prediction you can set the ClientPhysicsSimulation on the RunnerSimulatePhysics2D on your runner to SimulateForward.

On a side note, Fusion has an internal PlayerRef->NetworkObject mapping you can use instead of storing it in a dictionary, you can use it directly from the NetworkRunner.
You can see more here: Fusion 2 PlayerRef | Photon Engine

Fusion also has a max players property on the StartGameArgs that you can set and you don’t have to worry about denying connections request if the session is full.

Hi,

Thank you, This has worked. I removed the Interpolation and set the ClientPhysicsSimulation as you mentioned and it fixed.

Thanks for the other tips to improve as well!

1 Like