Unity实现弹幕功能

发表于2018-09-26
评论0 3.5k浏览
类似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

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: