見出し画像

ゲームが完成したので、用いたspineの技術、スロットの表示・非表示で脱衣、setAttachmentで画像変更(責め具強化)、trackでアニメ合成などを語る

喋った:

昨日もエミリアメルマガの感想ラジオで話したんですけど、忘れない様に別録しておこうかなと。
えー、[シン・アンゼリカ ~チーバ洋上に巨大娘あらわる~]って巨大娘防衛SLGの新作を出したんですけど、
https://twitter.com/becomegame/status/1376269721629192193
それに用いたspineの技術ですね。

どうも2年くらい前かな、ツクMVでspineのプラグインを作られた方が現れて、
それは僥倖である、そう思う訳ですが、
ただどうもアニメを再生するだけがspineだと思われてないかと。
最近ではそこからの流れでDL同人エロゲでもUnityにこういうぬるぬるアニメを使おうって人が増えて来てる感じなんで、案外そうでもないのかな? まぁいいや。

そんな訳で今の自分はspineのアニメというより、データ構造に向き合ってるのが楽しい感じ、という……
ただググっても情報出て来るのが大体英語っていう、そこでの咀嚼に一段も二段も布被されてる感じの隔靴掻痒感はありますねえ、
ただでさえ分からないって技術を調べていってるのに、
そこでのやり取りとおぼしきものが英語っていうね。あーっていう。

だからこそ自分のやった技術なりコードなりを残していった方が良いだろうなと、まぁ後々ブログとかにもメモしておこうと思いますが、
っていうのも調べてると自分の昔書いたspineの、全然分かってない記事が出て来てうっとなるんですよ……
それだけ日本語の情報が少ないぞって事なんですが、まぁ最近ことにこういう有益な情報って内輪で籠もりがちですし。
ツィッターでヘタな事書いたら叩かれるもんね、だったらdiscordで仲間内だけで情報交換したら良いじゃんって。
いつもの発言はここにしようと。
そうなると当然Googleには補足されず、最近のGoogle検索しょぼいなぁってのはまぁエンジン自体の劣化、
あと有用な個人サイトの無料サーバーみたいのがinfoseekだのyahooだのサービス停止して、消えたってのもある訳ですが、
その辺りも原因だろうなーと。
この流れは止まらないだろうなぁと思いつつ、まぁブログに残して無駄な抵抗もしてみるかと。
そんな所存でえぇっと、spineの今回使った技術の話、行ってみましょう。

一つ目は「スロットの非表示」で御座いますと。
これが初めはまぁいつもの事ですけど、ググっても出なくて、あ、これからspineやろうとする人は
それっぽい単語を英語でググって、掲示板のやり取りを翻訳してなんでも試していくしか無いですよ、
公式のAPIもあるんで、それをがっつり読み込んで理解できる人なら良いんですけど、まぁ構造が巨大過ぎますからね……

出て来なくて、でもスロット、ボーンやらメッシュ……まぁ画像を後々の画像変形の為に網の目状に分割した処理を施した物をメッシュって言うんですけど、
それらを収めたスロットっていう単位でなんかするんではというアタリは付いていて、
でJavaScriptだとメソッドやプロパティが一望できるんで、
それ見てたらあれ、RGBAあるじゃんと。
ここで他の知識が活きる訳です。

function slotAlpha(s_n , mode){
   let a = (mode == "show") ? 1.0 : 0.0;
   if(Array.isArray(s_n) == true){
       s_n.forEach((s_n_child)=>animation.skeleton.findSlot(s_n_child).color.a = a);
   }else{
       animation.skeleton.findSlot(s_n).color.a = a;
   }    
}

つまりRGBAのA、アルファですね、これを0にしてやったらどうだと。
これで当面上手く行きましたね。
難しいものに当たってる訳ですから、不格好でもとりあえずこの方法でやれば格好は付くな、
そういうやり方が見付かると気持ちが落ち着いて取り組めますよね。
”ちゃんとしたやり方”は追々分かっていくでしょう、とりあえずこれでやれる、って進める。

でも途中でフェードインみたくスーッとスロットが入ってくるアニメなんかを作っちゃって、
これアルファ値を弄った演出なんですね。
するとスロット表示だとアルファ1、表示しないと0って形だと非常に都合が悪いわけです。
表示している、表示しているんだけどその時はアルファは0で、フェードインさせたいよ、ってな訳で。

あ、設定されたアニメを流すんじゃなく、ゲーム中で値を弄るデータ形式をランタイムって言うかと思うんですが、
私の今の所の理解では、アニメさせる為にパーツの角度を変更させたり、大きさを弄ったりするかと思うんですが、
ランタイムはプログラム環境ごとにAPIを持つってだけで、リアルタイムにそれらに干渉するって事とほぼ変わらないと思います。
つまりアニメさせる為にspineエディタ上でやってる事を、ランタイム、プログラムから操作してやる。
spineエディタ上でやれる表現はランタイムでも出来るだろうし、
ちなみにアニメ設定で変更した値はランタイムにも受け継がれます。
つまりアニメの最後にアルファ値が0.5で終わったら、ただ表示してるだけの時もそのパーツは0.5になります。
でそのまま別のアニメ再生させると、そのパーツが0.5っていう半透明なまま再生したりします。
これがゲーム上の処理としては面白い、何か出来そうな所ですが、
同時にアニメとしては初めに原点復帰みたいな初期化処理をしておきたい所でもありますね。

考えるのは、ゲーム中、リアルタイムで何が起きたらプレイヤーは驚くだろうって事ですよね。
ゲームとアニメ(動画)の違いは、つまりキー操作に反応するって所かと思うんですが。
考えるべき辺りはその辺。

話戻りまして、フェードイン、不透明に関わるアニメを入れるので、透明度0で非表示って事にするのはどうも都合が悪い、
やっぱりちゃんと”表示・非表示”って設定をしたいと。
それからググり続けまして、見付けたのがこちらですね。


function hideSlot(s_n){
   if(Array.isArray(s_n) == true){
       s_n.forEach((s_n_child)=>hideSlot(s_n_child));
   }else{
       if(animation.skeleton.findSlot(s_n).getAttachment() != null)mesh_backup.set(s_n , animation.skeleton.findSlot(s_n).getAttachment());
       animation.skeleton.findSlot(s_n).setAttachment(null);
   }
}
function showSlot(s_n){
   if(Array.isArray(s_n) == true){
       s_n.forEach((s_n_child)=>showSlot(s_n_child));
   }else{
       const mesh_name = mesh_backup.get(s_n);
       animation.skeleton.findSlot(s_n).setAttachment(mesh_name);
   }
}

えー、読み上げますと、
animation.skeleton.findSlot(s_n).setAttachment(null);
spineはブラウザのDOM構造みたく……って、分かり易く説明する例えがもう入り組んでますが、
親から子へと順にfindXXXであったりで指定して辿っていく形式みたいなんですが、
Unityとかもこの形式ですっけ?
で、スロットの下にアタッチがあると。このアタッチってのはmeshとか、他にはbouding boxとかがあるって事だと思いますが、
とにかく無くても動くものをオプション的に付けてる、その飾りみたいな事でしょうね。

で非表示なんですが、私が調べた限りではなんかhide()とかshow()みたいなメソッドがあるんだろうなと思うじゃないですか?
無いです。
setAttachment(null);
っていう、これです。
アタッチメントにnull、なんにも無いを入れてやると。そうすると消えますよね、という事です。
そうだよね。
でももうこれ消えちゃってるんで、今度表示する時に困る。
なので消すときにプログラム的に保存しておきましょうって事ですね。

……いやマジかよって思うんですが、spine公式の掲示板でもそういうやり取りしてたんだから仕方無い。
ここら辺りで、ははぁ、これはspineが動画としてやりたい事と、ゲーム上でリアルタイムでやる事に帳尻合わせる為に、
プログラム面でケツ持ちをしてやる必要があるなと、そう悟ったのでした。
つまり案外、spineのデータをこっちで考えて管理すると割とすんなりやれる処理があるかも知れません。

さて皆さんは非表示としてsetAttachment(null);と、
その前に後々表示する為にそのアタッチを保存しておいて、また代入する事を覚えましねと。

これを応用すれば、じゃあ違う物もアタッチできるじゃんという発想が出来る訳ですね。
そんな訳で同一スロット内に複数アタッチを持っていて、切り替えるってのはこれ↓

function changeAttachment(slot_name , attach_name){
   const index = animation.skeleton.findSlotIndex(slot_name);
   const attachment = animation.skeleton.getAttachment(index , attach_name);
   animation.skeleton.findSlot(slot_name).setAttachment(attachment);
}

読み上げると、また同じ様にfindXXXの形で求めて、
setAttachment(attachment);
で変えたい物にセットするだけです。
changeみたいなAPI無いのかよと思ったけど、調べた限りは無かったですね……、ちょっと発想の転換が要る事ではありますけど、
分かればこっちの方がスマートだからかな?

これで本作で言うと、電マ戦車をパワーアップさせると、先端のアタッチメント、
まさしくアタッチメントですね、がつるつる電マからイボイボ電マに変わると思うんですけど、
それでこれをやっております。
パワーアップとかはプログラム側でデータ持ってる事ですね。
それを反映してspineの表示を変えると。

同じ様に、巨大娘アンゼリカの服の着脱も可能になりました↓

function wearChange(lv){ // ダメージ脱衣反映
   if(lv <= 1){lv = 1;girl_wear_lv = lv;}
   if(lv >= 5){lv = 5;girl_wear_lv = lv;}
   for(let i = lv;i > 1;i--){
       if(girl_slot_name.has("lv_" + i) == true)hideSlot(girl_slot_name.get("lv_" + i));
   }
}

これも結局はsetAttachment(null);の、非表示でやってます。
あとは今回、全裸の時のみアンゼリカのアソコを開かせる膣鏡ってのが表示されるかと思うんですが、
着衣の上からだとどう見てもおかしいんで。案外細かい事に気を遣ってるんです。
それも同じ仕組みですね。

function vaginaMirorOpen(){ // 特別な条件下(バイブヘッド開発済み+脱衣MAXならOPEN)
   const is_hidden = animation.skeleton.findSlot("32-膣鏡").getAttachment("32-膣鏡") == null;
   if(girl_wear_lv < 5 || $(".right .param .item:visible[data-type='tank_v']").length == 0){
       if(is_hidden == false)hideSlot("32-膣鏡");
   }else{
       if(is_hidden == true)showSlot("32-膣鏡");
   }
}

ただ唯一不満なのは、例えばオッパイが大きい場合にブラジャーを取ったら乳房がだうんと形を変えてくれたら良いんですが、
そういう、「今は脱衣状態だからアニメが変わる」っていうの、
もちろんプログラム側で状態管理して流すアニメを変えれば出来ますが、
そうじゃなくてもう乳の例えばメッシュだのが形質変化するよと、そういうのは凄く難しそうで出来そうにないですね。

今回は例えばボーン、動かすんじゃなくて差し込み具合みたいな設定をリアルタイムに変更させるとか、
ここでは乳の動きって事なんだからウェイトなのか? ウェイトってランタイムから変更できるんでしょうか?
そこは試せませんでしたね、試せてもかなり精度上げるのは大変そう。
spineエディタ上でウェイト変化させたものを試して、でそれを別にしておく……つまりもう、別のspineランタムとして吐き出して、
バレないよう切り替える事を考えた方がスマートか。
こんな風にして何が何だか明後日の方向を見てた所から、少しだけ目算が立つ、頭が働く様になる訳ですね。

で本作、プレイした人が感動するかはまぁ別ですが、少なくとも技術的には結構分水嶺だったなぁというのは、
6つとか7つとか、バイブ戦車であったり巨大ローターであったりが平行してゲージが溜まっており、
MAXになったものから随時巨大娘アンゼリカを責める訳ですけど、
この時、戦車でズボられながら、クリではローターもぶいんぶいん動く、というのが出来ておりますね。
平行したゲージの貯まり具合では、他の責められ発生も有り得るというか、その場でアニメが生成されるような感覚。
……ただ手の込んだ事をやってる割には、今いちおおっという動きにならないのは、
私にアニメ的なメリハリを強調する知識が無いからなんでしょうね…… そういう事もある。

ともかく、これは「設定された一つのアニメを再生する」ではけして出来ない事です。
具体的にはspineがウリにしてる事として、アニメの合成っていうのがあるんです。
サンプル例としては、歩いてるアニメがある、走ってるアニメがある、
これを続けて再生すると、その歩き→走りへの移り変わりを滑らかにサポートするぞ、って事ですね。
要は同じボーンに対して小さく作用するか、大きく作用するかで歩き、走りをやってる訳ですから。
それはその間、中ほど作用ってのも計算できる訳です。
歩きから走りへと移る過程で起きる、言うなら中ほどの小走りモーション、これをモーション補完と呼んでいる訳ですね。

でそれは一つのアニメからアニメへ、言わば時間と共に移り変わって行くって例なんですけど、
実は同時期、同じ時間にこれとこれのアニメを合成させたいってのも出来ます。
この場合は、パンチモーションと同時に走りモーションがあったとして、同時にやりたいって事ですね。
パンチは腕ボーンに作用、走りは足ボーンに作用しますので一応独立している。
その変化を同時にやったろうと。
これで「走りながらパンチも出来る」っていう、そういうサンプル例もありましたが、
アクション的なアイデアも湧いてくるでしょう?
ちなみに先程の例の、歩きアニメと走りアニメを同じ時間に適応すると、同じ足ボーンに作用してるものですから、
なんだか分かんない感じの合成のされ方がされます。そんな感じです。

で今回私がやりたいのは、「クリを責めつつ、あそこもズボズボ」、色んな器具が責めるって事ですから。
どうもspineの公式掲示板でのやり取りを眺めるに、
spineはtrackってのを複数持っていて、それに合成したいアニメを順に入れて、
後は混ざり具合とか設定して再生すれば、track内にある全てのアニメを考慮したアニメをしてくれるらしい。


let anime_track = [0,0,0,0,0,0,0]; // 平行して起こすアニメの管理
function trapAnime(type , lv){ // 待機ゲージ100%超えた時に起こる処理(責めアニメ)
   if(anime_name.has(type) == true){
       const name = anime_name.get(type);
       if(anime_track.every(v => v == 0)){ // waitアニメーション解除(でないと表示おかしい感じに)
           animation.state.addEmptyAnimation(0, 1.5, 0);
       }
       const index = anime_track.indexOf(0); // 空きトラックを探す
       anime_track[index] = 1;
       const track = animation.state.setAnimation(index ,name , false);
   }
}

そんな訳でとりあえず7つくらい、プログラム側で配列を作って、空いてたら所に入れる、という処理を書いたら
動いてくれて、実を言うと配列持たなくてもspine側で配列をデフォで持ったりもしてるんですけど、
まぁそれはAPIへのアクセスの仕方でなんとでもなりそうだったので。やりました。
これはspineのデータ構造、引いてはAPIに感謝する想いでしたね……
こういう大きな事を、簡単に出来る入口があると凄く楽しいよね。
spineみたいな物に期待するのはこういう所だよね。

timeScaleっていう、アニメ再生速度もありまして、それで感じて来ると動きが速くなる、とかも出来たり。

function waitAnimation(){ // 待機アニメ(感じると速くなる)
   let track;
   if(eroLv() <= 2){
       track = animation.state.setAnimation(0 ,"wait2", true);
       track.timeScale = 1.0 + getHp() / 50;
   }else{
       track = animation.state.setAnimation(0 ,"wait1", true);
       track.timeScale = 1.0 + (getHp()-50) / 50;
   }
   track.listener = {
       complete : (track_entry)=>faceChange()
   }
}

ただ乳首ローターに戦車バイブみたいに、完全に上・下パーツと分かれてる場合は良いんですが、
たまたま同じ部分を責める組み合わせになった時は微妙な動きになるかもっていう、そこら辺りが弱点ですかね。
ただそれをどうするかなぁ、っていうのは閃かなかったです、
見栄えの話ですから、アニメそのもののノウハウ的な解決も必要でしょうねえ……
そういうものがたまたま同時再生された時には、特別なアニメとか用意するのか。
でその特別な用意と他の組み合わせは……とか考えると、見栄えの良い合成動きの為には、無限に構造が複雑になっていきそうな気配……
もう責め器具のアニメとは別に、腕、足、腰の動きとか用意して、そういう肉体部位と責めとを組み合わせてアニメ再生とか?
それこそtrackの1つめに責め器具の動きを、trackの2つめに部位をって決めれば
責め器具に応じた迎え腰、みたいのも出来るかな。
今書いてて思いつきましたが、そうやって考えられる楽しさがありますね……

最後にリアルタイムにアニメ再生されてる物にどう効果音を流すかですけど、
それはアニメ設定に、例えば40フレームめに”イベント”という形で、プログラム側へパッシングが出来るので。
listenerという形でそのイベントを拾えて、その中身、文字列とか数値ですね、それを取り出せるので、
それに対応したSEなり、まぁ他にも色々演出とかも流せばアニメに同期した感じになるでしょうと。
spineのランタイム内に効果音を含むとかは出来ないのでね。

const name = anime_name.get(type);
const track = animation.state.setAnimation(0 ,name, loop);
// *このlistenerはJavaScriptデフォのオブジェクト{}でやってて(インターフェイスが合ってるので)動くけど、本来はListenerオブジェクトを使うっぽい?
track.listener = {
   event : function(track_entry , event){ // イベント(SE用の名前が渡される)
       if(eroLv() >= event.intValue)preloadPlaySound(event.stringValue);
   }
}        

まぁそんな塩梅でしたよと、noteにも載せるので、
多分必要な情報だけ欲しくてぐぐって来た人にはなんだこのダラダラした長文はとなっているでしょうが、
まぁ読んでおくと、結構spineへの理解の形が変わって飲み込みが早くなるかも知れない、そういう事でした。

宣伝ですけどもそんなspineの技術も用いた新作、
[シン・アンゼリカ ~チーバ洋上に巨大娘あらわる~]
https://twitter.com/becomegame/status/1376269721629192193
をよろしくお願いします、ですね。

この記事が気に入ったらサポートをしてみませんか?