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

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

要开始,创建一个新的空的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”(依次循环)。

?