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

上から順に見ます。

  1. OnBoot では、4つとも値が入っています。
  2. OnClose では、a(let)は消えています。トークが終わると let は消えるためです。b と d は、起動中のあいだは残っています。
  3. @save はゴーストを終了して保存し、save.json の save の中身を見せています({"c":3})。c だけです。
  4. 再起動後の 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. 単語群に進んでください。