文章目录
前言
本文开始,介绍unity网络通信组件应用,先来一个用户手册的范例。用户手册上给出了一个简单的范例,并指出可以以此为基础进行相开发,再次对范例进行精讲。(NetworkManager对使用unity的轻量级游戏开发有很大帮助,大大解决了开发时间,但是对于吃鸡、农药等游戏估计其整个消息传递机制还是自定义的(此处为猜测))。不做赘述,立马开始(完整脚本均在文末):
1. 使用流程
1.1 挂载NetworkManager
如果用过很多插件的,会知道,在使用一个插件之前肯定要挂载一个manager或者controller,NetworkManager也一样,需要关在NetworkManager进行组建数据初始化等。所以新建游戏物体,并添加NetworkManager组件并勾选Dont Destroy on Load。参数中除了network address和port以外,需要对Spawn Info进行一下简单解释。Spawn表示网路范围内产生的游戏物体(根据字面意思以及unity测试自己定义的),比如吃鸡游戏,自己控制人物在别人的场景中也存在,这就是一个Spawn。Player prefab玩家预制,即游戏玩家控制的角色,如果是聊天软件则可以是聊天窗口。RegisteredSpawnablePrefabs表示对需要对网络范围内产生的游戏物体进行注册,如子弹,你打出的子弹需要对方也可以收到,就需要在网络范围内(即所有玩游戏的人场景)产生,他跟Player prefab玩家预制本质上是一样的。
1.2 挂载NetworkManagerHUD
在NetworkManager所在的游戏物体挂在此组件,此组件为unity自定义的简单UI,运行时会有四个选项:
LANHost:局域网范围内,本机为主机(服务端),同时本机也为客户端。
LANServer:局域网范围内,本机只为主机。
LANClient:局域网范围内,本机为客户端。
Enable。。:(用户手册解释为网络端(外网),并未进行相关测试。点进去就可以进行相关外网连接设置)括号内的解释不严谨,会在下一篇中详细解释
在此说明一下:LANHost与LANServer区别在于本身是否存在一个客户端,这个对于玩儿过CS1.5/1.6的人来说好理解,因为局域网需要一个人见主机,其他人加入,同时它本身也要参与游戏,目前的FPS游戏均为局域网范围外,所以本身只是客户端。
1.3 生成玩家
添加一个3D游戏物体capsule,并生成预制体,作为一个间的游戏玩家。并拖到NetWorkManager中的PlayerPrefab槽中。对此游戏物体添加NetworkIdentity组件,此组件用来标识游戏玩家身份,只要是场景中每个客户端在交互中实时生成的并可以被广泛看到的均需要此组件。此组件有两个参数,一个人ServerOnly一个是LocalPlayer权限,表示此游戏物是本地有控制权限还是服务端有控制权限(因为服务端和客户端会均存在一个相同的游戏物体,理论上都可以进行控制,所以要对此进行一下区分,后续会讲到怎么处理)。如果需要实时更新位置信息的,则还需要NetWorkTransform组件,来试试更新位置。
1.4 添加脚本PlayerMove(控制移动开火等)
完整脚本在最后端,直接添加脚本并在脚本中添加如下代码:
float x = Input.GetAxis("Horizontal") * moveSpeed;
float z = Input.GetAxis("Vertical") * moveSpeed;
//transform.Translate(new Vector3(x, 0, z));
GetComponent<CharacterController>().Move(new Vector3(x, 0, z));
注:原程序用的transform.translate,本文通过添加角色控制器来移动
打包一个程序,运行选择客户端,editor运行选择LanHost。会发现两个场景中均存在两个游戏物体,移动游戏物体则两个游戏物体均移动。而我们需要的是只移动本客户端控制的游戏物体。所以需要作如下修改:
把MonoBehaviour换成NetworkBehaviour(后者继承在前者),然后通过NetworkBehaviour中的isLocalPlayer参数来来判断是否为本地运行,此部分代码如下:
private void Update()
{
if (isLocalPlayer)
{
PlayerMove();
}
}
private void PlayerMove()
{
float x = Input.GetAxis("Horizontal") * moveSpeed;
float z = Input.GetAxis("Vertical") * moveSpeed;
//transform.Translate(new Vector3(x, 0, z));
GetComponent<CharacterController>().Move(new Vector3(x, 0, z));
}
下面为添加子弹:
新建一个球形游戏物体,调整一下初始位置和大小,添加NetworkIdentity和NetWorkTransform组件,并生成预制体,把此预制体拖动到NetworkManager中的RegisteredSpawnablePrefabs中进行注册。当按下F键时表示开火,在update中添加如下代码:
if (Input.GetKeyDown(KeyCode.F))
{
GameObject playerBullet = Instantiate(bullet, (4*transform.forward + transform.position), Quaternion.identity);
playerBullet.GetComponent<Rigidbody>().velocity = playerBullet.transform.forward * bulletSpeed;
}
运行测试会发现,客户端的两个游戏物体均发射子弹,而且服务端无子弹生成,若增加第二个客户端,则第二个客户端也不会生成子弹。代码放在isLocalPlayer触发的模块下可解决均发射子弹的问题。第二个问题则是client产生游戏物体不会在服务端产生也不会在其他client端产生。如果自己编写消息机制,则需要发送消息告诉服务端和其他client,产生子弹,但是但是Unet为我们提供好了解决方案,即通过[Command](表示此方法客户端调用,服务端执行,对应的方法开头为Cmd;对应的[ClientRpc]表示服务端调用,客户端执行)让服务端去执行发射子弹命令,并通过NetworkServer.Spawn方法在每个客户端生成代码如下:
[Command]
private void CmdFire()
{
GameObject playerBullet = Instantiate(bullet, (4*transform.forward + transform.position), Quaternion.identity);
playerBullet.GetComponent<Rigidbody>().velocity = playerBullet.transform.forward * bulletSpeed;
NetworkServer.Spawn(playerBullet);
Destroy(playerBullet, 2f);
}
1.5 添加血量控制
在Player预制上添加MyCombat脚本以及HealthBar脚本(显示血量)。其中MyCombat中包含两个公共变量maxHealth最大生命值,currentHealth当前生命值。HealthBar通过最大生命值和当前生命值通过GUI来显示相关数值。HealthBar见本文最后。MyCombat脚本需要一个公共方法,当受到子弹碰撞时减少血量,并当减少到一定数值是死亡或者重新生成,方法如下:
public void DecreaseHealth(float delta)
{
currentHealth -= delta;
if(currentHealth <= 0) { currentHealth = 0; }
if(currentHealth==0)
{
if (isEnemy)
{
Destroy(gameObject);
}
else
{
RpcReset();
currentHealth = maxHealth;
}
}
}
同时我们还需要给子弹添加碰撞体,并添加碰撞事件,完整代码如下:
using UnityEngine;
public class MyBullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
MyCombat combat = collision.collider.gameObject.GetComponent<MyCombat>();
Destroy(gameObject);
if(combat != null)
{
combat.DecreaseHealth(10);
}
}
}
如果我们需要所有数据均在服务端执行则需要如下处理。把MyCombat继承的MonoBehaviour改为NetworkBehaviour,并让DecreaseHealth方法在服务端执行,通过[SyncVar](同步变量,即把服务端的变量值同步到各个客户端)把当前血量同步到各个客户端,同时当期血量低于0时,让player回到初始位置,代码如下
public void DecreaseHealth(float delta)
{
if (!isServer) return;
currentHealth -= delta;
if(currentHealth <= 0) { currentHealth = 0; }
if(currentHealth==0)
{
if (isEnemy)
{
Destroy(gameObject);
}
else
{
RpcReset();
currentHealth = maxHealth;
}
}
}
[ClientRpc]//服务端调用,client执行,运动(位置)权限在本地,服务端更改位置无法同步到每个客户端,只有本地更改才可以
private void RpcReset()
{
transform.position = new Vector3(0, 0, 0);
}
1.6 其他
官方例子中还给出了生成初始敌人的代码,以及客户端控制的Player更改颜色的方法。这两种处理比较简单,NetworkBehaviour中的方法,前者在服务端启动服务时调用,后者在启动游戏玩家时调用,代码分别如下
using UnityEngine;
using UnityEngine.Networking;
public class MyEnemySpawner : NetworkBehaviour {
public GameObject enemy;
public int enemyCount = 4;
public override void OnStartServer()
{
for (int i = 0; i < enemyCount; i++)
{
GameObject en = Instantiate(enemy);
en.transform.position = new Vector3(Random.Range(-40, 40), 1, Random.Range(-40, 40));
NetworkServer.Spawn(en);
}
}
}
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
2.完整代码
2.1 PlayerMove 类
using UnityEngine.Networking;
using UnityEngine;
public class MyPlayerMove : NetworkBehaviour {
public float moveSpeed = 2f;
public float bulletSpeed = 50f;
public GameObject bullet;
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
private void Update()
{
if (isLocalPlayer)
{
PlayerMove();
if (Input.GetKeyDown(KeyCode.F))
{
CmdFire();
}
}
}
private void PlayerMove()
{
float x = Input.GetAxis("Horizontal") * moveSpeed;
float z = Input.GetAxis("Vertical") * moveSpeed;
//transform.Translate(new Vector3(x, 0, z));
GetComponent<CharacterController>().Move(new Vector3(x, 0, z));
}
[Command]
private void CmdFire()
{
GameObject playerBullet = Instantiate(bullet, (4*transform.forward + transform.position), Quaternion.identity);
playerBullet.GetComponent<Rigidbody>().velocity = playerBullet.transform.forward * bulletSpeed;
NetworkServer.Spawn(playerBullet);
Destroy(playerBullet, 2f);
}
}
2.2 Combat 类
using UnityEngine;
using UnityEngine.Networking;
public class MyCombat : NetworkBehaviour {
public bool isEnemy = false;
[SyncVar]
public float currentHealth = maxHealth;
public static float maxHealth = 100;
public void DecreaseHealth(float delta)
{
if (!isServer) return;
currentHealth -= delta;
if(currentHealth <= 0) { currentHealth = 0; }
if(currentHealth==0)
{
if (isEnemy)
{
Destroy(gameObject);
}
else
{
RpcReset();
currentHealth = maxHealth;
}
}
}
[ClientRpc]
private void RpcReset()
{
transform.position = new Vector3(0, 0, 0);
}
}
2.3 HealthBar 类
using UnityEngine;
public class HealthBar : MonoBehaviour
{
GUIStyle healthStyle;
GUIStyle backStyle;
MyCombat combat;
void Awake()
{
combat = GetComponent<MyCombat>();
}
void OnGUI()
{
InitStyles();
// Draw a Health Bar
// 绘制一个血条
Vector3 pos = Camera.main.WorldToScreenPoint(transform.position);
// draw health bar background
// 绘制血条背景
GUI.color = Color.grey;
GUI.backgroundColor = Color.grey;
GUI.Box(new Rect(pos.x - 26, Screen.height - pos.y + 20, Combat.maxHealth / 2, 7), ".", backStyle);
// draw health bar amount
// 绘制当前血量
GUI.color = Color.green;
GUI.backgroundColor = Color.green;
GUI.Box(new Rect(pos.x - 25, Screen.height - pos.y + 21, combat.currentHealth / 2, 5), ".", healthStyle);
}
void InitStyles()
{
if (healthStyle == null)
{
healthStyle = new GUIStyle(GUI.skin.box);
healthStyle.normal.background = MakeTex(2, 2, new Color(0f, 1f, 0f, 1.0f));
}
if (backStyle == null)
{
backStyle = new GUIStyle(GUI.skin.box);
backStyle.normal.background = MakeTex(2, 2, new Color(0f, 0f, 0f, 1.0f));
}
}
Texture2D MakeTex(int width, int height, Color col)
{
Color[] pix = new Color[width * height];
for (int i = 0; i < pix.Length; ++i)
{
pix[i] = col;
}
Texture2D result = new Texture2D(width, height);
result.SetPixels(pix);
result.Apply();
return result;
}
}
2.4 Bullet 类
using UnityEngine;
public class MyBullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
MyCombat combat = collision.collider.gameObject.GetComponent<MyCombat>();
Destroy(gameObject);
if(combat != null)
{
combat.DecreaseHealth(10);
}
}
}
2.5 EnemySpawner 类
using UnityEngine;
using UnityEngine.Networking;
public class MyEnemySpawner : NetworkBehaviour {
public GameObject enemy;
public int enemyCount = 4;
public override void OnStartServer()
{
for (int i = 0; i < enemyCount; i++)
{
GameObject en = Instantiate(enemy);
en.transform.position = new Vector3(Random.Range(-40, 40), 1, Random.Range(-40, 40));
NetworkServer.Spawn(en);
}
}
}