前言
随着Unity版本的更新,新版的网络系统Multiplayer也渐渐地越来越被重视,5.3.4版本测试很好用。使用这套网络系统可以轻松开发联机网络游戏,而且其中封装的API也针对于开发者的层次作了区分。HighLevelAPI(简称HLAPI)针对于简单的网络系统搭建,封装的比较严重,只需轻松几部即可完成网络环境的搭建;LowLevelAPI(简称LLAPI)偏向底层,网络环境的搭建,需要依靠底层类层层搭建,但较为灵活。根据不同的需求,可以选择不同的API,当然通常HLAPI和LLAPI是混合起来一起用的。本文会简单讲解Multiplayer的API架构,主要通过项目将所有内容串联。
-
HLAPI架构图
首先给大家看一下UnityAPI中提供的一张Multiplayer的HLAPI架构图,清晰的了解我们常用的类的层次。
- Transport/Configuration — 底层API类
- Connection/Reader/Writer — 消息发送类、序列化与反序列化类
- NetworkClient/NetworkServer — 网络环境搭建类
- NetworkIDentity/NetworkBehaviour — 网络对象状态同步
- NetworkManager — 网络游戏控制(一个组件搞定一个网络)
- NetworkLobbyManager — 集成了网络游戏大厅功能
- NetworkTransform/NetworkAnimator — 引擎继承的状态同步组件
-
使用基础类(NetworkServer/NetworkClient)搭建网络环境
服务器端
NetworkServer.Listen(7777);//创建服务器监听本机网卡7777端口客户端
NetworkClient client;//创建客户端对象
client.Connect("127.0.0.1",7777);//连接服务器
-
使用基础类(NetworkServer/NetworkClient)创建网络游戏对象
- 客户端
ClientScene.Ready(msg.conn); //通知服务器已准备完毕
ClientScene.RegisterPrefab(playerPrefab); //注册网络预设体
ClientScene.AddPlayer(0); //通知服务器实例化预设体 - 服务器(只有服务器才能创建网络对象)
GameObject player = (GameObject)Instantiate(playerPrefab);
//给予该客户端该对象的权限
NetworkServer.AddPlayerForConnection(netMsg.conn, player, 0);
//卵生[同步到其他客户端]
NetworkServer.Spawn(player);
- 客户端
-
远程过程调用(RPC)
Command:由客户端发送给服务器[在服务器执行方法]
ClientRPC:由服务器发送给客户端[在客户端执行方法]
客户端调服务器方法(Command方法名必须以Cmd开头)
[Command]
/// <summary>
/// 发射炮弹
/// </summary>
void CmdFire ()
{
GameObject bullet = (GameObject)Instantiate (MyLobbyManager.instance.spawnPrefabs [0],firePoint.position, firePoint.rotation);
bullet.GetComponent<Rigidbody> ().velocity = bullet.transform.forward * 20;
NetworkServer.Spawn (bullet);
}服务器调客户端方法(ClientRPC方法必须以Rpc开头)
[ClientRpc]
/// <summary>
/// 播放特效稍后销毁
/// </summary>
/// <param name="eff">Eff.</param>
void RpcStopEffect (GameObject eff)
{
eff.GetComponent<ParticleSystem> ().Play ();
Destroy (eff, 1.05f);
}
当然是用NetworkManager/NetworkLobbyManager组件同样可以搭建网络环境,且更为方便,这里不再赘述,详见项目。
-
实战项目坦克大战
挑几个重点脚本看看
1.大厅管理
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
public class MyLobbyManager : NetworkLobbyManager
{
//单例
public static MyLobbyManager instance;
//是否开启切换场景标志位
public bool beginChange = false;
//背景音乐
public GameObject backAud;
//玩家位置编号
private int playerPositionIndex = 0;
void Awake ()
{
instance = this;
}
void Start ()
{
DontDestroyOnLoad (backAud);
}
/// <summary>
/// 当所有玩家都已准备完毕
/// </summary>
public override void OnLobbyServerPlayersReady ()
{
//启动协程等待动画播放完毕
StartCoroutine (PlayProgress ());
//遍历所有客户端发送播放指令
foreach (NetworkLobbyPlayer item in lobbySlots) {
if (item) {
(item as MyLobbyPlayer).RpcBeginPlay ();
}
}
}
IEnumerator PlayProgress ()
{
//如果还没有开始切换场景,继续播放动画,保持等待
while (!beginChange) {
yield return null;
}
base.OnLobbyServerPlayersReady ();
}
//当服务器添加玩家对象时调用
public override void OnServerAddPlayer (NetworkConnection conn, short playerControllerId)
{
base.OnServerAddPlayer (conn, playerControllerId);
//判断是否在游戏场景而非游戏大厅
if (beginChange) {
//创建坦克
GameObject player = Instantiate (gamePlayerPrefab) as GameObject;
//通过新场景的NetworkStartPosition确定坦克的创建位置
player.transform.position = startPositions [playerPositionIndex++].position;
//设置坦克脚本中的网络变量--坦克编号
player.GetComponent<MyPlayer> ().tankNum = playerPositionIndex - 1;
//给予客户端该坦克的使用权限
NetworkServer.AddPlayerForConnection (conn, player, playerControllerId);
//卵生坦克
NetworkServer.Spawn (player);
}
}
}
2.大厅玩家
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.UI;
public class MyLobbyPlayer : NetworkLobbyPlayer
{
//玩家大厅名称显示
private Transform content;
//玩家大厅准备按钮
private Button readyButton;
//单例LobbyManager
private MyLobbyManager manager;
//倒计时进度条
private GameObject progress;
void Awake ()
{
manager = MyLobbyManager.instance;
content = GameController.instance.content.transform;
readyButton = transform.GetChild (0).GetComponent<Button> ();
}
void Start ()
{
//设置当前对象到UI中显示
GameController.instance.SetParent (this.transform);
//获取进度条
progress = transform.parent.parent.parent.GetChild (3).gameObject;
//重置缩放
transform.localScale = Vector3.one;
//非主机客户端更新玩家名称
if (!isServer) {
CmdUpdateItemName ();
}
}
[Command]
public void CmdUpdateItemName ()
{
//服务器开始下发指令
RpcUpdateItemName ();
}
[ClientRpc]
public void RpcUpdateItemName ()
{
//非主机客户端设置字体颜色
content.GetChild (1).GetComponent<Image> ().color = Color.red;
//非主机客户端设置玩家名称
content.GetChild (1).GetChild (1).GetComponent<Text> ().text = "Player2";
}
/// <summary>
/// 本地玩家执行
/// </summary>
public override void OnStartLocalPlayer ()
{
base.OnStartLocalPlayer ();
//设置准备按钮可用
readyButton.interactable = true;
//移除所有监听
readyButton.onClick.RemoveAllListeners ();
//设置准备按钮事件监听
readyButton.onClick.AddListener (OnReadyButtonClick);
}
/// <summary>
/// 玩家准备按钮点击事件
/// </summary>
public void OnReadyButtonClick ()
{
//向服务器发送准备指令
SendReadyToBeginMessage ();
//移除该按钮所有事件监听
readyButton.onClick.RemoveAllListeners ();
//设置该按钮取消准备的事件监听
readyButton.onClick.AddListener (OnNotReadyButtonClick);
}
/// <summary>
/// 玩家取消准备按钮点击事件
/// </summary>
public void OnNotReadyButtonClick ()
{
//向服务器发送取消准备的指令
SendNotReadyToBeginMessage ();
//取消该按钮的所有事件监听
readyButton.onClick.RemoveAllListeners ();
//添加该按钮准备的事件监听
readyButton.onClick.AddListener (OnReadyButtonClick);
}
/// <summary>
/// 当客户端主播完毕后调用
/// </summary>
/// <param name="readyState">If set to <c>true</c> ready state.</param>
public override void OnClientReady (bool readyState)
{
base.OnClientReady (readyState);
//如果准备好了
if (readyState) {
//按钮文字显示为Done
readyButton.GetComponentInChildren<Text> ().text = "Done";
} else {
//否则显示Ready
readyButton.GetComponentInChildren<Text> ().text = "Ready";
}
}
[ClientRpc]
/// <summary>
/// 客户端开始播放切换场景动画
/// </summary>
public void RpcBeginPlay ()
{
progress.SetActive (true);
}
}
3.主场景玩家(坦克)
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.UI;
public class MyPlayer : NetworkBehaviour
{
[SyncVar]
//坦克编号
public int tankNum = 0;
[SyncVar]
//坦克血量
public int health = 100;
//坦克移动速度
public float tankMoveSpeed = 3f;
//坦克旋转速度
public float tankTurnSpeed = 10f;
//坦克发射的炮弹飞行速度
public float fireSpeed = 20f;
//声音片段
public AudioClip idle;
public AudioClip run;
private Rigidbody rig;
//观察点
private Transform targetPoint;
//坦克炮头
private Transform gun;
//发射点
private Transform firePoint;
//操纵轴
private float hor, ver, gunDir;
//坦克血条颜色
private Color[] colors = new Color[]{ Color.red, Color.green };
//坦克血条背景图片
private Image healthColor;
//坦克血条
private Slider healthSlider;
//结果UI
private GameObject resultUI;
//声音片段
private AudioSource aud;
void Awake ()
{
rig = GetComponent<Rigidbody> ();
aud = GetComponent<AudioSource> ();
targetPoint = transform.Find ("TargetPoint");
gun = transform.Find ("TankTurret");
firePoint = transform.Find ("TankTurret/FirePoint");
healthColor = transform.Find ("HealthCanvas/Slider/Fill Area/Fill").GetComponent<Image> ();
healthSlider = transform.Find ("HealthCanvas/Slider").GetComponent<Slider> ();
resultUI = GameObject.FindWithTag ("UI");
}
/// <summary>
/// 本地玩家Start触发
/// </summary>
public override void OnStartLocalPlayer ()
{
//如果是本地玩家
if (isLocalPlayer) {
//设置摄像机跟踪点
Camera.main.GetComponent<MyCameraFollow> ().SetTarget (targetPoint);
}
}
[ClientCallback]
void Update ()
{
//设置血条背景颜色
healthColor.color = colors [tankNum];
//设置血条值
healthSlider.value = health;
//如果是本地玩家
if (isLocalPlayer) {
//操纵坦克
hor = Input.GetAxis ("Horizontal");
ver = Input.GetAxis ("Vertical");
gunDir = Input.GetAxis ("GunDirection");
rig.MovePosition (transform.position + transform.forward * ver * Time.deltaTime * tankMoveSpeed);
transform.eulerAngles += Vector3.up * hor * tankTurnSpeed;
gun.transform.eulerAngles += Vector3.up * gunDir * tankTurnSpeed;
//如果坦克移动
if (hor != 0 || ver != 0) {
if (aud.clip == idle) {
aud.Stop ();
aud.clip = run;
} else {
if (!aud.isPlaying) {
aud.Play ();
}
}
} else {
if (aud.clip == run) {
aud.Stop ();
aud.clip = idle;
} else {
if (!aud.isPlaying) {
aud.Play ();
}
}
}
//发射炮弹
if (Input.GetKeyDown (KeyCode.Space)) {
CmdFire ();
}
}
//如果血量见底
if (health <= 0) {
//本地玩家失败
if (isLocalPlayer) {
resultUI.transform.GetChild (0).gameObject.SetActive (true);
resultUI.transform.GetChild (0).GetChild (0).GetComponent<Text> ().text = "GameOver";
resultUI.transform.GetChild (0).GetChild (1).GetComponent<Text> ().text = "GameOver";
}
//非本地玩家胜利
else {
resultUI.transform.GetChild (0).gameObject.SetActive (true);
resultUI.transform.GetChild (0).GetChild (0).GetComponent<Text> ().text = "Victory";
resultUI.transform.GetChild (0).GetChild (1).GetComponent<Text> ().text = "Victory";
}
}
}
[Command]
/// <summary>
/// 发射炮弹
/// </summary>
void CmdFire ()
{
GameObject bullet = (GameObject)Instantiate (MyLobbyManager.instance.spawnPrefabs [0],
firePoint.position, firePoint.rotation);
bullet.GetComponent<Rigidbody> ().velocity = bullet.transform.forward * 20;
NetworkServer.Spawn (bullet);
}
}
结束语
相比从前的老网络系统,新版网络解决了很多Bug,也进一步做了优化,没有出现老网络的尴尬问题,只是新网络同步帧速率有些低,有时候会出现延迟较大的情况,这方面还有待改进。新网络类多内容也多,感兴趣的同学还需要多去看API,关于新网络今后还会有续集喔,敬请期待。本次项目链接:https://pan.baidu.com/s/1hRC7C-diHdGeEtFnnMJQ-Q 密码:rhjh