こんにちは
何となくご無沙汰しております。きゅっきゅです。(あるいは、しのやんです。)
Alexaでいい感じの楽しいゲームを公開することを目標に、日々精進を続けております。
コタツが恋しい季節になりましたね!(何かをリマインド)
もうすぐ2020年も終わりを迎えますが、Alexaで画面表示を制御する言語であるAPL(Alexa Presentation Language)が早いペースで更新され、声や音が重ね合わせできるAPLA(APL for Audio)も登場、そしてアレクサ姉さんの声色も楽しい表現ができるようになったり(amazon:emotion)で、ゲームに実装できそうな要素がどんどん出てくる一年でした。そんななか、いざ実際に作ろうとなると「プラットフォームで表現できる幅が広がるほど工数が爆増する問題」という名のブラックホールに飲み込まれそうになるのでそこから脱出するためには光速よりも速く(開発を)進めるか、ポータルのように(開発を)リープする道具を生み出すしかないのでありました。(つづく)
それはさておき
本記事はスマートスピーカー Advent Calendar 2020の20日目の記事であります。
APLでhandleTickを用いた短時間ループによるコマンド実行も可能になったので、先ほどの開発工数リープする道具という程ではないですが、往年のコンシューマー機に搭載されている「キャラクタを自由自在に動かしたりパターンアニメーションさせたりするスプライト機能っぽいもの」を作って汎用化できないか、と考えています。
まだ発展途上で「スプライトもどき」でしかありませんが、紹介してみたいと思います。中途半端なネタですいません。
コンセプト
MSXやファミコンなど、(最近だとScratchやSmileBasic?)小さなキャラクタを動かすスプライト機能をなるべく実現しようとしてみる。
- スプライトは高級な(制御が容易な)表示オブジェクトである。
- スプライトはオブジェクトの生成・抹消ができる。
- スプライトは一定のパターンでアニメーションできる。
- スプライトはアニメーションパターンを差し替えできる。
- スプライトは画面中を滑らかに移動・拡縮できる。
結果
先に結果から言うと、今のところは以下のような感じです。
見た目上の動作は、まずまずいけそう
全APLソース+実働サンプルを用意しました。
https://apl.ninja/document/Synoyan/VenR4NUT
(丁度いいサイトを知ったので早速公開しました。@igarashisan_tさん、ありがとうございます!)
このソースでは1体のキャラクタを左右に動かすところまでとしていますが、その後キャラクタをジャンプさせたりマップのあたり判定組み込み程度までは実現できました。発話インテントでexecutecommandディレクティブだけを投げるようにすることで、キャラクタを動かしながら音声による指示を織り交ぜることもできそうです!
アニメーション可能、アニメパターン変更も可能
今までPagerを使ってアニメーション表現を行ってきましたが、現在のAlexaの仕様ではどうやら、隣り合わないページへの切り替えは一瞬ちらついてしまうようなのです。そのため今回は別の方法を模索し、「スプライト機能っぽい」方法でアニメーション表現を行うことが出来ました。(あまり大きい画像には向かないと思います)
移動可能、但し滑らかとは言い難い
AnimateItemというコマンドでコンポーネントを滑らかに移動することができますが、これにはアクションゲームにとって致命的な弱点があります。「画面をタッチするとAnimateItemで指定した最終移動地点にスキップする」という動作です。そのため、今回はhandleTickを「アニメーション更新頻度」として、毎回少しづつtransformプロパティで表示位置をずらす、という実装にしています。こうすることで画面タッチしても動きがスキップしなくはなりますが、見た目の滑らかさがhandleTickに設定したminimumDelayプロパティに依存します。例えば100ミリ秒に設定すると全体的にカクカクした感じになり、10ミリ秒に設定すると表示が間に合わずコマ落ちが多くなります。
コマ落ち状況をよく見ていると、バインド変数の更新など一瞬で終わるコマンドは間引かれずに実行され、画面更新に影響する重い処理は間引かれているようです。スローダウンにならない点はアクションゲーム作成にとってはうれしいポイントかな?と感じました。
transformプロパティは移動(translate)の他にも拡縮(scale)、回転(rotate)、傾斜(skew)とあり、どれも楽しいアニメーション表現に使えそうです。
オブジェクトの生成・抹消はできない
APLはrenderdocumentを投げて画面のレイアウトを決めた後は、動的にコンポーネントを増やしたり構成を変えたりはできません(レイアウトを変える時=画面を書き直す時)。なので、最初に画面上に表示する最大個数のレイアウト(自作のSpriteコンポーネント)を呼び出し、画面の片隅で非表示にしておく必要があります。(今回実例では1キャラしか登場させてないので、複数キャラの場合の後述)
実装
ソースの中から抜粋して解説していきます。
レイアウト
表層はなるべくシンプルに…
MyCharaというAPLレイアウトを作り、オブジェクトに見立てて呼び出しているだけです。
サイズ、初期位置、キャラクタの状態を渡しています。
(後にして思ったんですが正直ここで渡さなくてもいいなと思いました)
{"type":"MyChara","id":"sprite_my","position":"absolute","state":"stay","sx":20,"sy":20,"ix":30,"iy":35}
ここで必要なキャラクタの個数だけ先にレイアウト設置しておかないと、あとでキャラクタを増やしたりできないので、例えば敵を最大5キャラ表示する予定がある場合は以下のようにContainerコンポーネントを使って敵登場枠を複製しておくことになります。
{"type":"Container","width":"100vw","height":"100vh","data":[1,2,3,4,5],"items":[{"type":"Enemy","id":"sprite_enemy_${data}","position":"absolute"}]}
MyCharaレイアウト
ここが一番込み入っている部分なのですが、長いので小分けに解説します。
パラメータ受け取り(parameters)
"parameters":[{"name":"sx","type":"number","default":"${100}"},{"name":"sy","type":"number","default":"${100}"},{"name":"ix","type":"number","default":"${0}"},{"name":"iy","type":"number","default":"${0}"},{"name":"state","default":"stay"}]
レイアウトを呼び出したときのパラメータを受け取ります。それだけです。
変数定義(containerコンポーネント内のbind)
"bind":[{"name":"state","type":"string","value":"${state}"},{"name":"imgUrl","value":"${SpriteData.properties.Usagi.img}"},{"name":"div_x","type":"integer","value":"${SpriteData.properties.Usagi.div_x}"},{"name":"div_y","type":"integer","value":"${SpriteData.properties.Usagi.div_y}"},{"name":"size_x","type":"number","value":"${sx}"},{"name":"size_y","type":"number","value":"${sy}"},{"name":"animePtn","type":"array","value":"${SpriteData.properties.Usagi.ptn}"},{"name":"animeNum","value":"${0}"},{"name":"x","type":"number","value":"${ix}"},{"name":"y","type":"number","value":"${iy}"},{"name":"dx","type":"number","value":"${0}"},{"name":"dy","type":"number","value":"${0}"},{"name":"dir","type":"integer","value":"${1}"},{"name":"cmd","value":""}]
Containerコンポーネントでくるんで、キャラクタの座標や移動量などのあらゆる変数をここで定義します。
Containerでくるむ事は、意外と行き詰った時の解決方法だったりします。思った通りの挙動にならない場合はまずContainerを挟んでみて、そのContainerにIDやパラメータを指定してみましょう。
また、bind変数やSetValueコマンドなどはjsonで特に見づらくなる部分なので、開発時は上記のように適当に行をまとめて見やすくしています。自分自身のメインメモリが乏しいので、ソースコードの見通し度合が開発速度に直結します。。
スプライト呼び出し(Sprite)
Spriteレイアウトを呼び出します。それだけです。
中身の挙動は後程。
{"type":"Sprite","sid":"my","imgUrl":"${imgUrl}","div_x":"${div_x}","div_y":"${div_y}","size_x":"${size_x}","size_y":"${size_y}","pos_x":2,"pos_y":1,"opacity":1}
表示位置変更(transform)
TransformでこのMyChara自身の位置を移動します((0,0)からの絶対座標)。
"transform":[{"translateX":"${ix}vh","translateY":"${iy}vh"}],
ここで横の座標もvh(画面縦幅を100分割した単位)で指定していますがvwの書き間違いではなくて、どんな機種でも1.6:1の比率を保つために、変数に格納する座標単位をvhに統一しています。こうすると座標の演算が楽になり、「いつの間にか文字列になっている」「何故か割り算できない」「NaNって何なん!?」などの悪夢から解放されます。ちなみに"${60vh}"などと記述すると自動的にdp単位に数値変換されますが、機種により値が変わってくるので使いどころが分かれる感じでしょうか。
動作処理(handleTick)
ここがMyCharaレイアウトの肝なのですが、このコードでは「最小100ミリ秒間隔」で無限に回り続けています。「座標変数を更新する」「表示内容を変える」などMyCharaの動きに関わる処理を一手に担っています。
"handleTick":[{"type":"Sequential","minimumDelay":100,"commands":[{"when":"${cmd == 'walk_l'}","type":"Sequential","commands":[{"type":"SetValue","property":"dx","value":"${-1}"},{"type":"SetValue","property":"dir","value":"${-1}"}]},{"when":"${cmd == 'walk_r'}","type":"Sequential","commands":[{"type":"SetValue","property":"dx","value":"${1}"},{"type":"SetValue","property":"dir","value":"${1}"}]},{"when":"${cmd == 'stay'}","type":"SetValue","property":"dx","value":"${0}"},{"type":"SetValue","property":"x","value":"${x + dx}"},{"type":"SetValue","property":"transform","value":[{"translateX":"${x}vh","translateY":"${y}vh"}]},{"type":"Select","commands":[{"when":"${dx != 0}","type":"SetValue","property":"state","value":"${dir==-1?'walk_l':'walk_r'}"},{"type":"SetValue","property":"state","value":"${dir==-1?'stay_l':'stay_r'}"}]},{"type":"SetValue","property":"animeNum","value":"${(animeNum + 1) % animePtn[state].length}"},{"type":"SetValue","componentId":"_sprite_img_my","property":"transform","value":[{"translateX":"${size_x * -animePtn[state][animeNum][0]}vh","translateY":"${size_y * -animePtn[state][animeNum][1]}vh"}]}]}]
・・・実は後述のSpriteレイアウトでは動きにまつわる部分は何もしていません。本来であれば動きの部分を簡単なコマンドで実現するところまでをSprite内部に実装したいのですが、懸念材料がいろいろあって後々の課題という感じです。
SetValueコマンドでtranslateプロパティを変更している箇所が2つありますが、
- 1回目:キャラクタ本体の移動
- 2回目:表示するキャラクタの差替
を行っています。
特に2回目の仕組みについては次項で説明します。
Spriteレイアウト
今回のネタの中心部分です。
動きに関わる部分はMyCharaレイアウトが行っているのでじゃぁここでは何やってるの、というところですが、ここでは
「スプライトアニメーションを実現するためのレイアウト」
を組んでいます。
"Sprite":{"parameters":["sid","imgUrl","div_x","div_y","size_x","size_y","pos_x","pos_y"],"items":[{"type":"Container","id":"_sprite_${sid}","width":"${size_x}vh","height":"${size_y}vh","items":[{"type":"Image","id":"_sprite_img_${sid}","width":"${div_x * size_x}vh","height":"${div_y * size_y}vh","source":"${imgUrl}","position":"absolute","scale":"best-fill","transform":[{"translateX":"${size_x * -pos_x}vh","translateY":"${size_y * -pos_y}vh"}]}]}]}
図を描きました。
使うキャラクタがずらっと並べられた1枚絵があり、これをImageコンポーネントで描画しています。が、その親であるContainerコンポーネントは縦横1キャラ分の大きさしかありません。そうすると左上のキャラクタ1つ分しか表示されませんが、ここでImageコンポーネントをtransformプロパティで縦横に移動してContainerコンポーネントの枠内に収まる部分を変えることにより、表示させたいキャラクタを変更することが出来ます。実際のキャラクタ表示位置は、前述のMyCharaレイアウト内でこのSpriteレイアウトごとtransformプロパティで移動させます。
本来スプライト処理はメモリの1か所に置いた画像データを表示したい場所に転送するという当時(昔々の)CPUやVRAMにとって大変エコなロジックとして重宝されましたが、APLには展開した画像リソースを複数のコンポーネントで共有する仕組みは無いため、もし10体同じキャラクタを出そうとしたら10枚の同じ画像のImageコンポーネントをレイアウトする必要があり、多くのキャラクタを表示するには向かなそうです。この方法でキャラクタアニメーションする場合は一連のアニメーションを構成するのに必要な最低限の並びにしておいて、例えばモーションを変える時は画像を差し替えるとか、複数の画像を重ね合わせておいてopacityで表示非表示を制御する、等の工夫が必要になるかもしれません。
課題
handleTickをまとめたい
キャラクタの数だけ100ミリ秒のhandleTickを回すロジックなので、EchoShow5だと10体も回したらもう限界でした。(Alexa側でいい感じに割り込み処理をまとめてくれたり…とかは無かった)、最終的にhandleTickはどこか一か所にまとめたいと考えています。
あとがき
あくまで休日の趣味として、APLがバージョンアップするたびにマクロ言語特有の「できるできないの境界線を探って試して試して試し尽くした末に一見境界線だと思ってた部分を押し広げられた時の達成感」をまるでナンプレ問題集を解くようにじっくりねっちり楽しんだりしてますが、客観的にみると、やっぱり結構マゾいなって思います。もっと流行れ、このマゾい楽しさ。
でももう「猫のスロット」を公開してから半年経つし、成果物をそろそろ…ということで、来年春ごろに新しいスキル公開できるよう頑張ってます。「うさぎの迷路」も課金スキルにしてみない?とamazonさんからお声が掛かったので、あの強欲なうさぎをどうパワーアップさせるかも検討中です!