[{"content":"最近我在《Hello, World. Goodbye.》中構建了一個最能表達情緒的功能之一 —— 影片彈幕系統。這是向 NicoNico 式的彈幕留言致敬，也是一種讓玩家在沉默中向虛空吶喊，或是傾聽他人曾經吶喊過的話語的方式。\n🎯 目標 設計一個模組化、流暢、高效能的彈幕系統，具有以下特點：\n獨立於主遊戲相機運作（透過專用 Viewport 實現） 支援多種內容類型（文字、圖片等） 彈幕可均勻分布於各軌道，或採用聚焦式分布風格 動態計算彈幕間距，智慧避免重疊 🎬 Step-by-Step Construction 第一步：從簡單開始 我們從一個基於 RichTextLabel 的彈幕節點開始：\n上圖展示了一個最基礎的彈幕節點場景結構。每條彈幕都是一個 RichTextLabel 節點，掛載在彈幕專用的父節點下，便於統一管理和批次操作。\n附加的 C# 腳本如下：\npublic override void _Process(double delta) { Position -= new Vector2(Speed * (float)delta, 0); if (Position.X + GetWidth() \u0026lt; 0) QueueFree(); } 這個方案一開始運作良好，直到我們發現彈幕出現了奇怪的偏移——即使彈幕移動到 (width, 0)，它似乎還是從不對的位置出現。\n彈幕不僅沒有從正確位置出現，還過早地消失了。為什麼會這樣？原來玩家有一個相機，導致螢幕顯示的位置與實際位置並不一致。這就是問題所在。\n第二步：與相機解耦 為了解決這個問題，我們將彈幕系統移動到一個獨立的 Viewport 層（640x360），不再受主相機影響。這樣彈幕無論世界如何移動，都會始終從螢幕右側出現。\n問題：彈幕是如何分布在螢幕上的？\n以 B 站演唱會彈幕為例，可以看到螢幕上有多條軌道，彈幕從右向左捲動。\n實際上，所有彈幕都分布在螢幕上的「隱形軌道」中，如下圖所示：\n第三步：新增軌道管理器 接下來，我們引入了 DanmakuTrackManager，用於管理彈幕軌道，確保彈幕不會重疊。\npublic class DanmakuTrackManager { private readonly List\u0026lt;List\u0026lt;DanmakuTrackEntry\u0026gt;\u0026gt; _tracks; private readonly Node _danmakuLayer; private readonly int _trackCount; private readonly float _trackHeight; private readonly float _screenWidth; private readonly float _spacing; public bool IsTrackAvailable(int trackIndex) { var track = _tracks[trackIndex]; if (track.Count == 0) return true; var last = track[^1]; float lastRight = last.Entry.Position.X + last.Entry.Call(\u0026#34;GetWidth\u0026#34;).AsSingle(); return lastRight + 20f \u0026lt; _screenWidth; } } 系統會根據彈幕節點（如 RichTextDanmakuLabel）的尺寸自動計算軌道數量和間距。\n第四步：新增分配器 為了靈活地管理彈幕分配，我們需要新增一個分配器。因此我建立了一個 IDanmakuTrackDistributor，用於控制彈幕分配到哪個軌道。支援兩種分配模式：\n均勻分配：輪流填充各軌道 聚焦分配：集中堆疊到少數軌道，營造視覺衝擊 public class EvenDistributor : IDanmakuTrackDistributor { public int GetTrackIndex(int danmakuCount, int trackCount) { return danmakuCount % trackCount; } } 這是最簡單的輪詢分配方式。實際應用中，還需結合軌道管理器判斷軌道是否可用。\n第五步：新增佇列系統 通常我們希望彈幕批次出現但不是一次性全都畫在螢幕上，因此需要一個佇列系統，統一分發彈幕。例如遊戲觸發某事件時，批次新增彈幕。\npublic partial class DanmakuSystemController : Node, ISystemModule { private Queue\u0026lt;DanmakuQueueEntry\u0026gt; _danmakuQueue; private DanmakuTrackManager _trackManager; private double _lastSpawnTime = 0; public override void _Ready() { _danmakuQueue = new Queue\u0026lt;DanmakuQueueEntry\u0026gt;(); _trackManager = new DanmakuTrackManager(); } public void AddDanmaku(DanmakuQueueEntry entry) { _danmakuQueue.Enqueue(entry); ProcessQueue(); } public override void _Process(double delta) { if (_danmakuQueue.Count == 0) return; // Peek queue and if time passed, spawn a danmaku if (Time.GetTicksMsec() \u0026gt; _lastSpawnTime + SpawnInterval) { // If the next track is available on the track manager if (_trackManager.HasTrackSpace()) { var danmakuData = _danmakuQueue.Dequeue(); _trackManager.AddDanmakuToTrack(danmakuData.Data); _lastSpawnTime = Time.GetTicksMsec(); } } } } 這樣可以在 _Process 方法中逐幀檢查佇列和軌道空間，軌道可用時再生成彈幕，避免重疊。\n第六步：修復超車問題 出現了一個 bug：同一軌道上速度快的彈幕會「超車」慢彈幕，導致重疊。B 站等平台也有類似現象。解決方法是引入「速度修正」機制，確保在軌道新增的彈幕速度不會超過軌道裡已有彈幕的最大速度。\n可以在 DanmakuTrackManager 中新增如下程式碼：\n// 設置初始速度，限制最大速度防止超車 danmakuData.Speed = Math.Max(GetMaxSpeedInTrack(trackIndex), danmakuData.Speed); 然後實現 GetMaxSpeedInTrack 方法：\nprivate float GetMaxSpeedInTrack(int trackIndex) { var track = _tracks[trackIndex]; if (track.Count == 0) return 0; // 遍歷軌道，獲取最大速度 return track.Max(entry =\u0026gt; entry.Entry.Speed); } 第七步：系統整合 現在我們已經實現了 DanmakuTrackManager、IDanmakuTrackDistributor、DanmakuSystemController 和 RichTextDanmakuLabel，可以在主場景中將它們整合起來。\n最終效果 總結 現在的彈幕系統已經模組化、高效能且易於擴展，支援文字、圖片等多種內容類型，彈幕分布可以均勻或聚焦，動態計算間距有效避免重疊。\n這也是我用 Godot 4 和 C# 實現複雜功能、同時保持架構清晰的一個實踐。如果你也想在 Godot 4 裡做彈幕系統，希望這篇文章能幫到你！有問題或想法歡迎留言交流。Happy coding!\n","permalink":"https://kasuri.works/zh-tw/posts/building-danmaku-system/","summary":"\u003cp\u003e最近我在《Hello, World. Goodbye.》中構建了一個最能表達情緒的功能之一 —— 影片彈幕系統。這是向 NicoNico 式的彈幕留言致敬，也是一種讓玩家在沉默中向虛空吶喊，或是傾聽他人曾經吶喊過的話語的方式。\u003c/p\u003e","title":"在 Godot 4 中構建彈幕系統"},{"content":"HelloWorld.GoodBye()\n\u0026ldquo;Hello World.\u0026rdquo; —— 是我最初寫下的第一行程式碼。 \u0026ldquo;GoodBye()\u0026rdquo; —— 是我在系統盡頭，留給世界的函式呼叫。\n👋 我是誰 我是一名程式設計師。也是一名獨立遊戲開發者。 目前，這款遊戲是我一個人做的。企劃、美術、程式、寫作，全靠夜裡那一點點不甘心和心裡那些說不出口的事。\n從前我真的很喜歡寫程式。直到有一天，我發現自己已經不再為自己寫東西了。 我寫給 Jira，寫給老闆，寫給面試官，寫給 KPI，寫給那個「足夠理性、足夠穩定」的職業形象。\n後來我意識到，我不是不熱愛寫程式了。 我只是，太久沒有為自己寫下什麼了。\n💬 為什麼做這款遊戲？ 因為有太多話，我在現實中說不出口。\n我想說我很努力，但還是很累。 我想說我不想再背面試題了。 我想說，我寫的每一行程式碼不是為了積分，不是為了 review 評論裡的「well done」， 而是因為我曾經真的相信，程式碼是創造的語言。\n這不是一個勵志故事。 這是一場「從 Hello 到 GoodBye()」的過程。 不是失敗，而是一次自我恢復。\n🎮 這是什麼遊戲？ 它是一個平台動作遊戲。像素風，帶敘事。 它的主角是一名程式設計師。你可以理解為我，也可以理解為你。\n這款遊戲沒有宏大敘事，沒有救世主，沒有打敗邪惡的結局。 它只是記錄了一個人，在職場系統中逐漸失控，然後重新找回自我的過程。\n或者更準確地說：\n它是我為自己寫下的，一行還沒 return 的函式。\n🛠 當前狀態 遊戲仍在開發中。Demo 製作進行中，沒有發布日期，沒有宣傳片。 但我會持續更新這個部落格，記錄製作過程和一路上的小情緒。\n如果你也曾有過一瞬間， 覺得自己好像不是「人」了，只是一個「行程」， 那歡迎你來看看這款遊戲。\nHelloWorld.GoodBye() 不是一個專案，是一句話。 寫給系統，也寫給你自己。\n「我懂。」 「你沒有問題。」 「你被看見了。」\n","permalink":"https://kasuri.works/zh-tw/posts/hello-world-goodbye/","summary":"這是一個「從 Hello 到 GoodBye()」的過程，也許你也曾走過那條路。","title":"HelloWorld.GoodBye()"},{"content":"歡迎來到 Kasuri Works 部落格！在這裡，我們將分享遊戲開發的最新動態、技術深度解析，以及專案背後的創作心得。\n你可以期待什麼 遊戲開發日誌： 專案進展、遇到的挑戰與重要里程碑。 技術文章： 教學、程式碼片段，以及我們使用的工具和技術的討論。 幕後故事： 影響我們遊戲的美術、設計和音樂探索。 敬請期待更多內容，和我們一起成長、學習、分享。感謝你的關注與支持！\n歡迎在 Misskey 關注我們，參與更多討論！\n","permalink":"https://kasuri.works/zh-tw/posts/hello-world/","summary":"歡迎來到 Kasuri Works 部落格。這裡記錄著我們的開發過程、靈感與技術點滴。","title":"你好，世界"}]