「 👆 ウィンドウが 増えてきそうなので、
ツリー階層を 再編したいというところだぜ」
「 じゃあ どっちも ウィンドウなのだから スクリプトは共通化できるはずなのよ。
メッセージ
と センター
のスクリプトは何が違うの?」
📄 Director/Windows/メッセージ
:
# メッセージ・ウィンドウ(Message Window)
extends Node2D
# 状態遷移機械
var statemachine = load("scripts/MessageWindowStatemachine.gd").new()
# メッセージ・ウィンドウを閉じる
func initialize():
$"TextBlock".initialize()
self.statemachine.all_page_flushed()
# ウィンドウを空っぽにして、次の指示を待ちます
func clear_and_awaiting_order():
print("[メッセージ・ウィンドウ] ウィンドウを空っぽにして、次の指示を待ちます")
$"TextBlock".text = ""
# メッセージウィンドウは指示待ちだ
$"../../AssistantDirector".is_message_window_waiting_for_order = true
# 先頭行と、それ以外に分けます
func split_head_line_or_tail(text):
# 最初の改行を見つける
var index = text.find("\n")
var head = text.substr(0, index)
var tail = text.substr(index+1, text.length() - (index+1))
# print("[メッセージ・ウィンドウ] head: [" + head + "]")
# print("[メッセージ・ウィンドウ] tail: [" + tail + "]")
return [head, tail]
# メッセージを追加
func push_message(text):
# print("[メッセージ・ウィンドウ] 台詞追加")
print("[メッセージ・ウィンドウ] 台詞: [" + text + "]")
$"TextBlock".push_message(text)
# 表示
self.visible = true
# タイプライター風表示へ状態遷移
self.statemachine.scenario_seted()
# 選択肢を追加
func push_choices(row_numbers, text):
print("[メッセージ・ウィンドウ] 選択肢: [" + text + "]")
$"TextBlock".push_choices(row_numbers, text)
# 表示
self.visible = true
# タイプライター風表示へ状態遷移
self.statemachine.scenario_seted()
# ページ送り
func on_page_forward():
# 効果音
$"../../Musician".playSe("ページめくり音")
# ブリンカーを消す
$"TextBlock".clear_blinker()
# ウィンドウを空っぽにして、次の指示を待ちます
self.clear_and_awaiting_order()
# 下位ノードで選択肢が選ばれたとき、その行番号が渡されてくる
func on_choice_selected():
# カーソル音
$"../../Musician".playSe("選択肢確定音")
var row_number = $"TextBlock/ChoiceCursor".selected_row_number
print("[メッセージ・ウィンドウ] 選んだ選択肢行番号:" + str(row_number))
# 選択肢の行番号を、上位ノードへエスカレーションします
$"../../AssistantDirector".on_choice_selected(row_number)
# サブツリーが全てインスタンス化されたときに呼び出される
func _ready():
# ステートマシーンを、子にも参照させる
$"Background".statemachine = self.statemachine
$"TextBlock".statemachine = self.statemachine
$"TextBlock/BlinkerTriangle".statemachine = self.statemachine
$"TextBlock/BlinkerUnderscore".statemachine = self.statemachine
$"TextBlock/ChoiceCursor".statemachine = self.statemachine
# ウィンドウを空っぽにする
$"TextBlock".text = ""
# テキストボックスなどにフォーカスが無いときの入力を拾う
func _unhandled_key_input(event):
# 完全表示中
if self.statemachine.is_completed():
# 選択肢モードなら
if $"TextBlock".is_choice_mode:
# 何かキーを押したとき
if event.is_pressed():
# 確定ボタン以外は無効
if event.keycode != KEY_ENTER:
# print("[メッセージ・ウィンドウ] 選択肢モードでは、エンターキー以外ではメッセージ送りしません")
return
else:
# 選択肢を確定した
self.on_choice_selected()
return
# それ以外なら
else:
# 何かキーを押したとき
if event.is_pressed():
if event.keycode == KEY_R:
# print("[メッセージ・ウィンドウ] Rキーは、メッセージの早送りに使うので、メッセージ送りしません")
return
# ページ送り
self.on_page_forward()
📄 Director/Windows/センター
:
# センター・ウィンドウ(Center Window;中央窓)
extends Node2D
# 現在表示中のセンターウィンドウ画像のノード名
var current_name = null
# ウィンドウを表示する
func show_window(name):
print("[センター・ウィンドウ] 表示:[" + name + "]")
# 既に表示中の画像を非表示にする(上に乗っかっていて、表示したい絵が見えないケースがある)
if self.current_name != null:
self.get_node(self.current_name).hide()
self.current_name = name
self.show()
self.get_node(self.current_name).show()
$"System".show()
$"System/Frame".show()
# ウィンドウを非表示にする
func hide_window():
if self.current_name == null:
return
print("[センター・ウィンドウ] 非表示:[" + str(self.current_name) + "]")
self.get_node(self.current_name).hide()
$"System/Frame".hide()
self.current_name = null
「 👆 そら、メッセージ・ウィンドウには
メッセージ表示をコントロールするスクリプトが書いてるよな」
「 そういえば メッセージ・ウィンドウの背景は 半透明の黒で、
センター・ウィンドウの背景は 画像 という違いもあるぜ」
「 半透明の黒も 窓から切り離して 背景画像という扱いにしたらいいんじゃない?」
「 メッセージ・ウィンドウ用の画像と、センター・ウィンドウ用の画像は 縦横のサイズが違う」
「 じゃあ 画像ファイルは、サイズ別のフォルダーに入れることにしましょう」
「 直交性を考えると ウィンドウ名別のフォルダーにした方が 記述が簡潔になるぜ」
「 Godot のツリー構造と、ファイルシステムのツリー構造に 依存性があると不利だぜ」
「 ゲームプログラムの観点から言うと 自由自在なウィンドウが欲しいような
設計の固まってない状態が いつまでも続いて 進捗が進んでたら 良くないぜ」
「 試しに ゲームを終了するための システム・メニューを設計してみればいいんじゃない?」
「 👆 グリッドに合わさないと サイズ感は 分からないものだ」
「 そんな小さなウィンドウでは 英語が入らないだろう。
ローカライズして大丈夫か?」
「 半透明の黒い所は RGBAが 32, 32, 32, 192 か。 192 は、百分率の 75% でもいいことにしよう」
「 👆 位置も サイズも 色も 素材を作る手間がかからないように妥協したぜ」
「 👆 分かった。名前が悪いんだ。名前を変えよう。
こいつは システム・ウィンドウ ではなくて、
中央メッセージ・ウィンドウ だぜ」
「 👆 どのメッセージ・ウィンドウを使うか指定できるようにしようぜ?」
「 👆 テキストの表示位置をどうするか。 Godot の思想だと コピー貼り付けして 座標変えて……」
「 コピー貼り付けするしか ないんじゃない? 動的にやったら レイアウトの機能の利便性を損なうんだし」
「 👆 同じものが コピーされて DRYの法則が破れているように見えるが、
座標位置を覚えておくデータだから 残しておいた方が エディターが活きるのか~」
「 👆 文字数を調整しないと
CSSチョットワカル みたいになるんだな」
「 ゲームの進行を止めて、システム・メニューを出すんだっけ?」
📂 Director
├── 📂 Main
└── 📂 SystemMenu
📂 Director
├── 📂 Main
├── 📂 SystemMenu
👉 └── 📂 Musician
📂 Director
👉 └── 📂 ScenarioBook
├── Main
└── SystemMenu
「 大改造すると 時間が無くなってしまうから、今回は 分けずに行こうぜ?」
「 エスケープ・キーを押したら メニューが出るようにするにしても、
メッセージ・ウィンドウが出てないときにも メニューは出したいから、
キー・イベントを取得するのは メッセージ・ウィンドウより 上位のノードだよな」
「 メッセージ・ウィンドウが出てるときは キーが反応しないな。
func _unhandled_key_input(event):
を2か所で使うとか よくないのか?」
「 ルートで全部取って、必要なら 子ノードに配るようにしたらどうだぜ?」
「 子ノードが先に _unhandled_key_input()
をキャッチするのか?
後ろ向き探索?」
「 いや、会話イベント中に エスケープ・キー を押して システム・メニューを出そうなんてのが
間違いなんだぜ」
「 しかし 現在の設計では 会話シーンしかない。
マップの上を移動するような シーンや、 アドベンチャーのようなメニューの並んだシーンがない」
📖 ツイート
「 👆 Z-index で前景、後景の調整ができない…… 分けわからん……」
「 サブ・ウィンドウが どうのこうのより
メインで ゲームが進行していて それを 停止させるのが難しい」
「 _process()
で動かしてるんだから 嚙み合わせを外したらいいじゃない」
「 それを1個1個 仕込んでいくのが大変だ。 いったん休憩するぜ」
「 エスケープ・キーを押したら 一時停止する機能は実装してきたぜ」
「 じゃあ ついでに 現在表示しているメッセージ・ウィンドウも非表示にしてくれだぜ」
# サブツリーの visible を設定
func set_visible_subtree(is_visible):
print("[チョイス・カーソル] 可視性:" + str(is_visible))
# 見せろ(true) という指示のとき、見えてれば(true) 、何もしない(pass)。
# 隠せ (false)という指示のとき、見えてれば(true) 、隠す (false)。
# 見せろ(true) という指示のとき、隠れてれば(false)、見せる (true)。
# 隠せ (false)という指示のとき、隠れてれば(false)、何もしない(pass)
if is_visible != self.visible:
self.visible = is_visible
# 子ノード
for child in self.get_children():
if child.has_method("set_visible_subtree"):
child.set_visible_subtree(is_visible)
「 👆 こんな感じのメソッドを ノードに持たせていくかだぜ」
「 前に cwnd
って命令を作ってたよな。あれを v-wnd
に名前を変えようかな」
「 👆 あっ、これは ビューイング・ウィンドウだ。間違えた」
「 前に msg
って命令を作ってたよな。あれを m-wnd
に名前を変えようぜ?」
「 じゃあ そこに 選択肢を表示してくれだぜ。 再開と 続行でいいかな」
「 じゃあ そこで エスケープ・キーを押したら 元の状態に戻してくれだぜ」
「 Godot での z-index
の処理は 分けが分からないのではなかったか?」
「 Visual Novel パートと System Menu パートを分けるべきか、
パートという呼称は 適切か?」
「 画面なら スクリーン(Screen)とか シーン(Scene)かな」
「 Scene
は Godot に用語を取られたから使いたくない」
「 ビジュアル・ノベル・セッション、 バトル・セッション、 メニュー・セッション……、
良い案だぜ 今んとこ候補」
「 セパレーション(Separation;離別)は どうだぜ?」
「 ビジュアル・ノベル・セパレーション、 バトル・セパレーション、 メニュー・セパレーション……、
おかしいか。どうすれば?」
「 デパートメント(Department;部)で いいんじゃない?」
「 それだぜ!
ビジュアル・ノベル・デパートメント、 バトル・デパートメント、 システム・メニュー・デパートメント、
これで行こう」
「 👆 シナリオを書くというのは、
VisualNovelDepartment
のスナップショットを変更することだと、
そういう概念にしてしまおう」
「 👆 スナップショットではない ビジュアル・ノベル部 も作っておこう」
「 ロケーションの名前も デパートメント毎に覚えておく必要があるか」
「 スナップショットは グローバル変数のように 変数を持っておきたくて、
オブジェクト指向のカプセル化とは 反するんだが、
データを1か所で管理したいときは オブジェクト指向じゃない方がいいんだ」
「 テキストブロックが タイプライター表示をしていて
文字列を切り分けたりしているが、この機能は デパートメント の方に持たせたい」
# メッセージを追加
func push_message(new_text):
# print("[テキストブロック] 台詞追加")
print("[テキストブロック] 台詞: [" + new_text + "]")
self.get_snapshot("VisualNovelDepartment").is_choice_mode = false
self.get_snapshot("VisualNovelDepartment").choice_row_numbers = []
self.get_snapshot("VisualNovelDepartment").text_block_buffer = new_text
# 空欄化
self.emptize()
# 選択肢を追加
func push_choices(row_numbers, new_text):
print("[テキストブロック] 選択肢: [" + new_text + "]")
self.get_snapshot("VisualNovelDepartment").choice_row_numbers = row_numbers
self.get_snapshot("VisualNovelDepartment").text_block_buffer = new_text
self.get_snapshot("VisualNovelDepartment").is_choice_mode = true
# 空欄化
self.emptize()
# さらに、ブリンカーは無いことにする
$"BlinkerTriangle".initialize()
$"BlinkerUnderscore".initialize()
「 👆 通常のメッセージと、選択肢でメソッドが分かれているの、
改造の邪魔なんで 1本化したいぜ」
「 前処理として .setup_normal_mode()
と、 .setup_choices_mode()
を作ったら?」
# メッセージを追加
func push_message(new_text, choice_row_numbers = null):
# テキスト設定
self.get_snapshot("VisualNovelDepartment").text_block_buffer = new_text
# 空欄化
self.emptize()
# 選択肢なら
if choice_row_numbers != null:
print("[テキストブロック] 選択肢: [" + new_text + "]")
self.get_snapshot("VisualNovelDepartment").is_choice_mode = true
self.get_snapshot("VisualNovelDepartment").choice_row_numbers = choice_row_numbers
# メッセージエンド・ブリンカーは無いことにする
$"BlinkerTriangle".initialize()
$"BlinkerUnderscore".initialize()
# それ以外なら
else:
print("[テキストブロック] 台詞: [" + new_text + "]")
self.get_snapshot("VisualNovelDepartment").is_choice_mode = false
self.get_snapshot("VisualNovelDepartment").choice_row_numbers = []
「 部門切り替え時に 下メッセージ・ウィンドウの初期化をやってしまっていて、そのとき透明になってるようだぜ」
「 初期化するのは 正しいんじゃない? そのあと表示しないのが悪いだけで」
「 👆 可視性ではなく、不透明性で 見えなくなっていたのだった」
「 Godot のオブジェクトは、存在しない、という設定にできないのかだぜ? .set_active()
みたいな」
「 不可視だったら 存在しない という取り決めにするしかなくない?」
「 その前に スクリプトの機能分担が 煩雑になってきたので シンプルにしていこうぜ?」
「 台詞は 最後に ▼ が出て、
選択肢は 最後に移動できる ▶ が出るのが 違いなんだよな」
「 メッセージエンド・ブリンカー(Message-end Blinker※造語)が違うだけか」
「 ここらへんのスクリプトを 内部的に1種類に統合したいぜ」
# メッセージエンド・ブリンカー(Message-end Blinker)
extends Node
# 状態遷移図
# ーーーーー
#
# Entry
# +
# |
# |
# +ーーーーーーーーーー>+
# | |
# | | resolved ※解決済み
# | |
# | V
# | +ーーーーーー+
# | | None | ※メッセージエンド・ブリンカーが存在しない唯一の状態
# | +ーー+ーーー+
# | |
# | | worry ※悩む
# | |
# | +ーーーーーー>+
# | | |
# | | V
# | | +ーーーーーーーーー+
# | | | BlinkHere | ※その場で点滅中
# | | +ーー+ーーーーーー+
# | | |
# | | |
# | | ◇ ーーーーーーーーーーーーーーーーーーーーー+
# | | | |
# | | | |
# | | | move ※カーソルを動かす |
# | | | |
# | | V |
# | | +ーーーーーーーーーー+ |
# | | | BlinkMoving | ※点滅しながら |
# | | +ーー+ーーーーーーー+ カーソル移動中 |
# | | | |
# | | | |
# | | | |
# | +ーーーーーーー+ moved ※移動完了 |
# | |
# | |
# | |
# +ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー+
#
enum States {None, BlinkHere, BlinkMoving}
# 状態
var state = States.None
# 関数の変数
var on_resolved = null
var on_worry = null
var on_move = null
var on_moved = null
func is_none():
return self.state == States.None
func is_blink_here():
return self.state == States.BlinkHere
func is_blink_moving():
return self.state == States.BlinkMoving
func resolved():
if on_resolved != null:
on_resolved.call()
print("[メッセージエンド・ブリンカー] 解決済み")
self.state = States.None
func worry():
if on_worry != null:
on_worry.call()
print("[メッセージエンド・ブリンカー] 悩む")
self.state = States.BlinkHere
func move():
if on_move != null:
on_move.call()
print("[メッセージエンド・ブリンカー] カーソルを動かす")
self.state = States.BlinkMoving
func moved():
if on_moved != null:
on_moved.call()
print("[メッセージエンド・ブリンカー] カーソルは移動した")
self.state = States.BlinkHere
# ブリンカー(Blinker;点滅するもの)
extends Node
# 状態遷移図
# ーーーーー
#
# Entry
# +
# |
# |
# +ーーーーーーーーーー>+
# | |
# | | switch_off ※スイッチ・オフ
# | |
# | V
# | +ーーーーーー+
# | | None | ※ブリンカーが存在しない唯一の状態
# | +ーー+ーーー+
# | |
# | |
# | | switch_on ※スイッチ・オン
# | |
# | V
# | +ーーーーーーーーーーー+
# | | BrightAtFirst | ※初回はすぐ表示
# | +ーー+ーーーーーーーー+
# | |
# | |
# | +ーーーーーー>+
# | | |
# | | |
# | | ◇ ーーーーーーーーーーーーーーーーーーーーーーーーー+
# | | | |
# | | | |
# | | | turn_off ※時間経過による消灯 |
# | | | |
# | | V |
# | | +ーーーーーー+ |
# | | | Off | ※消える |
# | | +ーー+ーーー+ |
# | | | |
# | | | |
# | | ◇ ーーーーーーーーーーーーーーーーーーーー>+<ーー+
# | | | |
# | | | |
# | | | turn_on ※時間経過による点灯 |
# | | | |
# | | V |
# | | +ーーーーーーー+ |
# | | | Bright | ※灯る |
# | | +ーー+ーーーー+ |
# | | | |
# | | | |
# | | | |
# | +ーーーーーーー+ |
# | |
# | |
# | |
# +ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー+
#
enum States {None, BrightAtFirst, Off, Bright}
# 状態
var state = States.None
# 関数の変数
var on_switched_on = null
var on_switched_off = null
var on_turned_on = null
var on_turned_off = null
func is_none():
return self.state == States.None
func is_bright_at_first():
return self.state == States.BrightAtFirst
func is_off():
return self.state == States.Off
func is_bright():
return self.state == States.Bright
func switch_on():
if on_switched_on != null:
on_switched_on.call()
print("[ブリンカー] スイッチ・オン")
self.state = States.BrightAtFirst
func switch_off():
if on_switched_off != null:
on_switched_off.call()
print("[ブリンカー] スイッチ・オフ")
self.state = States.None
func turn_on():
if on_turned_on != null:
on_turned_on.call()
print("[ブリンカー] 点灯")
self.state = States.Bright
func turn_off():
if on_turned_off != null:
on_turned_off.call()
print("[ブリンカー] 消灯")
self.state = States.Off
「 Godot の .show()
、 .hide()
メソッドは アホが考えたメソッドなんだ。
わたしが考えた .set_visible_subtree()
メソッドを使うことで解決!」
「 👆 ウィンドウの表示/非表示まででけた。
中の文章の復元は まだ」
「 👆 芋づる式に 次から次へと できてないところが 出てくるぜ」
「 ビジュアルノベル部門とか、システムメニュー部門も 一般化しないと きつくなってきた」
func get_current_snapshot():
if self.statemachine_of_director.is_playing_visual_novel():
return self.get_snapshot("VisualNovelDepartment")
elif self.statemachine_of_director.is_playing_system_menu():
return self.get_snapshot("SystemMenuDepartment")
else:
return null
「 👆 状態遷移にしていたが、 Department
は、ただの変数にしたい」
「 Department
は状態ではないという建付けにするわけだな」
「 👆 再開の機能付けたんだが なぜだか知らないが このメニューは1回使うと 2回目以降から出てこないぜ」
(カタ カタ カタ カタ)
.
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント