<?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>HelloWorldGoodbye on カスリワークスブログ</title>
    <link>https://kasuri.works/ja/tags/helloworldgoodbye/</link>
    <description>Recent content in HelloWorldGoodbye on カスリワークスブログ</description>
    <generator>Hugo -- 0.147.6</generator>
    <language>ja</language>
    <lastBuildDate>Fri, 30 May 2025 14:32:08 -0700</lastBuildDate>
    <atom:link href="https://kasuri.works/ja/tags/helloworldgoodbye/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Godot 4 で作るビデオ弾幕システム</title>
      <link>https://kasuri.works/ja/posts/building-danmaku-system/</link>
      <pubDate>Fri, 30 May 2025 14:32:08 -0700</pubDate>
      <guid>https://kasuri.works/ja/posts/building-danmaku-system/</guid>
      <description>Godot 4 と C# を使って、ニコニコ風の弾幕コメントシステムをゼロから構築したプロセスを紹介します。独立した Viewport、トラック制御、速度補正などを備えた高性能な実装例です。</description>
      <content:encoded><![CDATA[<p>最近、<strong>Hello, World. Goodbye.</strong> で特に感情表現が豊かな機能として実装しているのが、<em>弾幕コメントシステム</em>です。これはニコニコ動画の弾幕コメントへのオマージュであり、プレイヤーが静かに叫んだり、他の人の声を聞いたりできる仕組みです。</p>
<h2 id="-目標">🎯 目標</h2>
<p>モジュール化され、滑らかで高性能な弾幕（弾幕コメント）システムを設計します。要件は以下の通りです：</p>
<ul>
<li>メインゲームカメラと独立して動作（専用 Viewport を使用）</li>
<li>複数のコンテンツタイプ（テキスト、画像）に対応</li>
<li>弾幕をトラックごとに均等または集中して分配</li>
<li>間隔を動的に計算し、重なりを回避</li>
</ul>
<h2 id="-実装ステップ">🎬 実装ステップ</h2>
<h3 id="ステップ-1-シンプルな実装から開始">ステップ 1: シンプルな実装から開始</h3>
<p>まずは <code>RichTextLabel</code> ベースの弾幕ノードを作成し、右から左へ移動させます。</p>
<p><img alt="Damaku Scene" loading="lazy" src="/posts/building-danmaku-system/danmaku-scene.en.png#center"></p>
<p>C# コード例：</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>この実装では、弾幕が意図しない位置から出現し、早すぎるタイミングで消えてしまう問題が発生しました。</p>
<p><img alt="First Attempt Animation" loading="lazy" src="/posts/building-danmaku-system/first-attempt.en.gif#center"></p>
<p>原因はプレイヤーにカメラがアタッチされており、画面上の座標と実際の位置が一致しないためです。</p>
<p><img alt="Player Camera Tree" loading="lazy" src="/posts/building-danmaku-system/player-camera.en.png#center"></p>
<h3 id="ステップ-2-カメラからの分離">ステップ 2: カメラからの分離</h3>
<p>この問題を解決するため、弾幕システムをメインカメラの影響を受けない専用の Viewport（例：640x360）に移動しました。これで、ワールドの動きに関係なく、常に画面右端から弾幕が流れます。</p>
<p><img alt="Danmaku Viewport" loading="lazy" src="/posts/building-danmaku-system/danmaku-viewport.en.png#center"></p>
<blockquote>
<p>Q: 弾幕はどのように画面上に分配される？</p></blockquote>
<p>Bilibili のコンサート動画例を見ると、複数のトラックに弾幕が流れているのが分かります。</p>
<p><img alt="Example Miku Concert Danmaku - Bilibili" loading="lazy" src="/posts/building-danmaku-system/danmaku-example.en.png#center"></p>
<p>弾幕は画面上の見えないトラックに分配されているイメージです。</p>
<p><img alt="Danmaku Tracks" loading="lazy" src="/posts/building-danmaku-system/danmaku-example-track.en.png#center"></p>
<h3 id="ステップ-3-トラックマネージャの導入">ステップ 3: トラックマネージャの導入</h3>
<p><code>DanmakuTrackManager</code> を導入し、各トラック（行）ごとに弾幕が重ならないよう管理します。</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>トラック数や間隔は、サンプル弾幕（例：<code>RichTextDanmakuLabel</code>）のサイズから自動計算します。</p>
<h3 id="ステップ-4-配分ロジックの追加">ステップ 4: 配分ロジックの追加</h3>
<p><code>IDanmakuTrackDistributor</code> を実装し、弾幕のトラック割り当て方法を制御します。</p>
<p>均等分配（ラウンドロビン）と集中分配（少数トラックに集中）を実装：</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>実際には、トラックが空いているかどうかをトラックマネージャで確認する必要があります。</p>
<h3 id="ステップ-5-キューシステムの導入">ステップ 5: キューシステムの導入</h3>
<p>大量の弾幕を一度に追加する場合、キューに溜めてから順次分配します。</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>このコードで、弾幕をキューに溜め、トラックに空きがあれば順次追加します。</p>
<h3 id="ステップ-6-オーバーラン問題の修正">ステップ 6: オーバーラン問題の修正</h3>
<p>速い弾幕が遅い弾幕を追い越して重なるバグが発生しました。これを防ぐため、「スピードクリップ」を導入し、前の弾幕が安全領域を抜けるまで次の弾幕を追加しません。</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><code>GetMaxSpeedInTrack</code> メソッド例：</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="ステップ-7-全体の統合">ステップ 7: 全体の統合</h3>
<p><code>DanmakuTrackManager</code>、<code>IDanmakuTrackDistributor</code>、<code>DanmakuSystemController</code>、<code>RichTextDanmakuLabel</code> を組み合わせて、メインシーンに統合します。</p>
<p><img alt="Danmaku System Diagram" loading="lazy" src="/posts/building-danmaku-system/system-diag.en.png#center"></p>
<h3 id="最終結果">最終結果</h3>
<p><img alt="Final Danmaku Result" loading="lazy" src="/posts/building-danmaku-system/final-result.en.gif#center"></p>
<h3 id="まとめ">まとめ</h3>
<p>この弾幕システムは、モジュールごとに役割を分けて設計しているため、機能追加や調整がしやすくなっています。テキストや画像など複数のコンテンツタイプに対応し、弾幕の分配や重なり回避も柔軟に制御できます。Godot 4 と C# でこうした仕組みを作る際の参考になればうれしいです。ご質問やフィードバックがあればお気軽にどうぞ。</p>
]]></content:encoded>
    </item>
    <item>
      <title>HelloWorld.GoodBye()</title>
      <link>https://kasuri.works/ja/posts/hello-world-goodbye/</link>
      <pubDate>Thu, 29 May 2025 14:00:11 +0000</pubDate>
      <guid>https://kasuri.works/ja/posts/hello-world-goodbye/</guid>
      <description>一人のプログラマーが、感情をドットにして書き残す——これは世界に向けた return のない goodbye。</description>
      <content:encoded><![CDATA[<p>HelloWorld.GoodBye()</p>
<blockquote>
<p>「Hello World.」—— 最初に書いた一行のコード。<br>
「GoodBye()」—— システムの果てに、静かに呼び出したファンクション。</p></blockquote>
<hr>
<h2 id="-自己紹介">👋 自己紹介</h2>
<p>私はプログラマーです。<br>
そして今、このゲームを一人で開発しているインディーゲーム開発者です。<br>
企画も、コードも、ドットも、文章も——全部、夜中の「まだ終わってない気持ち」だけで作っています。</p>
<p>かつて、コードを書くことが本当に好きでした。<br>
でもいつの間にか、それが「自分のため」ではなくなっていました。</p>
<p>書いているのは Jira のため、上司のため、面接官のため、KPI のため。<br>
そして、&ldquo;ちゃんとした&quot;エンジニア像のため。</p>
<p>気づいたんです。<br>
コードが嫌いになったわけじゃない。<br>
ただ、<strong>自分のために書くことを、あまりに長く、やってなかっただけ。</strong></p>
<hr>
<h2 id="-なぜこのゲームを作っているのか">💬 なぜこのゲームを作っているのか</h2>
<p>現実では言葉にできなかったことが、あまりに多すぎました。</p>
<p>「一生懸命やってるけど、もう疲れた」<br>
「もう面接のために勉強したくない」<br>
「僕のコードはポイントのためでも、レビューコメントの &lsquo;Well done&rsquo; のためでもない」</p>
<p>——ただ、<strong>“ものづくりが好きだったから”</strong></p>
<p>これは成功や夢の話じゃありません。<br>
<strong>“Hello” から “GoodBye()” までの、ある一人の物語です。</strong><br>
失敗ではなく、回復の物語。</p>
<hr>
<h2 id="-どんなゲーム">🎮 どんなゲーム？</h2>
<p>ドット絵の、アクションゲームです。物語があります。<br>
主人公はプログラマー。<br>
それは私かもしれないし、あなたかもしれません。</p>
<p>壮大なストーリーも、世界を救う使命もありません。<br>
ただ、システムの中で壊れていき、<br>
もう一度「自分」という人間を取り戻そうとする——<strong>それだけのゲームです。</strong></p>
<p>もっと正確に言えば、</p>
<blockquote>
<p><strong>自分自身のために書いた、一行のファンクションです。</strong></p></blockquote>
<hr>
<h2 id="-現在の進捗">🛠 現在の進捗</h2>
<p>現在、デモ版を制作中です。<br>
リリース日は未定です。<br>
トレーラーも、マーケティングも、何もありません。</p>
<p>でも、ここで少しずつ制作過程や感情を記録していきます。<br>
本当に小さな一歩ずつですが、作り続けています。</p>
<hr>
<p>もしあなたも、ふとした瞬間に<br>
「自分って、人間じゃなくて、ただのプロセスなんじゃないか」<br>
そんな風に感じたことがあるなら——</p>
<p>このゲームを、見てくれたら嬉しいです。</p>
<hr>
<p><strong>HelloWorld.GoodBye()</strong><br>
それはプロジェクトじゃなくて、<br>
<strong>ひとつのメッセージ。</strong></p>
<p>世界に向けた return のない goodbye。<br>
そして、あなた自身への small commit。</p>
<blockquote>
<p>「わかるよ」<br>
「あなたは、間違ってない」<br>
「ちゃんと、ここにいるよ」</p></blockquote>
]]></content:encoded>
    </item>
  </channel>
</rss>
