射击伤害同步

单例模式

我们在GameManager.cs 文件中进行一些游戏中的全局状态管理,为了节省资源、方便调用等,我们可以将GameManager对象写成单例模式。具体代码如下:

public static GameManager singleton;

private void Awake()
{
    if (singleton == null)
    {
        singleton = this;
    }
}

OnNetworkSpawn()函数

OnNetworkSpawn()函数在客户端成功连接服务端后执行。

场景:三个玩家匹配作战,一共会绘制四幅游戏地图(包含着本地玩家的所有要素)。

在联机对战中,如果当前玩家是player1,那么在成功加入网络后,会执行 OnNetworkSpawn这个函数一次,将player1加入到players中。

显然,对于服务器端来说,这个操作也要执行一次;那么是不是就这两次了呢?

答案显然不是,当player2成功加入网络之后,player1也和player2存在同一个server端里,那么同样的操作又会执行一遍。

因此如果有三个玩家的话,player1加入到players里,这个行为一共会发生四次。

怎么在客户端1中,同步player2和player3?

目前情况是client1已经同步了player1和player2。

首先,第三名玩家加入到服务器端之后,服务器端会根据当前的信息,来传给第三名玩家,并且告诉他当前server的信息。

然后服务器端加上player3的信息,并且告诉player3,player1和player2的一些信息,并且将其渲染在player3的gamemap中。

当服务器端接受到player3的信息后,也会将这个信息广播告诉给player1和player2,接受到信息后再各自渲染出player3。

在画面中间贴上准星

在贴准星的时候,我们最好使用rowImage类型的图片类型。

RowImage:显示 UI 系统的 Texture2D(未加工的图片,也就是直接下载导入即可使用)

玩家伤害同步

PlayerObject.cs 中,定义maxHealth 表示最大血量,默认值为100,可以修改。再定义currentHealth属性,注意这是一个需要同步的变量,所以类型必须是NetworkVariable<int> ,并且需要初始化。最后需要定义isDead ,用于表示玩家是否死亡,这同样是一个需要同步的变量,因为一个玩家死亡是需要其他客户端的。

[SerializeField]
private int maxHealth = 100;

public NetworkVariable<int> currentHealth = new NetworkVariable<int>(100);

private NetworkVariable<bool> isDead = new NetworkVariable<bool>(false);

public void Setup()
{
    SetDefaults();
}

private void SetDefaults()
{
  if (IsServer)
  {
    currentHealth.Value = maxHealth;
    isDead.Value = false;
  }
}
public void TakeDamage(int damage)
{
  if (isDead.Value) return;
  currentHealth.Value -= damage;
  if (currentHealth.Value <= 0)
  {
    currentHealth.Value = 0;
    isDead.Value = true;
  }
}

在PlayerSetup.cs中,在玩家连接到服务器后,在OnNetworkSpawn()函数中,执行一些初始化工作,例如对Player命名,将Player加入字典等。

public class PlayerSetup : NetworkBehaviour
{
    public override void OnNetworkSpawn()
    {
        base.OnNetworkSpawn();
        string name = "Player " + GetComponent<NetworkObject>().NetworkObjectId;
        PlayerObject player = GetComponent<PlayerObject>();
        player.Setup();
        GameManager.singleton.RegisterPlayer(name, player);
    }
}

玩家死亡即玩家复活

死亡

玩家去世的操作在两个地方都需要执行。

server端执行一遍,然后每个client端的去世的那名玩家的去世函数也会调用一遍。

为了方便实现,这里我们引入了ClientRpc,表示每个客户端会调用引入该注解的函数,但是server端却不会被调用。

因此需要我们分开写函数才行,

当玩家去世的时候,需要将玩家的组件和碰撞检测的功能全部取消掉。

组件:Collider组件,没有继承Behavior,因此需要我们单独实现

脚本:玩家控制、玩家输入、玩家射击

复活

咱们player的位置坐标,是由客户端来控制的本地实现了(client Network transform)

本质上就是更新player的位置,以及将哪些禁用的组件恢复至生前的状态即可

用到了协程技术:

协程本质上是一个用返回类型 IEnumerator 声明的函数,并在主体中的某个位置包含 yield return 语句。

yield return null 行是暂停执行并随后在下一帧恢复的点。

要将协程设置为运行状态,必须使用 StartCoroutine 函数:

可以将其理解为c++中的sleep函数,需要注意的是这个函数使用的是异步操作;如果是同步操作的话,那么游戏画面将也会消失,直至进程结束。

client端和server端生成:动画效果不太好,经由弹簧组件后,动画效果会差一些。

server端:和本地效果相比,效果好多了(对于同时开启客户端和服务器端而言,即host和cli)

(开启服务器端,两个客户端,即server cli cli)玩家去世后会直接在原地出现,没有从天而降

的效果(可能是我们本地是客户端控制玩家移动,而重生则是在server端实现)

islocal:效果最好

PlayerSetup.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;

public class PlayerSetup : NetworkBehaviour
{

    [SerializeField]
    private Behaviour[] componentsToDisable;

    private Camera sceneCamera;

    public override void OnNetworkSpawn()
    {
        base.OnNetworkSpawn();
        if (!IsLocalPlayer)
        {
            DisableComponents();
        }
        else
        {
            sceneCamera = Camera.main;
            if (sceneCamera != null)
            {
                sceneCamera.gameObject.SetActive(false);
            }
        }
        string name = "Player " + GetComponent<NetworkObject>().NetworkObjectId;
        PlayerObject player = GetComponent<PlayerObject>();
        player.Setup();
        GameManager.singleton.RegisterPlayer(name, player);
    }

    private void DisableComponents()
    {
        for (int i = 0; i < componentsToDisable.Length; i++)
        {
            componentsToDisable[i].enabled = false;
        }
    }

    public override void OnNetworkDespawn()
    {
        base.OnNetworkDespawn();
        GameManager.singleton.UnRegisterPlayer(transform.name);
        if (sceneCamera != null)
        {
            sceneCamera.gameObject.SetActive(true);
        }
    }

}

PlayerObject.cs

using System.Collections;
using Unity.Netcode;
using UnityEngine;

public class PlayerObject : NetworkBehaviour
{
  [SerializeField]
  private int maxHealth = 100;

  public NetworkVariable<int> currentHealth = new NetworkVariable<int>(100);

  private NetworkVariable<bool> isDead = new NetworkVariable<bool>(false);

  [SerializeField]
  private Behaviour[] componentsToDisabled;

  private bool[] componentEnabled;

  private bool colliderEnabled;

  public void Setup()
  {
    componentEnabled = new bool[componentsToDisabled.Length];

    Collider collider = GetComponent<Collider>();
    colliderEnabled = collider.enabled;

    for (int i = 0; i < componentsToDisabled.Length; i++)
    {
      componentEnabled[i] = componentsToDisabled[i].enabled;
    }
    SetDefaults();
  }

  private void SetDefaults()
  {
    for (int i = 0; i < componentsToDisabled.Length; i++)
    {
      componentsToDisabled[i].enabled = componentEnabled[i];
    }
    Collider collider = GetComponent<Collider>();
    collider.enabled = colliderEnabled;

    if (IsServer)
    {
      currentHealth.Value = maxHealth;
      isDead.Value = false;
    } 
  }

  public void TakeDamage(int damage)
  {
    if (isDead.Value) return;
    currentHealth.Value -= damage;
    if (currentHealth.Value <= 0)
    {
      currentHealth.Value = 0;
      isDead.Value = true;
      DieOnServer();
      DieClientRpc();
    }
  }

  private void DieOnServer()
  {
    Die();
  }

  [ClientRpc]
  private void DieClientRpc()
  {
    Die();
  }

  private void Die()
  {
    for (int i = 0; i < componentsToDisabled.Length; i++)
    {
      componentsToDisabled[i].enabled = false;
    }
    Collider collider = GetComponent<Collider>();
    collider.enabled = false;

    StartCoroutine(Respawn());
  }


  private IEnumerator Respawn()
  {
    yield return new WaitForSeconds(3f);

    SetDefaults();

    if (IsLocalPlayer)
    {
      transform.position = new Vector3(0f, 10f, 0f);
    }
  }
}

PlayerShoot.cs

using Unity.Netcode;
using UnityEngine;

public class PlayerShoot : NetworkBehaviour
{
    private const string PLAYER_TAG = "Player";

    [SerializeField]
    private PlayerWeapon weapon;

    private Camera eyesCamera;

    void Start()
    {
        eyesCamera = GetComponentInChildren<Camera>();
    }


    private void Shoot()
    {
        RaycastHit hit;
        if (Physics.Raycast(eyesCamera.transform.position, eyesCamera.transform.forward, out hit, weapon.range))
        {
            if (hit.collider.tag == PLAYER_TAG)
            {
                ShootServerRpc(hit.collider.name, weapon.damage);
            }
        }
    }

    [ServerRpc]
    private void ShootServerRpc(string name, int damage)
    {
        PlayerObject player = GameManager.singleton.GetPlayer(name);
        player.TakeDamage(damage);
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetButtonDown("Fire1"))
        {
            Shoot();
        }
    }
}

GameManager.cs

using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    private Dictionary<string, PlayerObject> players = new Dictionary<string, PlayerObject>();

    public static GameManager singleton;

    private void Awake()
    {
        if (singleton == null)
        {
            singleton = this;
        }
    }

    public PlayerObject GetPlayer(string name)
    {
        return players[name];
    }

    public void RegisterPlayer(string name, PlayerObject player)
    {
        player.transform.name = name;
        players.Add(name, player);
    }

    public void UnRegisterPlayer(string name)
    {
        players.Remove(name);
    }

    private void OnGUI()
    {
        GUILayout.BeginArea(new Rect(200f, 200f, 200f, 400f));
        GUILayout.BeginVertical();

        GUI.color = Color.black;
        foreach (string name in players.Keys)
        {
            GUILayout.Label(name + ":" + GetPlayer(name).currentHealth.Value);
        }

        GUILayout.EndVertical();
        GUILayout.EndArea();
    }
}