<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Kasuri Works Blog</title>
    <link>https://kasuri.works/</link>
    <description>Recent content on Kasuri Works Blog</description>
    <generator>Hugo -- 0.147.6</generator>
    <language>en</language>
    <lastBuildDate>Fri, 30 May 2025 14:32:08 -0700</lastBuildDate>
    <atom:link href="https://kasuri.works/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Building Video Danmaku in Godot 4</title>
      <link>https://kasuri.works/posts/building-danmaku-system/</link>
      <pubDate>Fri, 30 May 2025 14:32:08 -0700</pubDate>
      <guid>https://kasuri.works/posts/building-danmaku-system/</guid>
      <description>A deep dive into the modular danmaku comment system design for a narrative-driven action game built in Godot 4 with C#.</description>
      <content:encoded><![CDATA[<p>One of the most emotionally expressive features I’ve been building recently in <strong>Hello, World. Goodbye.</strong> is a <em>danmaku-style</em> comment system — a tribute to NicoNico’s bullet comments and a way for players to silently scream into the void, or listen to what others once shouted.</p>
<h2 id="-goal">🎯 Goal</h2>
<p>Design a modular, smooth, high-performance danmaku (弾幕 / bullet comment) system that:</p>
<ul>
<li>Works independently of the main game camera (via a dedicated Viewport)</li>
<li>Supports multiple content types (text, image)</li>
<li>Distributes danmaku across tracks evenly or with focused styles</li>
<li>Dynamically calculates spacing and avoids overlapping</li>
</ul>
<h2 id="-step-by-step-construction">🎬 Step-by-Step Construction</h2>
<h3 id="step-1-start-simple">Step 1: Start Simple</h3>
<p>We began with a simple <code>RichTextLabel</code>-based danmaku node, moving from right to left:</p>
<p><img alt="Damaku Scene" loading="lazy" src="/posts/building-danmaku-system/danmaku-scene.en.png#center"></p>
<p>Then in the C# code attached add the following.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">void</span> _Process(<span style="color:#66d9ef">double</span> delta) {
</span></span><span style="display:flex;"><span>    Position -= <span style="color:#66d9ef">new</span> Vector2(Speed * (<span style="color:#66d9ef">float</span>)delta, <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (Position.X + GetWidth() &lt; <span style="color:#ae81ff">0</span>) QueueFree();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This worked fine until we realized something odd — the danmaku appeared offset and at (width, 0) it still appears to be from somewhere odd.</p>
<p><img alt="First Attempt Animation" loading="lazy" src="/posts/building-danmaku-system/first-attempt.en.gif#center"></p>
<p>This danmaku not only is not playing from the correct location, it is also disappearing way too early. Why is that? It appears the player has a camera attached, and because of that, what we see on the screen does not map 1:1 to the real location. This is a problem.</p>
<p><img alt="Player Camera Tree" loading="lazy" src="/posts/building-danmaku-system/player-camera.en.png#center"></p>
<h3 id="step-2-decouple-from-camera">Step 2: Decouple from Camera</h3>
<p>To solve this, we moved the danmaku system to a separate Viewport layer (640x360), unaffected by the main camera. Now comments would always start from the right edge of the screen, regardless of world movement.</p>
<p><img alt="Danmaku Viewport" loading="lazy" src="/posts/building-danmaku-system/danmaku-viewport.en.png#center"></p>
<blockquote>
<p>Question: How Danmaku is distributed on the screen?</p></blockquote>
<p>Using this example concert video on Bilibili, we can see a bunch of tracks on the screen with rolling Danmaku from right to left.</p>
<p><img alt="Example Miku Concert Danmaku - Bilibili" loading="lazy" src="/posts/building-danmaku-system/danmaku-example.en.png#center"></p>
<p>We can view the entire Danmaku as distributed in invisible tracks on the screen, as shown below.</p>
<p><img alt="Danmaku Tracks" loading="lazy" src="/posts/building-danmaku-system/danmaku-example-track.en.png#center"></p>
<h3 id="step-3-add-track-manager">Step 3: Add Track Manager</h3>
<p>Next, we introduced a <code>DanmakuTrackManager</code> to manage rows (or tracks) and ensure danmaku didn’t overlap.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DanmakuTrackManager</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> List&lt;List&lt;DanmakuTrackEntry&gt;&gt; _tracks;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> Node _danmakuLayer;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">int</span> _trackCount;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">float</span> _trackHeight;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">float</span> _screenWidth;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> <span style="color:#66d9ef">float</span> _spacing;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> IsTrackAvailable(<span style="color:#66d9ef">int</span> trackIndex)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> track = _tracks[trackIndex];
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (track.Count == <span style="color:#ae81ff">0</span>) <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> last = track[^<span style="color:#ae81ff">1</span>];
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">float</span> lastRight = last.Entry.Position.X + last.Entry.Call(<span style="color:#e6db74">&#34;GetWidth&#34;</span>).AsSingle();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> lastRight + <span style="color:#ae81ff">20f</span> &lt; _screenWidth;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The system auto-calculates track count and spacing based on the size of a sample danmaku (like <code>RichTextDanmakuLabel</code>).</p>
<h3 id="step-4-add-distributor">Step 4: Add Distributor</h3>
<p>We then implemented <code>IDanmakuTrackDistributor</code> to control how tracks are assigned. Two distribution modes were added:</p>
<p>Even Distribute: Round-robin fill</p>
<p>Focus Few: Stack into fewer rows for visual intensity</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EvenDistributor</span> : IDanmakuTrackDistributor
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> GetTrackIndex(<span style="color:#66d9ef">int</span> danmakuCount, <span style="color:#66d9ef">int</span> trackCount)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> danmakuCount % trackCount;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is a simple round-robin distributor that assigns danmaku to tracks evenly. But in reality, we need to access the track manager to check if the track is available.</p>
<h3 id="step-5-add-queue-system">Step 5: Add Queue System</h3>
<p>Normally when a bunch of danmaku is added, we want to queue them up and then distribute them all at once. This is useful for when we want to add a bunch of danmaku at once, like when the game system is triggered to add a bunch of danmaku at once.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">partial</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DanmakuSystemController</span> : Node, ISystemModule
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> Queue&lt;DanmakuQueueEntry&gt; _danmakuQueue;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> DanmakuTrackManager _trackManager;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">double</span> _lastSpawnTime = <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">void</span> _Ready()
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _danmakuQueue = <span style="color:#66d9ef">new</span> Queue&lt;DanmakuQueueEntry&gt;();
</span></span><span style="display:flex;"><span>        _trackManager = <span style="color:#66d9ef">new</span> DanmakuTrackManager();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> AddDanmaku(DanmakuQueueEntry entry)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _danmakuQueue.Enqueue(entry);
</span></span><span style="display:flex;"><span>        ProcessQueue();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">void</span> _Process(<span style="color:#66d9ef">double</span> delta)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (_danmakuQueue.Count == <span style="color:#ae81ff">0</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Peek queue and if time passed, spawn a danmaku</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (Time.GetTicksMsec() &gt; _lastSpawnTime + SpawnInterval)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// If the next track is available on the track manager</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> (_trackManager.HasTrackSpace())
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">var</span> danmakuData = _danmakuQueue.Dequeue();
</span></span><span style="display:flex;"><span>                _trackManager.AddDanmakuToTrack(danmakuData.Data);
</span></span><span style="display:flex;"><span>                _lastSpawnTime = Time.GetTicksMsec();
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This code allows us to queue up danmaku and process them in the <code>_Process</code> method, checking if the track manager has space before spawning. If the track is available, it will dequeue the danmaku and add it to the track manager.</p>
<h3 id="step-5-fix-overrun-issue">Step 5: Fix Overrun Issue</h3>
<p>There is one issue, because the danmaku are using random speeds, faster danmaku would overtake slower ones in the same track, causing overlaps. The same effect is also shown on the Bilibili example above. The fix was to introduce a &ldquo;speed clip&rdquo; to ensure that the danmaku speed does not exceed the maximum speed of all danmakus in the track.</p>
<p>The following code is added to the <code>DanmakuTrackManager</code> to check if the track is available:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span><span style="color:#75715e">// Set initial speed, cap the speed to prevent overrun</span>
</span></span><span style="display:flex;"><span>danmakuData.Speed = Math.Max(GetMaxSpeedInTrack(trackIndex), danmakuData.Speed);
</span></span></code></pre></div><p>And then we need to implement the <code>GetMaxSpeedInTrack</code> method to get the maximum speed in the track.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">float</span> GetMaxSpeedInTrack(<span style="color:#66d9ef">int</span> trackIndex)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> track = _tracks[trackIndex];
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (track.Count == <span style="color:#ae81ff">0</span>) <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Loop through track to get max speed</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> track.Max(entry =&gt; entry.Entry.Speed);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="step-6-tieing-it-all-together">Step 6: Tieing It All Together</h3>
<p>Now we talked about how to implement <code>DanmakuTrackManager</code>, <code>IDanmakuTrackDistributor</code>, <code>DanmakuSystemController</code> and the <code>RichTextDanmakuLabel</code>,
we can now tie it all together in the main game scene.</p>
<p><img alt="Danmaku System Diagram" loading="lazy" src="/posts/building-danmaku-system/system-diag.en.png#center"></p>
<h3 id="final-result">Final Result</h3>
<p><img alt="Final Danmaku Result" loading="lazy" src="/posts/building-danmaku-system/final-result.en.gif#center"></p>
<h3 id="conclusion">Conclusion</h3>
<p>The danmaku system is now modular, efficient, and easy to extend. It supports different content types, distributes comments across tracks as needed, and manages spacing to prevent overlaps.</p>
<p>This approach keeps the codebase organized and maintainable, making it easier to add new features or tweak behavior later. Hopefully, this walkthrough gives you a solid starting point for building your own danmaku system in Godot 4 with C#. If you have questions or ideas, let me know—happy coding!</p>
]]></content:encoded>
    </item>
    <item>
      <title>HelloWorld.GoodBye()</title>
      <link>https://kasuri.works/posts/hello-world-goodbye/</link>
      <pubDate>Thu, 29 May 2025 14:00:11 +0000</pubDate>
      <guid>https://kasuri.works/posts/hello-world-goodbye/</guid>
      <description>A solo project from a developer trying to say something. This game is made of code, pixels, and everything I couldn&amp;#39;t say in words.</description>
      <content:encoded><![CDATA[<p>HelloWorld.GoodBye()</p>
<blockquote>
<p>“Hello World.” — the very first line of code I ever wrote.<br>
“GoodBye()” — the function I silently called when the system had finally worn me down.</p></blockquote>
<hr>
<h2 id="-who-am-i">👋 Who am I?</h2>
<p>I’m a programmer.<br>
And right now, I’m also a solo indie game developer.</p>
<p>This game is made by just one person — me.<br>
Design, code, art, writing — everything built slowly in quiet hours fueled by frustration, burnout, and a tiny spark of something I thought I had lost.</p>
<p>I used to love writing code.<br>
But somewhere along the way, I stopped writing for myself.</p>
<p>Now it’s for Jira, for managers, for interviewers, for KPI dashboards, for whatever counts as “professional growth.”</p>
<p>But I’ve realized something:<br>
It’s not that I stopped loving programming.<br>
It’s just been so long since I wrote anything for myself.</p>
<hr>
<h2 id="-why-make-this-game">💬 Why make this game?</h2>
<p>Because there are so many things I couldn’t say out loud.</p>
<p>“I’m trying my best, but I’m tired.”<br>
“I don’t want to study for another interview.”<br>
“My code is not for points or compliments in a PR comment — it’s because I used to believe code was a language of creation.”</p>
<p>This isn’t a story about success.<br>
It’s a story about moving from <code>Hello</code> to <code>GoodBye()</code>.</p>
<p>Not a failure — but an attempt to recover.</p>
<hr>
<h2 id="-so-what-is-this-game">🎮 So what is this game?</h2>
<p>It’s a pixel-art action platformer. It has a story.<br>
The protagonist is a programmer.<br>
That might be me. It might be you.</p>
<p>It’s not about saving the world. There’s no epic destiny.<br>
It’s just someone breaking down inside a system,<br>
and trying to find their way back to being human.</p>
<p>Or more precisely:</p>
<blockquote>
<p>It’s a single function I wrote for myself —<br>
<strong>one that never returns.</strong></p></blockquote>
<hr>
<h2 id="-current-status">🛠 Current status</h2>
<p>The game is still in development.<br>
I’m working on the demo now.<br>
There’s no release date. No trailer. No hype.</p>
<p>But I’ll keep updating this blog — with progress, breakdowns, and maybe a few breakthroughs.</p>
<p>Slowly, and stubbornly, I keep going.</p>
<hr>
<p>If you’ve ever felt like you weren’t a person anymore —<br>
just a process, a resource, a task in someone else’s system —</p>
<p>Then maybe this game is for you.</p>
<hr>
<p><strong>HelloWorld.GoodBye()</strong><br>
It’s not a project. It’s a message.<br>
A quiet <code>GoodBye()</code> that never needs to explain itself.</p>
<p>Maybe it’s for you.<br>
Or maybe it’s for the version of me that’s still trapped in that system.</p>
<blockquote>
<p>“I understand.”<br>
“You’re not broken.”<br>
“You’re still here.”</p></blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Hello, World</title>
      <link>https://kasuri.works/posts/hello-world/</link>
      <pubDate>Thu, 29 May 2025 10:00:11 +0000</pubDate>
      <guid>https://kasuri.works/posts/hello-world/</guid>
      <description>Welcome to the Kasuri Works blog — devlogs, code, and creativity from a tiny indie studio.</description>
      <content:encoded><![CDATA[<p>Welcome to the Kasuri Works blog! Here, we&rsquo;ll share updates on our game development journey, technical deep dives, and insights into the creative process behind our projects.</p>
<h2 id="what-to-expect">What to Expect</h2>
<ul>
<li><strong>Game Development Logs:</strong> Progress updates, challenges, and milestones from our current projects.</li>
<li><strong>Technical Articles:</strong> Tutorials, code snippets, and discussions on tools and technologies we use.</li>
<li><strong>Behind the Scenes:</strong> Art, design, and music explorations that shape our games.</li>
</ul>
<p>Stay tuned for more posts as we build, learn, and share together. Thanks for joining us on this adventure!</p>
<hr>
<p>Follow us on <a href="https://misskey.kasuri.works">Misskey</a> for more updates and community discussions!</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
