5. 変数
変数は、里々から移るときにいちばん事故が起きやすいところです。里々では何もしなくても保存されていた変数が、湊では保存されません。 型のない文字列だった値が、湊では型を持ちます。順に見ていきます。
里々の変数は1種類、湊は3種類
里々では、$変数 と書けばそれがゴースト全体で共有され、終了時に自動で satori_savedata.txt に保存されました。里々Wikiにも「変数が自動で保存され、不要になった変数は消す必要がある」と書かれているとおり、里々の変数は基本的にすべてグローバルで、すべて保存されるものでした。
湊には、次の3種類があります。
| 種類 | 書き方 | 有効範囲 | 保存 | 里々の感覚でいうと |
|---|---|---|---|---|
| ローカル変数 | let x = 1 | そのトークの中(ブロックの中) | されない | 該当なし(一時的な計算用の変数) |
| セーブ変数 | global save.x = 1 | ゴースト全体 | save.json に保存 | $x(普通の変数) |
| 起動中グローバル | global work.x = 1 | ゴースト全体 | されない(終了で消える) | 該当なし(保存したくない変数) |
里々の変数は、ほとんどの場合 global save.x に置き換えます。「保存しなくてよい」「その場の計算にしか使わない」変数だけを、let や work に分けます。
自動保存されない
保存されるのは、save の下に置いた値だけです。let で作った変数も、global work.x で作った変数も、終了すると消えます。
OnBoot => {
let a = 1
b = 2
global save.c = 3
global work.d = 4
湊: ${a} ${b} ${save.c} ${work.d}
}
OnClose => {
湊: [${a}] [${b}] [${save.c}] [${work.d}]
}
\01 2 3 4\e
\0[] [2] [3] [4]\e
{"c":3}
\0[] [] [3] []\e
上から順に見ます。
OnBootでは、4つとも値が入っています。OnCloseでは、a(let)は消えています。トークが終わるとletは消えるためです。bとdは、起動中のあいだは残っています。@saveはゴーストを終了して保存し、save.jsonのsaveの中身を見せています({"c":3})。cだけです。- 再起動後の
OnCloseでは、save.cだけが残っています。
「セーブしたいものには必ず global save. を付ける」、これがいちばん大事なルールです。書き忘れても、エラーは出ません。
保存が行われるのは、ゴーストの終了時(とロード時)だけです。里々の $手動セーブ や $自動セーブ間隔 に当たる機能はありません。SSPごと強制終了すると、その起動中に覚えたことは消えます。
裸の代入は起動中グローバルになる
let を付けずに x = 5 と書くと、その名前のローカル変数がなければ、起動中グローバルが作られます。他のトークからも見え、ゴーストを終了するまで残ります。保存はされません。
OnBoot => {
counter = 5
}
OnClose => {
湊: counter=${counter}
}
(204)
\0counter=5\e
{}
OnBoot のトークで作った counter が、別のトーク OnClose から見えています(OnBoot は何も喋らないので 204 です)。save には入っていません。
里々では変数がすべてグローバルなので、この動きは意外に感じないかもしれません。ただし、保存されない点が里々と違います。里々の感覚のまま計算用の変数を書くと、他のトークとの間で名前がぶつかり、原因の分かりにくい不具合になります。また、再起動すると値が消えます。
- トークの中だけの一時変数は、必ず
letで宣言する。 - 起動中だけ共有したい値は
global work.…と書く(名前がworkで始まる、というだけの約束です)。
関数(func)の中でも同じです。let で宣言したものは関数の中だけ、裸の代入はグローバルになります。
let の有効範囲
let の変数は、宣言したブロック({ })の中だけで有効です。if、for、foreach、match の中で let すると、ブロックを出たときに消えます。
一方、let なしの代入(c = 5)は、外側に同名のローカル変数があればその変数を更新します。
OnBoot => {
let a = 1
let c = 1
if (true) {
let a = 2
c = 5
let b = 3
}
湊: a=${a} b=${b} c=${c}
}
\0a=1 b= c=5\e
let a = 2は、ifの中に新しいaを作ります。外のaは 1 のままです。c = 5は、外のcを更新します。bはifの中で消えるので、外では空です。
call や関数呼び出しで呼んだ先のトークや関数からは、呼び出し元の let 変数が見えます(呼び出しの間は、呼び出し元のローカル変数も有効なままだからです)。let なしの代入(x = 5)を呼び出し先で書くと、同名の呼び出し元の let 変数が更新されます。
呼び出し先が let で作った変数は、呼び出しが終わると消えます。呼び出し元の変数に依存すると、名前が偶然ぶつかったときに追いにくい不具合になるので、値は引数か、save / work で渡すのが安全です。
型と暗黙の型変換
里々では、変数の中身はすべて文字列で、式の中で数値として解釈されました。湊には、数値・文字列・真偽値・配列・マップ・null の型があります。
多くの場面で、値は自動的に変換されます。その規則が里々と違うので、最も気をつけてください。
足し算 + は文字列があると連結になる
OnBoot => {
湊: ${"1" + 2}|${1 + "2"}|${"3" + "4"}|${1 + 2}
湊: ${"a" < "b"}|${"9" < "10"}|${1 == "1"}|${"0" == 0}
湊: ${!"0"}|${!0}|${!""}|${!null}|${!"false"}
}
\012|12|34|3\nfalse|true|true|true\nfalse|true|true|true|false\e
+: どちらかが文字列なら連結です。"1" + 2は12です。両方が数値のときだけ加算します。-*/%: 文字列は数値に変換して計算します。数値にできなければ 0 として扱います。<<=>>=: 常に数値に変換して比較します。文字列の辞書順比較はできません。"a" < "b"は偽です(両辺が 0 になるため)。==!=: 数値と文字列は、表示した形で比べます。1 == "1"は真です。- 真偽: 偽になるのは、
false、0、空文字列、空の配列、空のマップ、null。"0"や"false"は真です。
数値のつもりの文字列に注意
reference の値、SAORIの戻り値、セーブデータから移した値は、文字列です。数値のつもりで + すると連結になります。
OnBoot => {
global save.好感度 = "10"
湊: ${save.好感度 + 1}|${save.好感度 >= 5}|${save.好感度 == 10}|${to_num(save.好感度) + 1}
}
\0101|true|true|11\e
save.好感度 が文字列 "10" のとき、+ 1 は 101 になります。比較の >= 5 や == 10 は期待どおりなので、問題に気づきにくいのが厄介です。文字列で保存されている数値は、to_num() で数値にしてから使ってください。 特に、global save.x += 1 を文字列に対して行うと、"10" は "101" になります。
全角の数字は数値になりません。 里々では全角の数字も数値として計算できましたが、湊では 0 になります。
OnBoot => {
let z = "3"
湊: ${z >= 3}|${to_num(z)}|${z + 1}
}
\0false|0|31\e
里々のセーブデータを移行するときは、全角数字を半角にして、数値として保存し直してください(9. セーブデータ)。
値の表示形式
${…} や文字列連結で表示されるとき、値は次のようになります。
| 値 | 表示 |
|---|---|
| null | 空文字列 |
| 真偽値 | true / false |
| 数値 | 整数なら 5、小数なら 2.5。小数は誤差がそのまま出る |
| 配列 | [1, a](要素をカンマ区切り) |
| マップ | [map] |
OnBoot => {
湊: ${true}|${[1, "a"]}|${{"k": 1}}|${null}|
湊: ${0.1 + 0.2}|${format("%.1f", 0.1 + 0.2)}|${10 / 4}|${floor(10 / 4)}|${7 % 3}
}
\0true|[1, a]|[map]||\n0.30000000000000004|0.3|2.5|2|1\e
除算は実数です。10 / 4 は 2.5 になります。整数にしたいときは floor() などを使います。小数の桁を揃えたいときは format("%.1f", …) を使います。ゼロで割ると、null になり、警告がログに残ります(エラーで止まりはしません)。
未定義の変数は null
湊では、存在しない変数は null です。参照してもエラーにならず、表示は空文字列です。
OnBoot => {
湊: [${x}][${save.foo}][${a.b.c}]
湊: ${save.n == 0}|${(save.n ?? 0) == 0}|${is_null(save.n)}
}
OnClose => {
global save.n += 1
global save.s += "a"
湊: ${save.n}|${save.s}
}
\0[][][]\nfalse|true|true\e
\01|a\e
${x}のように、書き間違えた変数名は、警告も出ずに空になります。里々では、未定義の(変数)は括弧ごとそのまま表示されたので、間違いに気づけましたが、湊では気づけません。save.n == 0は、nが未設定のとき偽です。nullは空文字列として表示され、0の表示"0"と一致しないためです。初期値が 0 の前提で比較するなら、(save.n ?? 0) == 0と書くか、あらかじめ?=で初期化しておきます。+=は、nullに対しては 0(数値の場合)や空文字列(文字列の場合)として働きます。
初期値の与え方
里々では satori_conf.txt の *初期化 に初期値を書きました。湊では、main.mnt の直下(トークの外)に global 文を書きます。ロード時に実行されます。
global save.好感度 ?= 5
OnBoot => {
global save.好感度 += 1
湊: ${save.好感度}
}
\06\e
(reload)
\07\e
?= は「値が null のときだけ代入する」です。1回目は初期値の 5 に 1 が足されて 6、再起動後は保存された 6 がそのまま使われて 7 になります。初期値には = でなく ?= を使ってください。 = だと、起動のたびに保存済みの値が初期値で上書きされます。
変数の存在確認と削除
| 里々 | 湊 |
|---|---|
(変数「x」の存在) | has_key(save, "x")、または save.x が null かどうか(is_null(save.x)) |
| 変数の削除 | global save = delete(save, "x") |
OnBoot => {
global save.stats.win += 1
global save.stats.lose += 2
global save.old = "x"
global save = delete(save, "old")
湊: ${save.stats.win}勝${save.stats.lose}敗|${has_key(save, "old")}|${has_key(save, "stats")}
}
\01勝2敗|false|true\e
{"stats":{"win":1,"lose":2}}
global save.stats.win += 1 のように、途中のマップは自動で作られます。save.json に保存されたマップのキーの順序は、再起動後は名前の辞書順になります(挿入順は保存されません)。順序に意味がある処理は、配列で持ってください。
配列変数の代わり
里々には配列がなく、$野菜0、$野菜1、(野菜(n)) のように番号付きの変数で代用しました。湊には配列があります。
OnBoot => {
let 野菜 = ["にんじん", "たまねぎ", "じゃがいも"]
let n = 2
湊: ${野菜[n]}|${len(野菜)}
foreach 野菜 as i, v {
湊: ${i}:${v}\n
}
}
\0じゃがいも|30:にんじん\n1:たまねぎ\n2:じゃがいも\n\e
配列の添字は 0 から始まります。範囲外を指すと null になり、警告がログに残ります。範囲外でも警告を出したくないときは、get(配列, 添字, 既定値) を使います。ループの中のセリフの末尾に \n を付けているのは、ループの中では自動で改行が入らないためです(10. 改行)。
情報取得変数の代わり
里々の情報取得変数((現在時) など)に当たるものです。
| 里々 | 湊 |
|---|---|
(現在年) (現在月) (現在日) | now.年 now.月 now.日 |
(現在時) (現在分) (現在秒) | now.時 now.分 now.秒 |
(現在曜日)(「日」「月」…) | now.曜日(数値。0 が月曜、6 が日曜) |
(R0) (R1) | reference["0"] reference["1"] |
(S0)(SAORIの戻り値) | 戻り配列の [0](8. SAORI) |
| 起動回数・累計時間 | なし。global save.起動回数 += 1 のように自分で数える |
(隣で起動しているゴースト) など | get_property("…")(8. SAORI) |
now は、湊が起動したあとSSPの時刻を取得できるまでは、日本標準時(UTC+9)の現在時刻です。取得後はSSPの仮想時刻になります。
エラーは静かに出る
湊は、変数まわりのミスをほとんど止めません。代わりに応答のヘッダにエラーを載せます。
| ミス | 挙動 |
|---|---|
未定義変数を ${} で参照 | 空文字列。通知なし |
未定義の変数に .名前 でアクセス | null。通知なし |
| 配列の範囲外 | null、警告 |
マップに存在しないキーを ["キー"] で参照 | null、通知(notice) |
| ゼロ除算 | null、警告 |
警告と通知は、SHIORI応答の ErrorLevel / ErrorDescription ヘッダに載ります。SSPがこれをどう表示するかは、SSPのバージョンや設定によります。台本を書きながら確認するなら、config.toml の debug_log = true で minato_load.log を出します。ただし、この設定にも注意が必要です(9. セーブデータ)。
YAYAから来た方へ
- YAYAでは、
_で始まらない変数はグローバルで、自動で保存されます(yaya_variable.cfg)。湊はsaveの下だけが保存されます。 - YAYAの変数の初期値は空文字列で、
i++のような処理を数値で初期化せずに使うと文字列になります。湊はnullから始まり、+=は数値として働きます。 - YAYAの
_変数のスコープは「その{ }とそれより深い{ }」で、湊のletと同じ考え方です。
次は6. 単語群に進んでください。