本节将指导您创建Player UI系统。我们需要显示玩家的名称及其当前的健康状况。我们还需要管理UI位置来跟随玩家。
这部分与网络无关,但是,它提出了一些非常重要的设计模式,提供一些关于网络的高级特性,以及开发中的约束。
所以,UI不会做成联网的,仅仅是因为我们不需要,有许多方式可以去做这件事,避免占用带宽。如果你能实现一个功能而不需要联网,总是件好事,这些努力是值得的。
那么现在的问题是:我们如何为每个联网的玩家提供一个UI?
我们将有一个UI Prefab与专用PlayerUI脚本。我们的PlayerManager脚本将保存此UI Prefab的引用,并将在PlayerManager开始时简单地实例化此UI Prefab,并告诉预制跟随该玩家。
主要内容
- 创建UI Prefab
- PlayerUI脚本基础
- 实例化和Player绑定
- 跟随目标Player
创建UI Prefab
- 打开任何一个有UI Canvas的场景
- 向canvas添加Slider UI对象,将其命名为Player UI
- 将RectTransform垂直锚点设置为居中,将水平锚点设置为中心
- 将RectTransform宽度设置为80,高度设置为15
- 选择background子节点,将其图像组件颜色设置为红色
- 选择子节点"Fill Area/Fill",将其图像颜色设置为绿色
- 添加一个文本UI对象作为Player UI的子节点,将其命名为Player Name Text
- 将Player UI从Hierarchy拖动到Assets中的Prefab文件夹中,生成Prefab
- 删除场景中的实例,我们不再需要它了。
PlayerUI脚本基础
创建新的C#脚本,命名为PlayerUI
-
下面是PlayerUI脚本的框架
using UnityEngine; using UnityEngine.UI; using System.Collections; namespace Com.MyCompany.MyGame { public class PlayerUI : MonoBehaviour { #region Public Properties [Tooltip("UI Text to display Player's Name")] public Text PlayerNameText; [Tooltip("UI Slider to display Player's Health")] public Slider PlayerHealthSlider; #endregion #region Private Properties #endregion #region MonoBehaviour Messages #endregion #region Public Methods #endregion } }
保存脚本
现在让我们编辑Prefab。
- 把PlayerUI脚本添加到PayerUI prefab上
- 把 "Player Name Text"对象拖拽到PlayerNameText公共字段上面
- 把Slider组件拖拽到公共字段PlayerHealthSlider上面
实例化和Player绑定
绑定PlayerUI到Player
PlayerUI脚本需要知道它代表了哪个Player,因为其中的一个原因是:能够显示它的健康和名称,让我们创建一个公共的方法来完成这个绑定。
打开PlayerUI脚本
-
在私有属性区块添加一个私有属性
PlayerManager _target;
我们在这里需要思考,我们会定期寻找Health,所以缓存一个Player Manager的引用以提高效率是有意义的。
-
在公共方法区块添加下面的代码
public void SetTarget(PlayerManager target){ if (target == null) { Debug.LogError("
Missing PlayMakerManager target for PlayerUI.SetTarget.",this); return; } // Cache references for efficiency _target = target; if (PlayerNameText != null) { PlayerNameText.text = _target.photonView.owner.name; } } -
在MonoBehaviour Messages区块添加这个方法
void Update() { // Reflect the Player Health if (PlayerHealthSlider != null) { PlayerHealthSlider.value = _target.Health; } }
保存代码
有了这些,我们就可以显示目标Player的名字和health了。
实例化
OK,所以我们已经知道如何实例化这个Prefab。每次我们实例化,最好的方法是在PlayerManager的初始化过程中。
打开脚本PlayerManager
-
添加公共字段来持有Player UI的引用
[Tooltip("The Player's UI GameObject Prefab")] public GameObject PlayerUiPrefab;
在Start()方法中添加下面的代码
保存脚本
所有这些都是标准的Unity编码。但请注意,我们正在向刚刚创建的实例发送消息。我们需要一个接收器,这意味着如果SetTarget没有找到响应它的组件,我们将被警告。另一种方法是从实例中获取PlayerUI组件,然后直接调用SetTarget。通常的建议是,直接使用组件,但是知道你可以以不同的方式实现同样的事情也很好。
然而这远远不够,我们需要处理删除Player时,不能有孤独的UI实例,所以当UI实例发现被分配到的目标不存在的话,我们就需要销毁它。
打开PlayerUI脚本
-
在Update()中添加代码
// Destroy itself if the target is null, It's a fail safe when Photon is destroying Instances of a Player over the network if (_target == null) { Destroy(this.gameObject); return; }
保存PlayerUI脚本
这个代码,虽然容易,但实际上是相当方便。由于Photon删除联网实例的方式,如果发现目标引用为空,则UI实例更容易简单地销毁自身。这避免了很多潜在的问题,并且非常安全,无论为什么目标丢失的原因,相关的UI会自动破坏自己,非常方便快捷。
但等等...当一个新的级别被加载,UI正在被销毁,但我们的Player还在...所以我们需要实例化它,当我们知道一个级别被加载时,让我们这样做:
打开PlayerManager脚本
-
在CalledOnLevelWasLoaded()方法中添加
GameObject _uiGo = Instantiate(this.PlayerUiPrefab) as GameObject; _uiGo.SendMessage("SetTarget", this, SendMessageOptions.RequireReceiver);
保存PlayerManager脚本
注意,有更复杂/强大的方法来处理这一点,UI可以使用单例模式实现,但它会很快变得复杂,因为其他Players加入和离开房间也需要处理他们的UI。在我们的实现中,这是直接的,代价是我们实例化我们的UI预制的重复。作为一个简单的练习,您可以创建一个私有方法来实例化和从各个地方发送“SetTarget”消息,调用该方法,而不是复制代码。
Player绑定
Unity UI系统的一个非常重要的约束是,任何UI元素必须放置在Canvas对象下面。因此当这个PlayerUI Prefab被实例化时,我们需要处理这个约束,我们将在PlayerUI的初始化期间这样做。
打开PlayerUI脚本
-
在MonoBehaviour Messages区块添加下面的方法
void Awake(){ this.GetComponent
().SetParent (GameObject.Find("Canvas").GetComponent ()); } 保存PlayerUI脚本
为什么要蛮力查找Canvas这种方式? 因为当场景要被加载和卸载时,我们的Prefab和Canvas每一次都不相同。为了避免更复杂的代码结构,我们将采用最快的方式。真的不建议使用“Find”,因为这是一个缓慢的操作。实现更复杂的处理方法超出了本教程的范围。当你觉得习惯于Unity和脚本,并能找到编写更好的管理Canvas元素引用、并考虑到加载和卸载的方法,这会是一个很好的练习。
跟随目标Player
这是一个有趣的部分,我们需要Player UI跟随屏幕上的Player目标。这意味着几个小问题要解决:
- UI是2d元素,而播放器是3d元素。 在这种情况下我们如何匹配位置?
- 我们不想让UI稍微高于Player,我们如何在屏幕上从Player的位置偏移?
打开PlayerUI脚本
-
在Public Properties 区块添加公共属性
[Tooltip("Pixel offset from the player target")] public Vector3 ScreenOffset = new Vector3(0f,30f,0f);
-
在Private Properties区块添加下面私有属性
float _characterControllerHeight = 0f; Transform _targetTransform; Vector3 _targetPosition;
-
在SetTarget()方法中_target设置之后,添加下面代码
CharacterController _characterController = _target.GetComponent
(); // Get data from the Player that won't change during the lifetime of this Component if (_characterController != null){ _characterControllerHeight = _characterController.height; }
我们知道我们的Player是基于一个CharacterController,它有一个Height属性,我们需要这个做一个适当的偏移,使得UI元素在player上面。
-
在Public Methods区块添加公共方法
void LateUpdate () { // #Critical // Follow the Target GameObject on screen. if (_targetTransform!=null) { _targetPosition = _targetTransform.position; _targetPosition.y += _characterControllerHeight; this.transform.position = Camera.main.WorldToScreenPoint (_targetPosition) + ScreenOffset; } }
保存PlayerUI脚本
因此,将2d位置与3d位置匹配的诀窍是使用Camera的WorldtoScreenPoint函数。由于在我们的游戏中我们只有一个相机,所以可以依赖于访问Unity场景的默认设置主相机 。
注意,我们如何在几个步骤中设置偏移:首先我们获得目标的实际位置,然后添加_characterControllerHeight,最后,在我们推导出Player顶部的屏幕位置之后,我们添加屏幕偏移。
原文
http://doc.photonengine.com/en-us/pun/current/tutorials/pun-basics-tutorial/player-ui-prefab