• 从头开始设置多人游戏项目
    • 1、NetworkManager设置
    • 2、设置玩家预制体
    • 3、注册玩家预制体
    • 4、玩家移动(单人版)
    • 5、测试托管游戏
    • 6、测试客户端玩家的移动
    • 7、使玩家移动联网
    • 8、测试多人玩家运动
    • 9、识别你的玩家
    • 10、射击子弹(不联网)
    • 11、子弹射击(联网)
    • 12、子弹碰撞
    • 13、玩家状态(非联网血量)
    • 14、玩家状态(联网血量)
    • 15、死亡和重生
    • 16、非玩家对象
    • 17、摧毁敌人
    • 18、玩家生成的位置

    从头开始设置多人游戏项目

    本文档描述了使用新的网络系统设置新的多人项目的步骤。这个分步过程是通用的,但是可以为许多类型的多人游戏开始定制。

    要开始,创建一个新的空的Unity项目。

    1、NetworkManager设置

    第一步是在项目中创建一个NetworkManager对象:

      • 创建一个空物体并添加NetworkManager组件。该组件管理游戏的网络状态。

      • 继续添加NetworkManagerHUD组件。该组件在您的游戏中提供了一个简单的用户界面来控制网络状态。

    2、设置玩家预制体

    下一步是设置代表游戏中玩家的预制体。默认情况下,NetworkManager通过克隆玩家预制体来为每个玩家实例化一个对象。在这个例子中,玩家对象将是一个坦克。

      • 给玩家的预制体添加NetworkIdentity组件。此组件用于标识服务器和客户端之间的对象。

      • 将NetworkIdentity上的“Local Player Authority”复选框设置为true。这将允许客户端控制玩家的移动。

    3、注册玩家预制体

    一旦玩家预制体已创建,它必须向网络系统注册。

      • 将NetworkManager组件上的Spawn Info中的Player Prefab设置为玩家预制体。

      • 可以将当前场景保存为“offlineScene”。

    4、玩家移动(单人版)

    第一个游戏功能是移动玩家对象。这将首先在没有任何网络的情况下完成,因此它只能在单人模式下工作。

    编写玩家移动脚本:

    1. using UnityEngine;
    2. public class TankMove : MonoBehaviour
    3. {
    4. void Update()
    5. {
    6. var x = Input.GetAxis("Horizontal")*0.1f;
    7. var z = Input.GetAxis("Vertical")*0.1f;
    8. transform.Translate(x, 0, z);
    9. }
    10. }

    5、测试托管游戏

    运行游戏,此时应该显示NetworkMangerHUD默认用户界面:

     6.22 从头开始设置多人游戏项目  - 图1

      • 选择“LAN Host(H)”。这会创建玩家对象,并且HUD将更改以显示服务器处于活动状态。这个游戏作为一个“主机”运行。(这是同一个进程中一个服务器和一个客户端。)

    6、测试客户端玩家的移动

      • 将当前场景发布成一个可执行程序并运行。

      • 移动玩家。

      • 在编译器中运行游戏。HUD界面选择“LAN Client(C)”作为客户端连接到主机。

      • 此时场景中应该有两个玩家的角色,但是角色移动无法同步,这是因为移动脚本不是网络感知的。

    7、使玩家移动联网

      • 给玩家预设添加NetworkTransform组件。该组件使对象同步整个网络中的位置。

      • 修改玩家移动脚本:

    1. using UnityEngine;
    2. using UnityEngine.Networking;
    3. public class TankMove : NetworkBehaviour
    4. {
    5. void Update()
    6. {
    7. if (!isLocalPlayer)
    8. return;
    9. var x = Input.GetAxis("Horizontal")*0.1f;
    10. var z = Input.GetAxis("Vertical")*0.1f;
    11. transform.Translate(x, 0, z);
    12. }
    13. }

    8、测试多人玩家运动

      • 再次构建并运行独立播放器,并作为主机启动。

      • 在编辑器中进入播放模式并作为客户端连接。

      • 玩家对象现在应该彼此独立地移动,并由他们的客户端上的本地玩家控制。

    9、识别你的玩家

    游戏中的多个玩家的颜色一样,因此用户无法辨认其中的哪一个是自己。要识别玩家,我们将使本地玩家的颜色变红。

      • 添加OnStartLocalPlayer函数的实现来更改玩家对象的颜色。

    1. public override void OnStartLocalPlayer()
    2. {
    3. MeshRenderer render = transform.Find("TankTurret").GetComponent<MeshRenderer>();
    4. Material[] materials = render.materials;
    5. if (materials.Length > 0)
    6. materials[0].SetColor("_Color", Color.red);
    7. }

    10、射击子弹(不联网)

      • 制作子弹预制,并添加Rigidbody组件。

      • 修改玩家移动脚本,并为子弹预设赋值:

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Networking;
    5. public class TankMove : NetworkBehaviour
    6. {
    7. public GameObject bulletPrefab;
    8. Camera m_LocalCamera;
    9. public Vector3 m_CameraPosOffset = new Vector3(0, 4.5f, -6f);
    10. public Vector3 m_CameraLookOffset = new Vector3(0, 2f, 0);
    11. public float m_RotSpeed = 15;
    12. Transform firePos;
    13. float mouseX = 0;
    14. void Awake()
    15. {
    16. firePos = transform.Find("FirePos");
    17. }
    18. void Update()
    19. {
    20. if (!isLocalPlayer)
    21. return;
    22. var x = Input.GetAxis("Horizontal1") * 0.1f;
    23. var z = Input.GetAxis("Vertical1") * 0.1f;
    24. transform.Translate(x, 0, z);
    25. if (Input.GetMouseButton(0))
    26. {
    27. mouseX += Input.GetAxis("Mouse X");
    28. }
    29. transform.rotation = Quaternion.Euler(0, mouseX * m_RotSpeed, 0);
    30. if (Input.GetKeyDown(KeyCode.Space))
    31. {
    32. Fire();
    33. }
    34. }
    35. void LateUpdate()
    36. {
    37. if (m_LocalCamera && isLocalPlayer)
    38. {
    39. m_LocalCamera.transform.position = transform.TransformPoint(m_CameraPosOffset);
    40. m_LocalCamera.transform.LookAt(transform.position + m_CameraLookOffset);
    41. }
    42. }
    43. /// <summary>
    44. /// 创建本地玩家时调用
    45. /// </summary>
    46. public override void OnStartLocalPlayer()
    47. {
    48. m_LocalCamera = Camera.main;
    49. MeshRenderer render = transform.Find("TankTurret").GetComponent<MeshRenderer>();
    50. Material[] materials = render.materials;
    51. if (materials.Length > 0)
    52. materials[0].SetColor("_Color", Color.red);
    53. }
    54. void Fire()
    55. {
    56. var bullet = Instantiate<GameObject>(bulletPrefab, firePos.position, firePos.rotation);
    57. bullet.GetComponent<Rigidbody>().AddForce(transform.forward * 1500);
    58. Destroy(bullet, 2f);
    59. }
    60. }

    11、子弹射击(联网)

      • 给子弹预设添加NetworkIdentity组件。

      • 给子弹预设添加NetworkTransform组件。

      • 选择NetworkManager并打开“Spawn Info”折叠。

      • 用加号按钮添加新的生成预制。

      • 将子弹预设拖到新生成的预制插槽中。

     6.22 从头开始设置多人游戏项目  - 图2

      • 更新TankMove脚本以连接子弹:

      • 通过添加[Command]自定义属性和“Cmd”前缀,将Fire函数更改为联网命令。

      • 在子弹对象上使用NetworkServer.Spawn()

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Networking;
    5. public class TankMove : NetworkBehaviour
    6. {
    7. public GameObject bulletPrefab;
    8. Camera m_LocalCamera;
    9. public Vector3 m_CameraPosOffset = new Vector3(0, 4.5f, -6f);
    10. public Vector3 m_CameraLookOffset = new Vector3(0, 2f, 0);
    11. public float m_RotSpeed = 15;
    12. public Transform firePos;
    13. float mouseX = 0;
    14. void Awake()
    15. {
    16. firePos = transform.Find("FirePos");
    17. }
    18. void Update()
    19. {
    20. if (!isLocalPlayer)
    21. return;
    22. var x = Input.GetAxis("Horizontal1") * 0.1f;
    23. var z = Input.GetAxis("Vertical1") * 0.1f;
    24. transform.Translate(x, 0, z);
    25. if (Input.GetMouseButton(0))
    26. {
    27. mouseX += Input.GetAxis("Mouse X");
    28. }
    29. transform.rotation = Quaternion.Euler(0, mouseX * m_RotSpeed, 0);
    30. if (Input.GetKeyDown(KeyCode.Space))
    31. {
    32. // 本地调用 服务器执行
    33. CmdFire();
    34. }
    35. }
    36. void LateUpdate()
    37. {
    38. if (m_LocalCamera && isLocalPlayer)
    39. {
    40. m_LocalCamera.transform.position = transform.TransformPoint(m_CameraPosOffset);
    41. m_LocalCamera.transform.LookAt(transform.position + m_CameraLookOffset);
    42. }
    43. }
    44. /// <summary>
    45. /// 创建本地玩家时调用
    46. /// </summary>
    47. public override void OnStartLocalPlayer()
    48. {
    49. m_LocalCamera = Camera.main;
    50. MeshRenderer render = transform.Find("TankTurret").GetComponent<MeshRenderer>();
    51. Material[] materials = render.materials;
    52. if (materials.Length > 0)
    53. materials[0].SetColor("_Color", Color.red);
    54. }
    55. // 设置为本地调用 服务器执行的命令
    56. [Command]
    57. void CmdFire()
    58. {
    59. var bullet = Instantiate<GameObject>(bulletPrefab, firePos.position, firePos.rotation);
    60. bullet.GetComponent<Rigidbody>().AddForce(transform.forward * 1500);
    61. NetworkServer.Spawn(bullet);
    62. Destroy(bullet, 2f);
    63. }
    64. }

    12、子弹碰撞

      • 为子弹预设及玩家预设添加Collider组件。

      • 为子弹预设添加脚本“Bullet”。

    1. using UnityEngine;
    2. public class Bullet : MonoBehaviour
    3. {
    4. void OnCollisionEnter(Collision col)
    5. {
    6. var hit = col.gameObject;
    7. var hitPlayer = hit.GetComponent<TankMove>();
    8. if (hitPlayer != null)
    9. {
    10. Destroy(gameObject);
    11. }
    12. }
    13. }

      • 当子弹击中玩家时,它会被销毁。当服务器上的子弹被销毁时,由于它是由NetworkServer产生的对象,所以在客户端也将被销毁。

    13、玩家状态(非联网血量)

      • 玩家被子弹攻击后,血量减少。

      • 给玩家添加脚本,添加血量属性及受伤害函数。

    1. using UnityEngine;
    2. public class Combat : MonoBehaviour {
    3. public const int k_MaxHealth = 100;
    4. public int m_Health = k_MaxHealth;
    5. public void TakeDamage(int amount)
    6. {
    7. m_Health -= amount;
    8. if(m_Health < 0)
    9. {
    10. m_Health = 0;
    11. Debug.Log("Dead!");
    12. }
    13. }
    14. }

      • 更新Bullet脚本,玩家被子弹击中后调用TakeDamage函数。

    1. using UnityEngine;
    2. public class Bullet : MonoBehaviour
    3. {
    4. void OnCollisionEnter(Collision col)
    5. {
    6. var hit = col.gameObject;
    7. var hitPlayer = hit.GetComponent<TankMove>();
    8. if (hitPlayer != null)
    9. {
    10. hit.GetComponent<Combat>().TakeDamage(10);
    11. Destroy(gameObject);
    12. }
    13. }
    14. }

      • 为了使玩家被子弹击中后能观察到血量的变化,给玩家添加血条(使用OnGUI)。

    1. using UnityEngine;
    2. public class HealthBar : MonoBehaviour {
    3. GUIStyle healthStyle;
    4. GUIStyle backStyle;
    5. Combat combat;
    6. void Awake()
    7. {
    8. combat = GetComponent<Combat>();
    9. }
    10. void OnGUI()
    11. {
    12. InitStyle();
    13. var pos = Camera.main.WorldToScreenPoint(transform.position);
    14. // 绘制血条背景
    15. GUI.color = Color.grey;
    16. GUI.backgroundColor = Color.grey;
    17. GUI.Box(new Rect(pos.x - 26, Screen.height - pos.y + 20, Combat.k_MaxHealth/2, 7), ".", backStyle);
    18. // 绘制血条
    19. if (combat.m_Health != 0)
    20. {
    21. GUI.color = Color.green;
    22. GUI.backgroundColor = Color.green;
    23. GUI.Box(new Rect(pos.x - 25, Screen.height - pos.y + 21, combat.m_Health / 2, 5), ".", healthStyle);
    24. }
    25. }
    26. void InitStyle()
    27. {
    28. if (healthStyle == null)
    29. {
    30. healthStyle = new GUIStyle(GUI.skin.box);
    31. healthStyle.normal.background = MakeTex(2, 2, new Color(0, 1, 0, 1));
    32. }
    33. if (backStyle == null)
    34. {
    35. backStyle = new GUIStyle(GUI.skin.box);
    36. backStyle.normal.background = MakeTex(2, 2, new Color(0, 0, 0, 1));
    37. }
    38. }
    39. Texture2D MakeTex(int width, int height, Color col)
    40. {
    41. Color[] pix = new Color[width * height];
    42. for (int i = 0; i < pix.Length; i++)
    43. {
    44. pix[i] = col;
    45. }
    46. Texture2D tex = new Texture2D(width, height);
    47. tex.SetPixels(pix);
    48. tex.Apply();
    49. return tex;
    50. }
    51. }

    14、玩家状态(联网血量)

      • 修改Combat脚本。

    1. using UnityEngine;
    2. using UnityEngine.Networking;
    3. public class Combat : NetworkBehaviour {
    4. public const int k_MaxHealth = 100;
    5. [SyncVar] // 同步变量
    6. public int m_Health = k_MaxHealth;
    7. /// <summary>
    8. /// 只在服务器上应用
    9. /// </summary>
    10. /// <param name="amount">Amount.</param>
    11. public void TakeDamage(int amount)
    12. {
    13. if (!isServer)
    14. return;
    15. m_Health -= amount;
    16. if(m_Health < 0)
    17. {
    18. m_Health = 0;
    19. Debug.Log("Dead!");
    20. }
    21. }
    22. }

    15、死亡和重生

      • 当玩家血量为零时死亡,死亡以后重生。

      • 添加一个[ClientRpc]函数来重生玩家对象。

      • 当血量为零时,调用服务器上的重生功能。

      • 修改Combat脚本:

    1. using UnityEngine;
    2. using UnityEngine.Networking;
    3. public class Combat : NetworkBehaviour {
    4. public const int k_MaxHealth = 100;
    5. [SyncVar] // 同步变量
    6. public int m_Health = k_MaxHealth;
    7. /// <summary>
    8. /// 只在服务器上应用
    9. /// </summary>
    10. /// <param name="amount">Amount.</param>
    11. public void TakeDamage(int amount)
    12. {
    13. if (!isServer)
    14. return;
    15. m_Health -= amount;
    16. if(m_Health <= 0)
    17. {
    18. m_Health = k_MaxHealth;
    19. RpcReSpawn();
    20. }
    21. }
    22. [ClientRpc]
    23. void RpcReSpawn()
    24. {
    25. if(isLocalPlayer) // hasAuthority
    26. {
    27. transform.position = Vector3.zero;
    28. }
    29. }
    30. }

      • 在这个游戏中,客户端控制玩家对象的位置 - 玩家对象在客户端上具有“本地权限”。如果服务器只是将玩家的位置设置为起始位置,那么客户端将被覆盖,因为客户端有权限。为了避免这种情况,服务器告诉拥有的客户端将玩家对象移到起始位置。

    16、非玩家对象

    当玩家对象在客户端连接到主机时产生,大多数游戏具有游戏世界中存在的非玩家对象,例如敌人。在本节中,添加了一个spawner,创建可以被拍摄和杀死的非玩家对象。

      • 创建一个空物体,重命名为“EnemeySpawner”。

      • 添加组件NetworkIdentity

      • 在NetworkIdentity中选择“Server Only”复选框。这使得spawner不会被发送到客户端。

      • 添加“EnemySpawner”脚本。

      • 实现虚函数OnStartServer来创建敌人。

    1. using UnityEngine;
    2. using UnityEngine.Networking;
    3. public class EnemeySpawner : NetworkBehaviour
    4. {
    5. public GameObject enemyPrefab;
    6. public int numEnemies;
    7. public override void OnStartServer()
    8. {
    9. for (int i = 0; i < numEnemies; i++)
    10. {
    11. var pos = new Vector3(Random.Range(-8.0f, 8.0f),
    12. 0.2f,
    13. Random.Range(-8.0f, 8.0f));
    14. var rotation = Quaternion.Euler(0, Random.Range(0, 180), 0);
    15. var enemy = Instantiate<GameObject>(enemyPrefab, pos, rotation);
    16. NetworkServer.Spawn(enemy);
    17. }
    18. }
    19. }

    现在创建一个敌人预制:

      • 为敌人预制添加NetworkIdentity组件。

      • 为敌人预制添加NetworkTransform组件。

      • 在NetworkManagerSpawn Info中添加一个新的可生成的预制。

      • 为敌人预制添加Combat脚本。

      • 为敌人预制添加HealthBar脚本。

      • 修改Bullet脚本的碰撞检测。

    1. using UnityEngine;
    2. public class Bullet : MonoBehaviour
    3. {
    4. void OnCollisionEnter(Collision col)
    5. {
    6. var hit = col.gameObject;
    7. var hitCombat = hit.GetComponent<Combat>();
    8. if (hitCombat != null)
    9. {
    10. hitCombat.TakeDamage(10);
    11. Destroy(gameObject);
    12. }
    13. }
    14. }

    17、摧毁敌人

    当敌人血量为零时被销毁。

      • 修改Combat脚本。

      • 添加“destroyOnDeath”变量。

    1. using UnityEngine;
    2. using UnityEngine.Networking;
    3. public class Combat : NetworkBehaviour {
    4. public const int k_MaxHealth = 100;
    5. public bool m_DestroyOnDeath;
    6. [SyncVar] // 同步变量
    7. public int m_Health = k_MaxHealth;
    8. /// <summary>
    9. /// 只在服务器上应用
    10. /// </summary>
    11. /// <param name="amount">Amount.</param>
    12. public void TakeDamage(int amount)
    13. {
    14. if (!isServer)
    15. return;
    16. m_Health -= amount;
    17. if(m_Health <= 0)
    18. {
    19. if (m_DestroyOnDeath)
    20. {
    21. Destroy(gameObject);
    22. }
    23. else
    24. {
    25. m_Health = k_MaxHealth;
    26. RpcReSpawn();
    27. }
    28. }
    29. }
    30. [ClientRpc]
    31. void RpcReSpawn()
    32. {
    33. if(isLocalPlayer) // hasAuthority
    34. {
    35. transform.position = Vector3.zero;
    36. }
    37. }
    38. }

    18、玩家生成的位置

    目前创建玩家的位置全部在零点。这可能会造成玩家之间相互重叠。为了使玩家在不同的出生地点,可以使用NetworkStartPosition组件来实现。

      • 创建一个空物体,并添加NetworkStartPosition组件。调整其位置。

      • 用以上方法创建多个。

      • 在NetworkManager组件上打开“Spawn Info”折叠。

      • 将“Player Spawn Method”改为“Round Robin”(依次循环)。

    ?