效果如下

实现功能

  • 支持从左到右和从右到左的方向指定
  • 支持纵向的弹幕行数的动态扩展
  • 支持特殊的文本外框(如用于表示弹幕为玩家自己发送的)
  • 支持富文本
  • 支持在屏幕没有占满的情况下,两条弹幕不重叠,并满足指定的最小间距

换行规则

弹幕信息:假设有两条弹幕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 widthvar sizeFitter = _text.GetComponent<ContentSizeFitter>();if (!sizeFitter) {sizeFitter = _text.gameObject.AddComponent<ContentSizeFitter>();}//text should extend in horizontalsizeFitter.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 initializationvoid 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.RandomObjectByWeightUtility.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);}}
}

Unity实现弹幕功能相关推荐

  1. unity弹幕功能实现

    近年来直播app和视频app如日中天,在这些app里往往会有一个弹幕功能.实际的项目中肯定是用服务器客户端直接的数据来控制的,这里只在客户端进行测试实现弹幕核心功能. 下面说的就是我是如何实现弹幕功能 ...

  2. JavaScript css3模拟简单的视频弹幕功能

    最近相对比较空闲,想写一些东西写着玩.就尝试写了一个demo模拟了最简单的视频弹幕功能~~. 思路: 设置一个<div>和所播放的video的大小一致,把这个div标签蒙在video上面用 ...

  3. 因弹幕系统技术升级 B站即日起至6月6日关闭弹幕功能

    昨日晚间,B站官方微博发布声明称,因弹幕系统技术升级,从即日起至6月6日网站将暂时关闭弹幕功能. 在日前的第七届网络视听大会上,B站董事长兼CEO陈睿透露,2019年至今,已有2027万人在这家名为哔 ...

  4. html段落自动删除,利用JS代码自动删除稿件的普通弹幕功能

    事情的起因是在b站投稿了一个高级弹幕测试的视频(av9940487),但是由于b站的弹幕池机制是新的弹幕顶掉旧的弹幕,所以导致一些人发的高级弹幕很快就被顶掉了. 所以就想着写个脚本来自动删除属性为普通 ...

  5. php怎么实现弹幕,HTML如何利用canvas实现弹幕功能

    本篇文章主要介绍HTML如何利用canvas实现弹幕功能,感兴趣的朋友参考下,希望对大家有所帮助. 简介 最近在做大作业的时候需要做一个弹幕播放器.借鉴了一下别人的源码自己重新实现了一个,演示如下 主 ...

  6. 简单实现一个手持弹幕功能+文字抖动特效

    简单实现一个手持弹幕功能+文字抖动特效 效果展示 代码如下 小程序内更多配置效果预览 总结 前段时间抖音上有个抖动弹幕挺火的,于是决定仿写一个,话不多说,先看效果- 效果展示 GIF图看着有点模糊,但 ...

  7. 直播电商平台开发,video组件实现视频弹幕功能

    直播电商平台开发,video组件实现视频弹幕功能 发送弹幕 WXML文件代码如下: <!--pages/video/video.wxml--> <video class=" ...

  8. html5 canvas实现高并发视频弹幕功能

    由于项目需要,分享自己开发的高并发弹幕功能 以下为源代码,仅80行而已,可以根据canvas 的宽高自动适配: /*!*@作者: 赵玉*@邮箱: sailiy@126.com*@公司: 彩虹世纪文化传 ...

  9. flutter 弹幕插件_Flutter 实现虎牙/斗鱼 弹幕功能

    用Flutter实现弹幕功能,轻松实现虎牙.斗鱼的弹幕效果. 先来一张效果图: 实现原理 弹幕的实现原理非常简单,即将一条弹幕从左侧平移到右侧,当然我们要计算弹幕垂直方向上的偏移,不然所有的弹幕都会在 ...

最新文章

  1. 【每日一题】 面试题 17.14. 最小K个数
  2. pycharm中怎么实现批量修改变量名称
  3. 【学习笔记】python - pyecharts
  4. Matlab的Floor, Ceil, Fix, Round
  5. boost::mp11::tuple_for_each相关用法的测试程序
  6. c#中 uint--byte[]--char[]--string相互转换汇总
  7. HTML和XHTML的区别
  8. 【小题目】 输出分数对应的等级 >=90-A >=80-B >=70-C >=60-D <60-E,从控制台获取数据
  9. java 负数 位移运算_Java中的位移运算
  10. golang基础-etcd介绍与使用、etcd存取值、etcd监测数据写入
  11. android如何展示富文本_Android中如何在textView实现富文本
  12. java中的io系统详解[转]
  13. 思科交换机绑定MAC
  14. itx机箱尺寸_itx主机还需要显卡吗?极限尺寸s18 itx机箱装机示范
  15. Docker命令(二)
  16. 公众号选题方向有哪些?
  17. matlab toolbox 介绍,Matlab Robotic Toolbox使用简介(1)
  18. hihoCoder#1082 : 然而沼跃鱼早就看穿了一切
  19. 【核心基础知识】javascript的数据类型
  20. 虚拟机linux下扩充硬盘的方法

热门文章

  1. Tutorial Master 2⭐九、Module:引导Module中的通用属性讲解
  2. MySQL事件的创建和执行
  3. VMware ESXi 宕机分析过程
  4. javaWeb项目设置error页面
  5. 成为软件行业的福尔摩斯,还是苏格兰场?
  6. c语言程序设计教程中国农业出版社答案,C语言程序设计教程杨路明课后习题答案北京邮电大学出版社.pdf...
  7. css3动画与过渡效果结合出现的树叶飘落效果
  8. mysql 查询 select_mysql 查询select语句汇总
  9. Elasticsearch 入门
  10. C语言为什么不会过时?