Unity实现弹幕功能
发表于2018-09-26
类似B站与一些直播APP,这些软件中往往都会有一个弹幕功能,在实际的项目中肯定是用服务器客户端直接的数据来控制的,下面就来看看实现弹幕功能的方法。
效果如下
实现功能
- 支持从左到右和从右到左的方向指定
- 支持纵向的弹幕行数的动态扩展
- 支持特殊的文本外框(如用于表示弹幕为玩家自己发送的)
- 支持富文本
- 支持在屏幕没有占满的情况下,两条弹幕不重叠,并满足指定的最小间距
换行规则
弹幕信息:假设有两条弹幕D1、D2,;分别对应长度L1、L2.频幕宽度为Lp,最小间隔为Ld.
当L1<=L2时:
S1 = L1 + Lp + Ld, V1 = (L1 + Lp)/T, S2 = Lp, V2 = (L2 + Lp)/T
提前走的时间 = delta_t = S1/V1 – S2/V2 = T*[ (L1+ Lp + Ld)/(L1+Lp) – Lp/(L2+Lp) ]
当L1>L2时:
提前走的距离=ahead_s = (L1 + Ld), V1 = (L1 + Lp)/T
提前走的时间=delta_t = ahead_s/V1 = T* (L1 + Ld)/(L1 + Lp)
弹幕文本组件
using UnityEngine; using UnityEngine.UI; using System.Collections; using DG.Tweening; namespace Joker.CustomComponent.BulletScreen { public enum ScrollDirection { RightToLeft = 0, LeftToRight = 1 } public class BulletTextInfo { public float TextWidth; public float SendTime; } public class BulletScreenTextElement : MonoBehaviour { [SerializeField]private BulletScreenDisplayer _displayer; [SerializeField]private string _textContent; [SerializeField]private bool _showBox; [SerializeField]private ScrollDirection _scrollDirection; [SerializeField]private Text _text; [SerializeField]private float _textWidth; [SerializeField]private Vector3 _startPos; [SerializeField]private Vector3 _endPos; public static BulletScreenTextElement Create(BulletScreenDisplayer displayer, string textContent, bool showBox = false, ScrollDirection direction = ScrollDirection.RightToLeft) { BulletScreenTextElement instance = null; if (displayer == null) { Debug.Log("BulletScreenTextElement.Create(), displayer can not be null !"); return null; } GameObject go = Instantiate(displayer.TextElementPrefab) as GameObject; go.transform.SetParent(displayer.GetTempRoot()); go.transform.localPosition = Vector3.up*10000F; go.transform.localScale = Vector3.one; instance = go.AddComponent<BulletScreenTextElement>(); instance._displayer = displayer; instance._textContent = textContent; instance._showBox = showBox; instance._scrollDirection = direction; return instance; } private IEnumerator Start() { SetBoxView(); SetText(); //get correct text width in next frame. yield return new WaitForSeconds(0.2f); RecordTextWidthAfterFrame(); SetRowInfo(); SetTweenStartPosition(); SetTweenEndPosition(); StartMove(); } /// <summary> /// The outer box view of text /// </summary> private void SetBoxView() { Transform boxNode = transform.Find(_displayer.TextBoxNodeName); if (boxNode == null) { Debug.LogErrorFormat( "BulletScreenTextElement.SetBoxView(), boxNode == null. boxNodeName: {0}", _displayer.TextBoxNodeName); return; } boxNode.gameObject.SetActive(_showBox); } private void SetText() { _text = GetComponentInChildren<Text>(); //_text.enabled = false; if (_text == null) { Debug.Log("BulletScreenTextElement.SetText(), not found Text!"); return; } _text.alignment = _scrollDirection == ScrollDirection.RightToLeft ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight; //make sure there exist ContentSizeFitter componet for extend text width var sizeFitter = _text.GetComponent<ContentSizeFitter>(); if (!sizeFitter) { sizeFitter = _text.gameObject.AddComponent<ContentSizeFitter>(); } //text should extend in horizontal sizeFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; _text.text = _textContent; } private void RecordTextWidthAfterFrame() { _textWidth = _text.GetComponent<RectTransform>().sizeDelta.x; } private void SetTweenStartPosition() { Vector3 nor = _scrollDirection == ScrollDirection.RightToLeft ? Vector3.right : Vector3.left; _startPos = nor * (_displayer.BulletScreenWidth / 2F + _textWidth / 2F); transform.localPosition = _startPos; } private void SetTweenEndPosition() { Vector3 nor = _scrollDirection == ScrollDirection.RightToLeft ? Vector3.left : Vector3.right; _endPos = nor * (_displayer.BulletScreenWidth / 2F + _textWidth / 2F); } private void SetRowInfo() { var bulletTextInfo = new BulletTextInfo() { SendTime = Time.realtimeSinceStartup, TextWidth = _textWidth }; var rowRoot = _displayer.GetRowRoot(bulletTextInfo); transform.SetParent(rowRoot, false); transform.localScale = Vector3.one; } private void StartMove() { //make sure the text is active. //the default ease of DoTewwen is not Linear. transform.DOLocalMoveX(_endPos.x, _displayer.ScrollDuration).OnComplete(OnTweenFinished).SetEase(Ease.Linear); } private void OnTweenFinished() { Destroy(gameObject, _displayer.KillBulletTextDelay); } } }
弹幕播放器组件
using System.Collections.Generic; using UnityEngine; namespace Joker.CustomComponent.BulletScreen { public class BulletScreenDisplayer : MonoBehaviour { public bool Enable { get; set; } public BulletTextInfo[] _currBulletTextInfoList; [SerializeField]private BulletScreenDisplayerInfo _info; public float ScrollDuration { get { return _info.ScrollDuration; } } private float _bulletScreenWidth; public float BulletScreenWidth { get { return _bulletScreenWidth; } } public GameObject TextElementPrefab { get { return _info.TextPrefab; } } public string TextBoxNodeName { get { return _info.TextBoxNodeName; } } public float KillBulletTextDelay { get { return _info.KillBulletTextDelay; } } public Transform ScreenRoot { get { return _info.ScreenRoot.transform; } } public static BulletScreenDisplayer Create(BulletScreenDisplayerInfo displayerInfo) { BulletScreenDisplayer instance = displayerInfo.Owner.gameObject.AddComponent<BulletScreenDisplayer>(); instance._info = displayerInfo; return instance; } public void AddBullet(string textContent, bool showBox = false, ScrollDirection direction = ScrollDirection.RightToLeft) { BulletScreenTextElement.Create(this, textContent, showBox, direction); } private void Start() { SetScrollScreen(); InitRow(); } private void InitRow() { Utility.DestroyAllChildren(_info.ScreenRoot.gameObject); _currBulletTextInfoList = new BulletTextInfo[_info.TotalRowCount]; for (int rowIndex = 0; rowIndex < _info.TotalRowCount; rowIndex++) { _currBulletTextInfoList[rowIndex] = null; string rowNodeName = string.Format("row_{0}", rowIndex); GameObject newRow = new GameObject(rowNodeName); var rt = newRow.AddComponent<RectTransform>(); rt.SetParent(_info.ScreenRoot.transform, false); } } private void SetScrollScreen() { _info.ScreenRoot.childAlignment = TextAnchor.MiddleCenter; _info.ScreenRoot.cellSize = new Vector2(100F, _info.RowHeight); _bulletScreenWidth = _info.ScreenRoot.GetComponent<RectTransform>().rect.width; } public Transform GetTempRoot() { return _info.ScreenRoot.transform.Find(string.Format("row_{0}", 0)); } public Transform GetRowRoot(BulletTextInfo newTextInfo) { const int notFoundRowIndex = -1; int searchedRowIndex = notFoundRowIndex; newTextInfo.SendTime = Time.realtimeSinceStartup; for (int rowIndex = 0; rowIndex < _currBulletTextInfoList.Length; rowIndex++) { var textInfo = _currBulletTextInfoList[rowIndex]; //if no bullet text info exist in this row, create the new directly. if (textInfo == null) { searchedRowIndex = rowIndex; break; } float l1 = textInfo.TextWidth; float l2 = newTextInfo.TextWidth; float sentDeltaTime = newTextInfo.SendTime - textInfo.SendTime; var aheadTime = GetAheadTime(l1, l2); if (sentDeltaTime >= aheadTime) {//fit and add. searchedRowIndex = rowIndex; break; } //go on searching in next row. } if (searchedRowIndex == notFoundRowIndex) {//no fit but random one row. int repairRowIndex = Random.Range(0, _currBulletTextInfoList.Length); searchedRowIndex = repairRowIndex; } _currBulletTextInfoList[searchedRowIndex] = newTextInfo; Transform root = _info.ScreenRoot.transform.Find(string.Format("row_{0}", searchedRowIndex)); return root; } /// <summary> /// Logic of last bullet text go ahead. /// </summary> /// <param name="lastBulletTextWidth">width of last bullet text</param> /// <param name="newCameBulletTextWidth">width of new came bullet text</param> /// <returns></returns> private float GetAheadTime(float lastBulletTextWidth, float newCameBulletTextWidth) { float aheadTime = 0f; if (lastBulletTextWidth <= newCameBulletTextWidth) { float s1 = lastBulletTextWidth + BulletScreenWidth + _info.MinInterval; float v1 = (lastBulletTextWidth + BulletScreenWidth) / _info.ScrollDuration; float s2 = BulletScreenWidth; float v2 = (newCameBulletTextWidth + BulletScreenWidth) / _info.ScrollDuration; aheadTime = s1 / v1 - s2 / v2; } else { float aheadDistance = lastBulletTextWidth + _info.MinInterval; float v1 = (lastBulletTextWidth + BulletScreenWidth) / _info.ScrollDuration; aheadTime = aheadDistance / v1; } return aheadTime; } }
封装的配置脚本
using UnityEngine; using UnityEngine.UI; namespace Joker.CustomComponent.BulletScreen { [System.Serializable] public class BulletScreenDisplayerInfo { [Header("组件挂接的节点")] public Transform Owner; [Header("文本预制")] public GameObject TextPrefab; [Header("弹幕布局组件")] public GridLayoutGroup ScreenRoot; [Header("初始化行数")] public int TotalRowCount = 2; [Header("行高(单位:像素)")] public float RowHeight; [Header("字体边框的节点名字")] public string TextBoxNodeName; [Header("从屏幕一侧到另外一侧用的时间")] public float ScrollDuration = 8F; [Header("两弹幕文本之间的最小间隔")] public float MinInterval = 20F; [Header("移动完成后的销毁延迟")] public float KillBulletTextDelay = 0F; public BulletScreenDisplayerInfo(Transform owner, GameObject textPrefab, GridLayoutGroup screenRoot, int initialRowCount = 1, float rowHeight = 100F, string textBoxNodeName = "text_box_node_name") { Owner = owner; TextPrefab = textPrefab; ScreenRoot = screenRoot; TotalRowCount = initialRowCount; RowHeight = rowHeight; TextBoxNodeName = textBoxNodeName; } } }
例子:
using System.Collections; using UnityEngine; using System.Collections.Generic; using Joker.CustomComponent.BulletScreen; public class ExampleBulletScreen : MonoBehaviour { public BulletScreenDisplayer Displayer; public List<string> _textPool = new List<string>() { "ウワァン!!(ノДヽ) ・・(ノ∀・)チラ 実ゎ・・嘘泣き", "(╯#-_-)╯~~~~~~~~~~~~~~~~~╧═╧ ", "<( ̄︶ ̄)↗[GO!]", "(๑•́ ₃ •̀๑) (๑¯ิε ¯ิ๑) ", "(≖͞_≖̥)", "(`д′) ( ̄^ ̄) 哼! <(`^′)>", "o(* ̄︶ ̄*)o", " 。:.゚ヽ(。◕‿◕。)ノ゚.:。+゚", "号(┳Д┳)泣", "( ^∀^)/欢迎\( ^∀^)", "ドバーッ(┬┬_┬┬)滝のような涙", "(。┰ω┰。", "啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊" }; // Use this for initialization void Start() { Displayer.Enable = true; StartCoroutine(StartDisplayBulletScreenEffect()); } private IEnumerator StartDisplayBulletScreenEffect() { while (Displayer.Enable) { Displayer.AddBullet(GetText(), CheckShowBox(), GetDirection()); yield return new WaitForSeconds(0.2f); } } private string GetText() { int textIndex = Random.Range(0, _textPool.Count); var weightDict = new Dictionary<object, float>() { {"<color=yellow>{0}</color>", 10f}, {"<color=red>{0}</color>", 2f}, {"<color=white>{0}</color>", 80f} }; string randomColor = (string)Utility.RandomObjectByWeight(weightDict); string text = string.Format(randomColor, _textPool[textIndex]); return text; } private bool CheckShowBox() { var weightDict = new Dictionary<object, float>() { {true, 20f}, {false, 80f} }; bool ret = (bool)Utility.RandomObjectByWeight(weightDict); return ret; } private ScrollDirection GetDirection() { var weightDict = new Dictionary<object, float>() { {ScrollDirection.LeftToRight, 5f}, {ScrollDirection.RightToLeft, 80f} }; ScrollDirection direction = (ScrollDirection)Utility.RandomObjectByWeight(weightDict); return direction; } }
补充
关于有的同学提到的Utility.cs这个脚本,我来解释一下。这个脚本是我的工具脚本,里面包含了很多工具方法。其中我们这个案例中用到的 Utility.RandomObjectByWeight 和Utility.DestroyAllChildren() 这两个方法都是其中的工具方法。因为这个脚本太大,所以我就只把这2个方法给提供出来,以方便大家正常的把例子工程运行起来。
using UnityEngine; using System.Collections.Generic; public static class Utility { /// <summary> /// 不是每次都创建一个新的map,用于减少gc /// </summary> private static readonly Dictionary<object, Vector2> _randomIntervalMap = new Dictionary<object, Vector2>(); /// <summary> /// 根据权重配置随机出一种结果。 /// </summary> /// <param name="weightInfo"></param> /// <returns></returns> public static object RandomObjectByWeight (Dictionary<object, float> weightInfo) { object randomResult = null; //count the total weights. float weightSum = 0f; foreach (var item in weightInfo) { weightSum += item.Value; } //Debug.Log( "weightSum: " + weightSum ); //value -> Vector2(min,max) _randomIntervalMap.Clear(); //calculate the interval of each object. float currentWeight = 0f; foreach (var item in weightInfo) { float min = currentWeight; currentWeight += item.Value; float max = currentWeight; Vector2 interval = new Vector2(min, max); _randomIntervalMap.Add(item.Key, interval); } //random a value. float randomValue = UnityEngine.Random.Range(0f, weightSum); //Debug.Log( "randomValue: " + randomValue ); int currentSearchCount = 0; foreach (var item in _randomIntervalMap) { currentSearchCount++; if (currentSearchCount == _randomIntervalMap.Count) { //the last interval is [closed,closed] if (item.Value.x <= randomValue && randomValue <= item.Value.y) { return item.Key; } } else { //interval is [closed, opened) if (item.Value.x <= randomValue && randomValue < item.Value.y) { randomResult = item.Key; } } } return randomResult; } /// <summary> /// 删除所有子节点 /// </summary> /// <param name="parent"></param> public static void DestroyAllChildren (GameObject parent) { Transform parentTrans = parent.GetComponent<Transform>(); for (int i = parentTrans.childCount - 1; i >= 0; i--) { GameObject child = parentTrans.GetChild(i).gameObject; GameObject.Destroy(child); } } }
来自:https://blog.csdn.net/lingyanpi/article/details/53072119