はじめに
こんにちは、突然ですが今回は先日公開した新作幻境である 「幻境!チュンボール」 を例に、状態とイベントという観点から星々の幻境で動きのあるゲームを作る際の考え方について紹介したいと思います。
この記事を読むことで原神内で実装されているような簡単なミニゲームレベルの幻境は作れるようになるはずです。
自身の幻境を観光幻境や壺幻境のようなゲームから進化させたい方、これから星々の幻境でゲームを作ってみたい方は良ければ参考にしてみてください。
海外の方の記事やHoYoLAB以外での情報(YouTube動画など)についてはあまり把握できてないのでそんなことはないのかもしれませんが、表題のような観点でゲームの作り方について解説している記事は見たことがないのでそれなりに有益な情報だと思います、多分・・・。
![]()
なおこの記事ではチュンボールの物理シミュレーションに関する具体的な話はしません。
そういった情報を期待している方は回れ右でお願いします。
なぜ話をしないのかですが、詳しくは後述しますがそのようなことについてはわりとどうでもよく、そこについて深く考えなくてもゲームは作れるからです。
![]()

本記事の対象者および前提条件
先にも記載した通り、本記事は星々の幻境でマップ/フィールド/景観は作れるけどゲームとして動きを持たせるにはどうしたら・・・といったような悩みを抱えている方を対象としています。
ここで言う「動き」とはキャラクターやオブジェクトの動きではなくゲーム自体の動き、つまりロードから始まりゲームが開始した後は様々なフェーズを経て終わりを迎えるまでの一連の流れのことです。
逆に動きがないゲームの例として観光幻境が挙げられます。
![]()
前提条件ですが、プログラミングの基本的な概念や用語は理解しておく必要があります。
ここら辺の知識がないと流石に星々の幻境でも動きのあるゲームを作るのは難しいと思います。
- 変数とデータ型
- 条件分岐
- ループ処理
- 関数
ついでに言うとオブジェクト指向プログラミングの「クラス」や「インスタンス」の概念を理解していると「プレハブ」や「エンティティ」についての理解がしやすくなるのでなお良いですが、そのくらいの知識があれば十分です。
ゲーム開発の経験については不要です。
![]()
そしてプログラミングの文法に相当するもの、つまり星々の幻境のエディタの使い方や機能等のチュートリアルはここにまとまっているのであまり理解できていないと思う方は一読しておくと良いでしょう。
https://act.hoyoverse.com/ys/ugc/tutorial/course/home
![]()

動きのあるゲームの作り方
結論から書きますが、ゲーム内で取り得る状態と状態を遷移させるイベントを整理すれば動きのあるゲームは作れます。
逆に言うなら普段ゲームをプレイしている際はあまり意識しないであろう状態の存在をイメージできないと、いくら星々の幻境のエディタの使い方を勉強してもゲームを作ることは難しいでしょう。
公式・非公式含めて、このような状態とイベントに関する話がないまま突然ノードを組みだしたりする記事が多いですが、私はこの状態とイベントの理解がゲーム作成において重要な部分だと考えています。
![]()
なおこの考え方はゲームのジャンルに影響しません。
つまり今回は「幻境!チュンボール」を例に説明を行いますが、アクションゲーム、シミュレーションゲーム、パズルゲーム、シューティングゲーム、レースゲーム、カードゲーム、PvE、PvPなどあらゆるジャンルのゲームに等しく適用できます。
それだけこの記事で紹介する内容は汎用的で普遍的なことであり、それ故に一部抽象的な内容にはなると思いますがその点についてはご理解いただけると幸いです。
![]()
それでは肝心の状態とイベントについてですが、まずはイベントから説明します。
![]()

イベント
イベントとは処理のきっかけになるもので、例えば「プレイヤーがボタンを押した」、「プレイヤーが特定の場所に到達した」、「一定時間経過した」などがイベントに該当します。
![]()
星々の幻境のプログラミングは、main関数があり上から順に処理が実行される的ないわゆる一般的にプログラミングと言われたらイメージするであろうものではなく、イベント駆動型のプログラミングであるということをまずは理解する必要があります。
私は星々の幻境以外でゲーム制作の経験はありません。
しかしWindowsのデスクトップアプリケーションを作った経験はあったためイメージはしやすく、開発の考え方としてはまさにそんな感じです。
あれは画面のどこが押されたらどういう処理を実行するのかというのを個別に記載していきますが、星々の幻境でも同様に各イベントに対してどのような処理を実行するのかを設定していくというのが基本的な考え方になります。
![]()
なので私に言わせれば星々の幻境でゲームを作ろうと思ったらまずはどのようなイベントが存在し、どのような場面でそれらが使えるのかを把握することが重要です。
幸いドキュメントにはイベント、正確にはイベントを受け取るノードの一覧が記載されているので、一度は目を通しておくと良いでしょう。
https://act.hoyoverse.com/ys/ugc/tutorial/detail/mhn7ko01v3yw
![]()
様々なイベントがありますが、この中でどんなゲームを作る際も絶対に必要になるし重宝するものが2つあります。
とりあえずこの2つを把握しておけばどんなゲームを作る際にも応用できるので、必ずこの2つのイベントを理解し使いこなせるようになりましょう。
- 信号
- タイマー
それでは信号とタイマーについて簡単に説明します。
![]()

信号
信号というのはいわゆるPub/Sub的なものだと私は理解しています。
いやPub/Subって何?となる人も多いと思うので雑に説明するとこれは糊のようなもので、ある処理と別の処理をこれを使ってつなげることができます。
言葉だけだと分かりにくいと思うので、実際の利用例については後で紹介します・・・。
![]()
一応公式のチュートリアル動画です(英語版のチュートリアルは こちら )。

タイマー
タイマーというのはそのままで、予め時間を決めておいてその時間が経過したら何かの処理を実行するというものです。
遅延や繰り返しの処理に利用できます。
![]()
一応公式のチュートリアル動画です(英語版のチュートリアルは こちら )。

信号とタイマーの利用用途
ゲームの中で以下のようなことをイベントとして扱いたくなるはずです。
ぞれぞれの用途に応じて信号とタイマーを使うことができます。
- 何かを実行したときの通知:信号
- 一定期間何も実行しなかったときの通知:タイマー
- 繰り返し実行される通知:タイマー

状態
普段は無意識に考えており、改めて言葉にしようとしても状態って何だ?となったので調べてみました・・・。
辞書的には「人や物事の、ある時点でのありさま」のことを状態と言うそうです。
つまりゲームにおいての状態とは、ゲームの進行状況におけるゲームのありさまを指すと理解してもらえば良いでしょう。
![]()
例えば「フィールド上に置いてある宝箱を開けたらクリア」の幻境を例に考えてみます。
まず「プレイヤーのロード待ち」も状態ですし、ロードが完了した後の「ゲーム中」というのも状態です。
ゲーム中にプレイヤーは宝箱を開けますが、宝箱を開けた後に今回のプレイ結果(クリアまでの秒数など)を表示するとしたら「ゲーム結果表示中」というのも状態になるでしょう。
この状態の存在を認識することが大切で、ここがふわっとしたままだといくら開発を進めてもゲームとして始まりも終わりもない動きのない観光幻境になってしまいがちです。
![]()
そしてこれらの状態は上述のイベントによって変化します。
イベントとは処理のきっかけになるものでした。
つまり「プレイヤーのロード待ち」から「ゲーム中」に変化するきっかけは「プレイヤーのロードが完了した」というイベントで、「ゲーム中」から「ゲーム結果表示中」に変化するきっかけは「宝箱を開けた」というイベントになりますね。
![]()

状態遷移図
以上が状態とイベントに関する説明ですが、ここで状態がイベントにより変化する様を「状態遷移」と言い、状態の変化を図で表現したものを「状態遷移図」と言います。
![]()
この状態遷移図を書くことでゲームの開始から終了までの流れを整理することができます。
恐らく開発経験のある方は意識せずともこの状態遷移図を頭の中で書いていると思います。
逆にこれが書けないということは、ゲームの開始から終了までの流れを自分自身で理解できていないということですから、当然そんな状態で実装に入ってもゲームとして形にならないですよね。
![]()
例として2026/7/19時点の「幻境!チュンボール」の開始から終了までの流れを状態遷移図として記載してみます。
・・・その前に、実際のゲームの動きを知らないといろいろイメージがしにくいと思うので、参考までに幻境のプレイ動画も添付しておきます。
一番は実際にプレイしてみることですが、プレイする時間も惜しい方はとりあえずこちらの動画だけでも見てみてください。
改めて状態遷移図です。
ゲームはいきなり始まるわけではなく開始前の準備があったり、メインの部分が終わった後も集計や結果の表示など様々な状態が存在します。

これらの状態はかなり抽象化されておりそのまま実装しても遊びとして成立しません。
特にゲーム中状態は、ゲームの進行に応じてさらに細かい内部状態に分けられます。
それぞれの状態の内部状態も図示したものがこちらです。

ゲームは大きくラウンドの繰り返しによって構成されており、ラウンドはターンの繰り返しによって構成されています。
プレイヤーはターンの中でチュンボールを発射することができます。
例えば2人のプレイヤーで遊ぶ場合、各ラウンドは全2ターンで構成されます。
プレイヤーがそれぞれのターンでチュンボールを発射し、全てのターンが終了することで1ラウンドが終了します。
そしてゲームは全4ラウンドで構成され、全てのラウンドが終了することでゲームの集計に移るという感じですね。
![]()
この幻鏡はシングルプレイもできるように実装しています。
1人で遊ぶ場合、各ラウンドは全1ターンで構成されます。
プレイヤーがターンの中でチュンボールを発射し、ターンが終了することで1ラウンドが終了します。
そしてシングルプレイの場合はゲームは全5ラウンドで構成され、全てのラウンドが終了することでゲームの集計に移るという感じです。
![]()
私が冒頭で「この記事でチュンボールの物理シミュレーションに関する具体的な話はしない」と書いたのはまさにこれが理由で、ゲームを作るにおいてぶっちゃけどうでもいい話だからです。
だって上の図を見てもらえれば分かる通り、チュンボールの物理シミュレーションのことなんて一言も書かれていませんよね。
それでもちゃんと開始から終了までのゲームの流れが表現できています。
つまり物理シミュレーションのことなんか後づけで良いってことです。
![]()

状態とイベントの実体について
そしてここからが重要です。
この状態ですが、星々の幻境には対応するコンポーネントや概念が存在しません・・・。
![]()
どういうことかと言うと、イベントについては星々の幻境にもイベントという概念がありそれがそのまま対応しますが、状態についてはそのようなものがないのです(ちなみに「ユニット状態」というものはありますが、これは全く異なるものです)。
これが厄介なところで、星々の幻境で状態に相当するコンポーネントや概念がない以上ドキュメントやチュートリアルでもこれについて触れられることはありません。
故に公式のチュートリアルを見ても分かったようで実際にゲームを作り始めるとよく分からんとなるわけですね。
![]()
では状態とは具体的に何なのかですが、これはクリエイター自らが定義する必要があります。
しかも定義というのも「ノードグラフを1つ作ったらそれが一つの状態になる」とかそういった明確に形のあるものではなく、あくまでクリエイターの頭の中で仮想的に定義する必要があります。
![]()

実装
状態遷移について一通り説明したところでようやく具体的な実装の話に入ります。
やることは先ほど図示した状態遷移図を元に実装するだけなのですが・・・。
![]()
順番が前後して申し訳ないですが、分かりやすいようにまず初めに一番単純なゲーム終了状態とそこに遷移するイベントから説明します。
![]()
ちなみにですがゲームの中核となる状態を管理する場所にはステージエンティティを使用することをお勧めします。
ゲーム内には様々なエンティティが存在しますが、このステージエンティティは特殊なエンティティでゲーム中必ず1つ存在することが保証されており、ゲームの進行に関わる状態を管理するのに最適です。
今後特に言及がなければノードグラフはステージエンティティ上で実行されていると思ってください。
![]()

ゲーム終了
ゲームを終了する状態です。
星々の幻境では「ステージ終了」ノードを実行するとゲームが終了するので、ここではこのノードを実行するだけです。
非常に単純ですがこの処理も一つの状態と捉えることができます。
![]()
実際のノードグラフです。
状態遷移のきっかけとなるイベントはゲーム終了信号を受信することとしました。
「信号をモニタリング」ノード を使用することで、指定した信号を受信した際にこのノードが動き出します。
この信号はゲーム準備完了待ち/ゲーム結果表示中状態から送信されます(されるように実装します)。

上の方で信号とは糊のようなものだと説明しました。
これはつまり、後述のゲーム準備完了待ち/ゲーム結果表示中状態とゲーム終了状態を信号によって繋いでいるという意味です。
何となく状態やイベントについては理解できたでしょうか?
こんな感じで他の状態とイベントについても説明していきたいと思います。
![]()

ゲーム準備完了待ち
ゲームの開始前に必要な準備を整えるための状態です。
この状態で行われる主な処理は以下の通りです。
- プレイヤー全員のロード完了を待つ。
- プレイヤーによるステージの選択を行う。
- 選択されたステージの作成を行う。
- ステージやプレイヤーに関する設定の初期化を行う。
実際のノードグラフです。
内部状態等いろいろありますが、私の中ではこのノードグラフの塊をゲーム準備完了待ち状態と定義しています。
ノードグラフの数も複雑さも先ほどとは大違いですね。
状態に明確な形がなくクリエイター自らが定義する必要があるという言葉の意味が何となく理解できたでしょうか。

そして詳細な説明ですが、この辺りの処理についてはすみませんが割愛します。
自分で言うのもあれですが、ここはゲーム開始前の待ち時間を最小化したり、プレイヤーが快適にゲームを開始するために結構複雑なことをしています。
![]()
例えばプレイヤーのロード待ちとステージの選択待ちは本来別々に行った方が実装しやすいです。
しかし別々に行うと、ロードが遅くてステージの選択は早かったプレイヤーと、ロードが早くてステージの選択は遅かったプレイヤーがいる場合、ゲーム開始までの待ち時間が長くなります。
これを解消するために現在はロード待ちとステージの選択待ちを区別せずにシームレスに行っています。
![]()
一応簡単(簡単?)に説明しておくと60秒という期限を設けて、その中でロードが完了したプレイヤーに対して順次ステージの選択画面を表示していきます。
例えばロードが5秒で完了したプレイヤーはステージの選択時間が55秒、ロードが20秒で完了したプレイヤーはステージの選択時間が40秒与えられるという感じです。
そしてその60秒の中で全プレイヤーがステージの選択まで完了したらステージを作成し、タイマーの残り時間を7秒に変更し、そのままタイマーの発動によってゲーム準備状態に移行します(7秒という値に意味はなく、単に読み物を表示するために丁度良いくらいの時間というだけです)。
もし60秒の中で全プレイヤーのステージ選択が完了しなかった場合(操作を放置されたような場合)は、タイマーの発動によってまずはステージの強制決定およびステージ作成の処理を実行します。
その後再び数秒で発動するタイマーを起動させ、そのタイマーの発動によってゲーム準備中状態に移行します。
本来は「ステージ作成が完了した」というイベントなので信号を使うべきですが、UI上でカウントダウンタイマーを表示している関係上、タイマーを使ってイベントを発火させています。
ゲーム準備中状態ではゲーム中状態に移行する前に必要な処理を実行しますが、これについては処理が完了した際に信号によってゲーム中状態に移行します。
![]()
下図は右下のノードグラフを拡大したものでゲーム準備完了待ち状態の内部状態であるゲーム準備中状態の一部です。
とにかくここから次のゲーム中状態に遷移する際、信号というイベントがトリガーになっていることだけを理解してもらえれば構いません。
信号の名前は「ゲーム準備完了」としました。

少し脱線しますが、上のノードグラフおよびノードグラフ群はステージエンティティ上で動いていますが、他にもプレイヤーエンティティ上で動いているノードグラフも存在します。
プレイヤーによるステージの選択にはカードセレクターを用いているため、カードセレクターでの選択を検知できる 「カードセレクターが完了したとき」ノード を使用するのですが、このノードはプレイヤーエンティティ上でしか発火しないため、このノードから始まる一連の処理もプレイヤーエンティティ側で処理しています。
![]()
このようにイベントノードは種類によって発火する場所が限定されているものがあるため使用する際は注意が必要です。
よく分からなければドキュメントにそのノードがどこでイベントを受け取れるのか記載があるはずなので確認してください。
例えば「カードセレクターが完了したとき」ノードに関する説明には "This event is triggered on the Player's Node Graph when the Player completes the Deck Selector, or when it is forcibly closed due to time constraints" とあり、このイベントノードはプレイヤーエンティティ上で使用できることが分かります。


ゲーム中
メインのゲームプレイが行われる状態です。
この状態ではプレイヤーが実際にゲームを進行させるための様々な処理が行われます。
![]()
改めてゲーム中状態の内部状態について整理します。
- ゲーム中はラウンドの繰り返しによって構成される。
- ラウンドは初期化とターンの繰り返しによって構成される。
- ターンは初期化、チュンボールの発射、チュンボールの停止確認、スコアの集計の一連の流れによって構成される。
とりあえずゲーム中状態への遷移は先ほど記載したゲーム準備完了信号によって引き起こされるので「信号をモニタリング」ノードを用います。
ゲーム中状態に入ったらラウンドを開始したいので、ここでも信号というイベントを用いて次の状態に遷移させます。
信号の名前は「次のラウンドの開始」としました。
なお知らない人は覚えておいてほしいのですが、信号には任意の情報を含められますから、ここでは次のラウンド番号を含めて送信します。


ラウンド中
ラウンド中状態では、ラウンドの初期化とターンの繰り返しが行われます。
ラウンドの初期化ではラウンドの中で必要な変数値の初期化や、ターンの順番の決定などが行われます。
ターンの順番は固定ではなく前回のラウンドの勝者が次のラウンドの先攻になるようにすることで、勝者が有利になりすぎないようにしています(なお現状引き分けだった際に常に先攻後攻が入れ替わらないので、これについてはそのうち修正する予定です・・・)。
ラウンドの初期化が終わったらそのまま第1ターンに移行します。
![]()
ラウンド(実際にはラウンド内の全てのターン)が終了すると、次のラウンドに移行します。
全てのラウンドが終了すると、ゲーム集計中状態に移行します。
これらの状態遷移のイベントには全て信号を用いています。



ターン中
ターン中状態では、ターンの初期化、チュンボールの発射、チュンボールの停止確認、スコアの集計が行われます。
ターンの初期化、チュンボールの停止確認、スコアの集計はステージエンティティ上で行われます。
チュンボールの発射はプレイヤーエンティティとチュンボールエンティティ上で行われます。
![]()
とりあえずステージエンティティ上のノードグラフの全体像です。


ターン初期化中
まずターンの初期化ではターンの中で必要な変数値の初期化や、チュンボールの準備、チュンボールの発射ボタンの表示が行われます。



チュンボール発射待ち
チュンボールの準備が完了したら操作がプレイヤーに委ねられ、チュンボールの発射はプレイヤーのボタン操作によって行われます。
ボタン操作を検知するのに 「UIコントロールグループ発動時」ノード を使用するため、発射までの処理はプレイヤーエンティティ側で行います。
一応イメージしやすいようにノードグラフの全体像は添付しておきますが、こちらについても全体の解説は割愛します・・・。

一番下の部分の拡大画像です。
ボタンの3回目の押下により動作するのですが、ここでチュンボール発射信号とチュンボール停止確認の開始信号が送られている事が分かります。
チュンボール発射信号によりチュンボールは発射され、チュンボール停止確認の開始信号によりチュンボールの停止確認が開始されます。

何回か記載している通り、発射された後のチュンボールの物理シミュレーションについてはここでは詳しく記載しません。
とにかくボタンを押した際にチュンボールの発射が行われ、次の状態に遷移する新たな信号が送られるということだけ理解してもらえればそれで十分です。
![]()

チュンボール停止確認中
プレイヤーの操作によりチュンボールの発射が行われると、信号によりチュンボール停止確認中状態に移行します。
チュンボールの停止確認は定期的に行いたいため、まずは信号の受信をトリガにループするタイマーを起動します(下のノードグラフの左側の部分)。
そしてタイマーの発動をトリガにチュンボールの停止確認を行います(下のノードグラフの右側の部分)。
やっていることは単純で、フィールド上に存在するチュンボールの数と速度が0のチュンボールの数を比較して全てのチュンボールが止まっていることを確認します。
チュンボールの停止が確認されると、スコア集計中状態に移行するための新たな信号が送られます。


スコア集計中
チュンボールが停止すると、信号によりスコア集計中状態に移行します。

信号の受信部分の拡大図です。
まずはスコア集計に関する変数を初期化します。

スコア集計部分の拡大図です。
フィールド上に存在するチュンボールのリストをループさせ、4点のエリア内に存在するか、2点のエリア内に存在するか、1点のエリア内に存在するかの順に評価していき、一番高い得点をプレイヤー毎のスコア辞書に加算していきます。

スコアの集計が完了すると、次のターンに移行するための新たな信号が送られます。


ゲーム集計中
ゲーム集計中状態では、全てのラウンドが終了した後にプレイヤーの勝ち/負け/引き分けを決定する処理が行われます。
集計が終わったらゲーム結果表示中状態に移行します。
![]()
なおゲームの集計は一瞬で終わってしまいます。
プレイヤーから見たらチュンボールが停止した後一瞬でゲーム結果の表示が行われて不自然なため、ゲーム結果表示中状態への移行はタイマーで遅延させます。


ゲーム結果表示中
ゲーム結果表示中状態では、ゲーム結果と再挑戦/退出の選択肢の表示が行われます。
プレイヤーの選択に応じて次の状態への遷移を行います。
![]()
とりあえずステージエンティティ上のノードグラフの全体像です。

まずは画面の表示です。
画面の実体は浮遊インタラクションページなので、表示には 「浮遊インタラクションページを呼び出す」ノード を使用します。

状態遷移図には記載がなくて申し訳ないですが、実際にはゲーム結果表示中状態ではタイマーも併用しており、60秒間無操作のプレイヤーがいた場合もゲーム終了状態に移行するよう実装しています。

こちらはプレイヤーエンティティ側の処理で、浮遊インタラクションページの操作を検知して動きます。
再挑戦を選択したら再挑戦を選択した旨の信号を送り、退出を選択もしくはデフォルトで退出を選択した旨の信号を送ります。
なぜデフォルトで退出なのかですが、浮遊インタラクションページには閉じるボタンがデフォルトで実装されており、つまり画面を閉じた際にも退出の処理が働くようにデフォルトの分岐も退出を選択した旨の信号を送る側に繋いでいます。

再びステージエンティティ側の処理に戻ります。
全プレイヤーが再挑戦を選択した場合はゲーム初期化中状態に移行し、プレイヤーが一人でも退出を選択した場合はゲーム終了状態に移行します。


ゲーム初期化中
ゲーム初期化中状態では、ステージやプレイヤーのデータのリセット、ゲーム内に存在するエンティティの削除など、ゲームを再度プレイできる状態に戻すための処理が行われます。
![]()
処理が完了すると最終的にゲーム準備完了待ち状態に移行します。
説明を割愛しましたが、プレイヤーのロード待ち&ステージ選択画面の表示はタイマーを使って一定間隔ごとに行っているため、ここからゲーム準備完了待ち状態に移行する際も非常に短時間でカウントダウンが終わるタイマーを起動しタイマーのカウントダウンが完了することをイベントとしています。


なお処理が2つに分かれているのはノードグラフ自体が2つに分かれているからです。
どちらもステージエンティティ上で動く処理ですが、各ノードグラフ内にノードグラフ変数があるため、それら変数の初期化のためにそれぞれのノードグラフ内でゲーム再挑戦信号を受信しています。
信号を受信した後、ゲーム準備完了待ち状態に移行するための処理を行っているのは一方のノードグラフのみです。


まとめ
以上です。
とにかく言いたかったことは状態とイベントの理解が重要ということです。
![]()
なおいろいろ端折った部分はありますが、ノード内のマジックナンバーなどは複合ノードを使うことで見れば分かる形式で表現しているのでちゃんと読めば何をやってるかはだいたい分かると思います。
別に説明のためにやっているわけではなく、普段から各種IDが変わった際にノードグラフ側の変更が不要になるようにこんな感じでID関連はローカル変数に設定し複合ノード化しています。
こうしておけば各種IDが変わった際に複合ノード側の値を変えるだけで変更が全ノードグラフに適用されるため、ノードグラフの保守性が向上します。

終盤力尽きて説明が雑になっているような気もするので、読み返して気が向いたら修正したり追記する箇所があるかもしれません。
それではまた~。
![]()

