.NET鬥魚直播彈幕客戶端(下)
.NET鬥魚直播彈幕客戶端(下)
在上篇文章中,我們提到了如何使用.NET
連線鬥魚TV直播彈幕的基本操作。然而想要做得好,做得容易擴充套件,就需要做進一步的程式碼整理。
本文將涉及以下內容:
- 介紹如何使用
Reactive Extensions
(Rx
),演示這一系列操作用起來,就像寫Hello World
一樣簡單; - 用我自制的“準遊戲引擎”
FlysEngine
,只需少量程式碼,即可實現桌面彈幕的效果; - 最後提供一波“伸手黨”福利,文中所有可執行、完整程式碼,將按原樣奉上。
Rx.NET
Rx
,是Reactive Extensions
的縮寫,據說Rx
發明於.NET 2.0
時代的微軟。那時候還沒有async/await
RX
對程式語言要求不高(如不要求內建協程
-coroutine
),RX
反倒在.NET
之外的其它程式語言中大行其道。如rx.js
、RxJava
等等。
C#
從.NET 2.0
就提供了yield
關鍵字,然後3.0
提供了LINQ
,5.0
提供了async/await
,因此很多時候RX
的意義不大。但在某些情況下(如這種情況),就有意義了,原因請見下圖:
- | 單資料 | 多資料 |
---|---|---|
同步 | T |
IEnumerable<T> |
非同步 | Task<T> |
Observable<T> /IAsyncEnumerable<T> |
C#
的協程
支援同步多資料,非同步單資料,但不支援同步多資料(C# 8.0
IAsyncEnumerable<T>
),本文將使用Rx
來包裝上一篇文章的鬥魚TV直播彈幕客戶端。
來先看一波老程式碼:
注意剪頭所指的位置,那是基礎程式碼“出口”,或者業務邏輯“入口”,基礎程式碼不能簡單地return
打斷,因為它要不停地輸出資料,這時就需要像協程
等程式語言功能,或者Rx
的支援。
Rx
-Hello World
首先引入NuGet
包System.Reactive
,一個簡單的“非同步多值返回”的Rx
示例程式碼如下:
Observable.Create<int>(async (o, cancellationToken) => { for (var i = 0; i < 5; ++i) { await Task.Delay(1000); o.OnNext(i); } o.OnCompleted(); })
麻雀雖小,五臟俱全,如程式碼所示,幾乎只需在正常程式碼外包一層Rx
,即可享受Rx
的好處。
使用Rx
使用起來就更簡單了,上篇展示的長達252
行程式碼的demo
,現在只需一行程式碼,即可無侵入式地呼叫:
DouyuBarrage.ChatMessageFromUrl("https://www.douyu.com/scboy")
呼叫結果如下(和昨天效果完全一樣):
Rx
的其它好處
除了呼叫簡單之外,Rx
的擴充套件也非常非常簡單,比如完成以下操作,以前可能非常麻煩,需要改多處程式碼,而使用Rx
,只需像LINQ
一樣加幾個指令即可:
同時抓多個直播間的彈幕
#load ".\barrage.linq"
DouyuBarrage.ChatMessageFromUrl("https://www.douyu.com/scboy")
.Select(x => new { Room = "scboy", Message = x.Message })
.Merge(DouyuBarrage.ChatMessageFromUrl("https://www.douyu.com/topic/lscs?rid=633019")
.Select(x => new { Room = "lalala", Message = x.Message}))
效果如下:
只需一個Merge
指令即可合併兩個直播間的彈幕(Observable<T>
)
擴充套件簡單
比如只想提取特殊的彈幕,或者資料之前想做一些轉換,可以使用Where
,Select
等資料過濾和轉換操作符,符合LINQ
的習慣,非常好用。比如我正常彈幕的提取,其實是從JObjectFromUrl
轉換而來,JObjectFromUrl
,又是從RawFromUrl
轉換而來,這提高了擴充套件性,又無需修改老程式碼,正是所謂“對擴充套件開放,對修改封閉”的開放-封閉原則:
public IObservable<JToken> JObjectFromUrl(string url)
=> RawFromUrl(url).Select(MsgTool.DecodeStringToJObject);
public IObservable<Barrage> ChatMessageFromUrl(string url) =>
JObjectFromUrl(url)
.Where(x => x["type"].Value<string>() == "chatmsg")
.Select(Barrage.FromJToken);
又比如可能我只想提取彩色彈幕,我只需ChatMessageFromUrl().Where(x => x.Color != 0xffffff)
即可,非常方便。
桌面彈幕
這可能是另一個主題——實時渲染,用到了我自己寫的“準遊戲引擎”FlysEngine
,因此需要安裝NuGet
包:FlysEngine.Desktop
。
桌面彈幕
不同於網頁彈幕
,只能在網頁中顯示,而桌面彈幕
可以直接顯示在螢幕最上方。有些公司年會可能用到了桌面彈幕
,這無疑增加了主持人與觀眾們的互動,提高了群眾參與的積極性。
注意:本文中所說
FlysEngine
的實質是Direct2D
和Windows API
-UpdateLayeredWindowIndirect
函式。如果不想使用FlysEngine
,完全可以使用其它方式代替。最簡單的方式是使用WPF
,然後設定AllowsTransparency=true
,但這樣效能會差一些。本文介紹的方法,CPU
使用率將保持在0%
左右!
桌面彈幕的要點
- 渲染文字
DirectWrite
; - 文字移動 將文字從螢幕右邊移動到左邊;
- 檢測是否離開螢幕 如果螢幕上不顯示彈幕,即可將彈幕刪除;
- 初始位置確定 如果一行顯示不下,則將彈幕放在下一行。
渲染文字
渲染文字一般是通過DirectWrite
,它效能很好,功能也強大。FlysEngine
將DirectWrite
封裝了,因此直接用便是。
注意:
DirectWrite
不僅渲染文字,還提供了.Metrics
屬性,可以計算文字渲染之後的大小,這會讓事情變得容易很多。
文字移動
文字移動首先需要一個位置,隨著時間變化,將該位置的X
座標不段減少即可。這可以通過FlysEngine
中的UpdateLogic
事件實現,它會定期呼叫,傳入一個float dt
,程式碼離上一次呼叫UpdateLogic
的時間間隔。因此可以利用這個dt
變數,計算是彈幕的新位置:
public void MoveLeft(float dt, float speed)
{
Position.X -= dt * speed;
}
檢測是否離開螢幕
由於我們已知彈幕是矩形,(很顯然螢幕也是矩形)因此這個檢測比較簡單,直接判斷文字的右邊緣
是否大於0
即可。
也由於需要經常/頻繁地刪除在螢幕上的彈幕物件,因此最好儲存彈幕的資料結構別使用O(n)
的集合,如最好別使用List<T>
,它是線性表。我這裡使用的是連結串列
,.NET
的連結串列實現是LinkedList<T>
(很多人以為是List<T>
)。
多說一句,連結的遍歷演算法如下(while
迴圈):
var node = barrages.First;
while (node != null)
{
var next = node.Next;
// do work here
node = next;
}
之所以不使用foreach
來遍歷,因為這樣遍歷可以實現高效能的“邊遍歷、邊刪除”的實現。
初始位置確定
這一點思想需要多想想,需要從第一行開始,從後往前看,看最後那一邊彈幕是否大於螢幕右邊。只要想清楚了,程式碼很容易:
float GetNewY()
{
float y = 0;
while (barrages.Reverse().Where(x => x.Position.Y == y).Select(x => x.Rect.Right).FirstOrDefault() > form.Width)
{
y += FontSize;
}
return y;
}
有了這些,就可以愉快地感受螢幕彈幕啦!
彩色emoji
表情
Direct2D
支援——但預設不顯示彈幕emoji
表情:
需要多加一個列舉讓其支援:
target.DrawText("