本文档将介绍使用新网络系统从头开始设置新的多人游戏项目的步骤。此分步过程是通用的,但是熟悉后,就可以针对许多类型的多人游戏进行自定义。
首先,创建一个空的 Unity 项目。
第一步是在项目中创建 NetworkManager 对象:
有关更多详细信息,请参阅使用 NetworkManager。
下一步是设置在游戏中代表玩家的 Unity 预制件。默认情况下,NetworkManager 通过克隆玩家预制件来为每个玩家实例化一个对象。在以下示例中,玩家对象将是一个简单立方体。
请参阅玩家对象。
创建玩家预制件后,必须将其注册到网络系统。
现在该第一次保存项目了。请通过菜单 File > Save Project 保存项目。还应该保存场景。我们将这个场景称为“离线”场景。
游戏的首个功能是移动玩家对象。我们先在没有任何网络的情况下实现此功能,所以只能在单人游戏模式下有效。
using UnityEngine;
public class PlayerMove : MonoBehaviour
{
void Update()
{
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
}
}
此代码将连接由箭头键或游戏手柄控制的立方体。立方体现在只在客户端上移动(没有联网)。
再次保存项目。
在编辑器中单击播放按钮进入播放模式。应该会看到 NetworkManagerHUD 默认用户界面:
按“Host”以游戏主机身份启动游戏。这将创建玩家对象,且 HUD 将更改以表明服务器处于活动状态。该游戏将作为“主机”运行,也就是在同一个进程中既作为服务器也作为客户端。
请参阅网络概念。
按箭头键应该能使玩家立方体对象四处移动。
在编辑器中按停止按钮退出播放模式。
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
}
}
游戏中的立方体目前都是白色的,所以用户无法分辨哪个是自己的立方体。为了识别玩家,我们将本地玩家的立方体变成红色。
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
此函数仅针对客户端上的本地玩家调用。这将使用户看到其立方体为红色。OnStartLocalPlayer 函数适合于执行仅限于本地玩家的初始化,例如配置摄像机和输入。
NetworkBehaviour 基类中还有一些其他有用的虚拟函数。请参阅生成。
多人游戏的一个共同特点是让玩家发射子弹。本节将在示例中添加非联网子弹。下一节添加子弹的联网功能。
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
public GameObject bulletPrefab;
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
if (Input.GetKeyDown(KeyCode.Space))
{
Fire();
}
}
void Fire()
{
// 通过子弹预制件创建子弹对象
var bullet = (GameObject)Instantiate(
bulletPrefab,
transform.position - transform.forward,
Quaternion.identity);
// 让子弹在玩家面前远离
bullet.GetComponent<Rigidbody>().velocity = -transform.forward*4;
// 让子弹在 2 秒后消失
Destroy(bullet, 2.0f);
}
}
本节将在示例中为子弹添加联网功能。
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
public GameObject bulletPrefab;
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
[Command]
void CmdFire()
{
// 此 [Command] 代码是在服务器上运行的!
// 在本地创建子弹对象
var bullet = (GameObject)Instantiate(
bulletPrefab,
transform.position - transform.forward,
Quaternion.identity);
bullet.GetComponent<Rigidbody>().velocity = -transform.forward*4;
// 在客户端上生成子弹
NetworkServer.Spawn(bullet);
// 当子弹在服务器上销毁时,也将自动在客户端上销毁
Destroy(bullet, 2.0f);
}
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
if (Input.GetKeyDown(KeyCode.Space))
{
// Command 函数从客户端中调用,但是在服务器上唤出
CmdFire();
}
}
}
此代码使用 [Command] 在服务器上发射子弹。有关更多信息,请参阅网络化操作。
此处将添加一个碰撞处理程序,以便子弹在击中玩家立方体对象后消失。
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent<PlayerMove>();
if (hitPlayer != null)
{
Destroy(gameObject);
}
}
}
现在,当子弹击中玩家对象后,子弹就会被销毁。当服务器上的子弹被销毁时,因为这个子弹是由网络管理的生成对象,所以也将在客户端上被销毁。
与子弹相关的一个常见特征是,玩家对象具有一个“health”属性,该属性开始时是一个最大值,然后在玩家受到子弹击中造成的伤害后随之下降。本节向玩家对象添加非联网生命值 (health)。
using UnityEngine;
public class Combat : MonoBehaviour
{
public const int maxHealth = 100;
public int health = maxHealth;
public void TakeDamage(int amount)
{
health -= amount;
if (health <= 0)
{
health = 0;
Debug.Log("Dead!");
}
}
}
Bullet 脚本需要更新,以便在击中时调用 TakeDamage 函数。 * 打开 Bullet 脚本 * 在碰撞处理程序函数中添加对 Combat 脚本中 TakeDamage() 函数的调用
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent<PlayerMove>();
if (hitPlayer != null)
{
var combat = hit.GetComponent<Combat>();
combat.TakeDamage(10);
Destroy(gameObject);
}
}
}
当被子弹击中时,将使玩家对象的生命值下降。但现在无法在游戏中看到这一点。我们需要添加一个简单的生命值血条。
这里有很多代码使用旧的 GUI 系统。这对联网没太大影响,所以我们现在只使用,但不作解释。
using UnityEngine;
using System.Collections;
public class HealthBar : MonoBehaviour
{
GUIStyle healthStyle;
GUIStyle backStyle;
Combat combat;
void Awake()
{
combat = GetComponent<Combat>();
}
void OnGUI()
{
InitStyles();
// 绘制一个生命值血条
Vector3 pos = Camera.main.WorldToScreenPoint(transform.position);
// 绘制生命值血条背景
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);
// 绘制生命值血条量
GUI.color = Color.green;
GUI.backgroundColor = Color.green;
GUI.Box(new Rect(pos.x-25, Screen.height - pos.y + 21, combat.health/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;
}
}
对生命值的更改现在将应用到所有位置,与客户端和主机无关。因此,不同玩家的生命值可能显示不同。只应在服务器上应用生命值,然后将更改复制到客户端。我们将这种情况称为生命值的“服务器授权”。
有关 SyncVars 的更多信息,请参阅状态同步。
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
health = 0;
Debug.Log("Dead!");
}
}
}
当前,当玩家的生命值达到零时,什么都不会发生(除了日志消息)。为了使游戏更像样,当生命值达到零时,玩家应该以完整生命值被传送回起始位置。
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
health = maxHealth;
// 在服务器上调用,将在客户端上唤出
RpcRespawn();
}
}
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
// 移回到零位置
transform.position = Vector3.zero;
}
}
}
在此游戏中,客户端控制玩家对象的位置 - 玩家对象在客户端上有“本地授权”。如果服务器只是将玩家的位置设置为起始位置,则将被客户端覆盖,因为客户端具有授权。为了避免这种情况,服务器告诉拥有玩家对象的客户端将玩家对象移动到起始位置。
虽然玩家对象是在客户端连接到主机时生成的,但大多数游戏都有存在于游戏世界中的非玩家对象,例如敌人。在本节中将添加一个生成器,作用是创建可以被射击和杀死的非玩家对象。
using UnityEngine;
using UnityEngine.Networking;
public class EnemySpawner : NetworkBehaviour {
public GameObject enemyPrefab;
public int numEnemies;
public override void OnStartServer()
{
for (int i=0; i < numEnemies; i++)
{
var pos = new Vector3(
Random.Range(-8.0f, 8.0f),
0.2f,
Random.Range(-8.0f, 8.0f)
);
var rotation = Quaternion.Euler( Random.Range(0,180), Random.Range(0,180), Random.Range(0,180));
var enemy = (GameObject)Instantiate(enemyPrefab, pos, rotation);
NetworkServer.Spawn(enemy);
}
}
}
现在创建敌人预制件:
在前面,Bullet 脚本被设置为只适用于玩家。现在,需要更新 Bullet 脚本,以便能够处理任何包含 Combat 脚本的对象:
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitCombat = hit.GetComponent<Combat>();
if (hitCombat != null)
{
hitCombat.TakeDamage(10);
Destroy(gameObject);
}
}
}
将 EnemySpawner 与 Enemy 对象连接起来:
测试敌人:
当敌人被子弹射中时,敌人的生命值就会下降,然后像玩家一样重生。敌人的生命值达到零时应该被销毁,而不是重生。
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
public bool destroyOnDeath;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
if (destroyOnDeath)
{
Destroy(gameObject);
}
else
{
health = maxHealth;
// 在服务器上调用,将在客户端上唤出
RpcRespawn();
}
}
}
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
// 移回到零位置
transform.position = Vector3.zero;
}
}
}
当生命值达到零时,敌人将被销毁,但玩家将重生。
目前,玩家在创建后都会出现在零点。这意味着多个玩家之间会发生重叠。玩家应该在不同的地方生成。可以使用 NetworkStartPosition 组件来实现这一目标。
创建一个新的空游戏对象
将对象重命名为“Pos1”
选择 Add Component 按钮,然后添加 NetworkStartPosition 组件
将 Pos1 对象移动到位置 (–3,0,0)
创建第二个新的空游戏对象
将对象重命名为“Pos2”
选择 Add Component 按钮,然后添加 NetworkStartPosition 组件
将 Pos2 对象移动到位置 (3,0,0)
找到 NetworkManager 并选中。
打开“Spawn Info”折叠三角形
将“Player Spawn Method”更改为“Round Robin”
构建并运行游戏
玩家对象现在应该在 Pos1 和 Pos2 对象的位置创建,而不是在零点。