How to handle when both client and server with Unity mirror?

3k views Asked by At

I am currently just trying to make it so my character is facing in the correct direction for all clients. That works but will not work when a client is both client and a server.

I've tried different methods of [command] with !islocalPlayer or [Client] attributes to no avail. It either flips my 2d character twice or does not call the function in the clients.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.CompilerServices;
using Mirror;

public class PlayerCtrl : NetworkBehaviour
{
    public float maxSpeed;
    private InputActions playerActionControls;
    private bool isFacingRight;
    private SpriteRenderer spriteRend;
    private Animator anim;

    private void Awake()
    {
        //rb2d = GetComponent<Rigidbody2D>();
        playerActionControls = new InputActions();
        spriteRend = GetComponent<SpriteRenderer>();
        isFacingRight = true;
        anim = GetComponent<Animator>();
    }

    private void OnEnable()
    {
        playerActionControls.Enable();
    }
    private void OnDisable()
    {
        playerActionControls.Disable();
    }

    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
        if (!isLocalPlayer) return;
        Vector2 movementInput = playerActionControls.Movement.WASD.ReadValue<Vector2>();
        FlipSprite(movementInput);
        StartWalkingAnim(movementInput);
        MovePlayer(movementInput);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    void MovePlayer(Vector2 movementInput)
    {
        Vector3 currentPosition = transform.position;
        currentPosition.x += movementInput.x * maxSpeed * Time.deltaTime;
        currentPosition.y += movementInput.y * maxSpeed * Time.deltaTime;
        transform.position = currentPosition;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    void StartWalkingAnim(Vector2 movementInput)
    {
        if (movementInput != Vector2.zero)
        {
            anim.SetBool("StartWalk", true);
        }
        else
        {
            anim.SetBool("StartWalk", false);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    void FlipSprite(Vector2 movementInput)
    {
        // Face left
        if (movementInput == Vector2.left && isFacingRight)
        {
            isFacingRight = !isFacingRight;
            //spriteRend.flipX = !spriteRend.flipX;
            RpcFlipSprite();
            CmdFlipSprite();
        }
        else if (movementInput == Vector2.right && !isFacingRight)
        {
            isFacingRight = !isFacingRight;
            //spriteRend.flipX = !spriteRend.flipX;
            RpcFlipSprite();
            CmdFlipSprite();
        }
    }

    [Command]
    void CmdFlipSprite()
    {
        spriteRend.flipX = !spriteRend.flipX;
    }

    [Client]
    void RpcFlipSprite()
    {
        spriteRend.flipX = !spriteRend.flipX;
    }
}
2

There are 2 answers

0
Joseph Luce On

If anyone else finds a better way, please lmk. I've basically executing the flip client side then telling the server to execute a flip for all other clients.

The problem with this is that the flip is not executed client side until the server tell the client to, even if that client was the one initiating it. The Docs say ClientRpc does this, but I cant seem to get it to work properly.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.CompilerServices;
using Mirror;

public class PlayerCtrl : NetworkBehaviour
{
    public float maxSpeed;
    private InputActions playerActionControls;
    private bool isFacingRight;
    private SpriteRenderer spriteRend;
    private Animator anim;

    private void Awake()
    {
        //rb2d = GetComponent<Rigidbody2D>();
        playerActionControls = new InputActions();
        spriteRend = GetComponent<SpriteRenderer>();
        isFacingRight = true;
        anim = GetComponent<Animator>();
    }

    private void OnEnable()
    {
        playerActionControls.Enable();
    }
    private void OnDisable()
    {
        playerActionControls.Disable();
    }

    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
        if (!isLocalPlayer) return;
        Vector2 movementInput = playerActionControls.Movement.WASD.ReadValue<Vector2>();
        FlipSprite(movementInput);
        StartWalkingAnim(movementInput);
        MovePlayer(movementInput);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    void MovePlayer(Vector2 movementInput)
    {
        Vector3 currentPosition = transform.position;
        currentPosition.x += movementInput.x * maxSpeed * Time.deltaTime;
        currentPosition.y += movementInput.y * maxSpeed * Time.deltaTime;
        transform.position = currentPosition;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    void StartWalkingAnim(Vector2 movementInput)
    {
        if (movementInput != Vector2.zero)
        {
            anim.SetBool("StartWalk", true);
        }
        else
        {
            anim.SetBool("StartWalk", false);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    void FlipSprite(Vector2 movementInput)
    {
        // Face left
        if (movementInput == Vector2.left && isFacingRight)
        {
            isFacingRight = !isFacingRight;
            CmdFlipSprite();
        }
        else if (movementInput == Vector2.right && !isFacingRight)
        {
            isFacingRight = !isFacingRight;
            CmdFlipSprite();
        }
    }

    [Command]
    void CmdFlipSprite()
    {
        RpcFlipSprite();
    }

    [ClientRpc]
    void RpcFlipSprite()
    {
         spriteRend.flipX = !spriteRend.flipX;
    }
}

EDIT 2: After reading as much material on this, I was able to reduce network traffic further using hooks. However, there is still a problem where if you are the host and client, you will execute the sprite flip twice. There doesn't seem to be any way to avoid calling the method twice. Will need a flag like isServerOnly or isClientOnly to avoid executing twice. Best practice is to move these unauthoritative operations to the client first.

There is also duplicated setting of the flip when the server sends the call to all clients, even the one that initiated it. This is wasted bandwidth. So every time the client flips, it will receive a call from the server to flip, redundant...

The framework seems limited on this issue and these boolean flags are anti-patterns and do not allow for extendable design. Wish there was a way to create a server side player class and a client side player class instead of mixing their responsibilities.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.CompilerServices;
using Mirror;
using UnityEngine.InputSystem;

public class PlayerCtrl : NetworkBehaviour
{
    public float maxSpeed;
    private InputActions playerActionControls;
    private SpriteRenderer spriteRend;
    private Animator anim;

    [SyncVar(hook = nameof(OnFacingChange))]
    private bool facing;

    private void Awake()
    {
        playerActionControls = new InputActions();
        InputAction wasd = playerActionControls.Movement.WASD;
        wasd.performed += context => OnMove();
        spriteRend = GetComponent<SpriteRenderer>();
        anim = GetComponent<Animator>();
        facing = spriteRend.flipX;
    }

    private void OnEnable()
    {
        playerActionControls.Enable();
    }
    private void OnDisable()
    {
        playerActionControls.Disable();
    }

    #region Client

    // Update is called once per frame
    [ClientCallback]
    void Update()
    {
        if (!isLocalPlayer) return;
        Vector2 movementInput = playerActionControls.Movement.WASD.ReadValue<Vector2>();
        SetWalkingAnim(movementInput);
        MovePlayer(movementInput);
    }

    [ClientCallback]
    void MovePlayer(Vector2 movementInput)
    {
        Vector3 currentPosition = transform.position;
        currentPosition.x += movementInput.x * maxSpeed * Time.deltaTime;
        currentPosition.y += movementInput.y * maxSpeed * Time.deltaTime;
        transform.position = currentPosition;
    }

    [ClientCallback]
    private void OnMove()
    {
        if (hasAuthority)
        {
            Debug.Log("OnMove");
            Vector2 movementInput = playerActionControls.Movement.WASD.ReadValue<Vector2>();
            SetSpriteFacing(movementInput);
        }
    }

    [ClientCallback]
    private void SetWalkingAnim(Vector2 movementInput)
    {
        if (movementInput != Vector2.zero)
        {
            anim.SetBool("StartWalk", true);
        }
        else
        {
            anim.SetBool("StartWalk", false);
        }
    }

    [ClientCallback]
    private void SetSpriteFacing(Vector2 movementInput)
    {
        if (movementInput == Vector2.left && !spriteRend.flipX)
        {
            Debug.Log("SetSpriteFacing, left");
            if (isClientOnly)
            {
                spriteRend.flipX = !spriteRend.flipX;
            }
            CmdFlipSprite();
        }
        else if (movementInput == Vector2.right && spriteRend.flipX)
        {
            Debug.Log("SetSpriteFacing, right");
            if (isClientOnly)
            {
                spriteRend.flipX = !spriteRend.flipX;
            }
            CmdFlipSprite();
        }
    }

    private void OnFacingChange(bool oldFacing, bool newFacing)
    {
        Debug.Log("OnFacingChange");
        spriteRend.flipX = newFacing;
    }

    #endregion

    #region Server

    [Command]
    private void CmdFlipSprite()
    {
        Debug.Log("CmdFlipSprite");
        //spriteRend.flipX = !spriteRend.flipX;
        facing = !facing;
    }

    #endregion
}
0
hypeofpipe On

I'm not an expert in Unity Mirror, but I found this solution:

  public override void OnStartServer()
    {
        base.OnStartServer();

        _bodiesPool = GameObject.Find("GravityAspect").GetComponent<Bodies>();
        _bodiesPool.Add(this);
    }

    public override void OnStartLocalPlayer()
    {
        base.OnStartLocalPlayer();

        if (isServer) return;

        if (_bodiesPool == null)
        {
            _bodiesPool = GameObject.Find("GravityAspect").GetComponent<Bodies>();    
        }
        
        _bodiesPool.Add(this);
    }

If you host server + client, you just put OnStartServer, it does it's job for your client (as host) and server stuff.

Then you have OnStartLocalPlayer(), it's when somebody connects. We check, if it's not server (because we already ran similar logic in OnStartServer, we don't want to duplicate), and then we do the stuff.

I hope that will help you.