关键词:对象池,UI Text,多人游戏
思路:利用预先设置好动画效果的 UI Text,来建立对象池(Object Pooling),当目标受到伤害时,从对象池中获取UI Text进行显示。
示例:
1 制作用于显示伤害数值的UI Text
首先,在Canvas下添加UI Text,Position和Pivot都设置成中心。设置颜色、字体等等。
然后,调整RectTransform,确定适当的初始位置。注意这里是相对位置,也就是局部坐标
接着,开始添加简单的动画,比如可以添加一个一边上升一边缩小的动画:
这里的设置是,PosY 100->600; Scale 1->0.5。因为是视觉效果,与Camera位置有关,需要自行调整。
最后,将它保存成Prefab。
Tips:血条和姓名板一般都是跟随角色移动,因此可以在角色下面单独创建一个Canvas用来显示这些内容,并且这个 Canvas 的 Render Mode要设置成 World Space。(图中的DamageText 是一开始用来测试的,忽略即可。)
Tips:为了让这个Canvas能始终朝向Camera,需要给Canvas添加一个简单的脚本,
//Billboard.cs
using UnityEngine;
public class Billboard : MonoBehaviour {
Transform m_Camera;
void Start(){
// 获取场景里的main camera
m_Camera = Camera.main.transform;
}
// 用LateUpdate, 在每一帧的最后调整Canvas朝向
void LateUpdate () {
if(m_Camera == null){
return;
}
// 这里我的角色朝向和UI朝向是相反的,如果直接用LookAt()还需要把每个UI元素旋转过来。
// 为了简单,用了下面这个方法。它实际上是一个反向旋转,可以简单理解为“负负得正”吧
transform.rotation = Quaternion.LookRotation (transform.position - m_Camera.position);
}
}
2 建立对象池(Object Pooling)
通俗点说,就是为了避免在游戏运行时,一个一个地地创建相同的GameObject;可以在游戏一开始的时候,先创建一组相同的GameOject,保存在一个List里,并且设置SetActive(false),在需要使用的时候,再去一个一个地激活。这样做可以节省运行时的资源占用。
废话说完,首先建立一个脚本用于创建一个对象池并且提供一个调用池中对象的方法:
//TextPooler.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TextPooler : MonoBehaviour {
// List 用来保存对象的引用,实际上的Pool
public List pooledText;
// 用来获得上一步创建好的UI Text,这一步非必须可通过其他方法得到。
public GameObject textToPool;
// 设置Pool的大小,根据一个对象激活的时间和激活频率来设定
public int amountToPool;
void Start(){
pooledText = new List ();
for (int i = 0; i < amountToPool; i++) {
// 在场景中创建对象;
GameObject obj = (GameObject)Instantiate (textToPool);
// 将对象放在上一步提到的那个Cavas中,伤害数值也可以时刻面向Camera
// 这里的false,是让对象继续使用局部坐标。如果继续使用世界坐标,上一步设置的位置会被改变,结果就是看不到。
obj.transform.SetParent (gameObject.transform,false);
// 创建后,默认不激活。
obj.SetActive (false);
// 将对象引用添加到List中。
pooledText.Add (obj);
}
}
// 提供一个获取方法
public GameObject GetPooledText(){
for(int i = 0; i < pooledText.Count; i++){
// 只会返回未在场景里激活的对象
if(!pooledText[i].activeInHierarchy){
return pooledText [i];
}
}
return null;
}
}
3 将TextPooler.cs 添加给Canvas
这样在SetParent()时可以方便地获得Parent.Transform。然后将第一步制作好的Text Prefab添加到 TextToPool,并设置数量。
4 在造成伤害时,激活Text。
实现伤害的方法一般都放在Health.cs中,因此,在Health.cs中首先可以添加一个变量,用于获得对应TextPooler的引用。在Health.cs中,添加:
[SerializeField] TextPooler TextPool;
然后,直接把Canvas拖到图中位置即可,因为TextPooler已经在Canvas中,所以Unity会自动识别出来。
接着,在Health.cs里实现伤害的方法中,加入对Text的控制:
public class Health : NetworkBehaviour {
// 以上省略
// 对数值的控制一般在服务器端进行操作,在客户端则控制数值以外的受伤效果、死亡重生等。
// 所以在服务端进行数值的计算,判定是否击杀,然后在客户端执行命中、击杀效果等。
[Server]
public bool TakeDamage(float amount, NetworkInstanceId CasterNetID){
bool died = false;
if (health <= 0)
return died;
health -= amount;
// 通过 CasterNetID获得法术释放着的ID,然后更新其伤害统计数据;
// 具体做法可以参考我的另一篇文章《多人游戏中法术伤害和击杀数据的实时显示》
if(!CasterNetID.IsEmpty())
GameManager.Instance.GameInitial.GetPlayers (CasterNetID).GetComponent ().DamageStatsUpdate(amount);
died = health <= 0;
if (died) {
// 更新法术释放者的击杀数据
if(!CasterNetID.IsEmpty())
GameManager.Instance.GameInitial.GetPlayers (CasterNetID).GetComponent ().KillStatsUpdate ();
// 计算是否存活的玩家数量,用于判断是否结束游戏;
GameManager.Instance.GameInitial.CalPlayers();
}
// 在客户端执行造成伤害的效果
RpcTakeDamage (died,amount);
return died;
}
[ClientRpc]
void RpcTakeDamage(bool died, float amount){
if (isLocalPlayer) {
// 这里可以添加玩家被击中后只在本机显示的效果,比如,被击中后屏幕闪红。
}
// 调用并激活一个Text,显示伤害值amount。
PopDamageText (amount);
// 用来控制死后是否重生
if (died){
var isReborn = true;
if (DestoryOndeath) {
isReborn = false;
} else {
isReborn = true;
}
Player.Die (isReborn);
}
}
void PopDamageText(float amount){
// 从Text对象池中,获取一个未激活的Text
var textObj = TextPool.GetPooledText ();
// 设定初始位置,如果和Prefab的一致,可以省略,注意是局部坐标
textObj.transform.localPosition = new Vector3 (0,100f,0);
// 设置Text的显示内容
textObj.GetComponent().text = "-" + amount;
// 激活Text
textObj.SetActive (true);
// 一定时间后,将该Text重新变为非激活,注意不是Destroy
StartCoroutine (DelText(textObj));
}
IEnumerator DelText(GameObject textObj){
yield return new WaitForSeconds(2f);
textObj.SetActive(false);
}
//以下省略
}
5 实际效果是这个样子
运行之后,可以看到生成的对象池: