Currently, the player’s team color is set in OnServerAddPlayer. What objects or scripts would you need to add to instead let the player choose a color in the offline scene and save that color going into the online scene? (As a bonus, can you also let players select from a list of colors and prevent them from choosing duplicate colors?)
Hi there, good question!
Locally you would need a a dropdown for selecting colour (assuming we are going the list of colours route). This would need it’s own script to update the color list when prompt by the server. Then a networkBehaviour script to sync the list of colors from the server and update the UI (this could be done by the RTSPlayer). This can have a syncvar hook to tell the dropdown list to update with the remaining colours whenever those are changed.
Note: You might have to give the colours int ids so they can be synced across the network. You can reference them using a enum or something. Else you might be able to sync it as a Vector3, which sent across the network.
When you select from the list, this would need to call a function on the RTSPlayer to set their colour. The script that manages the list could call to the RTSPlayer to tell it the colour has been udpated. The RTSPlayer would send a Cmd to the server to validate the colour and sync it to the other players, so they can see you changed your colour.
To summarize, you would probably need one new script to manage the colour change UI. Then methods on the RTSPlayer (and or a new networkBehaviour) to manage the colour validation and syncing to all the client’s UIs.
Hope that helps!
Thank you for the detailed response! I have made a few image buttons on the main menu.
This is my current approach:
TeamColorButton, on each button with SelectColorButton
called on click:
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
[RequireComponent(typeof(Image))]
public class TeamColorButton : MonoBehaviour
{
Color teamColor;
public Color TeamColor => teamColor;
public static event Action<TeamColorButton> OnColorButtonSelected; // Use TeamColorSelector reference instead? TeamColorButton relies on TeamColorSelector existing
[SerializeField] UnityEvent onColorPlayerSelected;
[SerializeField] UnityEvent onColorMadeAvailable;
[SerializeField] UnityEvent onColorMadeUnavailable;
void Awake()
{
teamColor = GetComponent<Image>().color; // Assumes Color image for now
}
public void SelectColorButton() // Or TrySelectColorButton?
{
OnColorButtonSelected?.Invoke(this);
}
public void HandleColorPlayerSelected()
{
onColorPlayerSelected?.Invoke();
}
public void HandleColorMadeAvailable()
{
onColorMadeAvailable?.Invoke();
}
public void HandleColorMadeUnavailable()
{
onColorMadeUnavailable?.Invoke();
}
}
TeamColorSetter, on the single Team Color Selection GameObject:
using System;
using System.Collections.Generic;
using Mirror;
using UnityEngine;
public class TeamColorSelector : NetworkBehaviour
{
[SerializeField] Transform colorButtonsParent;
List<TeamColorButton> colorButtons = new List<TeamColorButton>();
List<Color> allTeamColorOptions = new List<Color>();
[SyncVar(hook = nameof(HandleAvailableTeamColorsUpdated))]
List<Color> availableTeamColors;
Color selectedTeamColor;
bool hasSelectedTeamColor = false;
#region Server
public override void OnStartServer()
{
foreach (Transform colorButtonTransform in colorButtonsParent)
{
TeamColorButton colorButton = colorButtonTransform.GetComponent<TeamColorButton>();
colorButtons.Add(colorButton);
allTeamColorOptions.Add(colorButton.TeamColor);
}
availableTeamColors = new List<Color>(allTeamColorOptions);
TeamColorButton.OnColorButtonSelected += HandleTeamColorSelected;
}
public override void OnStopServer()
{
TeamColorButton.OnColorButtonSelected -= HandleTeamColorSelected;
}
[Command]
private void CmdSelectTeamColor(GameObject colorButtonGO)
{
TeamColorButton colorButton = colorButtonGO.GetComponent<TeamColorButton>(); // Custom type not supported by Mirror
Color teamColor = colorButton.TeamColor;
if (!availableTeamColors.Contains(teamColor)) { return; } // Validation
if (hasSelectedTeamColor)
{
availableTeamColors.Add(selectedTeamColor);
}
selectedTeamColor = teamColor;
colorButton.HandleColorPlayerSelected();
availableTeamColors.Remove(teamColor);
hasSelectedTeamColor = true;
}
#endregion
#region Client
private void HandleTeamColorSelected(TeamColorButton colorButton)
{
// Need authority check to prevent players who didn't select a color from running command?
CmdSelectTeamColor(colorButton.gameObject);
}
private void HandleAvailableTeamColorsUpdated(List<Color> oldAvailableColors, List<Color> newAvailableColors)
{
if (oldAvailableColors == null) { return; } // Upon oldAvailableColors initialization
foreach (TeamColorButton colorButton in colorButtons) // Optimize?
{
Color teamColor = colorButton.TeamColor;
if (oldAvailableColors.Contains(teamColor) && !newAvailableColors.Contains(teamColor))
{
colorButton.HandleColorMadeUnavailable();
}
else if (!oldAvailableColors.Contains(teamColor) && newAvailableColors.Contains(teamColor))
{
colorButton.HandleColorMadeAvailable();
}
}
}
#endregion
}
While playing, availableTeamColors
isn’t currently updating and I am getting these warnings:
-
NetworkWriter Red (UnityEngine.GameObject) has no NetworkIdentity.
(I think I would prefer a solution where I don’t put a NetworkIdentity on each button) Command Function System.Void TeamColorSelector::CmdSelectTeamColor(UnityEngine.GameObject) called on Team Color Selection without authority.
Later, I will be raising an event from TeamColorSelector to notify RTSPlayer to change a team color field.
Do you see anything that may cause issues now or down the line? Any feedback would be appreciated!
So that list of colors is syncing correctly?
I like the color buttons setup, looks simple and easier to handle.
I agree that the TeamColorButton should be a monoBehaviour and shouldn’t need a network identity.
Your approach looks solid. Am I following correctly?
- Press color button
- Event is triggered
- //TODO add authority check
- Player who triggered the event calls the Cmd
- Server updates the color list.
- SyncVar hook updates the color buttons on the clients.
Yes, that is correct.
How should I do the authority check for Step 3 since no one owns the Team Color Selection object? (Pass player connectionId in event? Add TeamColorSelector script reference in the button script to call function directly from button?)
Passing the connection Id in the event sounds good to me. Easy way to see who click the button.
How should I ensure the correct player runs the command? Currently, the Team Color Selection GameObject has a NetworkIdentity and is present on the scene before entering the networked main menu and no client owns the object.
This gives me a NullReferenceException:
private void HandleTeamColorSelected(TeamColorButton colorButton, int playerConnectionId) {
if (playerConnectionId != connectionToServer.connectionId) { return; }
...
Ah yes, this is a good point. Typically networkBehaviours need to spawned by the server, which does not work for UI features.
When you click the button, the button script needs to go looking for the correct player before initiating the command call.
One way to do this might be to initiate the command from the player. The button script can use a FindObjectsOfType call to look at all the players, find the player who is the authoritative client, and then initiate the command from that player.
Will the button know which player clicked the button and called the command, given that this will be on the offline scene?
I get you just need to use any networkBehaviour that has the connection to client, is there anything at that stage in your scene you can use? Perhaps the network manager ?
Thanks again for the help. I changed the lobby so that it is online in the sense that the host will call StartHost() before entering the lobby.
New code in RTSNetworkManager:
[SerializeField] GameObject teamColorSelectorPrefab;
[SerializeField] float teamColorSelectorSpawnDelay = 0.1f;
public override void OnServerAddPlayer(NetworkConnectionToClient conn)
{
base.OnServerAddPlayer(conn);
MyPlayer player = conn.identity.GetComponent<MyPlayer>();
// ...
if (players.Count == 1)
{
player.SetPartyOwner(true);
StartCoroutine(SpawnTeamColorSelector(player));
}
}
private IEnumerator SpawnTeamColorSelector(MyPlayer player)
{
yield return new WaitForSeconds(teamColorSelectorSpawnDelay);
Canvas lobbyCanvas = FindObjectOfType<LobbyMenu>().LobbyCanvas;
GameObject teamColorSelectorInstance = Instantiate(teamColorSelectorPrefab, lobbyCanvas.transform);
NetworkServer.Spawn(teamColorSelectorInstance, player.connectionToClient);
}
It seemed like I needed the WaitForSeconds
call to prevent some race condition (some issue with scene GameObjects, like putting NetworkIdentity GameObject in the scene during Editor mode?).
Now, the teamColorSelectorPrefab
is being instantiated, but it only appears in the host’s lobby and doesn’t synchronize to the non-host client’s lobby. (The prefab is instantiated twice in the host’s lobby and not in the non-host client’s lobby if the StartCoroutine
line is moved outside the if block.)
Do you see any reason why the non-host client won’t be able to see the prefab even after the NetworkServer.Spawn
call?
Additional details to what is happening when the StartCoroutine
line is moved outside the if block:
Player 1 is the host, Player 2 is the non-host client
Player 1 hosts a lobby from MainMenuScene
Player 1’s scene changes to LobbyScene
Player 1’s TeamColorSelector prefab spawns as a child of LobbyCanvas
Player 2 joins the lobby from MainMenuScene
Player 2 sees that Player 1’s TeamColorSelector prefab briefly appears before the scene change at the hierarchy root (and will not appear for Player 2 again)
Player 2’s scene changes to LobbyScene
Player 2 TeamColorSelector prefab spawns at the hierarchy root, but spawns as a child of LobbyCanvas on Player 1’s view
Hi there,
Did you add the prefab to the list of spawnable objects in the network manager?
Yes, the prefab is in the list of spawnable objects.
Let me know if the above sequence of actions makes sense or needs clarification!
Hi, yes it makes sense to me.
Instantiate places the prefab at the transform specified, so on the host it is getting placed in LobbyCanvas. Spawn does not seem to instantiate it the same way, so it’s getting placed in the hierarchy root.
It’s safe to assume that the client can’t see the instantiated prefab since it’s not on a canvas. So you need to find a solution to get it into a canvas. Try maybe:
- Find a way to add it to the lobbyCanvas after it’s spanwed.
or - Give the prefab it its own canvas so doesn’t need to childed.
hope that helps!
Sounds good, I’ll try giving each prefab its own canvas.
Currently, the client has the prefab spawn on their side after connecting but before the scene is changed and the prefab is destroyed afterwards. Is there a way to solve this (besides DontDestroyOnLoad)?
If you want something to persist from the menu scene to the game scene, do not destroy on load should be the first thing to try. If the object you want to persist doesn’t have any data needed in the next scene, you could also just spawn it again on scene load.
This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.