效果如下
實現功能
- 支持從左到右和從右到左的方向指定
- 支持縱向的彈幕行數的動態擴展
- 支持特殊的文本外框(如用於表示彈幕爲玩家自己發送的)
- 支持富文本
- 支持在屏幕沒有佔滿的情況下,兩條彈幕不重疊,並滿足指定的最小間距
換行規則
彈幕信息:假設有兩條彈幕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);
}
}
}