unity-game-enginenetwork-programmingphotonphoton-fusion

Board Game in Unity using Photon Fusion


I wish to make an online multiplayer board game using Photon Fusion. The basic gameplay involves people clicking shop buttons to purchase things, and a next turn button to pass the turn on to the next player. I would also like to only display certain text boxes to the person current taking their turn.

However, I am really struggling to find a tutorial on how I could make this work. Sorry if this is a dumb question, any pointers would be greatly appreciated. Networking is one of my weakest spots.

I have tried to do research on this (the Photon docs, Youtube, reddit, etc) but almost everything I have found uses Photon's predictive movement and keyboard buttons for fps or parkour games.

Being very new to networking, I am struggling to figure out how to make a scene that uses buttons controlled by different people on each turn and moves a gameObject for everyone.


Solution

  • TL;DR

    Use RPCs, [Networked] variables and OnChanged callbacks to communicate back and forth between the host and clients.

    You can create a [Networked] variable holding the PlayerRef of the current player with an OnChanged callback. When the [Networked] variable changes, all players will call the OnChanged callback. Each player can then check if it is their turn with Runner.LocalPlayer. If it is their turn, only show the buttons and textboxes for that player.

    When the player presses the next turn button, call an RPC from that player to the host, which will then change the [Networked] current player again. The next player will detect this change and show the corresponding UI for that player.

    You can either hide the UI for the current player when the next turn button is pressed, you can also hide the during the OnChanged callback. It is up to you.

    Highly recommend taking a look at the Fusion Imposter game sample. It is basically an AmongUs clone, where each player can complete individual tasks. The UI shown to each player is different, and they collectively affect the network state of the total number of tasks completed. It also uses RPCs to communicate from the client to the host.

    Useful Links

    The (really) long answer

    I get what you mean. Most of Photon Fusion's examples and documentation are geared towards games with continuous input. But after messing around with Fusion for some time, here are a few things I've learnt. Hope they could help you in figuring out a solution for your problem.

    There are many ways to do one thing, and there is no absolute "right" way to do it. The solution for your problem right now may change in the future when you face another problem, and that's ok. The codebase is living and breathing and changes all the time, constantly refactoring while adding new features. So it is important that you first understand the concepts behind multiplayer networking with Photon Fusion, so that you can find a solution to make things work now, fix bugs, and make decisions in the future if you need to change or try other solutions.

    Concept Overview

    In general, anything that is networked can only be changed by the StateAuthority. For example only the StateAuthority can change [Networked] variables and spawn NetworkObjects. We can call this the network state.

    The network state is synced across all clients, and most of the what the client sees are just responding to changes in the network state. For example, let's say we have a GameObject with a NetworkTransform component. The transform of this GameObject is a part of the network state because of the NetworkTransform component. If StateAuthority changes the transform.position of this GameObject, since the transform is part of the network state, the position of this GameObject will also change in all clients in response to the change of the network state. All clients will see the same GameObject moving. The position of the GameObject is then considered synced across all the clients. If any client tries to change the transform.position of this GameObject, nothing will happen because only the StateAuthority can change the network state.

    For a client to change or influence the network state, there are in two ways to do so in Fusion.

    1. Network Input

    The first method is via NetworkInput. This is most likely what you encounter the most in the Fusion documentation. With NetworkInput, clients can influence the NetworkObjects that they have InputAuthority over. The input is first collected from the client, then it is sent to the host and applied during the FixedUpdateNetwork loop, updating the network state of the NetworkObject that the client has InputAuthority over, and synchronizes the network state across all other clients.

    Fusion does this in a really powerful way, with prediction done out of the box so that the client can have instant feedback even if the network state haven't been changed yet in the host.

    But the concept is still the same. The network state can only be changed by StateAuthority. The network state is synchronized across all clients. Clients may influence the network state of the NetworkObjects that they have InputAuthority over, but ultimately it is the StateAuthority that allows these changes to the network state and synchronizes these changes across all the other clients.

    However, like you said, most of the documentation revolves around collecting keyboard input. There is a small paragraph showing how to poll input with UI, but in that context my guess is that this is for mobile games with UI buttons for movement. Not useful for your case with clicking buttons to purchase things and a next turn button.

    2. RPC

    The second method is via RPC. In the documentation you can feel that Fusion highly discourages the use of RPCs. I can understand why.

    RPCs are

    Therefore, RPCs are not suitable for tick-based simulation games such as fps and parkour games. In those types of games, NetworkInput is indeed more than enough in most cases as the player mostly interacts with the world via keyboard inputs and mouse clicks.

    RPCs not being part of the network state is also a problem. For example, let's say we have GameObject in the scene with a NetworkBehaviour script but WITHOUT a NetworkTransform component. A client can call an RPC to change the transform.position of this GameObject directly in all other clients. Indeed all clients can see this GameObject move from its old position to a new position. But if a new client joins the game, the GameObject will remain in its old position because (1) the position of the GameObject is not part of the network state and (2) the RPC is not part of the network state and will only be fired once. The RPC will not fire again for new clients joining the game. The position of the GameObject is then considered NOT synced across all the clients.

    Continuing the previous example, how can we sync the position of the GameObject without using NetworkTransform? Always remember that the network state can only be changed by StateAuthority, which is then synchronized across all clients. One way to add the position to the network state is by creating a [Networked] variable storing the position and changing the position of the GameObject using OnChanged callbacks.

    -> Client calls RPC to StateAuthority sending a new Vector3 position

    -> StateAuthority receives the RPC and changes the [Networked] position variable

    -> All clients detect that the [Networked] variable has changed

    -> All clients call the OnChanged callback to update the transform.position of the GameObject

    Then the position of the GameObject is now synchronized across all clients. The difference is that the RPC is used to change the network state, which is then synchronized across all clients, rather than the RPC directly changing the position of the GameObject.

    For a new client joining the game,

    -> New client sets the transform.position to the [Networked] position in Spawned()

    Thats all it takes to keep the position of the GameObject synchronized even for new clients which did not receive the RPC. This is because the result of the RPC is stored in the [Networked] variable and is part of the network state.

    In general, RPCs are great if

    My Suggestion

    Indeed all the documentation on predictive movement and keyboard buttons are not suitable for your case at all. You should highly consider using RPCs instead of NetworkInput. Your game is not a tick-based simulation, so RPCs would work great for your case.

    The difficult part is designing the architecture of your game, eg deciding how to store the network state in [Networked] variables, and which methods should be called with RPC and whether you should use OnChanged for reflecting changes in the clients or use an RPC from the host to a specific client.

    Just keep in mind that RPCs are not part of the network state, and you should find some way to store the result of an RPC in the network state. In most cases, clients would call an RPC to the host, the host changes the network state, then the clients would act based on the changed network state.

    In some rare cases, you may call an RPC from the host directly to the client, or very rarely a client to another client. Again, its a decision you have to make, and its ok to change your solution later on if it doesn't work well.

    If I were in your situation, I would have a [Networked] variable storing the PlayerRef of the player currently taking their turn. We can call it _currentPlayer.

    When currentPlayer changes

    -> Triggers the OnChanged callback on every player

    -> Each player checks if the currentPlayer is equal to their own local PlayerRef with Runner.LocalPlayer

    -> if it is their turn, then show the UI only for that player

    public class GameManager : NetworkBehaviour 
    {
        [Networked(OnChanged = nameof(OnCurrentPlayerChanged))] 
        private PlayerRef _currentPlayer {get; set;}
    
        // ...
    
        private static void OnCurrentPlayerChanged(Changed<GameManager> changed)
        {
            changed.Behaviour.OnCurrentPlayerChanged();   
        }
    
        private void OnCurrentPlayerChanged()
        {
            // If it is my turn
            if (_currentPlayer === Runner.LocalPlayer)
            {
                // show the buttons / textboxes
            }
    
            // If it is not my turn
            else
            {
                // you may want to hide the buttons and textboxes for other players
            }
        }
    }
    
    
    

    When the buttons are pressed,

    -> The player can call RPCs to the host.

    -> The host can then change the network state, eg. updating the amount of coins a player has or move a game object.

    -> The network state will then be synchronized across all the clients, so everyone can see the same game object moving.

    // Called locally on client only
    public void OnButtonPress()
    {
        int someDataFromThePlayer = ...; // Whatever data you want to sent to the host
        RPC_OnPlayerButtonPressed(someRandomDataFromThePlayer);
    }
    
    // Called on the host only
    [Rpc(RpcSources.All, RpcTargets.StateAuthority)]
    private void RPC_OnPlayerButtonPressed(int someRandomDataFromThePlayer)
    {
        // Do whatever you want here with someRandomDataFromThePlayer and change the network state
        // The data does not have to be an int. Check the docs for the supported types.
    }
    

    If a player ends their turn

    -> The player can call RPCs to the host.

    -> the host can change _currentPlayer to the next

    -> All players call OnChanged

    -> Previous player that had their UI open will close

    -> Current player that had their UI closed will now open

    public class GameManager : NetworkBehaviour 
    {
        [Networked(OnChanged = nameof(OnCurrentPlayerChanged))] 
        private PlayerRef _currentPlayer {get; set;}
    
        // ...
    
        // Called locally on client only
        public void OnEndTurnButtonPress()
        {
            RPC_OnPlayerEndTurn();
        }
    
        // Called on the host only
        [Rpc(RpcSources.All, RpcTarget.StateAuthority)]
        private void RPC_OnPlayerEndTurn()
        {
            PlayerRef nextPlayer = ...; // Somehow get the PlayerRef of the next player
            _currentPlayer = nextPlayer; // Triggers the OnChanged below on all clients
        }
    
        // ...
    
        private static void OnCurrentPlayerChanged(Changed<GameManager> changed)
        {
            changed.Behaviour.OnCurrentPlayerChanged();   
        }
    
        private void OnCurrentPlayerChanged()
        {
            // If it is my turn
            if (_currentPlayer === Runner.LocalPlayer)
            {
                // show the buttons / textboxes
            }
    
            // If it is not my turn
            else
            {
                // you may want to hide the buttons and textboxes for other players
            }
        }
    }
    

    Case Study: Fusion Imposter

    Fusion Imposter

    Although a lot of Fusion's documentation and examples show games with continous input, I found this great example that shows different UI for different players. The UI for different players also collectively affect the network state, which is synced and shown across all players.

    This game is basically AmongUs in 3D. Players walk around doing individual tasks while the imposter tries to kill everyone.

    It is quite advanced. But here is an overview and my understanding of what is going on when a player presses E near a TaskStation.

    1. PlayerMovement.FixedUpdateNetwork()
    1. PlayerMovement.TryUse()
    1. TaskStation.Interact() => TaskUI.Begin()
    1. TaskBase.Completed() => GameManager.Instance.CompleteTask()
    1. GameManager.Rpc_CompleteTask()
    1. GameManager.TasksCompletedChanged()

    As you can see, it is possible for each player to have their own UI to do their own thing. Each player communicates back to the host via RPC, which changes the network state, which is then synchronized across all clients.

    Summary

    Lol I got carried away and seemingly typed a really really long essay.

    Here are the takeaways

    Hope this helps!

    References

    By this all people will know that you are my disciples, if you have love for one another. John 13:35 (ESV)