I try to sync float value between server-client.
On server screen have boss HpBar script with MAXHP value setup in scene.
player prefab have their own boss HP UI and localhp script that will get currently boss HP value from a HpBar script in the server.
when the user press Attack button it will send damage value to HpBar script then it should update to player prefab. but it doesn't sync together. thanks
picture here
https://imgur.com/a/KrYtC3T
localPlayerHPBAR Script:
public class localHpBar : NetworkBehaviour
{
public HpBar serverHp;
public float localhpPoint;
public Image localhpBar;
// Start is called before the first frame update
void Start()
{
serverHp = GameObject.FindObjectOfType<HpBar>();
//Get OBJ of serverHP
}
public void sendDamage(float dmg)
{
serverHp.TakeDamage(dmg); //SendDamageToserver
}
// Update is called once per frame
void Update()
{
//get HP point from server
localhpPoint = serverHp.sum;
localhpBar.fillAmount = localhpPoint;
}
}
SERVER HpBar
public class HpBar : NetworkBehaviour
{
public Image HP;
public float MaxHP;
float currentHP;
public float localhp;
[SyncVar] public float sum;
void Start()
{
//set CURRENT HP
currentHP = MaxHP;
}
[ClientRpc]
void rpcDamage(float dmg)
{
sum = currentHP / MaxHP;
HP.fillAmount = sum;
}
public void TakeDamage(float dmg)
{
//recieve DamageFrom client
currentHP = currentHP - dmg;
rpcDamage(currentHP);
}
}
Note that a [SyncVar] is only synchronized Server → Client(s).
Never in the other direction. Changing this value on the client won't affect the server or other clients.
You are changing sum using a [ClientRpc]. A [ClientRpc] is invoked by the server but executed on all clients. You are not even allowed to call this as a client.
What you rather should do is making the TakeDamage method a [Command] so it is invoked by the client but only executed on the server where you then are allowed to change the synced value like
// This is called by clients but executed on the server
[Command]
public void CmdTakeDamage(float dmg)
{
currentHP -= dmg;
sum = currentHP / MaxHP;
// actually pass on the new sum
rpcDamage(sum);
}
// This is called ONLY by the server but executed on all clients
[ClientRpc]
// also make sure the prefix is capital Rpc otherwise it might not work
void RpcDamage(float newSum)
{
// if you set sum here then you should simply remove the
// [SyncVar] since you already sync it "manually"
// I would actually do this since it is not guaranteed that the syncvar
// transfers its new value before this method is executed
// on the other hand syncvar is additionally synchronized when connecting a new client
// so you might have to do it "manually"
sum = newSum;
HP.fillAmount = sum;
}
Related
I would like to learn the basics of testing, how to make a test
I am using the new unity input system (OnMove), I store that input in a vector2, later I use that vector2 in a function that moves the character (ProcessMovementOfShip).
The game works, I can move the player around with WASD, but I would love to have a test that verifies that the function responsible for movement works.
I have tried watching a couple of youtube videos about testing, it feels like the entry into tests are getting to steep, I would love to learn it, I can see the importance of it, I just dont know what I am doing and how to solve the problem at hand and I am starting to feel I should just put the whole thing on a shelf and hopefully return to it later.
How do I test that the player has moved?
PlayMode Test
public class player_movement
{
[UnityTest]
public IEnumerator player_moves_when_processship_is_fed_a_vector()
{
var gameObject = new GameObject();
var playerMovement = gameObject.AddComponent<PlayerMovement>();
Vector2 startPosition = playerMovement.transform.position;
playerMovement.ProcessMovementOfShip(new Vector2(1, 0));
yield return new WaitForFixedUpdate();
Vector2 endPosition = playerMovement.transform.position;
Assert.AreNotEqual(startPosition, endPosition);
}
}
EditMode Test
public class Movement
{
[Test]
public void start_position_of_player_is_0()
{
var gameObject = new GameObject();
var playerMovement = gameObject.AddComponent<PlayerMovement>();
var startPostion = playerMovement.transform.position;
playerMovement.ProcessMovementOfShip(new Vector2(1,0));
var endPosition = playerMovement.transform.position.x;
Assert.AreNotEqual(startPostion, endPosition);
}
}
PlayerMovement.cs
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerMovement : MonoBehaviour
{
[Header("Player Movement")]
[Range(5f, 20f)][SerializeField] float _moveSpeed = 15f;
private Rigidbody2D _rigidBody;
private Vector2 _rawInput;
void Awake()
{
_rigidBody = GetComponent<Rigidbody2D>();
if (_rigidBody == null) Debug.Log("No RigidBody2D detected!");
}
void FixedUpdate()
{
ProcessMovementOfShip(_rawInput);
}
public void ProcessMovementOfShip(Vector2 input)
{
Vector3 delta = input * _moveSpeed * Time.fixedDeltaTime;
delta += transform.position;
_rigidBody.MovePosition(delta);
}
private void OnMove(InputValue value)
{
Vector2 _rawInput = value.Get<Vector2>();
}
}
error
I try to check that the position of the character has changed, I get a "NullReferenceException" System.NullReferenceException : Object reference not set to an instance of an object
You would need to decouple the class with what Unity does and what you do.
I'll go with a simple example to demonstrate.
public class Provider : MonoBehaviour, IProvider
{
[SerializeField] private SomeClass m_someClass;
private Logic m_logic;
void Start()
{
m_logic = new Logic(this);
}
}
public interface IProvider
{}
public class Logic
{
private IProvider m_provider;
public Logic (IProvider provider)
{
m_provider = provider;
}
public int MethodToTest(int someValue)
{
}
}
At this point, we have three parts, the unity part where you will put everything that is Unity related, called Provider. This can be the Unity lifecycle such as Update, Start, any event related that the engine reports and all the connections. In this case, SomeClass is an object with some relevant data.
We have the interface which is the bridge between Provider and Logic. It will have its importance later on in the test.
And then the logic where all the code is stored.
We want to test MethodToTest, in which we are going to take an information from SomeClass and add someValue to it and return that value.
First thing, Logic does not know about Provider, it connects to it via the interface. We want to get some data from SomeClass, we will consider it has a member called Data returning an integer. We can now update the provider and the interface.
public class Provider : MonoBehaviour, IProvider
{
[SerializeField] private SomeClass m_someClass;
private Logic m_logic;
public int SomeClassData => m_someClass.Data;
void Start()
{
m_logic = new Logic(this);
}
}
public interface IProvider
{
int SomeClassData { get; }
}
You may think to pass the SomeClass object from the interface but doing it like this removes the dependency between Logic and SomeClass, they simply don't have any connection.
Now in Logic we can add the code:
public int MethodToTest(int someValue)
{
return m_provider.SomeClassData + someValue;
}
We can now move to the test
[Test]
public void LogicTest_MethodToTest()
{
// this would likely be in the Setup method to reuse provider and logic
IProvider provider = Subsitute.For<IProvider>();
Logic logic = new Logic(provider);
// Use NUnit to have the empty IProvider to return a given value when
// SomeClassData is called (else it returns default 0)
// This means that when MethodToTest will call for SomeClassData, it will return 5
provider.SomeClassData.Returns(5);
int result = logic.MethodToTest(10);
Assert.AreEqual(result, 10);
}
Thanks to the Interface, we no longer need to create all kind of object, the test is limited and NSubstitute takes care of the mocking (creating an empty object of an interface).
Now, this is fine but only one test is not so good, so we can start adding more test. We can copy paste the Test, rename the method and change the value to check but this is redundant. Let's use TestCase instead.
[TestCase(0, 0, 0)]
[TestCase(5, 0, 5)]
[TestCase(5, 5, 10)]
public void LogicTest_MethodToTest(int someClass, int someValue, int expected)
{
// this would likely be in the Setup method to reuse provider and logic
IProvider provider = Subsitute.For<IProvider>();
Logic logic = new Logic(provider);
// Assign the given value for flexible testing
provider.SomeClassData.Returns(someClass);
int result = logic.MethodToTest(someValue);
// compare with expectation
Assert.AreEqual(result, expected);
}
Each value given in the test case will be passed as parameter to the method when NUnit calls it. Instead of static values, you can now run a set of test to make sure the method works in many cases. You should then add corner cases like negative values or max int and so on to fiddle with your method until all green.
In this context, we do not text the Unity part. Simply because we know it works. Unity did the tests already so there is no need to check if the input works or if Start is being called properly.
Logic and the test rely on the fact that SomeClass would return specific values. In this case, we are only testing Logic so we assume SomeClass was implemented and tested properly so we don't need to test it here.
The IProvider can mock values via the return method.
To sum it up: Remove all the logic from Unity class (Provider) and move them to a Logic class. Create a bridge between Unity class and logic via an interface (IProvider). Anything needed in Logic from Provider goes through IProvider so to remove any dependency.
In the test, create the mock IProvider and pass it to the newly created Logic object. You can start testing.
The real benefit is that you now know the method works and if you were to modify it later on, you have your test to confirm it still does it all right.
When player press key if he touched other player he try markPlayer
When client touched host-player, log only "CmdServer" and stop, but on host-player, its full complete all methods and change color on another character;
Client code
//somemovementClass
private void TriggerEntered(PlayerContext playerContext)
{
Debug.Log($"Collided with {playerContext.name}");
CmdServer(playerContext);
}
[Command]
public void CmdServer(PlayerContext playerContext)
{
Debug.Log($"CmdServer");
playerContext.MarkPlayer();
}
// PlayerContext.cs
[SyncVar(hook = nameof(OnPlayerGetDamage))]
private bool wasDamaged;
[Server]
public void MarkPlayer()
{
Debug.Log("On Server");
wasDamaged = true;
}
private void OnPlayerGetDamage(bool oldState, bool newState)
{
Debug.Log("On Get Damage");
cashedMaterial.color = Color.red;
}
I think I solved the problem, although I'm not sure that the problem was specifically in this, but still, I didn't use the Monobehaviour class and hoped that the command would compile correctly, because usually mirror throws errors if something is wrong. I just moved the cmd method to a separate class from networkbehaviour
Long story: I have made a multiplayer chat using Mirror/Unet that works. I have made it so after x number of seconds, the gameobject that displays the chat (“textContainer”) goes inactive. I would like it so that when client 1 presses “Submit”, all the client’s gameobjects for textContainer goes active again. But it only works on the client that presses Submit, and not on ALL clients. I have set up the functions in [Client] [Command] and [ClientRpc] but am still getting an error about no authority on object.
I think it is because client 1 does not have authority to request client 2 to activate their UI panel. I thought the Command and ClientRpc would have fixed this issue?
Short story: So, for simplicity, say when client 1 presses the Input for “Submit”, I would like all client’s textContainer GameObjects to go active.
I am hoping someone might be able to point me in the right direction. Cheers.
This script would be attached to the player prefab.
public GameObject textContainer;
[Client]
void Update()
{
if (Input.GetAxisRaw("Submit") == 1)
{
CmdActivateChatClientRPC();
textContainer.SetActive(true);
}
}
[Command]
private void CmdActivateChatClientRPC()
{
ActivateChatClientRPC();
}
[ClientRpc]
private void ActivateChatClientRPC()
{
textContainer.SetActive(true);
}
You are not checking whether you have the authority over this object.
void Update()
{
// Does your device have the authority over this object?
// only then you can invoke a Cmd
if(!hasAuthority) return;
if (Input.GetAxisRaw("Submit") == 1)
{
CmdActivateChatClientRPC();
textContainer.SetActive(true);
}
}
[Command]
private void CmdActivateChatClientRPC()
{
ActivateChatClientRPC();
}
// This will be executed on ALL clients!
// BUT only for this one instance of this class
[ClientRpc]
private void ActivateChatClientRPC()
{
textContainer.SetActive(true);
}
If it is rather about only you trying to invoke a command on an object that belongs to the server it gets tricky ;)
You will have to relay the command through your local player object and then on the server forward the call to the according object e.g. referencing the actual target via it's Network identity.
To me it sounds a bit like you are having each player prefab has this class and you want to invoke the RPC on all of them.
This is something that can only be done by the server like e.g.
void Update()
{
// Does your device have the authority over this object?
// only then you can invoke a Cmd
if(!hasAuthority) return;
if (Input.GetAxisRaw("Submit") == 1)
{
CmdActivateChatClientRPC();
textContainer.SetActive(true);
}
}
[Command]
private void CmdActivateChatClientRPC()
{
foreach (var player in NetworkManager.singleton.client.connection.playerControllers)
{
if (player.IsValid)
{
player.gameObject.GetComponent<YourScriptName>().ActivateChatClientRPC();
}
}
}
Seems that .client.connection.playerControllers was removed.
I guess in that case you would need a custom NetworkManager and keep track of them yourself via OnServerConnect and OnServerDisconnect somewhat like
public CustomNetworkManager : NetworkManager
{
public static readonly HashSet<NetworkConnection> players = new HashSet<NetworkConnection>();
public override void OnServerConnect(NetworkConnection conn)
{
base.OnServerConnect(conn);
players.Add(conn);
}
public override void OnServerDisconnect(NetworkConnection conn)
{
base.OnServerDisconnect(conn);
players.Remove(conn);
}
}
and then instead access
foreach (var player in CustomNetworkManager.players)
{
player.identity.GetComponent<YourScriptName>().ActivateChatClientRPC();
}
How about
client->server: request to activate
and then
server->client: singal
[SyncVar(hook = nameof(AcivateText))]
bool textActivated = false;
[Command]
private void CmdActivateChatClientRPC()
{
ActivateChatClientRPC();
}
void AcivateText(bool oldval, bool newval)
{
textContainer.SetActive(newval);
}
I'm developing a multiplayer game based on turns.
So I have an script named gameController which is the one who has the global timer to alter the turns and also choose which players are attacking and which players are defending in the current turn.
This script is a singleton attached to a gameObject with a network identity on it (This network identity has no checkboxes marked to be controller by the server). I tried to have this game object spawned to all the clients when the server connects and also to have the game object already in the scene (booth cases aren't working).
Well, the main problem is that in the gameController script I have a checker in the update function to check if any new player is connected. In case is connected, it should call a syncEvent named EventOnNewPlayerAddedDelegate (I tried to call it directly, also using [command] and using [ ClientRpc]) to let the players know that they have to call their function named "OnRegisterPlayer", which is a function in their own player script that calls a function on gameController script, passing the player object (I tried via command and rpc also), something like this: GameController.instance.RegisterPlayer(this);
So anyone knows how can I trigger this SyncEvent to register the player to a non-player object controlled by the server?
Thank you very much.
I attach here a brief of booth scripts to make it easier to understand:
GameController:
public class GameController : NetworkBehaviour
{
public delegate void OnNewPlayerAddedDelegate();
[SyncEvent]
public event OnNewPlayerAddedDelegate EventOnNewPlayerAddedDelegate;
List<GamePlayerManager> players = new List<GamePlayerManager>();
public static GameController instance { get; private set; }
float timePerTorn = 30f;
bool isCountdown = false;
[System.NonSerialized]
public float countdownTime;
int roundNumber = 0;
int NumberOfPlayers;
int NumberOfPlayersChanged;
void Awake()
{
Debug.Log("Awaking ");
if (instance != null)
{
Debug.Log("Destoring ");
DestroyImmediate(this);
return;
}
instance = this;
}
// Use this for initialization
void Start()
{
if (!isServer)
{
return;
}
players = new List<GamePlayerManager>();
StartCountdown(5f);//20
}
void Update()
{
if (isServer)
{
if (isCountdown)
{
countdownTime -= Time.deltaTime;
if (countdownTime <= 0)
{
AlterGlobalTurns();
}
}
}
NumberOfPlayers = NetworkManager.singleton.numPlayers;
if (NumberOfPlayersChanged != NumberOfPlayers)
{
Debug.Log("num of players changed ---> " + NumberOfPlayers);
NumberOfPlayersChanged = NumberOfPlayers;
EventOnNewPlayerAddedDelegate();
//RpcNewPlayerAdded();
//CmdNewPlayerAdded();
}
}
[ClientRpc]
void RpcNewPlayerAdded()
{
Debug.Log("---------------- RpcNewPlayerAdded ------------");
EventOnNewPlayerAddedDelegate();
}
[Command]
void CmdNewPlayerAdded()
{
Debug.Log("---------------- CmdNewPlayerAdded ------------");
EventOnNewPlayerAddedDelegate();
}
public void RegisterPlayer(GamePlayerManager player)
{
Debug.Log("player ---> " + player.name);
if (players.Contains(player))
{
return;
}
Debug.Log("players ---> " + players);
players.Add(player);
}
}
PlayerScript:
public class GamePlayerManager : NetworkBehaviour
{
[System.NonSerialized]
public bool isPlayingOnTorn = true;
void Awake()
{
GameController.instance.EventOnNewPlayerAddedDelegate += OnRegisterPlayer;
}
private void Start()
{
if (!isLocalPlayer)
{
return;
}
}
public override void OnStartServer()
{
GameObject gc = (GameObject)Instantiate(NetworkManager.singleton.spawnPrefabs[2], transform.position, transform.rotation);
NetworkServer.Spawn(gc);
}
void OnRegisterPlayer(){
if (isLocalPlayer)
{
//GameController.instance.RegisterPlayer(this);
//RpcRegisterPlayer();
CmdRegisterPlayer();
}
}
[Command]
void CmdRegisterPlayer(){
Debug.Log("-------------Command Register player -------------");
GameController.instance.RegisterPlayer(this);
}
[ClientRpc]
void RpcRegisterPlayer()
{
Debug.Log("------------- RPC REgister Player -------------");
GameController.instance.RegisterPlayer(this);
}
}
I think I already saw the problem here.
The problem is that GameController is spawned by the playerScript(just if the player is also the host) in the function OnStartServer(). So the first problem is that the GameController doesn't detect the host player because it is not a new connections. And the second problem is when a second client is connected, the GameController detect the client connection and send the event signal faster than the client wakeup, so when the client is already working the signal is gone.
I solved the problem deleting this signal and checking directly in the playerScript if the GameController exists then check if the player is already registered.
My question now is that if there is anyway to instance and awake the server objects before the player(Host), which I understand that the answer is "no" because as the player is the host it needs to be running to have the server.
And the second question is if there is anyway or any signal to know that a new player is connected, and wait until it is awaked.
Thank you very much.
You can check the hole thread at Unity forums: https://forum.unity.com/threads/multiplayer-syncevent-problem-with-non-player-objects.589309/
I want to do the following.
When a player joins the room, he should receive all the gameobjects' (with a photonview) locations.
This should happen only once when the player enters the room.
How could this be implemented?
The best solution for sending only one piece of information is to use RPC messages system.
[PunRPC]
void changePos(int x, int y, int z)
{
Debug.Log("new pos =" + x + "," + y + ","+z);
}
PhotonView photonView = PhotonView.Get(this);
photonView.RPC("changePos", PhotonTargets.All, 1,1,1 );
You can read more about RPC messages here: https://doc.photonengine.com/en/pun/current/tutorials/rpcsandraiseevent
EDIT:
I'm guessing you're connecting through:
PhotonNetwork.JoinRoom(this.roomName);
So in the place resposible for connection to the server you can use:
public void OnCreatedRoom()
{
Debug.Log("OnCreatedRoom");
}
public void OnJoinedRoom()
{
Debug.Log("OnJoinedRoom");
RPCserver.Instance.AddNewPlayer(login)
}
And then, you can have a bridge to store all RPC enabled methods:
public class RPCserver : Singleton
{
public List<Player> players = new List<Player>();
public void addNewPlayer(string name)
{
Player p = new Player(name);
players.Add(p);
if( p.isNewPlayer() ) fetchOtherObjectsPositions();
}
private void fetchOtherObjectsPositions(){
// Go through all neccesery objects, and send their position via RPCserver
}
}
Is that make sense ?