![見出し画像](https://assets.st-note.com/production/uploads/images/129204442/rectangle_large_type_2_44d750ac6ac75154c0fef2e59772b5b8.png?width=1200)
【ティラノスクリプト備忘録】SLG的な会話・シナリオイベント発生システムを組む
「育成・恋愛シミュレーションゲームで良くある、
『○日目にイベントシナリオ××が発生』のような
イベント発生システムが作りたい!」
けど作り方がよく分からないので色々試行錯誤した、という覚書です。
2024.03.18 追記
本記事のコードをそのまま使うとまずい点が2点ほど見つかったので、折を見て再編集します!主に下記二点。
①イベントを呼び出している間にコンフィグなどを開くと固まる
②オブジェクト内に関数function()を入れると、セーブ&ロードで保存されない
こういう感じの処理を作ったぞ!という覚え書きです。
わーい イベントの処理の雛型ができた pic.twitter.com/2ibEk5fffO
— Kamivukuro (@Kamivukuro_tira) January 29, 2024
こんな感じの、「発生条件に応じたイベント(今回であれば日数)を任意のタイミング(今回であれば行動選択の前)で発生させる」、という感じの処理です。
1日目のイベントと2日目のイベントを呼び出していて、3日目は発生条件を満たすイベントが無いのでスキップ。
制作日数:2~3日(連想配列とかオブジェクトの勉強時間含め)
1. あらまし
1.1. イベント発生システムが欲しいぜ!
現在、シミュレーションベースなゲームをティラノスクリプトで作れないか?とシステム面を組んでます。
ゲームフローのイメージは下記のような感じ↓
ホーム画面で毎日の行動を選択
行動の結果によって能力値などのパラメータ変化
日数や能力値などの諸条件によってシナリオイベント発生
![ホーム画面イメージ。ホーム画面が始まると、行動前のイベントが発生するかの判定処理が始まり、発生するイベントが存在する場合はイベントが呼び出される。その後、プレイヤーが行動を選択すると、行動後のイベントが発生するかの判定処理が始まり、発生するイベントが存在する場合はイベントが呼び出される。行動後イベントの終了後、一日分の日数が経過し、ホーム画面の最初に戻る。](https://assets.st-note.com/img/1706530801929-b6kPIRypl2.png)
イベントの発生タイミングが行動の前後の2回ある予定
そこで、③の機能を実現するにあたって、
『イベントをデータで管理し、発生条件に一致するイベントのシナリオを呼び出す』ような、
イベント判定・呼び出し処理が欲しいと思ったので覚書です。
1.2 実装を考えると処理をなるべく楽にしたい
条件分岐と言えばif文やswitch文、パラメータcond="条件式"の出番。
と考えて、ティラノスクリプト・ティラノビルダーのタグ(switch文の場合はプラグイン)でシナリオファイルにベタ打ちでもできなくもないが、その場合下記のイメージになる。
;発生条件を満たすイベントかを判断して、該当するシナリオファイル&ラベルを呼び出す
;イベント1が発生する場合
[if exp="イベント1の発生条件"]
[jump storage="イベント1が記載されているシナリオファイル".ks target="*イベント1のラベル"]
;イベント2が発生する場合
[elsif exp="イベント2の発生条件"]
[jump storage="イベント2が記載されているシナリオファイル".ks target="*イベント2のラベル"]
;発生条件を満たすイベントが無い場合
[else]
[jump storage="ホーム画面処理ファイル".ks target="*イベント終了後のラベル"]
[endif]
あと、タグリファレンスより、下記のようなこともあるので、あんまり[if]みたいな処理の中に[jump]とか[call]と書きたくない。
(これはcondとか上手く使えば、多分ティラノのタグでもクリアできると思う。製作テクニックとかに載ってそう)
[if]やマクロの中でジャンプしたりすることを繰り返した場合、回収されないスタックが溜まっていきます。
スタックが溜まりすぎると、ゲームの動作が重くなったりセーブデータが肥大化したりする恐れがあります。
というわけで、イベントを上手い感じに変数で管理+JavaScriptの処理とかでどうにか簡易化できないか、という感じです。
2. 実際に書いたイベント発生システム
2.1 イベント処理のざっくりフロー
上記ホーム画面フローの、青色の前or後イベント判定分岐と前or後イベント呼び出し処理をもう少し整理したものが下図。
![イベント処理が始まると、イベントデータを取得し、発生条件を満たすかどうかを判定します。発生条件を満たすと発生イベント変数に代入し、該当イベントを呼び出し、発生済みにします。](https://assets.st-note.com/img/1706533614944-iBpUx2UBmO.png?width=1200)
ティラノのシナリオファイルはこんな感じの管理です。
![ティラノのシナリオファイルのイメージ図。①home.ksはホーム画面の処理の一連の流れを書くシナリオファイル。②event.ksはシナリオイベントの内容を書く。ラベル名を*event_01、*event_02、*event_xxと設定して、イベントごとにラベル名を付ける。③hensu.ksは変数定義を書くシナリオファイル。あらかじめ呼出し、イベントデータの変数を作っておきます。](https://assets.st-note.com/img/1706535473802-7jQAfjjfIN.png?width=1200)
2.2 実際に書いたコード
実際に書いたコードはこんな。
①home.ks:イベント判定・呼出し処理
;home.ks
;前略
;事前にhensu.ksを読み込んでおく
;前イベント判定処理
[iscript]
//イベントデータf.eventから、前イベントデータ(bef)のキー名(ev_1,ev_2)を取得して、find()で{}内の条件を満たすイベントを探す
tf.curt_ev = Object.keys(f.event.bef).find(function(key){
return (f.event.bef[key].read == false && //イベントが未読である
f.event.bef[key].isvalid == true && //イベントが有効である
f.event.bef[key].flag() == true //イベント発生条件を満たす
);
})
[endscript]
;中略
;イベント呼出し処理
;前イベントが発生しなければシナリオ呼出しスキップ
[jump target="*select" cond="tf.curt_ev==undefined"]
;前イベントが発生する場合はイベント呼出し
[eval exp="tf.rabel = '*'+f.event.bef[tf.curt_ev].id"]
[call storage="event_bef.ks" target="&tf.rabel"]
*select
;以降のホーム画面処理を記述
②event_bef.ks:イベントシナリオ
;event_bef.ks
;前イベントシナリオファイル
;イベントデータ内のidプロパティと同一のラベル名で管理
;regu_01:1日目発生:プロローグ
*regu_01
[chara_show name="akane" ]
#あかね
今はイベントid:[emb exp="f.event.bef[tf.curt_ev].id"]、イベント名「[emb exp="f.event.bef[tf.curt_ev].name"]」が発生中![p]
シナリオファイル:event_bef.ksの、ラベル:*[emb exp="tf.curt_ev"]を呼び出しているよ![p]
発生条件は「1日目であること」![r]
このイベントにはプロローグ的シナリオが入る予定![p]
それじゃ、ホームに戻るね![p]
#
[chara_hide name="akane"]
[return]
;regu_02:2日目発生:二日目です
*regu_02
[chara_show name="akane" ]
#あかね
今はイベントid[emb exp="f.event.bef[tf.curt_ev].id"]、イベント名「[emb exp="f.event.bef[tf.curt_ev].name"]」が発生中![p]
シナリオファイル:event_bef.ksの、ラベル:*[emb exp="tf.curt_ev"]を呼び出しているよ![p]
発生条件は「2日目であること」![r]
このイベントには2日目的なのゲームシステム説明シナリオが入る予定![p]
それじゃ、ホームに戻るね![p]
#
[chara_hide name="akane"]
[return]
③hensu.ks
;hensu.ks
;イベントデータの他に、判定処理に使いたい変数なども記述し、読み込んでおく
;イベントデータを定義
[iscript]
//befで前イベントの箱を作る
f.event={
bef:{
//イベント1
ev_1:{
id:"regu_01", //id名
type:"must", //イベントタイプ
name:"プロローグ", //イベント名
read:false, //既読フラグ
isvalid:true, //有効フラグ
flag:function(){
return(f.day == 1);
} //個別の発生条件
},
ev_2:{
id:"regu_02",
type:"must",
name:"2日目ですが",
cond:2,
read:false,
isvalid:true,
flag:function(){
return(f.day == 2);
}
}
}
}
[endscript]
2.3 コードの解説①:イベントデータを作る
シナリオイベントのデータはどう作るか。
イベントごとに「ラベル名」「発生条件」「発生済みか否か」「前イベントか後イベントか」などの様々な情報を記録したい……。
ということで、どうしたらいいか色々調べた結果、
オブジェクトでイベントの情報を管理してます。
f.event={
bef:{
//イベント1
ev_1:{
id:"regu_01", //id名
type:"must", //イベントタイプ
name:"プロローグ", //イベント名
read:false, //既読フラグ
isvalid:true, //有効フラグ
flag:function(){
return(f.day == 1);
} //個別の発生条件
},
//中略
}
}
オブジェクトとは何ぞや、という説明は割愛しますが、ざっくりというと情報を色々詰め込める箱みたいなもの(と理解していています)。
今回であれば、
①f.event={}という全てのイベントのデータを入れる大きい箱を作って、
②f.event={}の中にbef:{}という行動前に発生するイベントのみを分類するための仕切りを用意。
③bef:{}の中にev_1:{},ev_2:{},…という個別のイベントの情報(id名やイベント名、発生条件など)を入れた箱がいっぱい入っている。
ようなイメージです。
※後々使う予定のプロパティ(type)も入れているが、省略可能。
今回であれば、ev_1が1日目に発生するイベント、ev_2が2日目に発生するイベント、と設定しています。
以下、イベントデータev_xx:{}内の要点説明
//ev_1から、発生条件flagのみ抜粋
flag:function(){
return(f.day == 1); //return()内に発生条件を記載、複数は||や&&で指定。
} //個別の発生条件
flag:発生条件をfunction()で設定しています。
発生条件をどう設定したらいいか分からなかったので、色々試行錯誤した結果、function()のreturn()内にイベントの発生条件(今回であれば日数f.dayが1日目であること)を記述し、条件満たす場合はtrue、そうでなければfalseを返す、ように設定しました。
このflagをイベント発生処理で使います。
//ev_1から、発生条件flagのみ抜粋
id:"regu_01", //id名
id:idなどの一意な値を設け、シナリオファイルのラベル名と揃える
イベントシナリオファイル(event_bef.ks)の該当イベントのラベル名と一致させて、イベント発生処理で使います。
イベントデータをどう変数として作っていくかにあたって、こちらの記事がとても参考になりました。ありがとうございます。
(ティラノスクリプト 製作テクニックWikiは連想配列の項目の辺り。)
2.4 コードの解説②:発生条件を満たすイベントを取得する
;前イベント判定処理
[iscript]
//イベントデータf.eventから、前イベントデータ(bef)のキー名(ev_1,ev_2)を取得して、find()で{}内の条件を満たすイベントを探す
tf.curt_ev = Object.keys(f.event.bef).find(function(key){
return (f.event.bef[key].read == false && //イベントが未読である
f.event.bef[key].isvalid == true && //イベントが有効である
f.event.bef[key].flag() == true //イベント発生条件を満たす
);
})
[endscript]
発生条件を満たすイベントがあるかを判定する処理
tf.curt_evという変数に、発生条件を満たすイベントのkey(ev_1,ev_2などのこと)を入れる処理です。
①Object.key()という呪文をイベントデータが入っている箱(f.event.bef)に唱えて、箱の中に入っている個々のイベントのラベル名(key、今回であればev_1やev_2)をゲットしています。
②find()という呪文をfunction()という呪文とセットで唱えます。
※find()は結果が一個しか入らないので、複数の結果を入れたい場合はfilter()を使う
function()のreturn()内に「イベントが未読である(read==false)」「イベントが有効である(isvalid==true)」「イベント発生条件を満たす(flag()==true)」などのを記述することで、イベント一個一個が発生条件を満たすかどうかが調べられます。
この処理の結果、tf.curt_evは、1日目はev_1、2日目はev_2、3日目以降はundefinedが取得できるようになっています。
;イベント呼出し処理
;前イベントが発生しなければシナリオ呼出しスキップ
[jump target="*select" cond="tf.curt_ev==undefined"]
;前イベントが発生する場合はイベント呼出し
[eval exp="tf.rabel = '*'+f.event.bef[tf.curt_ev].id"]
[call storage="event_bef.ks" target="&tf.rabel"]
*select
;以降のホーム画面処理を記述
発生するイベントに対応するシナリオファイルを呼出す処理
[call]で発生イベントのシナリオ・ラベルを呼び出す際に、ラベル名の指定にtf.curt_evを使います。(f.event.bef[tf.curt_ev].idがラベル名になる)。
① イベントが発生する場合、tf.curt_evに発生するイベントのkey名(ev_xx)が入っている。
② イベント(ev_xx{}内)のidがイベントシナリオのラベル名と一致するように設定してある。
;イベントシナリオ event_bef.ks 抜粋
;イベントデータ内のidプロパティと同一のラベル名で管理
*regu_01 ;イベント1 ev_1のシナリオ ev_1のidと一致
;中略
[return]
*regu_02 ;イベント2 ev_2のシナリオ ev_2のidと一致
;中略
[return]
;イベントデータの設定
;hensu.ks 抜粋
f.event={
bef:{
//イベント1
ev_1:{
id:"regu_01", //id名 イベントシナリオのラベル名と一致
//中略
},
//イベント2
ev_2:{
id:"regu_02", //id名 イベントシナリオのラベル名と一致
//中略
}
}
}
また、発生する前イベントがない場合はイベントシナリオ呼出しの[call]をスキップするよう、
[call]の前に[jump]を置き、cond="条件"で前イベントが何も発生しない場合(tf.curt_ev=undefined)に実行するように指定します。
上記のシナリオファイルを用意することで、冒頭でお見せしたような処理が実行できるはず……。
3. まとめ・次やること
3.1 やったことの要点まとめ
①イベントデータをオブジェクトで作り、idなどの一意な値を設定、また、発生条件をfunction(){return("条件式")}で設定。
②イベントシナリオのラベル名をイベントデータのidの値などと一致させておく。
③イベント発生条件を満たすかの判定にはObject.key()メソッド、find()メソッド or filter()メソッドという2つの呪文を使う。
3.2 次やること・まだ足りない点
今回の処理だけでは下記のような点が足りないので、これから改造していきます。
①発生条件を満たすイベントが複数あった場合の処理
→find関数で良いかの検討
→イベント発生の優先順位決め
②行動後イベントの実装
③ランダムイベントや行動時の選択肢に依存したイベントの作成
④発生したイベントを既読済みにする
→イベントシナリオ読了後に発生したイベントの既読フラグ(readプロパティ)をtrueにしておく。
本記事が何かしら、制作の足しになれば。
ここもっと良くなるとか、何かありましたらご指摘くださると幸いです。
おまけ。「今回の備忘録で作ったシステムを使って、こういうゲーム作ろうって考えてます!」の紹介です。
この記事が気に入ったらサポートをしてみませんか?