- 投稿日:2019-07-24T19:23:44+09:00
リズムゲームの設計思想【初級】
はじめに
本職がリズムゲームのツール屋なので、今回はリズムゲームでの設計思想について記します。
恐らくリズムゲームの構想を練ったことのある人でないと理解できない所が少々あります。適宜読み飛ばしてください。
今回はかなり内容を絞って初級としました。リズムゲームのさわり部分です。
ごんびぃーがリズムゲームを作るとき・語るときに何を考えているか、思想が分かると思います。これは宣伝ですが、株式会社GONBEEE-projectはリズムゲーム制作・ツール販売屋です。
xR業界内では唯一の専門制作会社です(当社調べ)。リズムゲームのご用命は株ごんへ。リズムは人の感覚
そもそもリズムゲームとは「音楽のリズムに合わせてボタンを押したりコントローラを振るゲーム」ではありません。
リズムはあくまでも人間の認識上での表現に過ぎません。
(最近はコンピュータもリズムを理解するとか、そういう話ではありません。)ゲーム設計の観点からするとリズムゲームの本質は
「事前にデザインされた浮動小数点数と、プレイヤーの入力した浮動小数点数の差が如何に小さいか」にあります。
(本当は浮動小数点数は計算ズレが発生するので理想的ではありません。話の簡略化です。)リズムゲームはプレイヤーに対して
「何を以てジャストタイミングとするか」のルールを提示し、
プレイヤーはゲームの示したルールに則り
「ゲームの示したジャストタイミングと如何にズレずに反応できたか」を競うことになります。
ゲームとプレイヤーの間に”リズム”という概念は存在しません。
これが”人間の認識上での表現に過ぎない”とした根拠です。
もちろんゲーム側も”リズム”の存在を意識し、
プレイヤーがジャストタイミングを認識しやすいような譜面を提供したほうがいいです。
ただ、これも正確には「譜面デザイナーとプレイヤー」のやり取りになるので、ゲームシステムは関わりません。ゲームセンターで稼働中の筐体や、CytusやDeemoなどスマートフォンでプレイ可能なリズムゲームの大半は
スコア計算をタイミングで行っています。
前述の通りリズムゲームの本質は「ゲームの示したタイミングとプレイヤーの入力のズレが如何に小さいか」であるので、
タイミングベースでのスコア計算は必然です。
特にCytusなどRayark産のリズムゲームはこの志向が強く、
100%クリアを目指そうと思うとタイミングズレの許容幅が非常に厳しく、かなり苦しい戦いになるでしょう。とは言うものの
xR系では事情が大きく変わります。
現状のVRゲームなどでは基本的に入力に対する反作用が発生しないので、
プレイヤーが自身の入力タイミングを認識できないという問題点があります。
ゲームセンター筐体やスマートフォンがタイミング制でスコア計算するのは
筐体のボタンや画面を触ったときの物理的なフィードバックがあり、
人間が”触った”と感じる瞬間が認識しやすいためです。
反作用がないということは触った瞬間を認識できず、理不尽な判定になり得ます。アクションゲーム化した例
VRリズムゲームで話題になったBeatSaberやSEIYAはスコア計算方式がタイミング制ではなく、
極論リズムがわからなくても(無音状態でも)十分高いスコアを出すことが可能です。
本質的にはこれらはアクションゲームであり、リズムに乗らなくてもプレイができます。
特にSEIYAはVR Rhythm Actionと明示しています。
恐らくVRゲームプレイヤー層と音ゲーマー層は大きく違い、
タイミングでの戦いを重要視していないのではないかと分析しています。タイミング制を堅持した例
同じくVRリズムゲームで話題になったAirtoneはスコア計算がタイミング制です。
ただこちらは判定タイミングが非常に優しく、
コントローラを振るだけの黄色ノーツはどう振っても大体パーフェクト判定が出ます。
逆にトリガーを引く緑ノーツは”トリガー”という反作用があるため判定が厳し目に設定されています。
VRゲームの特徴を受け入れながら、従来のリズムゲームの性質を維持した素晴らしいバランスを保っています。
恐らくこちらは従来の音ゲーマー層をターゲットとしたゲームだったのではないかと思っています。”ジャスト”のいろいろ
前述の通り、リズムゲームはプレイヤーに対して
「何を以てジャストタイミングとするか」のルールを提示しなければなりません。
この項目では既に稼働中・リリース済みのリズムゲームが
どのような表現を以てジャストタイミングとしたかを、カテゴリ分けしていくつか紹介します。
リズムゲームは歴史をたどるとファミコンディスクシステムのオトッキーまで遡りますが、
現代のような”純粋にゲームの示したノーツを取る”システムとなったのは1997年に登場したbeatmania(初代)かと思われます。
当時はまだ5鍵+皿でした。(どうでもいいですが、生まれが同じ年です)
歴史を遡ると1996年にパラッパラッパーというゲームが存在します。
ただ、パラッパラッパーは示されたノーツに加え自分でアレンジを加えて入力をしないといけないため、
現代基準でのリズムゲームからは除外しました。参考として示しておきます。
思いの外長くなってしまったのでバッサリスキップでも構いません。レーン落下式
恐らくリズムゲームで最も使われている表現です。
画面内の一定の場所に判定ライン・場所が存在し、そこに向かってノーツが飛んでいきます。
プレイヤーはノーツと判定ラインが重なった瞬間に入力を行います。
リズムゲーム初見プレイヤーでもジャストがわかりやすく、とっつきやすいデザインです。
反面ズレの許容幅が厳しい物が多く、極めようとすると時間を要します。判定ラインが固定で、ラインに向かってノーツが移動する方式を落下式と呼称します。
・beatmania 2DX
・pop'n music
・太鼓の達人
・チュウニズム
・Deemo
その他多数挙げ始めるとキリがありません。
beatmania 2DXの画面です。画面左に7+1のレーンが表示されており、上からノーツが降ってきて
赤いラインに触れた瞬間がジャストとされています。
現代でプレイできるリズムゲームの親といっても過言ではありません。
太鼓の達人です。画面左にある円形の部分にノーツが重なった瞬間がジャストです。
こちらも判定ラインが固定で、ラインに向かってノーツが移動しているので落下式と捉えることが可能です。
ジャストはわかりやすいですが、叩くべき太鼓の場所が色によって区別される(=場所の違い等でひと目でわからない)ので
個人的には初見難度高めに含まれるかなと考えています。
Deemoです。画面下部にある黒いラインにノーツが重なった瞬間がジャストです。
こちらは画面上部ではなく3次元空間での奥からの移動ですが、人間の認識に過ぎないので関係ありません。
判定ラインが固定でノーツが移動してくる方式なので落下式としました。3パターン紹介しましたが、どれもジャストのタイミングが掴みやすく
初心者が入りやすいゲームであると言えるでしょう。
そのゲームを見たことがないプレイヤーでもすぐに馴染めるというのは大きなアドバンテージです。
リズムゲーム制作初心者はまず落下式のデザインから初めることをおすすめします。落下式亜種
次に落下式亜種を紹介します。こちらは捉え方によっては落下式として認識できるゲームです。
判定ラインが固定とは限らない、もしくは判定ラインが直線ではない物を落下式亜種と呼称します。
・GROOVE COASTER
・Cytus
・データカードダス アイカツ!
・maimai
・ラブライブ! スクールアイドルフェスティバル
その他こちらは落下式と比較して数が少ないかなという印象を受けます。
ちゃんと統計を取ったわけではないので断言はできません。GROOVE COASTER, Cytus, アイカツ!は判定ラインが固定ではないものです。
GROOVE COASTERです。プレイヤーとなるアバターが移動してノーツに近づいて行くタイプのゲームです。
曲によってカメラアングルやノーツの並んでいるラインがねじ曲がったりする特徴的なゲームで、
個人的な意見ですがリズムゲーム初心者にはあまりおすすめできないです。
また、譜面デザイナー側もノーツのタイミングとカメラアングル、ラインの曲げ方など様々な点を考えなければなりません。
かなりのデザインコストがかかっているはずです。
アバターとノーツが重なった瞬間がジャストです。本質的には落下式です。
画質悪めですがCytusです。こちらは判定ラインが横直線ですが、判定ラインが上下に動きます。
ラインとノーツの中心が重なった瞬間がジャストです。こちらもリズムゲーム初心者にはおすすめしにくいゲームです。
ラインと重なった瞬間がジャストという本質には変わりないため落下式としましたが、かなり特殊です。
アイカツも立派にリズムゲームです。初代アイカツはサービス終了しました。現在はアイカツフレンズ!です。
方向性はGROOVE COASTERに近く、判定ラインが一定の線に沿って動き、ラインとノーツが重なった瞬間がジャストです。
太鼓の達人とグルコスが合体したようなゲームです。難易度高めです。
女児向けと思って侮ってると痛い目見るほどしっかりリズムゲームです。
2018年段階ゲームセンターで稼働しているゲームの内、
アイカツのジャスト判定幅17msは、DDRの15msに次いで上位2位に食い込みます。
17msを競わせるゲームは女児向けとは言えないと思います。アイカツおじさんが湧くのも必然でした(?)。maimai, ラブライブは判定ラインは固定なものの、直線ではない形状をしているものです。
まじえんじぇー
それぞれmaimaiとスクフェスです。
maimaiは真円、スクフェスは半円の判定ラインを持ちます。
両方とも判定ラインが固定でラインとノーツが重なった瞬間がジャストなので、落下式と捉えることができます。
maimaiは画面内をスライドさせるノーツがありますが、スライド始点は落下式です。
やはりこれらも本質的には落下式です。VRゲームにおける落下式
現在SteamやOculusストアでリリースされているVRリズムゲームの内、大半が本質的に落下式と言えます。
上述のゲームと違うのは3次元空間であることだけで、
一定の場所にノーツが来たら入力を行うことに変わりはありません。
ただこちらは空間であるため判定ラインは明示されていません。”この辺りかな?”という曖昧なラインです。
曖昧なライン故、ジャストタイミングを示すことがほぼ不可能です。
なのでVRリズムゲームではタイミングによってスコアを計算するゲームが少なくなりました。・VR RHYTHM ACTION SEIYA
・Airtone
・BeatSaber
・polyfuru feat. MIYA KIMINO / ポリフる feat. キミノミヤ
その他
我らがSEIYAです。スコア計算方式はノーツを叩いたとき前後のコントローラの加速度です。
要するにパンチ力勝負なので、リズムゲームというよりはアクションゲームに近いと思われます。
ジャストタイミングはポジションリセットからプレイヤー前方10cmから15cmほどかと思われますが、正確には不明です。
空間奥からプレイヤーに向かってノーツが飛んできます。
プレイヤー(判定ライン)が固定で、目の前に来たらノーツを叩くので、落下式と言えるでしょう。
Airtoneです。スコア計算方式はVRリズムゲームでは珍しくタイミング制です。
ただ前述の通り判定が非常に優しいです。先行入力が全てジャスト扱いになるので50msくらいのズレだったら許容されます。
遅れた場合はこの限りではありません。容赦なく減点されます。
ジャストタイミングは空間内に白点で明示されていますが、プレイ中に意識して見ることはほとんどないと思います。
このゲームではプレイヤーが空を飛びノーツに向かっていく形になります。GROOVE COASTERと似ています。
判定ラインにノーツが重なった瞬間がジャストなので、落下式として考えられます。
BeatSaberです。スコア計算方式は斬る速度、角度、中心を通ったかです。完全にアクションゲームになりました。
ジャストタイミングはSEIYAと同じくプレイヤー前方50cmほどかと思われますが、SEIYAよりもわかりにくいです。
空間奥からプレイヤーに向かってノーツが飛んできます。
ノーツを迎え撃つ形になるので、これも落下式になるでしょう。
ミヤちゃんです。ポリフるです。リリースおめでとうございます。
スコア計算方式は公式から明かされていませんが、
プレイした感触ではノーツの中心が高得点、外側の白いラインがスコア減少という感じでした。
こちらはサイリウムでなぞるノーツやポーズノーツが多めに含まれます。
方向性としてはmaimaiのスライドノーツが近いですが、ポーズは前例がありません。
ただ主に飛んでくるノーツはプレイヤー前方で叩くため、落下式となります。
ところで、ロック曲が一向に解除されないんだが拡張式
落下式とは大きく異なるジャスト演出方法です。
こちらは基本的に判定ラインというものが存在しません。
あまり数は多くありません。
例外もありますが、何らかの形で枠が示されて、その枠が埋まり切る瞬間にプレイヤーは入力を行います。・Jubeat
・ミュージックガンガン!
・スクストトゥインクルメロディーズ
その他
拡張式で最も有名といえるJubeatです。
16のパネルがあり、それぞれ内側の画面がマーカーで埋まりきった瞬間がジャストです。
このゲームではプレイヤーが好きなマーカーを選ぶことができます。
全てのマーカーが内側から枠を埋めるものでもなく、外側から内側に閉まっていくシャッターマーカーもあります。
初心者でもわかりやすいゲームな上、ジャスト判定幅も33msと優しく入門ゲームにはおすすめです。
ミュージックガンガン!です。今はもう殆ど筐体を見ることはできません。
名古屋にはあるらしい・・・。
こちらは枠が埋まり切った瞬間に銃を撃つゲームで、画面奥から飛んでくる敵を撃つシューティングゲームとなっています。
ノーツ側が移動していますが、ジャストの表現は枠によるものなので拡張式としました。
スクメロは2018年9月13日にサービスを終了してしまったスマホリズムゲームです。つらいです
現在は保存版としてオフラインでも動くバージョンが配信されています。つらくないです
こちらも拡張式の一部ではありますが、ノーツが固定で枠が縮まっていきます。
相対的にはノーツが拡張していると言えないこともないので、拡張式とします。現状ゲームセンターで稼働しているリズムゲームで拡張式は極端に少ないです。
というかJubeat以外見たことがないような気がします。
ミューガンといいスクメロといい、拡張式はどんどん撤退してしまうんでしょうか。VRゲームにおける拡張式
ただでさえ総数の少ない拡張式ですが、VRリズムゲームでもごく少数拡張式が存在します。
(そもそもVRリズムゲームの総数が少ないのは別問題)
前述の通り拡張式の特徴は判定ラインに依存しない方法でジャストを表現できるため、
判定ラインを明示できないVRゲームと相性がいいはずです。・Star Bullet Refrain
・Audica
VR拡張式リズムゲームを語るのにこのゲームは抜かせません。今思えば酷い出来ですね。
2017年11月のxRTechTokyo#8で初登場したこのゲームは、
当時VRリズムゲームで初の試みである銃を使うデザインに挑戦しました。
上述の通り判定ラインを明示できないVRゲームは拡張式と相性がよく、
スタバレはスコア計算方式がタイミング制でした。
空間手前から奥に向かってノーツが飛び、青い枠が埋まり切る瞬間がジャストです。
トリガーによる反作用もあるため、プレイヤーが撃ったタイミングを認識しやすい作りになっていました。
ジャストの判定幅も±20msとゲームセンター筐体並に厳しく、総じて既存音ゲーマー向きのゲームとして制作されていました。
しかし、2018年2月に開催された学生限定VRコンテンツ展示会での出展を最後に一切の続報が流れません。
Refrain.Coreもあることですし、今真面目に作ったらもっといいゲームになると思います。
パクりAudicaです。ガッツリやられました。
一通りAudicaに殺されたので何も言うことはありません。
拡張式と落下式の複合でいいゲームだと思います。絶対数が少ないVRリズムゲームでも拡張式は特に少ないです。
誰か早くAudicaを倒せるゲームを作ってください。仇をシステム"は"シンプル
やっと本題です。でも大事な前振りでした。
ここまでで記した通り、リズムゲームとして成り立つ要素は
如何にしてジャストを表現するか・プレイヤーに受け入れてもらうかです。
恐らくリズムゲームの設計で一番注意しなくてはならない部分です。しかし、表現方法が確立してからいざゲームを作ろうという段階に入るとジャスト表現の方式は正直あまり関係ありません。
リズムゲームで必要なデータは究極的には楽曲のどの再生時間にノーツを置くかという浮動小数点数だけです。
もちろんそれだけでは豊かな演出はできませんが、
ゲームの示したタイミングに従って入力するだけのゲームであれば作れると思います。
譜面データを記録した外部ファイルをロードしてきて、そのデータ通りにノーツを設置し演出するだけでリズムゲームは成立します。
ではリズムゲーム制作で何が一番難しいのか?
それは譜面デザインです。
GROOVE COASTERの所でちらっと書きましたが、リズムゲームは譜面デザインにとんでもないコストがかかります。
例外はありません。
ゲームシステム自体は比較的シンプルにまとめることができます。
ランタイムで譜面データをロードしてきて、そのデータ通りにノーツや演出を表示すればいいんです。
面白いリズムゲーム(複雑・特徴的等)はその分だけ譜面データが肥大化していきます。
そのため、そのゲームの譜面データを出力するための専用譜面エディタが必要になります。
ゲーム本体、それに食わせる譜面データ、譜面を作る専用エディタの3方面に広げないとリズムゲームというのは成立しません。
もちろんゲーム本体が軽く済むとは限りませんが、譜面デザインのコストよりは軽いはずです。
また譜面のデザインは主にセンスによるもので、エンジニアとしての腕の良さは関わってきません。
冒頭にちらっと書きましたがリズムゲームは「譜面デザイナーとプレイヤー」のやり取りであって、
譜面デザイナーは極めて人間的な感性・センスが必要になります。
これもまたリズムゲーム制作の難易度を上げている要因かと思われます。次回予告
一通りリズムゲームが難しいという主張をして絶望を与えたところで、
今回はおしまいです。
次回はここから「少しでもリズムゲーム制作を簡単にするために」をテーマに、
リズムゲーム制作は楽しいものだよという主張をしていきたいと考えています。(説得力皆無)あとがき
書くだけ書いて推敲後回しにしてたらケロっと忘れて公開してませんでした。
今回は本当にリズムゲームの概要というか触りの部分だけでしたが、
普段ごんびぃーが何を考えているのか少し伝わったと思います。また何か追記してほしい情報や質問があれば、
Qiitaのコメント欄か @GONBEEE_project までリプDMでご連絡ください。
次回をお楽しみに!
- 投稿日:2019-07-24T17:44:03+09:00
コピぺでOK! DoTweenでウィンドウUIなどの出し入れ
DoTweenを使用したwindowの出し入れ
コード
windowController.csusing UnityEngine; using DG.Tweening;//忘れずに public class windowController : MonoBehaviour { public Vector2 BasePosition; public Vector2 TargetPosition; public float MoveSpeed; private bool WindowTrigger = false; //入力検知 private bool Base = true;//trueなら動いていない private float nowPosition; public GameObject Target; private RectTransform Window; void Start() { Target = GameObject.Find("Stamp"); Window = Target.GetComponent<RectTransform>(); } void FixedUpdate() { WindowMove(); } private void WindowMove() { nowPosition = Window.localPosition.x; if (Base && WindowTrigger) //今ウィンドウが動いていないとき押されたら { Window.DOLocalMove(TargetPosition,MoveSpeed).SetEase(Ease.OutQuart); WindowTrigger = false;//入力を無効にし Base = false;//下についたと知らせる } else if (Base == false && WindowTrigger) //移動先かつボタンが押されたら { Window.DOLocalMove(BasePosition,MoveSpeed).SetEase(Ease.OutQuart); WindowTrigger = false;//入力を無効にし Base = true;// } } public void StampButton() //ウィンドウを出すためのボタンなどで呼ぶ { WindowTrigger = true; } }簡単な解説
適当なスクリプトにアタッチし、
上から、始まる位置・移動終了位置・移動時間・移動するオブジェクトを指定する。注意
あくまでも自分のメモ用です。
バージョンによっては動かないこともある、、、かも!
2DのUIなどを移動することを想定しているコードです。
Vector2をVector3に変更するなどすれば、
3Dにも対応できると思います。あと、たぶんまとめられるところがいくつかあると思います、、
参考程度にお願いします。
- 投稿日:2019-07-24T13:58:02+09:00
【Unity】範囲外のオブジェクトを端っこで表示させ続けるミニマップ
概要
重要な建造物はミニマップやレーダー上で範囲外になっても端っこに表示させ続けるゲームは数多く存在します、多分。
今回はそんなミニマップをUnity上で作ってみようと思います。ちなみに完成するとこんな感じになります。
今回は範囲外であることがわかるように半透明で表示させています。
下準備
1. とりあえず動くキャラクターと適当にオブジェクトを配置する
- とりあえず地面と動かせるものを準備する。困ったら豆腐
- 地面は
Ground、動かせるものはPlayerとかわかりやすいようにしておく- 後はミニマップに表示するためのオブジェクトも適当に配置
- 名前は
Objectとか適当に- ついでにカメラもそれっぽく追尾させる
2. ミニマップのアイコンを用意する
それっぽいアイコンを作ります。
面倒くさかったらProjectからCreate->Spritesを使ってもいいです。
色はSpriteRendererで変えることもできるのでとりあえず白のみの画像で。
3. ミニマップのフレームとマスク画像を用意する
今回は円形で作るので円形のフレームとマスク画像を準備します。即席で作ったので使いたい方はどうぞ。
![]()
ミニマップを表示する
まずはシンプルな円形ミニマップを作ります。
1. ミニマップ用のカメラを作成する
- 空のゲームオブジェクト
Minimapを作るMinimapの子になるようにミニマップ用のカメラMinimap Cameraを追加
- Rotationを(90, 0, 0)にしてカメラを真下に向かせる
- プレイヤーを中心にカメラを動かすスクリプトを作って
Minimap Cameraにアタッチする
- InspectorからプレイヤーのTransformを設定するのを忘れずに
- ProjectionをOrthographicにしてSizeを適宜調整する
MinimapCamera.cspublic class Minimap : MonoBehaviour { [SerializeField] private Player player; void Update () { var pos = player.transform.position; pos.y = transform.position.y; transform.position = pos; } }
Minimap Cameraのカメラプレビューでプレイヤーを追尾していることを確認します。
2. ミニマップを円形で表示する
ミニマップカメラで映しているものを画面上に表示します。
- ProjectからRender Textureを作成、ファイル名は
MinimapRenderにする- Minimap Cameraを選択して、CameraのTarget Textureに作成した
MinimapRenderを設定するこれでRender TextureにMinimap Cameraの映しているものを表示することができます。
次にミニマップを円形にして表示します。
- フレーム用のImageを作成する
- 名前は
Minimap FrameにしてSource Imageにフレーム画像を設定するMinimap Frameの子にマスク画像のImageを作成する
- 名前は
Minimap MaskにしてSource Imageにマスク画像を設定する- 設定したらMaskをアタッチして
Show Mask Graphicのチェックを外すMinimap Maskの子にRaw Imageを作成する
- 名前は'Minimap Render'にしてTextureに
MinimapRenderを設定する- Rect TransformのWidthとHeightは適宜調整する
3. ミニマップにアイコンを表示する
現在のミニマップは上から撮ったものをそのまま映しているだけです。そのため、地面とアイコンのみを表示します。
- 地面用とアイコン用のレイヤーを追加する
- それぞれ
Ground,MinimapにするMain CameraのCulling MaskからMinimapを除外するMinimap CameraのCullin MaskをGroundとMinimapにするこれでメインカメラは
Minimap以外、ミニマップカメラはGroundとMinimapのレイヤーのみを映すことができます。
アイコンのレイヤーをMinimapに設定するとミニマップ上にアイコンを表示することができます。早速プレイヤーのアイコンをミニマップに表示させてみます。
- ヒエラルキーのPlayerにミニマップ用のアイコンをD&Dする
- 名前はMinimapIconとかにリネームする
- Rotationを(90, 0, 0)にする
- レイヤーをMinimapにするこれでミニマップ上にプレイヤーを示すアイコンを表示することができます。
Objectに対しても同じことを行い、オブジェクトのアイコンも表示させてみるとこんな感じ。
ミニマップ範囲外のオブジェクトを半透明で表示する
アイコン表示を制御するためのスクリプトを作成して
ObjectのMinimapIconにアタッチします。
minimapCameraはInspector上から設定していますが、ミニマップカメラにタグつけてGameObject.FindGameObjectWithTagを使ってもいいと思います。
表示範囲は何も考えずにカメラのorthographicSizeに設定しておきます(orthographicSizeはカメラ縦幅の半分の大きさ)。MinimapIcon.csusing System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(SpriteRenderer))] public class MinimapIcon : MonoBehaviour { [SerializeField] private Camera minimapCamera; // ミニマップ用カメラ [SerializeField] private Transform iconTarget; // アイコンに対応するオブジェクト(建造物等) [SerializeField] private float rangeRadiusOffset = 1.0f; // 表示範囲のオフセット // 必要なコンポーネント private SpriteRenderer spriteRenderer; private float minimapRangeRadius; // ミニマップの表示範囲 private float defaultPosY; // アイコンのデフォルトY座標 const float normalAlpha = 1.0f; // 範囲内のアルファ値 const float outRangeAlpha = 0.5f; // 範囲外のアルファ値 private void Start () { minimapRangeRadius = minimapCamera.orthographicSize; spriteRenderer = gameObject.GetComponent<SpriteRenderer>(); defaultPosY = transform.position.y; } private void Update () { } }1.オブジェクトがミニマップ範囲内か確認する
ミニマップ用カメラとオブジェクトの距離を求めて、その距離がミニマップ範囲内かチェックします。
今回はVector3.Distanceで距離を求めます。平面上の距離を求める必要があるので、Y座標は統一しておきます。MinimapIcon.csprivate bool CheckInsideMap() { var cameraPos = minimapCamera.transform.position; var targetPos = iconTarget.position; // 直線距離で判定するため、yは0扱いにする cameraPos.y = targetPos.y = 0; return Vector3.Distance(cameraPos, targetPos) <= minimapRangeRadius - rangeRadiusOffset; }2.ミニマップ範囲内のオブジェクトを表示
ミニマップ範囲内の場合は
SpriteRendererのアルファ値を1にしてそのまま表示すればOKです。MinimapIcon.csprivate void Update () { DispIcon(); } private void DispIcon() { // アイコンを表示する座標 var iconPos = new Vector3(iconTarget.position.x, defaultPosY, iconTarget.position.z); // ミニマップ範囲内の場合はそのまま表示する if (CheckInsideMap()) { spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, normalAlpha); transform.position = iconPos; return; } }3.ミニマップ範囲外のオブジェクトを表示
ミニマップ端にアイコンを表示するためにベクトルを計算する必要がありますが、Vector3.ClampMagnitudeを使うことで簡単に求めることができます。
このAPIはベクトルと最大距離を指定すると最大距離までに制限したベクトルを返してくれるすごいやつです。これを紹介するためにこの記事を書いてると言っても過言ではない。
方向ベクトルをVector3.ClampMagnitudeで制限することでミニマップ端までのベクトルを簡単に求めることができます。
半透明にするにはSpriteRendererのアルファ値を0.5くらいにすればOKです。MinimapIcon.cs// アイコンを半透明にする spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, outRangeAlpha); // カメラとアイコンの位置から方向ベクトルを求める var centerPos = new Vector3(minimapCamera.transform.position.x, defaultPosY, minimapCamera.transform.position.z); var offset = iconPos - centerPos; // 指定距離で制限した方向ベクトルを求めてアイコン位置を設定する transform.position = centerPos + Vector3.ClampMagnitude(offset, minimapRangeRadius - rangeRadiusOffset);4.コード全文
MinimapIcon.csusing System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(SpriteRenderer))] public class MinimapIcon : MonoBehaviour { [SerializeField] private Camera minimapCamera; // ミニマップ用カメラ [SerializeField] private Transform iconTarget; // アイコンに対応するオブジェクト(建造物等) [SerializeField] private float rangeRadiusOffset = 1.0f; // 表示範囲のオフセット // 必要なコンポーネント private SpriteRenderer spriteRenderer; private float minimapRangeRadius; // ミニマップの表示範囲 private float defaultPosY; // アイコンのデフォルトY座標 const float normalAlpha = 1.0f; // 範囲内のアルファ値 const float outRangeAlpha = 0.5f; // 範囲外のアルファ値 private void Start () { minimapRangeRadius = minimapCamera.orthographicSize; spriteRenderer = gameObject.GetComponent<SpriteRenderer>(); defaultPosY = transform.position.y; } private void Update () { DispIcon(); } /// <summary> /// アイコン表示を更新する /// </summary> private void DispIcon() { // アイコンを表示する座標 var iconPos = new Vector3(iconTarget.position.x, defaultPosY, iconTarget.position.z); // ミニマップ範囲内の場合はそのまま表示する if (CheckInsideMap()) { spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, normalAlpha); transform.position = iconPos; return; } // マップ範囲外の場合、ミニマップ端までのベクトルを求めて半透明で表示する spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, outRangeAlpha); var centerPos = new Vector3(minimapCamera.transform.position.x, defaultPosY, minimapCamera.transform.position.z); var offset = iconPos - centerPos; transform.position = centerPos + Vector3.ClampMagnitude(offset, minimapRangeRadius - rangeRadiusOffset); } /// <summary> /// オブジェクトがミニマップ範囲内にあるか確認する /// </summary> /// <returns>ミニマップ範囲内の場合、trueを返す</returns> private bool CheckInsideMap() { var cameraPos = minimapCamera.transform.position; var targetPos = iconTarget.position; // 直線距離で判定するため、yは0扱いにする cameraPos.y = targetPos.y = 0; return Vector3.Distance(cameraPos, targetPos) <= minimapRangeRadius - rangeRadiusOffset; } }5.動作確認
スクリプトのInspectorから
minimapCamera,iconTarget,Range Radius Offsetを設定しつつ、Objectのアイコン表示が期待通りに動作すればOKです。今回はRange Radius Offsetを3.5にしてみました。
おしまい
というわけで割とお手軽に作ることができました。
作り始めたときは「画面端へのベクトル計算とかどうするねん」って思っていたのですが、Vector3.ClampMagnitudeを使えば面倒な計算とかしなくても実装することができました。すごく簡単に作ったので「ここおかしくね?」みたいなのあったらぜひコメントください。
参考
- 投稿日:2019-07-24T11:30:38+09:00
【Unity】Observerパターンでクラス間の通知の仕組みを構築する
はじめに
今回はコードメインなのでObserverパターンについての詳細な説明はWikiを参照ください。
ざっと説明すると、1対多の通知の仕組みですね。
例えばシーンのロードが終わったタイミングで実行したい処理が複数のクラスに点在している場合に関数1つ呼び出して通知するだけで、
通知を受け取ったそれら全てが勝手に動く、というような事を実現出来るのがこのObserverパターンです。ではこのObserverパターンをコードに起こしていきます。
通知を出すObservable
こちらは通知する側を担うObservableというやつです。
他にもSubjectと言われたりもするそうで。
ジェネリックなのは通知用の関数の引数に好きな引数が渡せる方が実装の幅が広がるからです。Observable.csusing System.Collections.Generic; // 複数のオブジェクト(Observer)に通知を送りたいオブジェクトは // このObservableを、通知を受け取りたいオブジェクトはIObserverを継承し使う public class Observable<T> { // 通知対象 protected List<IObserver<T>> m_Observers= new List<IObserver<T>>(); // 通知対象へ追加 public void AddObserver(IObserver<T> observer) { m_Observers.Add(observer); } // 通知対象から除外 public void RemoveObserver(IObserver<T> observer) { m_Observers.Remove(observer); } // 通知送信 public virtual void NotifyObservers(T notifyObject) { foreach (var observer in m_Observers) observer.OnNotify(this,notifyObject); } }通知を受け取るObservar
こちらは通知を受け取る側のObservarというやつです。
MonoBehaviourを継承した上でObservarの機能も付加できるようインターフェースにしています。IObserver.cs// Observableから通知を受け取りたいクラスにこのインターフェースを継承し、 // ObservableにAddObserverで自身を通知対象に登録する // (通知対象から外してもらう場合はRemoveObserver) public interface IObserver<T> { void OnNotify(Observable<T> observer,T notifyObject); }MonoBehaviour版Observable
このMonoBehaviourをObservableとして使いたい!という時に上記のObservableクラスのままだとパッと使えないのでMonoBehaviour版のObservableクラスを用意しておきました。
ObservableMonoBehaviour.csusing System.Collections; using System.Collections.Generic; using UnityEngine; // 多重継承できないのでメンバにObservableを持つMonoBehaviourを用意 public class ObservableMonoBehaviour<T> : MonoBehaviour { protected Observable<T> m_Observable = new Observable<T>(); public void ChengeObservable(Observable<T> observable) { if (m_Observable.GetType() == observable.GetType()) return; // 型が同じ = 挙動は同じ m_Observable = observable; } // 通知対象へ追加 public void AddObserver(IObserver<T> observer) { m_Observable.AddObserver(observer); } // 通知対象から除外 public void RemoveObserver(IObserver<T> observer) { m_Observable.RemoveObserver(observer); } // 通知送信 public void NotifyObservers(T notifyObject) { m_Observable.NotifyObservers(notifyObject); } }MonoBehaviour版ObservableのSingletonバージョン
ついでにシングルトンのMonoBehaviour版Observableもあると便利なので用意しておきます。
SingletonMonoBehaviour.cs// シングルトンクラス public class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour { public bool m_DontDestoryOnLoad = true; private static T s_Instance = null; public static T Instance { get { if(s_Instance == null) { GameObject o = new GameObject(typeof(T).Name); s_Instance = o.AddComponent<T>(); if((s_Instance as SingletonMonoBehaviour<T>).m_DontDestoryOnLoad) DontDestroyOnLoad(o); } return s_Instance; } } public static bool HasInstance { get { return s_Instance != null; } } protected virtual void Awake() { if(s_Instance == null) { s_Instance = this as T; if ((s_Instance as SingletonMonoBehaviour<T>).m_DontDestoryOnLoad) DontDestroyOnLoad(this); return; } if (this == s_Instance) return; // this != Instance Destroy(this); } protected virtual void OnDestroy() { if(this == s_Instance) s_Instance = null; } }ObservableMonoBehaviour.cs// Singleton Ver public class ObservableSingletonMonoBehaviour<T,N> : SingletonMonoBehaviour<T> where T : MonoBehaviour { protected Observable<N> m_Observable = new Observable<N>(); public void ChengeObservable(Observable<N> observable) { if (m_Observable.GetType() == observable.GetType()) return; // 型が同じ = 挙動は同じ m_Observable = observable; } // 通知対象へ追加 public void AddObserver(IObserver<N> observer) { m_Observable.AddObserver(observer); } // 通知対象から除外 public void RemoveObserver(IObserver<N> observer) { m_Observable.RemoveObserver(observer); } // 通知送信 public void NotifyObservers(N notifyObject) { m_Observable.NotifyObservers(notifyObject); } }活用例
Observerパターンで簡単なイベントシステムなんかも作れます。
今回は例として丁度良かったのでプライベートで作ったMUNのネットワーク関連のイベントシステムを少し改変して紹介します。下記のようにEventクラスと対応するEventを受信した時のみ特定の関数を呼び出せる様にEventDispatcherクラスを用意します。
NetworkEvent.cs// ネットワークイベント public abstract class NetworkEvent { public abstract NetworkEventType GetEventType(); public abstract string GetName(); } // ネットワークイベントの処理の実行管理役 public class NetworkEventDispatcher { private NetworkEvent m_Event; public delegate void EventFunction<T>(T e); public NetworkEventDispatcher(NetworkEvent e) { m_Event = e; } public bool Dispatch<T>(EventFunction<T> func) where T : NetworkEvent { if(m_Event.GetType() == typeof(T)) { func((T)m_Event); return true; } return false; } }本当はNetworkEventを継承したサーバーへの接続完了,失敗、ルームへの入室,退室など各種イベントクラスを実装するんですが、コード量が多いので省きます。
継承先のクラスにはabstract関数とその他必要であれば変数を追加するだけ基本は大丈夫かと。接続管理クラスで下記の様に実装します。
ConnectionManager.cs// MonobitEngine.ObservableSingletonMonoBehaviour // ... MonobitEngine.MonoBehaviourバージョンのSingletonMonoBehaviour // を継承したバージョンのObservableSingletonMonoBehaviour public class ConnectionManager : MonobitEngine.ObservableSingletonMonoBehaviour<ConnectionManager,NetworkEvent> { // サーバーへ接続 public void ConnectToServer(string playerName,string gameVersion = "ver 1.0",Hashtable customAuthData = null) { ... } // サーバーから切断 public void DisconnectFromServer() { ... } #region 接続コールバック関数 // 接続失敗 public void OnConnectToServerFailed(MonobitEngine.DisconnectCause cause) { NetworkEvent e = new ConnectToServerFailedEvent { m_Cause = cause }; NotifyObservers(e); } // サーバーへ接続された public void OnConnectedToServer() { NetworkEvent e = new ConnectedToServerEvent { }; NotifyObservers(e); } // ルームが作成された public void OnCreatedRoom() { NetworkEvent e = new CreatedRoomEvent { }; NotifyObservers(e); } // ルームの作成に失敗した public void OnCreateRoomFailed(object[] codeAndMsg) { NetworkEvent e = new CreateRoomFailedEvent { m_CodeAndMsg = codeAndMsg }; NotifyObservers(e); } // ルームへ入った public void OnJoinedRoom() { foreach(var player in MonobitEngine.MonobitNetwork.otherPlayersList) { Debug.Log(player.name); } NetworkEvent e = new JoinedRoomEvent { }; NotifyObservers(e); } // ルームの入室に失敗した public void OnJoinRoomFailed(object[] codeAndMsg) { NetworkEvent e = new JoinRoomFailedEvent { m_CodeAndMsg = codeAndMsg }; NotifyObservers(e); } // サーバーから切断された public void OnDisconnectedFromServer() { NetworkEvent e = new DisconnectedFromServerEvent { }; NotifyObservers(e); } // ... etc #endregion }次に通知を受け取る側のIObserverインターフェースを継承したクラスを用意し、
イベント受取時の関数の実装とStart関数かどこかでConnectionManagerのAddObserver関数で自身を登録します。
あまり長いのもアレなので、実装例として簡単なイベントを受け取るとログを出力する簡単なクラスを紹介します。public class NetworkEventLogger : MonoBehaviour,IObserver<NetworkEvent> { private void Start() { ConnectionManager.Instance.AddObserver(this); } public void OnConnectToServer(ConnectedToServerEvent e) { Debug.Log("Connect To Server!"); } public void OnDisconnectedFromServer(DisconnectedFromServerEvent e) { Debug.Log("Disconnect From Server!"); } public void OnNotify(Observable<NetworkEvent> observer, NetworkEvent e) { NetworkEventDispatcher dispatcher = new NetworkEventDispatcher(e); dispatcher.Dispatch<ConnectedToServerEvent>(OnConnectToServer); dispatcher.Dispatch<DisconnectedFromServerEvent>(OnDisconnectedFromServer); } }実装例は以上です。
もしかしたら何処かミスしているかもですが、修正はおまかせします。(投げやり最後に
Observerパターンの紹介は以上です。
通知関連の設計で悩んでいる方はObserverパターンが非常に役立つと思うので活用してみてはどうでしょう。
- 投稿日:2019-07-24T11:20:36+09:00
Lerpで作るアニメーション(easing)を解析する
UIのトランジションなどでよくある実装として、一次補間を用いたものがあります。
この各Buttonはeasingとして一次補間を利用しています。実装が容易なので僕もよく使うのですが、今回はこれの性質を数学的に追求してみましょう。
実装
Unityの場合ですと Mathf.Lerp という関数があります。
p という値にX座標が入っているとして、移動目標のX座標を T とすると、sample.csp = Mathf.Lerp(p, T, 0.1f);Lerpの引数に自身のpを入れているのがポイント。これだけで次の値をゲットでき、上のような動きを作れます。0.1f を変更すると、どのぐらい急に移動するかを調整できます。(大きいほど急に動く。経験上、どんな状況でもまず0.1を入れておいて様子を見るのがいいです)
一般項
なぜ一般項?
先ほどの実装は、数学的に言えば「漸化式」と呼ばれるものでした。ほとんどの場合これで問題はないのですが、
・任意の時刻の状態を得たい
・逆再生したい
のような状況では漸化式での対応が難しくなります。一般項ならそういったことも解決できるわけです。Lerp
Mathf.LerpのLerpはLinear Interpolationの略で、ようするに線形補間です。
$a$ と $b$ をパラメータ$t$で線形補間すると
$result=(1-t)a + tb$
となり、$t$が0〜1の間で変化すれば結果は$a$と$b$の間に収まるよね、という話ですね。Mathf.Lerpの中ではこんな計算が行われています。
これはベクトルでも成立し、こんな図を高校で習っていると思います。この式は頻繁に利用する、暗記したほうが良い式ですね。「$t=0$のときは$a$に一致し、$t=1$のときは$b$に一致する」と覚えておけば、思い出すのは難しくないでしょう。ちなみに実装する場合は
$result=(1-t)a+tb=a-ta+tb=(b-a)t+a$
という具合に変形しておくのがセオリーです。(ペナルティなく乗算をひとつ減らせる)漸化式
では上のプログラムを漸化式に置き換えてみます。
$p_{n}$:nフレーム後の位置
$T$:目標位置
$\alpha$:移動率(プログラムでは0.1fが入っていた)として、さきほどの線形補間の式より、次のフレームの値 $p_{n+1}$は
$p_{n+1}=(1-\alpha)p_{n}+\alpha T$ ・・・(1)
こうなります。
(なお初期位置、つまり初項$p_{0}$は $p_{0}=0$ としておきましょう。もし最初の位置がゼロ以外だったら、目標位置$T$にオフセットを加えれば済むので)一般項を求める
漸化式から一般項を求めます。数列の一般項を求めるテクニックがあり、
$p_{n+1}=(1-\alpha)p_{n}+\alpha T$・・・(1)
この式とよく似た
$s=(1-\alpha)s+\alpha T$・・・(2)
が成立するsを定めます。これが数列の受験テクニック。
(1)-(2)より$p_{n+1}-s=(p_{n}-s)(1-\alpha)$
$q_{n}=p_{n}-s$とすると
$q_{n+1}=q_{n}(1-\alpha)$
これは等比数列なので一般項が出せます。
$q_{0}=p_{0}-s=-s$より
$q_{n}=-s(1-\alpha)^n$
$p_{n}=q_{n}+s=s-s(1-\alpha)^n=s\left[ 1-(1-\alpha)^n \right] $式(2)を解けば$s=T$となるので
$p_{n}=T\left[ 1-(1-\alpha)^n \right] $・・・(3)
はい、解けました。受験数学に取り組んだことがある方は懐かしさを覚えるのではないでしょうか。
フレームレートで一般化する
一般項がわかりましたが、実際に利用するにはもうひと工夫が必要です。
そもそもの Lerp の動作に $\Delta t$(デルタティー)が含まれていないため、このままでは実時間ベースのアプリケーションへの応用が難しいのです。項$n$はフレームに対応しますが、これを時間で一般化しましょう。ここで、元々の動作が60fpsであったとする仮定が必要になります。60回で1秒に相当することにするので、時間$t$について、
$n=60t$
とすれば、$t$を秒とすることができます。よって式(3)を時間$t$の関数にできて、
$f(t)=T\left[ 1-(1-\alpha)^{60 t} \right] $・・・(4)
これが任意の時刻における位置の一般式になります。べき乗はMathf.Powなどで算出できるので、$t$は任意の実数で成り立ちます。つまり120fpsにしても動きは変わりませんし、逆再生も可能になりました。やったね。
そのままグラフにするとこうなります。実装例は
sample2.csfloat x = (target - m_InitialX) * (1f - Mathf.Pow(1f - 0.1f, 60f * (Time.time - m_StartTime))) + m_InitialX;こんな感じ。時間を与えると位置が出せる式になっています。
フレームレートを一般化したLerp
まだ続きがあります。
easingでLerpが有用なのは、開始位置を保存しておく必要がない点にもありました。先ほどのプログラムを再掲します。sample.csp = Mathf.Lerp(p, T, 0.1f);マウスの位置に物体を引き寄せる処理を Lerp で作ってみましょう。
上の例のように移動中に目標位置が変化する場合、一般項だと開始位置が式の中に含まれているので、毎回計算式を作り直すことになってしまいます。これでは一般項の利便性を享受できません。インタラクティブなアプリケーションの場合はこんなふうに、一般項が有利でないケースが多いですね。
Lerpの係数を一般化する
通常のLerpを使用したアニメーションが60fpsで動作しているとしましょう。これが30fpsでも同じアニメーションを再生して欲しいとしたら、Lerpの係数はいくつであれば良いか。これが命題となります。
(3)式
$p_{n}=T\left[ 1-(1-\alpha)^n \right] $より、例えば60フレーム後の状態は
$p_{60}=T\left[ 1-(1-\alpha)^{60} \right] $
これが30フレーム後の $p_{30}$ に一致する係数を $\alpha$ の代わりに $\beta$ として、
$T\left[ 1-(1-\alpha)^{60} \right] = T\left[ 1-(1-\beta)^{30} \right] $
すなわち
$(1-\alpha)^{60} = (1-\beta)^{30} $
が成立する $\beta$ を求めれば良いことになります。60と30の関係は一般化できるので、60を基準 $n$ 、30を新規の変数 $m$ として、
$(1-\alpha)^{n} = (1-\beta)^{m} $
そして$m$を$n$の比で表すことにして
$m=kn$
とすれば
$(1-\alpha)^{n} = (1-\beta)^{kn} $
これを解いて
$ \beta = 1- (1- \alpha)^{\frac{1}{k}} $
これが求める $\beta$ となります。先に一般項を出しておいたのでスムーズに計算できました。
実装例
60fpsで調整したアニメーションを30fpsにした場合のLerp実行例です。
var ratio30 = 1f - Mathf.Pow(1f - ratio, 60f/30f); pos = Vector3.Lerp(pos, m_Target, raito30);ただし$\frac{60}{30}=2$なので、Powを使うまでもなく二乗で構いませんが。
次に、不定フレームレートの場合です。30で割る代わりにTime.deltaTimeを掛けます。var ratio = 1f - Mathf.Pow(1f - ratio, 60f*Time.deltaTime); pos = Vector3.Lerp(pos, m_Target, raito);これでどんなフレームレートでも(フレームレートが高い場合でも)同じ動きをするはずです。
一般項の性質を調べる
話は一般項に戻ります。せっかく一般項が出たので、性質も調べてみましょう。それが可能なのが一般項の良さです。
例えば
「目標に1秒で到達するには、移動率$\alpha$はいくつ以上であればいいんだろう?」
なんて、有用な情報になりそうです。これを調べてみましょう。指定時刻で到達するための移動率
実は、指定時刻で到達する条件というのは導出不能です。なぜなら、数式上は永遠に目標の値に到達しませんから。
ここは$\epsilon$(イプシロン)を小さな値として「目的地までの距離が$\epsilon$以下になる」という条件にしましょう。
式(4)に $t=1$(1秒)を入れて、$T\left[ 1-(1-\alpha)^{60} \right] > (1-\epsilon)T$
これを解いて
$\alpha > 1-\epsilon^{\frac{1}{60}}$
これが条件になります。
例えば、仮に$\epsilon=0.01$(すなわち1%)とするなら$\alpha > 0.073881271871207$
つまり0.08ぐらいの率を入れておけば、1秒後には1%未満のところまで進んでいる、ということが数学的に保証できます。もちろんこれは Mathf.Lerp で実装した場合も成立します。
まとめ
長々と書きましたが、Lerpの係数を一般化したところがハイライトでしょうか。Math.Powを許容すれば実装もシンプルです。
また、一般項を出してしまえば性質が数学的に明らかになるので、完全に制御が可能になる良さがあります。任意のフレームの位置を確定できるので、タイムライン(Unityの機能)と連携するのも容易になるんじゃないでしょうか。補間の性質を把握しておけば、共同作業でも正確な議論や意思の伝達が可能になりますよね。
Lerpを使いながら係数を一般化することもできたので、例えば「あるデバイスだけ120fpsで動作する」といった状況は普通にあり得ると思いますが、今回の結果を用いれば同じ動きのまま滑らかにすることができます。
めでたしめでたし。
- 投稿日:2019-07-24T10:39:56+09:00
Unityが非アクティブ時でもバックグラウンドで動作させる方法
- 投稿日:2019-07-24T01:40:48+09:00
【Unity】ECSを始めるには
こんにちは、Densyakunです。
Unity ECSを始めるために必要な準備、注意点について紹介します。この記事では、自分がUnityでECSをやろうと思ったときに、困ったことなどがたくさんありましたので、
見つけたことなどを紹介しています。
少しでも参考になれば幸いです。注意
- UnityのECSは2019年7月現在開発中の機能で、情報が少ないです。Unityに慣れていない人には向いていません。(と言っても、この記事を見ている人は、向いていると思いますが。)
- 沼が多く、とっつきにくいです。ですが、早いうちにやっておくと良いと思います。
- Unityのゲームを高速化する上では必要とも言える機能です。特に、大量のオブジェクトを追加、管理、動作するゲームにはECSを導入することをおすすめします。実際に約5倍の高速化を図ることができます。(C# Job System、Burst使用の場合)
- Unityのバージョンは最新版(Beta版!!!)にしてください。でないと古いバージョンのECSしか利用できません!!(後述)
- 私はECSについてはあまり詳しくありません。高速化について調べているうちにECSを知り、始めました。
- 推測もありますので、正確な情報ではない可能性があります。あくまでも参考程度に見てください。
Entity Component System (ECS) とは
- 簡単に言うと、(コンピューターにとって効率かつ高速な動作をするための)ゲームの設計、考え方のようなもの
- Unityに限る話ではない(Unityで導入するECSをUnity ECSと呼ぶ)
- HYBRID ECS と、 PURE ECS がある。HYBRID ECSは従来のシステムとECSを混ぜたもので、PURE ECSは完全にECSによってできたゲームらしい(私はよく分かっていない)。
Unity ECS について
- 開発中の機能であるため、使い方が変わる可能性が高い
- 以上の理由から、使い方などの情報が殆ど無いため、使い方を自分で調べる必要がある(クラスやメソッド、パッケージが違ったり、名前が変わってたり、ググってもわからんなので、パッケージの中からそれっぽいのを探して見つける)
まずはじめに、なんとなく理解するために、こちらの記事を見ることをおすすめします。
http://tsubakit1.hateblo.jp/entry/2018/03/25/180203ECSの仕組みは説明すると難しいのですが、
簡単に説明すると、ECSは処理をCPUやメモリの特性に合わせることで高速化します。なのでおそらく、従来のScriptのような使い方をしたり、MonoBehaviourと連携することは向いていません。
uGUIなどの従来のものは、従来の方法で作ってください。Unity ECS の使い方
Unityを最新版にする(ココ重要)
ECSを利用するにはUnityプロジェクトにパッケージを導入する必要がありますが、
2019年6月現在Unity 2018.4.1f1ではEntities 0.0.12-preview.24が最新バージョンとなってしまいます。preview.32などの最新のECSを利用するためには、Unity2019を利用する必要があります。
現在、Unity2019はベータ版となっています。
UnityからHelpを押してDownload Beta...を押すと、ダウンロードページに飛びます。
またはこちらからページに移動してください。
https://unity3d.com/unity/beta「Join our Open Beta program」を押すと最新版のダウンロードページに移動します。
ページ上部の「Download Unity (バージョン)」を押すとページ中間に移動します。現在のバージョンを利用するには、Unity Hubというランチャーが必要になりました。
Unity Hubを持っていない方は、「Download Unity Hub」を押してください。
Unity Hubを持っている、もしくはUnity Hubをインストールしたら、
「Install version from Unity Hub」を押すと、Unity Hubで最新版がダウンロードされます(ボタンを押すとブラウザで警告が出る場合がありますので、確認してください)。あとは、Unity Hubの方でインストールの設定をしてください。
必要なパッケージをインストール
UnityのWindow->Package Managerからパッケージをインストールできます。
Package Managerの上の「All packages」または「In Project」となっているところを押して、
「All packages」にしてください。
ここが「All packages」だとすべてのパッケージ、
「In Project」だとプロジェクトに導入されているパッケージのみが表示されます。その横にある、Advancedを押して、Show preview packagesを押して、
プレビュー版のパッケージを表示するようにしてください。
Unity ECSにおいて必須なEntitiesパッケージは現在プレビュー版となっています。そしたら、必要なパッケージを導入してください。
ECSに関する主なパッケージ
- Entities(必須)
- Jobs
- C# Job Systemというもので、合わせて使うと更に高速化できます(ここでは解説しません)。
- Burst
- これも合わせて使うと、更に高速化することができます(ここでは解説しません)。
- Mathematics
- 計算によく使うため、入れといたほうが良い
- Hybrid Renderer
- ECSで従来のMeshやMaterialを使ってメッシュを描画するには、これがあると便利になる
- Unity Physics
- ECSでは従来の物理シミュレーション(PhysX)が利用できない
- ECSで重力や衝突をシミュレーションするにはこれを使う
GameObjectとの違い
Unity ECSではGameObjectなどと分離されたシステムで、
Scriptが止まっていてもECSは動作します。Unity ECSは主にWorld、ComponentSystem、Entity、Componentなどにより構成されています。
これを見ると仕組みや使い方などが詳しくわかります。
【Unity】 ECS まとめ(前編) - エフアンダーバーWorld(ワールド)は従来のScene(シーン)のようなものです。
全てはWorldの中で動作します。Entity(エンティティ)は英語で実体のことです。従来のGameObjectのようなものになります。
Component(コンポーネント)はEntityが持っているデータになります。
ComponentSystem(コンポーネントシステム)は、World内で実際に処理を行うものになります。
Systemとも呼ばれます。
SystemがEntityが持っているComponentをいじることで、
Entityに働きを持たせることができます。ほかにも移動や時間などの各機能別にSystemを作成することでゲームを動作させることもできます。
EntityはEntityManagerが持っています。
EntityManagerはEntityを管理するSystemです。
EntityはWorldが持っているものではありません。基本的な使い方
Worldを作成する
ソース
Worldを作成しEntityManagerを追加するScriptです。
OnDisableでWorldを破棄しています。Worldは複数作成する事ができますが、推薦されていません。
自動的に追加される Default World
初期の状態では、Default Worldが自動的に追加されていますが、
Worldを自作する場合は無駄になることや、すべてのSystemがすでについているため、高速化の観点から不要だと言われています。Project SettingsのPlayerのOther Settingsにある、
scriptingDefineSymbolsに、UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAPを追記してEnterを押して少し待つと、
Default Worldを自動的に追加しないように設定できます。
scriptingDefineSymbolsに複数記入する場合は、;で区切ることができます。MeshとMaterialの描画について
2019年7月現在、以前の方法ではできません。(
EndFrameTransformSystemとRenderingSystemBootstrapがない)
MeshとMaterialの描画については今後見つかり次第追記します。Physicsについて
Unity Physicsパッケージが必要です。
こちらを参考: (テラシュールブログ)ComponentSystemのメソッド
ComponentSystemでは主にOnUpdateメソッドをオーバーライドして処理を行いますが、
それ以外にもメソッドは存在します。以前とは名前が変わっているのですが、
OnCreate、OnDestroy、OnStartRunning、OnStopRunningがあります。
以前はOnCreateManagerとOnDestroyManagerという名前でした。OnCreateは、
World.CreateManager()などによりSystemが作成されたときに呼び出されます。OnStartRunningは、
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World)を実行するときに呼び出されます。OnUpdateは、
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World)を実行してから、
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(null)を実行するまでの、
フレームごとに呼び出されます(MonoBehaviourのUpdateのようなものです)。OnStopRunningは、
World.Dispose()を実行すると呼び出されます。
(ScriptBehaviourUpdateOrder.UpdatePlayerLoop(null)では実行されません)。OnDestroyは上記と同じく、
World.Dispose()を実行すると呼び出されます。処理順は、
OnCreate->OnStartRunning->OnUpdate (毎フレーム繰り返し)->OnStopRunning->OnDestroy
となります。こちらも参考
解説はここまでとなります。
詳しく知りたい方はこちらをご覧ください。
- 公式のドキュメント がありました!
- Unity:はじめてのECS 古いバージョンのため、Hybrid Rendererが現在非対応
- 【Unity】Entity Component System入門(その1)【2018.2】 古いバージョンのため、Hybrid Rendererが現在非対応
- 【Unity】 ECS まとめ(前編) - エフアンダーバー
- 【Unity】新しい物理演算、Unity Physicsについて
Entitiesにあるはずのものがない場合
現在の大きな問題点はここにあります。
実際使ってみようと思うと、Entitiesにあるはずのものがなくなっていたり、名前が変わっている場合があります。ネット上には情報が少ないので、
パッケージの中に似ている名前のものがないか、パッケージが変わってないかなど、自力で調べてください。
見つけたらコメントに書いてもらえると助かります。それか、もしかするとUnityのバージョンが古いのではないでしょうか。
冒頭にある通り、Unityを最新版(ベータ版)にしてください。私は最初、Unity2019がベータ版であることを知らなかったので、
冒頭にある通り古いバージョンが最新版であると思っていました。
この記事を書いている途中でそれを知りました。
全部コードを書き直さないと...
A Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to get more details.と表示されるエディターで、ゲーム実行中にScriptなどのファイルを更新した際のコンパイルで表示されることがあります。
私の環境ではECS側から、このエラーが最低で4つ出てきます。これは、NativeArrayなどが、Disposeされていない事により、メモリーリークが発生するというエラーです。
このエラーは、ゲームの動作にはあまり問題はありません。ECS側の問題で出てくる場合があるので、
気になりますが、今のところは修正されるまで気にしなくて大丈夫です。ただし、自分の書いたコードでNativeArrayなどを使用している場合は、
高速化が悪くなるので、正しくDisposeするよう注意してください。さいごに
ここで紹介したものはほんの一部になります。
少しでも参考になれば幸いです。
ご覧いただきありがとうございました。
- 投稿日:2019-07-24T00:23:14+09:00
バーチャルキャスト内で使用可能なVCIスライドを自動作成
概要
画像のようにバーチャルキャスト内で使用可能なVCIスライドを作成し、そのVCIスライドを画像から自動作成できるツールを作りました。苦労話の愚痴に、もしかしたら役に立つかもしれない小ネタを混ぜつつ作り方?を書きます。似たような事を初めてする人の参考になれば幸いです。
https://120byte.booth.pm/items/1297409バージョン
Unity : 2018.4.0f1
バーチャルキャスト : 1.6.4b
VCI : 0.19まずVCIを作る
普通にVCIを作ります。同期が地獄です。HMD付けたり外したり、SteamVRがダダこねたり、RiftSが映らなかったり、コミュ障メンタルに鞭打って凸ったりしながら動確を取ります。
ポイント
VCI Sub Itemが提供してくれる拡縮機能には下限サイズ(おそらく0.2)があります。
上記から逃げるにはVCI Sub Itemの子に可視オブジェクトを入れて任意のサイズ(xyz比が重要)に設定します。
GetPositionの値にオフセットを乗せる場合は、オブジェクトにオフセットを乗せるとluaが綺麗に書けます。
オブジェクト移動の基準位置には、アンカーのようにして使う透明オブジェクトが便利だと思います。
アップしたVCIにluaがない場合は、ローカルで更新されるまでゲスト側に反映されない時があるようです。
oculusとviveでトリガーとグリップが逆になってるので、操作を説明する時には注意が必要です。
所有権は移動できる状態の握った時に移る。という事を忘れてはいけません。
on某系の関数は所有権を持った人側でしか動きませんのでログ(print)も出ません。
not ownerというログがあるようですが、エラーではないのでビビってはいけません。
オブジェクト非表示が無いのでサイズ0にすると中間フレームが補間されて縮む様子が見えてしまいます。
上記を逃げるためには極端に遠い位置へ移動させるのが良さそうです。
同期は状態変数、共有変数、メッセージ、ダミーオブジェクトなどがあり、適材適所に使い分けましょう。
奥の手発動!の例としてupdate関数に対し、on某系から遅延フレームを設定して遅延処理ができそうです。
状態変数(vci.state)は完全に予想ですが、挙動的にはアイテム生成者が真の持ち主感です。
同期はグループID全部1で大体揃ってほしいくらい雑な捉え方してますが、複数人で掴まれると崩壊します。
確証はないんですが、更新アップロードは怪しい気がするので、作業中は削除>新規が良いような気がします。
アップロードしたVCIはバーチャルキャスト再起動しないとダメっぽいです。詳細
0.2の下限から逃げるためスライドはSub Itemの子にします。が、これは私がスライドにCubeを使っていた頃の名残なので、Planeでやるなら要らなかったかもしれません。書いてて今気づきました。あとは、スライド自体は掴めるようにはせず、スライドを表示させる場所を取得するための透明オブジェクト(anchor)を使います。大きさや向きもこの透明オブジェクトを使ってユーザー操作を受け付け、全スライドに反映します。スライドはforの連番で取得するので連番名にします。
ページ送り用のボタンとレーザーポインタのレーザー部分は、レーザーポインタにFixed Jointでくっつけます。luaでも似た事ができますが、追従はこちらの方が綺麗に動くようです。jointの親子関係に気を付けましょう。jointを付けるとたぶん握れないので、握る方を親にした方が良さそうです。lua
local out_pos = Vector3.__new(0, 1000, 0) if vci.assets.isMine then for i = 1, 1000 do local item = vci.assets.GetSubItem(i) if item == nil then -- 最大ページの取得 vci.state.Set('max', i - 1) break end end vci.state.Set('page', 0) end function onGrab(target) if target == "pointer" then vci.assets.GetSubItem("laser").SetLocalScale(Vector3.one) end end function onUngrab(target) if target == "pointer" then vci.assets.GetSubItem("laser").SetLocalScale(Vector3.zero) end end function onUse(use) if use == "back" or use == "next" then local max = vci.state.Get('max') local page = vci.state.Get('page') -- onUseでは共有変数の加減算のみ if use == "next" and page < max then vci.state.Set('page', page + 1) end if use == "back" and page > 0 then vci.state.Set('page', page - 1) end end end function updateAll() local max = vci.state.Get('max') local page = vci.state.Get('page') if max == nil or page == nil then return end local anchor = vci.assets.GetSubItem("anchor") for i = 0, max do -- 一旦全ページを遠くへ移動して向きと大きさを揃える vci.assets.GetSubItem(i).SetPosition(out_pos) vci.assets.GetSubItem(i).SetRotation(anchor.GetRotation()) vci.assets.GetSubItem(i).SetLocalScale(anchor.GetLocalScale()) end -- 現在ページを指定位置に移動 vci.assets.GetSubItem(page).SetPosition(anchor.GetPosition()) end自動作成ツール
Unityで作ります。作ったスライドVCIをexe作成用のシーンに複製してページオブジェクトを削除します。
必要な情報の入力UIを作り、ボタンを押したらその情報をVCIに適用して、スライドにしたい画像をページオブジェクトとしてC#スクリプトから生成します。Cしゃーぷ
using System; using System.IO; using UnityEngine; using UnityEngine.UI; using VCI; using VCIGLTF; public class ExportVCI : MonoBehaviour { [SerializeField] GameObject Template; [SerializeField] InputField Title; [SerializeField] InputField Version; [SerializeField] InputField Author; [SerializeField] InputField Contact; [SerializeField] InputField Reference; [SerializeField] Text ExportVCI_Text; [SerializeField] Material Mat; public void Export() { var title = Title.text; var version = Version.text; var author = Author.text; var contact = Contact.text; var reference = Reference.text; if (title == "" || author == "") { ExportVCI_Text.text = "必須の項目を入力してください"; return; } else { ExportVCI_Text.text = "Export VCI"; } var temp = Instantiate(Template); temp.name = title; var vci = temp.GetComponent<VCIObject>(); vci.Meta.title = title; vci.Meta.version = version; vci.Meta.author = author; vci.Meta.contactInformation = contact; vci.Meta.reference = reference; var jpg = Directory.GetFiles(Application.dataPath + "/../IMAGE", "*.jpg"); var png = Directory.GetFiles(Application.dataPath + "/../IMAGE", "*.png"); var img = new string[jpg.Length + png.Length]; jpg.CopyTo(img, 0); png.CopyTo(img, jpg.Length); Array.Sort(img); for (int i = 0; i < img.Length; i++) { var tex = ReadTexture2D(img[i]); float x = tex.width >= tex.height ? 1 : (float)tex.width / tex.height; float y = tex.width <= tex.height ? 1 : (float)tex.height / tex.width; var go = new GameObject(); go.name = i.ToString(); go.transform.parent = temp.transform; go.transform.position = new Vector3(0, 1, 0); var sub = go.AddComponent<VCISubItem>(); sub.GroupId = 1; var rigid = go.AddComponent<Rigidbody>(); rigid.useGravity = false; var plane = GameObject.CreatePrimitive(PrimitiveType.Plane); plane.transform.parent = go.transform; plane.transform.position = new Vector3(0, 1, 0); plane.transform.eulerAngles = new Vector3(90, 0, 0); plane.transform.localScale = new Vector3(x / 10, 0.1f, y / 10); plane.GetComponent<Renderer>().material = Mat; plane.GetComponent<Renderer>().material.SetTexture("_MainTex", tex); Destroy(plane.GetComponent<MeshCollider>()); } var gltf = new glTF(); var exporter = new VCIExporter(gltf); exporter.Prepare(temp); exporter.Export(); var bytes = gltf.ToGlbBytes(); var path = Application.dataPath + "/../" + title + ".vci"; File.WriteAllBytes(path, bytes); Destroy(exporter.Copy); Destroy(temp); } Texture2D ReadTexture2D(string path) { byte[] read = ReadFile(path); Texture2D texture = new Texture2D(1, 1); texture.LoadImage(read); return texture; } byte[] ReadFile(string path) { FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader br = new BinaryReader(fs); byte[] read = br.ReadBytes((int)br.BaseStream.Length); br.Close(); return read; } }Export()をボタンに割り当てます。Planeのサイズってなんで他の10倍なんでしょうね。
参考
VCIスクリプトリファレンス
https://virtualcast.jp/wiki/doku.php?id=vci:script:referenceVCIをビルドしたクライアントからExportする
https://qiita.com/Nekomasu/items/5ab21c6d9359f6c18e46
- 投稿日:2019-07-24T00:19:12+09:00
ML-AgentsのデモはUnity 2017.4.**じゃないとダメっぽい件
はじめに
当初はML-Agentsを使ってUnity × 強化学習を学ぼう!って
息まいてたけど、ハマリにはまってようやく解決したので
タイトルを変えて今後のためのメモとして残す。結論
ML-Agentsのデモは、Unity 2019.1.10f1(2019/7/24時点最新)だと動作しないため
デモを試したいなら2017.4.30f1(2019/7/24時点でとれたバージョン)で起動する必要がある。
結構Unityのバージョンも気を付けないといけないんだなぁ…と
ブログ書いてる人が頻繁にバージョンを書いて解説しているのはこういうことなのか…と
色々と勉強になった。2019/7/25追記:
最新のUnityでもデモが実行可能とのこと。
ひょっとしたらバージョンが問題ではなく別の要因かもしれない。。。というメモ残します。
(nosadaMCさん情報ありがとうございます)とってきたデモのファイル
https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Installation-Windows.md
に記載のリンクからzipでもらった。直リンクはコチラ。
https://github.com/Unity-Technologies/ml-agents/archive/master.zip
解凍して、ローカルに保存した。で、どんな感じに失敗するのか?について
まずは、Unity 2019.1.10f1をNewProjectで起動。
File > Open SceneでデモのSceneファイルを開く。
↓↓実際に開いたファイルがコレ。
C:\Users\watya\ml-agents-master\UnitySDK\Assets\ML-Agents\Examples\3DBall\Scenes\3DBall.unityScene開いた直後の画面がコレ。すでに真っ赤なエラーログが出てる…
再生ボタンを押した結果がコチラ。
残念なくらいボードが動かないし、ボールもポロリと落ちて終了。。。うまくいったらどうなるの?
こちらは、
C:\Users\watya\ml-agents-master\UnitySDK\Assets\ML-Agents\Examples\3DBall\Scenes\3DBall.unity
をダブルクリックして起動。
起動待ち中に↓↓のウィンドウが出る。のでContinueをクリックすると
保存したときのバージョン(2017.4.30f1)で起動してくれる。
起動直後の画面がコチラ。
エラー一つなく起動してくれた。いい感じ。
再生ボタンをクリックした結果がコチラ。
プルプル震えながらボールが落ちないように頑張ってる。
コレコレ!この結果が欲しかった!
反省点
今振り返ると、アセットをインポートしたときもなんか似たようなウィンドウが出ていたのを思い出した。
バージョンがどうとかこうとか。最新のバージョンは互換性あるからうまくいくっしょ!
なんて感じでごり押してたけど、やっぱりよくないんだね、反省。。。
次回は改めてML-Agentsの良さを確認する検証がしたい。おわりに
Unityの結果は動いてなんぼ!ってのもあり、ついに重い腰を上げて
gif動画作成ツールを使って動画も残すようにした。
それにしても今はいいツールがあるもんで、ほんと作りやすかった。
せっかくなので紹介。ScreenToGif 2.18
https://all-freesoft.net/mm9/animationcapture/screen-to-gif/screen-to-gif.htmlとっても役に立ちました。作ってくれた人サンキュー!













































