射击伤害同步
单例模式
我们在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();
}
}
评论区