わたしのかんがえたさいきょうのしおり

こんばんは。ななっちです。
この記事は伺か・伺的アドベントカレンダー2024の第一会場の21日の記事です。20日は淀橋さんの記事でしたね。

はじめに⋯ 注意事項、この文章ははっきり言って怪文書です。ゴーストの作り方の解説はTipsとかはありません。SHIORIについて思いの丈を叫んでるような記事です。

これまで、汎用言語をSHIORIにあてはめてみたり、里々でいろんなゴーストをつくったりしてみましたが⋯やっぱりどうしても

  • 里々はトークがすごく書きやすい

    • 処理はあんまり書きやすくない

  • 汎用のスクリプト言語は処理を書きやすい

    • トークの書き方がよくわからない

このあたりのジレンマから抜け出せていませんでした。この記事はこのジレンマの根底になにがあるのかを自分なりに考えてあがいてみた記録的ななにかです。

里々と里々以外のプログラミングが多少分かる人じゃないとあんまりわからないかもしれません。そんな場合はアドベントカレンダーの第二会場にも楽しい記事がたくさんあるのでそちらものぞいてみるといいかもしれません。

前置きがだいぶながくなってしまいましたが⋯ まず、里々と汎用のスクリプト言語のそれぞれの強みや弱みについて、トークを書くときと処理を書くときでそれぞれくらべてみます。

トークをかく

里々の場合

とにかくトークが書ける!里々の利点はこれに尽きます。慣れもあるとは思いますが、実際書かないといけないのは表情指定とウェイトだけ。あとはほとんど本文といっていいでしょう。

*ランダムトーク【タブ】!(寝る時間)
:(4)ねー(ユーザ名)、\w9映画見よーよ。\w9\w9
(5)おうちじゃサブスクやってないんだもんー。

汎用的なスクリプトの場合

あまりこれでちゃんとした形のゴーストを作ったことがないので難しいですが、多分、Pythonだとこんな感じになりそうです。ちょっと return とか余計な感じがしますね。これだとまだシンプルなほうですが、色々やろうとすると大変そうに思う箇所もあります。

def RandomTalk():
  return f'\s[4]ねー{GetUserName()}、\w9映画見よーよ。\w9\w9\s[5]おうちじゃサブスクやってないんだもんー。'

比べてみて

例に上げたのはトーク1つずつでしたが、本格的にゴーストのトークをかくとなると、やっぱり里々のほうが書きやすいように思います。具体的に理由をあげるとこのあたりでしょうか。

  • 最も外側の表現が「文字列」なこと

    • 汎用のスクリプトだと「シンボル(変数や関数)」になるので、文字列を示すには ' ' で区切らないといけないのが地味面倒。

  • 発生条件をトーク側に指定できる

    • Pythonの場合はデコレータを使えばいけるのか?とか考えてみたのですがそこそこ難しそうな感覚。一般論的に、里々の発生条件に相当する言語機能がある汎用のプログラミング言語は多分ない。

  • 識別子(文の名前)を重複させられる

    • 里々の場合、同じ名前のトークを複数書けばその中からランダムで呼び出されますが、これも里々特有の機能です。

    • プログラミング言語にも「オーバーロード」という仕組みで名前を被らせる機能はあるけれど、流石にランダム呼出は無い。

処理をかく

ループ処理で、ちょっとしたゲージを書いてみます
HP: 10/20: ■■■■■□□□□□□

フォントによってズレるけど、SSPのバルーンはいいかんじ

里々の場合

多分こんな感じです。小数点の扱いが怪しいので計算を避けて百分率で比較してみました。比較的シンプルなのでこの例だと里々もわかりにくくはないですね。物によってwhenの条件が複雑化するとキツくなってきます。

*HPゲージ表示
$ゲージ数【タブ】10
$現在HP【タブ】10
$最大HP【タブ】20
HP: (現在HP)/(最大HP) (loop,HPゲージ表示ループ,(ゲージ数))

@HPゲージ表示ループ
(when,100*(現在HP)/(最大HP)>=100*(HPゲージ表示ループカウンタ)/(ゲージ数),■,□)

汎用的なスクリプトの場合

こんどはjavascript。思ったより長くなってしまったのでこれが良い対比かは微妙になっちゃいましたが⋯ 同じように条件をいろいろするとなると、こっちのほうが有利になってくるんじゃないかと思います。

function DrawHPGauge(){
    let gaugeCount = 10;
    let currentHp = 10;
    let maxHp = 20;
    let result = "";

    for(let i = 0; i < 10; i++){
        if(currentHp / maxHp > i / gaugeCount){
            result += "■";
        }
        else {
            result += "□";
        }
    }

    return result;
}

あと、里々との大きな違いは、里々のユーザ変数はすべてセーブデータかつグローバル変数ですが、javascriptはローカル変数が基本なので他の関数との被りを気にする必要が少なくてすみます。ここでは使っていませんが線形配列や連想配列など、1つの変数内に複数のデータを格納するなどデータの取り回しをしやすくする工夫もあります。

例はちょっと微妙でしたが、複雑な処理を書こうとするほど、やっぱり里々よりも汎用のスクリプト言語のほうが有利なように思います。

わたしのかんがえたさいきょうのしおり

ここまで簡単にですが里々と汎用のスクリプト言語とを比べてみました。ゴーストを作るうえではそれぞれ一長一短だというのが私の結論です。裏をかえせば、そのいいとこ取りができれば最強かもしれない。

つまりは必要なのは汎用スクリプト言語をゴーストに組み込むのではなく、このいいとこ取りをした、ゴースト開発に特化した「SHIORI」でゴーストを作ることがやっぱりベストなのではないかということです。

それが「わたしのかんがえたさいきょうのしおり」だと思いました。

ちょっと考えてみる

ということで、この最強のしおりについて、ちょっと考えてみることにしてみます。どういうSHIORIがいいか、記法や動作のコアになる部分を書き出してみます。

トークが書きたい

まずトークは里々っぽくこんなふうにかけたらいいなと思いました。

talk ランダムトーク {
  \s[4]ねー{ユーザ名}、\w9映画見よーよ、\w9\w9
  おうちじゃサブスクやってないんだもんー。
}
 
talk ランダムトーク {
  \s[20]地鶏の炭火焼、\w9自分で作れたらなあ。\w9\w9
  炭火がなぁ⋯
}
  • 汎用スクリプトの function のノリで talk { … } と書いてみる

  • 中身は里々のように文字列としてあつかうのがいい

  • { … } で変数や関数といったシンボルの埋め込み

    • 処理を書きたいことも考えると、やっぱり半角主義かな?

    • 全角で里々っぽくしてみてもいいかもしれないけど、どうだろう

  • 同じ名前での宣言を許容して、ランダムに呼んでほしい

こんな感じかな⋯ これなら一応、喋らせることだけをかけていそうです。(ちなみにウェイトの自動挿入は、個人的にあまり使わないようにしているので今回は考えませんでした。変なところにウェイトが差し込まれて困る事例もそこそこあるので⋯)
それとサーフェスの指定は里々のほうが簡単なので、なにかもっとできると良いかもしれません。

処理を書きたい

処理はやっぱり汎用のスクリプト言語のような雰囲気で書きたいものです。関数に必要な最低限の要素ということで、こんな感じを考えてみました。

function OnUserNameInput {
  local name = Shiori.Reference[0];
  if(!name) {
    return ユーザ名が空打ちされた();
  }
  else if(name == Save.Data.UserName){
    return ユーザ名変更なし();
  }
  Save.Data.UserName = name;
  return ユーザ名変更完了();
}
  • ローカル変数が使える

  • 結果をreturnで返す

  • 関数名をイベント名にしたら自動で呼んでほしい

  • if文やwhile文などのフロー制御

  • オブジェクトをネストできる

こう書けるとちょっとうれしいですね。(これくらいなら、全然里々のジャンプ記法「>」とかでも全然いいとは思うんですが、)プログラムっぽくこう書けると、条件や処理が長くなったりしても扱いやすいと思いました。

考えをまとめる

このあたりの考えを煮詰めて、最強のしおりにほしい機能を書き出してみます。

  • トークを書きたいときはトークの内容が最初にいきたい。ダブルクォーテーションは面倒。(中括弧で括るくらいならゆるす。)

  • 逆に関数、処理を書きたいときはよくあるタイプのスクリプトの感覚でシンボルが最初にきてほしい。里々みたいに全角カッコを大量に重ねたりはしたくない。

  • 全体的に動的型付けと連想配列で動かしつつメモリをGCで回収するようなゆるさがいい。

    • 推論をちゃんとするような元気はない。

  • 動作効率は度外視していい。

    • 作りたいのはプログラミング言語ではなくSHIORIだからだ。余裕のある環境で動く前提だし、すでにハードルが高いのでこの辺のハードルは低くいきたい。

    • リソースを無駄遣いするのが仕事

  • さとりすとみたいに簡単にトークを試したりしたい。


こんな感じでしょうか⋯
正直なところ今日の記事はだいぶ怪文書だと思っているのであまり共感を得られないかもしれないのですが、思いの丈をさけびます。

もっと考えてみる

それぞれのトピックについてもっと考えてみます。

関数とトークの連携

talk と functionの連携はこんな感じがいい。
talk は「文字列を返す関数」という扱いにして、改行などは\nタグ等の適切なさくらスクリプト化された状態で返す。下記の記述ではOnUserNameInputの中からはいずれかのトークを関数として呼び出すことで、状況に応じたトークを呼べるようにできる⋯という感じ。

1つのファイルにtalkとfunctionをスコープで分離し、その中でそれぞれの記法で記述ができると便利がよさそうです。

/*
	ユーザ名の変更
*/

//メニューから呼ばれるつもり
talk OnChangeUserName {
	え。\w9\w9
	ずっと{ユーザ名}だったのに?\w9\w9
	まあもうお互いに大人だし? \w9\w9なんて呼べばいいの?\![open,inputbox,OnUserNameInput,0,{ユーザ名}]
}

function OnUserNameInput {
	local name = Shiori.Reference[0];
	if(!name) {
		return ユーザ名が空打ちされた();
	}
	else if(name == Save.Data.UserName) {
		return ユーザ名変更なし();
	}
	Save.Data.UserName = name;
	return ユーザ名変更完了();
}

talk ユーザ名が空打ちされた {
	やっぱやめ?
}

talk ユーザ名変更完了 {
	{ユーザ名}。\w9\w9うい。\w9\w9頑張ってそう呼ぶね。
}

talk ユーザ名変更なし {
	やっぱり{ユーザ名}も慣れてんだよ。\w9\w9
	{一人称}に{ユーザ名}って呼ばれるのがさー。\w9\w9
	だからこれでいいじゃん。
}

逆に埋め込みでトークに関数を埋め込むのもあり。talkもfunctionも (…) に仮引数リストを持てるので、何かしらの情報のやり取りをして呼び出すこともできるとかもいいですね。

talk OnChagneTalkInterval {
	どうする?\_q
	{TalkIntervalItem(120, "2分")}
	{TalkIntervalItem(180, "3分")}
	{TalkIntervalItem(240, "4分")}
	{TalkIntervalItem(0, "喋らない")}
}

function TalkIntervalItem(seconds, label) {
	local item = "\![*]\q[${label},OnSetTalkInterval,${seconds}]";
	if(seconds == Save.Data.TalkInterval){
		item = item + "← いまこれ!";
	}
	return item;
}

里々の呼出条件の仕組みは結構使いやすいと思ったので、呼出条件もかけるようにしたいところ。関数にifをいっこかけるようにするのはどうだろうか。ここはフロー制御のifと異なり宣言的なものに近いのでelseブロックとかがかけるわけではないけど⋯条件指定としてはそれっぽく見えるかもしれません。

talk OnTestTalk if(Time.GetNowHour() == 17) {
  午後5時です。
}

最後に、里々的オーバーロードを導入することにしてみます。talkやfunction文でグローバル空間に展開されるのは関数そのものを示すオブジェクトではなく、関数のコレクションを示すオブジェクトになるという感じ。

talk ランダムトーク {
 ランダムトーク!
}
 
talk ランダムトーク {
  ランダムトーク、その2!
}

最初の「ランダムトーク」関数はグローバル空間にその名前で関数コレクションを作成して、それを追加。次の「ランダムトーク」関数では既存のグローバル空間の関数コレクションに対してそれを追加します。上書きするわけではなく。

この関数コレクションは関数呼び出し式をサポートして、あたかもそれが関数であるように呼び出すことができる。呼び出された関数コレクションは、呼出条件を考慮したうえで内部の関数を1つ呼び出す。このとき、重複回避を行えるともっと良さそうです。

ジャンプ記法

里々のジャンプ記法も場所によっては結構嬉しいですね。if文よりも簡素な記述でifの階段による分岐を実装できそうです。

talk OnBoot {
  >めりくり: Time.GetNowMonth() == 12 && Time.GetNowDate() == 24
  >起動
}
 
//ifで書くとこう
function OnBoot {
  if(Time.GetNowMonth() == 12 && Time.GetNowDate() == 24)
    return めりくり();
  return 起動();
}

連想配列

連想配列は里々にはないけど、ぜひ取り入れたい機能です。

触り判定なんかは里々を思うとこんなふうにかけると便利が良さそうですね。%{}は処理をかける範囲を示していてローカル変数に代入してみています。里々では文字列をくっつけた結果をトーク名など、スクリプトでいうシンボルにすることが多いので、そこにはリフレクションを使いたいところ。

リフレクションは言語によっては高度な使い方に分類されるものもありますが、ここでは使いやすくいきたい。

local collisions = {
  head: "頭",
  bust: "胸"
};
 
talk OnMouseDoubleClick {
  %{
    local colName = collisions[Shiori.Reference[4]];
  }
  >MainMenu : Shiori.Reference[4] == ""
  >Reflection.Get(colName + "つつかれ")
}

フロー制御

ifやfor、whileとかはよく使うので、それらしく書けるようにしたいですね。ループができればリスト系の画面がかなり作りやすくなりそうです。

//かいものリスト
local itemListCount = {};

talk OnItemList {
	\_q\s[0]何を買うんだっけ?

	>かいものリスト表示
}

function かいものリスト表示 {

	local itemList = [
		"おにく",
		"おさかな",
		"おやさい",
		"くだもの",
		"きのこ",
		"とうふ"
	];

	local result = "";
	for(local i = 0; i < itemList.length; i++){
		local itemName = itemList[i];
		local count = itemListCount[itemName];
		if(!count){
			count = 0;
		}
		result += "\q[▲増やす,OnIncrementItem,{itemName}] \q[▼減らす,OnDecrementItem,{itemName}] {itemName}: {count}個\n";
	}

	result += "\n\![*]\q[とじる,OnMenuClose]";
	return result;
}

開発環境によるサポート

いまどきのプログラミングの環境といえばやっぱりVisualStudio Code かな、ということでシンタックスハイライトなどがあるとうれしいですね。

トークも里々では「さとりて」で書いては喋らせての繰り返しで作るので、このSHIORIでも簡単に喋らせたいです。喋らせるボタンが出てくるとか。

実体化する

わたしのかんがえたさいきょうのしおり
Aosora SHIORI

デモゴーストになるくらいには頑張れました。もし興味があればのぞいてみてください。demo.nar がデモゴーストで、辞書を覗いてみたい場合はそこからどうぞ。ソースコードもこのリポジトリで公開してあるので、もし覗いてみたいという特殊な方もどうぞ。

デモゴーストなので内容的にはごくわずかです。
シェルはこちらのフリーシェルをお借りしました。シェル作者様に感謝。微妙なところで使ってすみません。

開発風景

ゴースト自体はVisualStudio Codeでトークなどをつくりました。それにあわせて拡張機能も雑なものではありますが用意はしてみていて、画像にちっちゃく出てますが「さとりて」的な機能としてトークを書くとゴーストに喋らせるボタンがエディタ上に出てくるようにしてあります。拡張機能はリリースに含まれる vsixファイルのほうからインストールできます。
右側の表情一覧は以前作成した拡張機能をつかっています。

VSCodeでかりかり

とはいえ

とはいえデモゴーストが動くだけで力尽きてるところがあります。そもそも自分が喋りメインのゴーストを作っていることが最近は多かったりで技術的な色々はやってみたいものの、適用先に関してモチベーションの確保がなかなかむずかしいですね。

あと結構動作をさくっと決めることができた箇所はがりがり書き進められたのですが、たとえばエラー時の動作とかが結構なやましく決めきれていません。 

なのでこのSHIORIそのものが完成レベルまで出来上がることができるかというと、なかなか大変で、もしやるにしてもまだまだがんばらないといけないです。

もし興味があれば

何かしら色々と「いけそうな感」があれば話題にしてもらえると嬉しいです。あと中身C++ですがRustで書いてみたら関わりやすいですかね⋯? ゴースト開発側のひとも何かもし興味があればデモゴーストの中とかちょっと覗いてみてもらえるとうれしいです。yayaとかで辞書を書いているけど、もっとトークをかきやすくできそうかもとか、そういうのもあったりしないかな⋯

そうでなくても、なにか、どなたかがSHIORIを考えるときの参考かインスピレーションにでもなれば幸いです。

余談

今回アドベントカレンダーの告知があってなにかいいネタはないかなと、うっすら考えていた(そこそこちゃんとした)SHIORIづくりというのを行動にうつしてみることにしたのでしたが、この記事のレベルに達するまでは1ヶ月以上かかりました。ふたをあけてみるとゴーストが起動するようになったのはいいのですが、記事自体は怪文書になってしまいましたね。

余談の余談

こういうのって名前を決めきれないのがいつものパターンなんですが、今回はちゃんと適当に(適切に)やりました。元ネタは安易に自分のゴーストのユグドラシルチェリィなんですけどね。物腰柔らかな「蒼」と、しっかり者の「空」で2人組なのですが、そういう雰囲気でトークとシステムというでこぼこなコンビを、それぞれをそれぞれに合った形式でかけるといいのかな、という思いからつけてみました。

おわりに

かなりがなくなってしまいましたがこれでおわりです。
もしここまでよみすすめていただけたのなら、とてもありがたくおもいます。ありがとうございます。

さて、アドベントカレンダーの次回は Zichqec さんの番です。英語圏からの参加とのことで、時差がわかんないんですが日本時間のいつ投稿になるんでしょうね? たのしみです。

いいなと思ったら応援しよう!