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.
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
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.
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.
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.
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
RPC
can be stored in the network state to be synchronized across clients.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
}
}
}
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.
PlayerMovement.FixedUpdateNetwork()
TryUse()
locally.PlayerMovement.TryUse()
TaskStation
. If so, call Interact()
locallyTaskStation.Interact()
=> TaskUI.Begin()
TaskBase.Completed()
is called.TaskBase.Completed()
=> GameManager.Instance.CompleteTask()
RPC
to the host called Rpc_CompleteTask()
GameManager.Rpc_CompleteTask()
TasksCompleted
, which triggers the TasksCompletedChanged
OnChanged
callback.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.
Lol I got carried away and seemingly typed a really really long essay.
Here are the takeaways
StateAuthority
NetworkInput
or RPC
RPCs
are a great solutionRPC
from player to host -> host changes network state -> all players detect change in network state with OnChanged
and decide on their own what to do with this change locallyHope this helps!
By this all people will know that you are my disciples, if you have love for one another. John 13:35 (ESV)