元々紅蜘蛛シリーズはAIRNovelを使っていました。本職がweb系なのでFlashの扱いに慣れていたこと、モバイル環境も含めたマルチプラットフォームで出したかったことが理由です。
しかしAdobeAIRが結局足枷になり、再始動にあたってゲームエンジンを変更する必要が出てきました。 最初は宴も検討したのですが、Excel管理の煩雑さに加え、原則1行1処理であるため「テキスト表示の途中で複数の音を順番に鳴らす」といった挙動の実装が難しく見送りました。
代わりに採用したのがティラノスクリプトです。jqueryベースなのでほとんど学習コストなしにカスタマイズできるのが大きかったです。後はノベルゲームコレクションというプラットフォームも魅力的だと思ったのですが、紅蜘蛛シリーズリマスター版のような劇画調グラフィックのゲームは敬遠されるようで、そちらでのプレイ数はごく少ないです。(音声ファイルなどが膨大でオンラインでプレイするには重すぎるというのも一つ原因としてあると思います)
私は基本的になんでもマクロ化してしまうタイプです。 例えば台詞を話しているこの画面のスクリプトファイルはこんな感じです。
ティラノスクリプトにspeakerというタグはありません。 以下のようにマクロで組んでいます。
わざわざマクロを組んだのは、
あたりが理由です。
紅蜘蛛シリーズの登場人物で一番多いのは華系なので基本的に名前は漢字表記になりますが、覚えやすくする為にはルビが必須になります。しかしカタカナの名前なんかはルビがないのでルビありの場合と同じ高さで表示すると少し違和感が出ます。台詞本文中で使っている[ruby2]というタグもルビと本文の間隔を調節する為に作ったプラグインです。主人公の越児は暗殺組織に属する工作員なので無数の偽名を持っていて、相手ごとに名前を変えて話します。なので表示名は分かりやすく台詞単位で変更したい。
また、紅蜘蛛にはいろんな国の登場人物がいて、いろんな言語で話します。この場面ではタイ人と話す為に石蒜は英語をチョイスしているのですが、例えばこのタイ人が、石蒜に会話の内容を悟られたくない時はタイ語で話したりするわけです。ボイスは全て日本語ですが──なので猶更、今何語で話しているのかは一目で分かるようにしておきたい。
このマクロでは言語ごとにCSSのClass指定までできるようになっていますが、言語が変わるごとに色やフォントを変えられるようにしたかったからです。(実際は視認性の問題で変えてはいませんが)
また、同じ人物の台詞が続く場合は話者の画像を再描画しないようにして少しでも画面がちらつかなくしたり、回想内で話されている台詞の場合はsepiaパラメーターをつけて話者画像にcssでsepiaフィルタをかける(しかし回想スチルを背景に、現在のキャラクタが話している時は話者画像はsepiaにしたくない)という機能もつけています。
こういうことを台詞単位でコントロールしたくなると、これだけこてこてにマクロで実装することが必須になります。ちなみにplayseで音声ファイルを再生する時にis_voiceというパラメーターが追加されてますけど、これは元々ティラノで定義されているis_voiceというステータスを利用して再生終了を待つようにする為です。(その為にkag.tag_audio.jsも書き換えてます)
なのでノーコードで書ける代わりに仕様がガチガチに固まっているエンジンではダメで、自分でカスタマイズがバリバリできるエンジンでないといけない訳です。
Ren'Pyもカスタマイズ性は高いですが、いろいろな国の言語(特にアジア系の文字)を表示する処理はやはりwebkit系がダントツに強いというのと、例えばこういう風にSMSでのやりとりを部分的にモーダルで表示したいという時なんかに慣れ親しんだjsとcssでサクッと書けてしまうのは大きいです。 ただ、ティラノスクリプトの難点はマクロファイルを書き換えたりするとそれ以前のセーブデータが使えなくなってしまったりすることです。 しかしこれは解決法が見つかったので次回の開発ログで書こうと思います。
紅蜘蛛2/Red Spider2リマスター版(Win/Mac/Android/iOSで広告・課金一切なしの完全非営利無料配信)のリリースは来年1月末を目指しています。 興味を持った方は是非ウィッシュリストにご登録ください。
また、昨年リリースしたシリーズ1作目のリマスター版「紅蜘蛛/Red Spider」リマスター版や一昨年リリースした紅蜘蛛外伝:暗花もフルスチル化作品ですのでお気軽に遊んでみてください。いずれもWin/Mac/Android/iOSで完全非営利無料配信中です。
[speaker name="yuet" image="chara/yuet/lycoris4.png" rb="石蒜" rt="シースァン" voice="rsl2_police_yuet_004" lang="english"]「金の問題じゃない。私は[ruby2 rb="張雷" rt="ジャン"]の知り合いでね。あなたが信頼できる人物かどうかを確認したいんだ」[pr][macro name="speaker"][nolog][position layer="message0" left=0 top=450 width=1280 height=270 page=fore visible=true opacity="255" frame="mesbg.png"][position layer="message0" page=fore margint="70" marginl="270" marginr="260" marginb="30"][stopse buf="5"][font color="0xffffff"][if exp="f.waitflag==true"][wait time="1000"][eval exp="f.waitflag=false"][endif][iscript]f.character=mp.name;if(!mp.tname){ f.tcharacter = mp.tname }else{ f.tcharacter = mp.name}if(!mp.lang){ f.language="cantonese"; }else{ f.language=mp.lang;}if(sf.speakerimg == "OFF"){mp.image="chara/noimg.png";}$(".chara_name_area + .chara_name_area").remove();
var langMap = {"cantonese":"廣東語","thinking":"思考","thai":"タイ語","english":"英語","northkorean":"文化語","southkorean":"韓国語","mandarin":"普通話","joseoneo":"中國朝鮮語","malay":"マレー語","cantonese_cn":"粵語","thinking_cn":"心想","thai_cn":"泰語","english_cn":"英語","mandarin_cn":"普通話","cantonese_en":"Cantonese","thinking_en":"thinking","thai_en":"Thai","english_en":"English","mandarin_en":"Mandarin"};var allLangClasses = Object.keys(langMap).join(" ");var $messageInner = $(".message0_fore > .message_inner");var $charaNameArea = $(".chara_name_area");$messageInner.removeClass(allLangClasses);$charaNameArea.removeClass(allLangClasses);tf.bllangclass = ' ' + f.language;tf.lang = "";if (langMap.hasOwnProperty(f.language)) { $messageInner.addClass(f.language); $charaNameArea.addClass(f.language); if (!f.language.includes('cantonese')) { tf.lang = "(" + langMap[f.language] + ")"; }}
if(mp.rt != null){ TYRANO.kag.stat.charas[mp.name].jname = '<span class=\'nameborder\'><span class=\'ruby\'><ruby class=\'name\'><rb>'+mp.rb+'</rb><rt>'+mp.rt+'</rt></ruby></span><span class=\'lang\'>'+tf.lang+'</span></span>'; tf.logname='<span class=\'backlog_line'+tf.bllangclass+'\'><span class=\'chara_name\'><span class=\'ruby\'><ruby class=\'name\'><rb>'+mp.rb+'</rb><rt>'+mp.rt+'</rt></ruby></span><span class=\'lang\'>'+tf.lang+'</span></span>'; }else{ TYRANO.kag.stat.charas[mp.name].jname = '<span class=\'nameborder\'><span class=\'name\'>'+mp.rb+'</span><span class=\'lang\'>'+tf.lang+'</span></span>'; tf.logname='<span class=\'backlog_line'+tf.bllangclass+'\'><span class=\'chara_name\'><span class=\'name\'>'+mp.rb+'</span><span class=\'lang\'>'+tf.lang+'</span></span>'; }[endscript]@layopt layer=message0 visible=true@layopt layer=message1 visible=true[chara_config ptext="chara_name_area"][if exp="mp.image != null"][eval exp="f.character = mp.name"][eval exp="f.tcharacter = mp.name"][eval exp="f.language = mp.lang"][if exp="(f.bfcharacter != f.character)"][chara_hide_all layer="message1" time="0"][chara_mod name="%name" storage="%image" time="0"][chara_ptext name="%name"][chara_show name="%name" layer="message1" left="-1" top="420" time="10" zindex="101"][if exp="f.issepia == true"][iscript] $(".tyrano_chara").addClass("sepia");[endscript][else][iscript] $(".tyrano_chara").removeClass("sepia");[endscript][endif][elsif exp="(f.bftcharacter != f.tcharacter)"][chara_ptext name="%name"][endif][endif][current layer="message0"][if exp="$.isElectron()"][iscript]if(!$("#root_layer_system").hasClass('iselectron')){ $("#root_layer_system").addClass("iselectron"); }tf.is_auto = tyrano.plugin.kag.stat.is_auto;[endscript][endif][if exp="mp.voice != null"][eval exp="tf.voicefile='voice/'+mp.voice+'.mp3'"][seopt volume="&sf.current_voice_vol" buf="5" effect=true][if exp="sf.current_voice_vol"][delay speed="200"][playse storage=&tf.voicefile buf="5" is_voice="true" chara_name="%name"][resetdelay][endif][endif][eval exp="f.bfcharacter = f.character"][eval exp="f.bftcharacter = f.tcharacter"][eval exp="f.bflanguage = f.language"][endnolog][if exp="tf.logname != ''"][pushlog text=&(tf.logname)][endif][eval exp="tf.logname=null"][endmacro]フォローすると、開発ログや新作ゲームの公開が通知されます