ティラノスクリプトでショップ画面を作る!ライブコーディング会の振り返り
こんなんやりました。
配信の内容が全てなんですが、アーカイブ見るのがめんどくさい人向けに配信中で説明不足だったところや流してしまったところなんかを重点的に解説していきたいと思います。
実際に作ったもの
ショップ機能自体は
最低限これだけできれば事足りますが、今回はそれに加えて
こんなかんじの機能を実装しました。
使用したツール・プラグインなど
エディタ:Visual Studio Code
天下のMicrosoftが出してるコードエディタです。なんと無料
ティラノスクリプト用の最強プラグインがあるのでティラノスクリプト書くならこれ一択、メモ帳を使うのは金輪際やめよう
VScode用拡張機能:TyranoScript syntax
さっき言ってたVisual Studio Codeのティラノスクリプト用最強プラグイン
VScodeの拡張機能タブで「tyrano」と検索すれば出てくるから入れていない民は今すぐ入れよう
デザインツール:Figma
UIは基本こいつで作ってる
デスクトップアプリもあるけどブラウザ上でも使えるので端末またいで作業可能、最近日本語対応したらしい
ティラノスクリプト用プラグイン:ボタンタグ機能補助プラグイン
[button]タグをいい感じに機能補助してくれるプラグイン、今回はボタンを特定条件で動作させないために使った
ティラノスクリプト用プラグイン:glinkをfixボタンにできるプラグイン
名前のまま、アイテム選択ボタンを画像ボタンでなく文字ボタンにしたかったので使った
ティラノスクリプト用プラグイン:デバッグ支援プラグイン
こいつがいなきゃ始まらねえ、ティラノスクリプトでのゲーム制作を5000兆倍加速させるプラグイン
まあ今回はホットキーリロードしか使ってないが
データ構成とか
ショップ画面の作成ということで、購入可能なアイテムの名前や値段等のデータを設定する必要があります。
データ自体はこんなかんじ↓でスプレッドシートで管理しています。
で、このデータをCSVで出力したものが↓です。
こいつを↓のコードで読み込みまして
TYRANO.kag.variable.sf.itemMaster = {}
$.get("./data/others/csv/item.csv", function (data) {
const br = data.split("\r\n")
const header = br[0].split(",")
br.forEach((val, idx) => {
if (idx > 0) {
const elm = val.split(",")
TYRANO.kag.variable.sf.itemMaster[elm[0]] = {}
TYRANO.kag.stat.f.haveItem[elm[0]] = 0
elm.forEach((elmVal, idx) => {
if (idx > 0) {
TYRANO.kag.variable.sf.itemMaster[elm[0]][header[idx]] = elmVal
}
})
}
})
})
すると、こんなかんじ↓のデータ構成で「sf.itemMaster」という変数に格納されます。
例えば、ティラノスクリプト上からアイテム名と値段にアクセスするにはこう書きます。
sf.itemMaster.item_01.name or sf.itemMaster["item_01"]name
sf.itemMaster.item_01.price or sf.itemMaster["item_01"]price
こういったデータ構造をオブジェクトといったりしますが、詳しくは↓のnoteを読もう!
画面のレイアウト
では本題、ショップ画面の実装に入りましょう。
まずはFigma上の配置を参考に、愚直に画像やボタンを配置していきます。
ですがずれることもままあるので、そういうときは開発者ツールから要素をいじって調整した数値を当て込んでいきます。
そしたら次に、購入可能なアイテム一覧のボタンを作っていきます。
とりあえずglinkを置いて
glink用のCSSを書いて
するとこうなります↓
今回、ひとつのglinkのテキストとして「アイテム名」と「値段」のふたつを表示していますが、その仕組みはこんな感じです。
[glink fix="true" color="glink_blur" name="item_01" text="&'<span>' + sf.itemMaster.item_01.name + '</span><span>' + sf.itemMaster.item_01.price + '</span>'" x="1050" y="130" exp="tf.item = 'item_01'" target="*item_click" cond="tf.item !== 'item_01'"]
横長なので見にくいですが、「text」パラメータに「 sf.itemMaster.item_01.name」と「sf.itemMaster.item_01.price」、つまりアイテム名と値段をそれぞれHTMLの<span>タグで囲って記述しています。
すると画面上ではこんな感じのタグ構成になります↓
こういう構成にすると、CSSで<div>タグ、つまりglinkのスタイルとして次のように記述することでふたつの<span>タグを両端揃えにして配置できます。
.glink_blur{
/* 中略 */
display: flex;
justify-content: space-between;
/* 中略 */
}
詳しくは「CSS flex」とかでググってください。
アイテムをクリックで説明表示
アイテムをクリックすると説明が表示されるようにします。
これはまあそんなに変なことしてないんであんまり解説することもないんですが
*item_click
[clearfix name="item_click" ]
[free layer="0" name="item_click" ]
[iscript ]
tf.detail_01 = sf.itemMaster[tf.item].outline
tf.detail_02 = sf.itemMaster[tf.item].detail
[endscript ]
[ptext layer="0" text="&tf.detail_01" name="item_detail_1" size="32" x="1070" y="590" width="680" overwrite="true" color="#111856" ]
[ptext layer="0" text="&tf.detail_02" name="item_detail_2" size="24" x="1070" y="695" width="680" overwrite="true" color="rgba(17, 24, 86, 0.66)" ]
[return ]
先ほど作ったglinkのtargetとして「*item_click」を指定しています。
で、今回アイテム説明が2種類あるので、それぞれ[ptext]タグで表示しています。
[ptext]のoverwriteパラメータを「true」にすることで内容の上書き可能にしています。
購入数の増減
選択したアイテムの購入数を変更できるようにします。
先ほどの「*item_click」ラベルに記述を追加します。
*item_click
[clearfix name="item_click" ]
[free layer="0" name="item_click" ]
[iscript ]
tf.detail_01 = sf.itemMaster[tf.item].outline
tf.detail_02 = sf.itemMaster[tf.item].detail
tf.qt = 1
[endscript ]
[call target="*item_list" ]
[call target="*item_detail" ]
[ptext name="item_click" text="購入:" layer="0" x="1350" y="796" size="32" color="#111856" ]
[ptext name="item_click" text="合計:" layer="0" x="1350" y="873" size="32" color="#111856" ]
[image name="item_click" layer="0" storage="market/label_qt.png" folder="image" x="1560" y="790"]
[call target="*qt_count" ]
[button name="item_click" fix="true" graphic="market/button_qt_minus_default.png" enterimg="market/button_qt_minus_hover.png" exp="tf.qt--" target="*qt_change" x="1500" y="790"]
[button name="item_click" fix="true" graphic="market/button_qt_plus_default.png" enterimg="market/button_qt_plus_hover.png" exp="tf.qt++" target="*qt_change" x="1680" y="790"]
[return ]
*qt_count
[ptext name="item_qt" layer="0" text="&tf.qt" x="1560" y="796" align="center" size="32" color="#111856" width="120" overwrite="true" ]
[ptext name="item_price" layer="0" text="&sf.itemMaster[tf.item].price * tf.qt" x="1440" y="873" align="right" size="32" color="#111856" width="300" overwrite="true" ]
[return ]
「tf.qt」が購入数を格納する変数です。初期値として「1」を代入しています。
「*qt_count」ラベルで購入数と合計金額の表示を行っています。
「*item_click」ラベルの[return]タグ前の2つの[button]タグが購入数の増減ボタンです。
[button name="item_click" fix="true" graphic="market/button_qt_minus_default.png" enterimg="market/button_qt_minus_hover.png" exp="tf.qt--" target="*qt_change" x="1500" y="790"]
[button name="item_click" fix="true" graphic="market/button_qt_plus_default.png" enterimg="market/button_qt_plus_hover.png" exp="tf.qt++" target="*qt_change" x="1680" y="790"]
それぞれtargetパラメータの指定は同じです。
expパラメータで、マイナスボタンなら「tf.qt--」、プラスボタンなら「tf.qt++」としています。
これによって、マイナス/プラスのボタンを押した時点で「tf.qt」、つまり購入数を1ずつ増減します。
そしてジャンプ先の「*qt_change」ラベルで次の処理を行っています。
*qt_change
[iscript ]
if(tf.qt < 1){
tf.qt = 1
}else if(tf.qt > 99){
tf.qt = 99
}
[endscript ]
[call target="*qt_count" ]
[return ]
「tf.qt」が1未満、つまり0なら1を代入し、99より多い、つまり100なら99を代入しています。
これは何をしているのかと言うと、購入数の上限と加減を設定しています。
これによって、購入数を「1~99」の範囲に限定することができます。
で、「*qt_count」ラベルの呼び出し、つまり購入数と合計金額の再表示を行い、[return]タグで戻っています。
購入処理
購入数の指定と合計金額の計算ができたので、実際の購入処理を作っていきます。
*buy
[if exp="f.money < sf.itemMaster[tf.item].price * tf.qt"]
[dialog text="所持金が足りません" type="alert" target="*buy_ng" ]
[s]
[endif ]
[iscript ]
f.haveItem[tf.item] += tf.qt
f.money -= sf.itemMaster[tf.item].price * tf.qt
tf.item = ""
[endscript ]
[clearstack stack="call" ]
[jump target="*after"]
[return ]
*buy_ng
[return ]
まず、合計金額と現在の所持金の比較を行い、所持金が合計金額未満であれば購入できないようにします。
[if exp="f.money < sf.itemMaster[tf.item].price * tf.qt"]
[dialog text="所持金が足りません" type="alert" target="*buy_ng" ]
[s]
[endif ]
*buy_ng
[return ]
ここです。
やってることは単純に、購入不可の場合はダイアログを出してreturnさせています。
つまり何もしないってこと
購入処理の本体はこちら↓
[iscript ]
f.haveItem[tf.item] += tf.qt
f.money -= sf.itemMaster[tf.item].price * tf.qt
tf.item = ""
[endscript ]
[clearstack stack="call" ]
[jump target="*after"]
[s ]
「f.haveItem」という変数がアイテムの所持数を格納する変数です。
この変数はこんな感じ↓の構成になっています。
この変数に購入数を加算し、合計金額を所持金から減算すれば購入処理は完了です。
最後に[clearstack]した上で「*after」ラベルにジャンプしていますが、「*after」ラベルとは
*start
[mask time="300" ]
[bg storage="../image/market/bg.png" time="0" ]
[image layer="0" storage="market/label_place.png" x="0" y="30" folder="image" ]
[image layer="0" storage="market/label_money.png" x="1370" y="10" folder="image" ]
[image layer="0" storage="market/base_item_list.png" x="1050" y="120" folder="image" ]
[image layer="0" storage="market/base_item_detail.png" x="1050" y="570" folder="image" ]
[image layer="0" storage="market/base_item_buy.png" x="1050" y="760" folder="image" ]
*after
[free layer="0" name="have_money" ]
[clearfix name="back_home" ]
[clearfix name="glink_blur" ]
[clearfix name="buy" ]
[ptext layer="0" name="have_money" text="&f.money" x="1370" y="26" align="right" color="#111856" overwrite="true" size="40" width="500" ]
[button name="back_home,button_blur" fix="true" graphic="market/button_home_default.png" enterimg="market/button_home_hover.png" x="-10" y="120" target="*back_home" ]
[call target="*item_list" ]
[button name="buy,button_blur" fix="true" graphic="market/button_buy_default.png" enterimg="market/button_buy_hover.png" x="1370" y="960" target="*buy" ]
[button_ex name="buy,button_blur" disable="!tf.item" disableimg="market/button_buy_disable.png"]
[call target="*item_detail" ]
[mask_off time="300" ]
[s]
ここです。
要するに、強制的に画面上の表示を更新しています。
この辺はもうちょいやりようがある気がするのであとあと修正するかもしれません。
アイテムボタンにマウスホバーで説明表示
アイテムボタンクリックではなく、マウスホバーでも説明を表示させたいので処理を追加します。
簡単な仕様はこんな感じです↓
マウスホバーでの処理実行は、タグの機能ではできないのでJS(jQuery)を書いていきます
[iscript ]
$(".glink_blur").on("mouseenter", function(){
Object.keys(sf.itemMaster).forEach(val => {
if($(this).hasClass(val)){
$(".item_detail_1").text(sf.itemMaster[val].outline)
$(".item_detail_2").text(sf.itemMaster[val].detail)
}
})
})
$(".glink_blur").on("mouseleave", function(){
if(tf.item){
$(".item_detail_1").text(sf.itemMaster[tf.item].outline)
$(".item_detail_2").text(sf.itemMaster[tf.item].detail)
}else{
$(".item_detail_1").text("")
$(".item_detail_2").text("")
}
})
[endscript ]
このコードを、glinkタグのを通過したあと(超重要)に記述します。
glinkが画面上に存在していないと、この処理は実行されないからです。
$(".glink_blur").on("mouseenter", function(){
Object.keys(sf.itemMaster).forEach(val => {
if($(this).hasClass(val)){
$(".item_detail_1").text(sf.itemMaster[val].outline)
$(".item_detail_2").text(sf.itemMaster[val].detail)
}
})
})
こっちがマウスホバー時の処理です。
あらかじめglinkにアイテムのIDをnameとしてつけています。ティラノスクリプトのタグで指定できるnameパラメータは、HTML上ではclassとして記述されています。
なので、glinkがアイテムIDと同名のclassを持っているかを判定して、持っていた場合は[ptext]で配置しておいた説明文表示エリアのテキストを書き換えています。
$(".glink_blur").on("mouseleave", function(){
if(tf.item){
$(".item_detail_1").text(sf.itemMaster[tf.item].outline)
$(".item_detail_2").text(sf.itemMaster[tf.item].detail)
}else{
$(".item_detail_1").text("")
$(".item_detail_2").text("")
}
})
こっちはマウスアウト時の処理です。
「tf.item」という変数に選択したアイテムIDが格納されています。
tf.itemになんらかの値が入っていた場合、つまり何らかのアイテムを選択中の場合はそのアイテムの説明文を表示します。
そうでない場合は説明分に空白を代入、つまり表示を消去します。
選択中のアイテムをハイライト表示
こうなってくると、「どのアイテムを選択中か」が視覚的に分かるようにしておきたいです。
というわけでglinkの表示処理を追加します。
[glink fix="true" color="glink_blur" name="item_01" text="&'<span>' + sf.itemMaster.item_01.name + '</span><span>' + sf.itemMaster.item_01.price + '</span>'" x="1050" y="130" exp="tf.item = 'item_01'" target="*item_click" cond="tf.item !== 'item_01'"]
[glink fix="true" color="glink_blur" name="item_02" text="&'<span>' + sf.itemMaster.item_02.name + '</span><span>' + sf.itemMaster.item_02.price + '</span>'" x="1050" y="190" exp="tf.item = 'item_02'" target="*item_click" cond="tf.item !== 'item_02'"]
[glink fix="true" color="glink_blur" name="item_03" text="&'<span>' + sf.itemMaster.item_03.name + '</span><span>' + sf.itemMaster.item_03.price + '</span>'" x="1050" y="250" exp="tf.item = 'item_03'" target="*item_click" cond="tf.item !== 'item_03'"]
[glink fix="true" color="glink_blur" name="item_04" text="&'<span>' + sf.itemMaster.item_04.name + '</span><span>' + sf.itemMaster.item_04.price + '</span>'" x="1050" y="310" exp="tf.item = 'item_04'" target="*item_click" cond="tf.item !== 'item_04'"]
[glink fix="true" color="glink_blur" name="item_05" text="&'<span>' + sf.itemMaster.item_05.name + '</span><span>' + sf.itemMaster.item_05.price + '</span>'" x="1050" y="370" exp="tf.item = 'item_05'" target="*item_click" cond="tf.item !== 'item_05'"]
[glink fix="true" color="glink_blur" name="item_06" text="&'<span>' + sf.itemMaster.item_06.name + '</span><span>' + sf.itemMaster.item_06.price + '</span>'" x="1050" y="430" exp="tf.item = 'item_06'" target="*item_click" cond="tf.item !== 'item_06'"]
[glink fix="true" color="glink_blur" name="item_07" text="&'<span>' + sf.itemMaster.item_07.name + '</span><span>' + sf.itemMaster.item_07.price + '</span>'" x="1050" y="490" exp="tf.item = 'item_07'" target="*item_click" cond="tf.item !== 'item_07'"]
[glink fix="true" color="glink_blur" name="item_01,glink_active" text="&'<span>' + sf.itemMaster.item_01.name + '</span><span>' + sf.itemMaster.item_01.price + '</span>'" x="1050" y="130" exp="tf.item = ''" target="*item_list" cond="tf.item === 'item_01'"]
[glink fix="true" color="glink_blur" name="item_02,glink_active" text="&'<span>' + sf.itemMaster.item_02.name + '</span><span>' + sf.itemMaster.item_02.price + '</span>'" x="1050" y="190" exp="tf.item = ''" target="*item_list" cond="tf.item === 'item_02'"]
[glink fix="true" color="glink_blur" name="item_03,glink_active" text="&'<span>' + sf.itemMaster.item_03.name + '</span><span>' + sf.itemMaster.item_03.price + '</span>'" x="1050" y="250" exp="tf.item = ''" target="*item_list" cond="tf.item === 'item_03'"]
[glink fix="true" color="glink_blur" name="item_04,glink_active" text="&'<span>' + sf.itemMaster.item_04.name + '</span><span>' + sf.itemMaster.item_04.price + '</span>'" x="1050" y="310" exp="tf.item = ''" target="*item_list" cond="tf.item === 'item_04'"]
[glink fix="true" color="glink_blur" name="item_05,glink_active" text="&'<span>' + sf.itemMaster.item_05.name + '</span><span>' + sf.itemMaster.item_05.price + '</span>'" x="1050" y="370" exp="tf.item = ''" target="*item_list" cond="tf.item === 'item_05'"]
[glink fix="true" color="glink_blur" name="item_06,glink_active" text="&'<span>' + sf.itemMaster.item_06.name + '</span><span>' + sf.itemMaster.item_06.price + '</span>'" x="1050" y="430" exp="tf.item = ''" target="*item_list" cond="tf.item === 'item_06'"]
[glink fix="true" color="glink_blur" name="item_07,glink_active" text="&'<span>' + sf.itemMaster.item_07.name + '</span><span>' + sf.itemMaster.item_07.price + '</span>'" x="1050" y="490" exp="tf.item = ''" target="*item_list" cond="tf.item === 'item_07'"]
通常のglinkに加えて、選択中アイテムのglinkを配置します。
タグの最後の方にあるcondパラメータとexpパラメータを見ると分かるかと思いますが、
//通常のglink
exp="tf.item = 'item_01'" cond="tf.item !== 'item_01'"
//選択中アイテムのglink
exp="tf.item = ''" cond="tf.item === 'item_01'"
というように、逆の条件判定となっています。
こうすることで、アイテムの選択/非選択状態での表示を切り替えています。
また、選択中アイテムのglinkでは、クリック時に「tf.item」に空白を代入しています。
これにより、選択中アイテムをクリックで選択解除をすることができます。
アイテムを選択するまで購入ボタンを押せなくする
この処理、別にアイテムを選択したときに購入ボタンを表示させるとかでもいいんですが、ショップ画面だし最初から購入ボタンは見えていたほうが良いよねということで
これは自作プラグインの機能をそのまま使っています。
[button name="buy,button_blur" fix="true" graphic="market/button_buy_default.png" enterimg="market/button_buy_hover.png" x="1370" y="960" target="*buy" ]
[button_ex name="buy,button_blur" disable="!tf.item" disableimg="market/button_buy_disable.png"]
[button_ex]タグに「disable="!tf.item」と指定することで、「tf.item」が空の状態の場合はボタンが非活性となり、dsableimgパラメータに指定した画像が表示されます。
詳しくはプラグインのreadmeを読んでもろて
ライブコーディングここまで
というわけで、およそ2時間でざっくりショップ画面を作ってみました。
細かい処理とかデバッグとかは全然なので、実際のゲームではもうちょっと手を入れたものになるかと思いますが、とりあえず動くものはできたので良かったです。
他人がどうやってゲームを作っているかは私も気になるので、この記事を読んで「自分もやってみるか~」と思う方がいれば幸いです。
配信じゃなくても、noteとかに制作過程を書いてもらえればウヒウヒ言いながら読むので…
反省点など
案の定というか、私が独り言を言いながらキーボードを叩く会になってしまったので猛省しています…
そもそも喋りが上手くないというのもありますが、今回YouTubeライブでの開催だったことで、コメントしにくいと感じる方もいたのかな~と
あと、やっぱり実況と解説が必要だなと思ったので、次にライブコーディングやるときはこれらの要員を確保できた時になると思います。そんな日は来ない
というわけで、ティラノスクリプトでショップ画面を作る!ライブコーディング会の記録でした。
チャンネル登録、高評価よろしくお願いします!(は?
実際に作った.ksファイルはこちら↓
サポートをしていただけると私がたいへんよろこびます。 ちなみに欲しい物リストはこちら→https://www.amazon.jp/hz/wishlist/ls/2DBRPE55L3SQC?ref_=wl_share