原カバンは鞄のお店ではありません。

Unityを使ったゲーム制作のあれこれを綴っていきます。

音ゲー製作には必須?Koreographerを使ってみたよ

長いからいきなり本題

Unityで音楽とオブジェクトの動きを連動させたい場合「Koreographer」というアセットが有名ですが、アセットストアの中では結構高価なアセットということもあってか中々「Koreographer」を取り上げたブログ記事が少ないです。

 
Google先生に何度尋ねても「Koreographer」について詳しい使い方が載っているのは以下の記事しか見つかりませんでした。

qiita.com

なので自身の備忘録も含めて「Koreographer」について調査した内容を記事にしたいと思います。(たぶん数回にわたる)

 

Koreographerの概要

「Koreographer」がどんなアセットなのかをザックリ言えば、楽曲に合わせて事前に設定したタイミングでイベントを発行してくれるアセット、と言えます。
イベントを発行してくれるだけなので、「Koreographer」が曲に合わせてオブジェクトを飛び跳ねさせたり、色を変えたりしてくれるわけではありません。

そこらへんの動作は自分で作り込む必要があります。


楽曲(AudioClip)とKoreographerと動かしたいGameObjectの関係を図で表すと下のような感じになります。

f:id:Karvan:20180828213757p:plain

イベントを発行してくれるだけ、とはいえ、「Koreographer」ではその後の動作が作りやすいように、イベント発行時に「Payload」と呼ばれるメタデータを指定してGameObject側へ渡すことができます。


例えば、歌詞が歌われるタイミングでその歌詞を「Payload」で渡し、画面に表示するようにすればカラオケっぽいものが作れます。
(そこらへんの話は上記のリンク先でも書いてあります)

 

指定できる「Payload」の種類は以下の通り

  • Int:int値を指定
  • Float: float値を指定
  • Text: 文字列を指定
  • Curve: AnimationCurveを指定
  • Color: Colorを指定(Professional版のみ)
  • Gradient:ColorGradientを指定(Professional版のみ)

「Koreographer」にはLite版とProfessional版があり、「Payload」にColorとGradientを指定できるのはProfessional版のみとなっています。当然ながらProfessional版は結構な値段がするので用途に合わせてどちらを購入するか検討する必要があります。

 

イベントの種類

イベントの設定には専用のエディタを使用します。

専用エディタで楽曲のファイルを指定するとその波形が画面に表示されます。そこに楽曲のBPMを指定するとビートに沿ったグリッドを表示してくれるので、そのグリッドをクリックしてイベント発生のタイミングを設定します。

(つまりビートに沿ったタイミングでイベントが発行できる)

  f:id:Karvan:20180828214423p:plain

白い線がグリッドで1ビートの間隔を表しています。上の図では4ビート(1小節)単位に太い線が引かれています。

 

「Koreographer」には二種類のイベントがあります。イベント発生のタイミングを設定する際にどちらのタイプのイベントなのかを指定し、そのタイプに沿った「Payload」を使用してイベントが発行されるようにします。

  • OneOffイベント:開始タイミングのみ指定⇒使用するPayload:int,float,Text,Color
  • Spanイベント:イベントの開始と終了を指定⇒使用するPayload:Curve,Gradient

 

OneOffイベントの場合、一回だけイベントを発行します。何かの処理の開始タイミングを計りたい場合などにこのイベントを使用します。同じイベントを繰り返し発行したい場合は、その数だけエディタで指定する必要があります。


Spanイベントの場合、指定した期間内では毎フレームイベントが発行されます。徐々に変化するような動作をさせたい場合などにこのイベントを使用します。フレーム単位でイベントが発行されるので重い処理をする際には注意が必要です。また、イベントの発行がビート単位ではないことも考慮する必要があります。(私はここを勘違いしてた)

 

では実際に使ってみよう

「Koreographer」のシーンへの導入方法やスクリプトへの組み込み方などは、上記のリンク先に記載がありますし、アセットのサンプルシーンでも確認できるので省くとして、実際にエディタでイベントを設定した画像がこちら

 

f:id:Karvan:20180828214929p:plain

 

 真ん中で帯状に表示されているものがSpanイベントで、「Payload」にはGradient(徐々に色が変わる)が指定されています。

見づらいですが赤の線はOneOffイベントで「Payload」にはColorが設定されています。

 

ここまで専用エディタで設定したら、ゲームシーン内に音に合わせて動かしたいGameObjectを設置します。今回は一列に並んだCubeを使用します。

 

f:id:Karvan:20180828214403p:plain

こんな感じ

 

Spanイベントが発行されたら全てのCubeが同時にGradientに沿って色が変わるように、OneOffイベントが発行されたら先頭から順にColorの色へ変わるようにスクリプトを組みます。


それを実行した結果が下の動画になります。(注意:結構音量がデカいです)

youtu.be

 音楽はFLASH☆BEATさんのNew Departureを使用しています。

 

dova-s.jp

一つ一つは単純な動作なんですが「Koreographer」を使って楽曲に合わせてリンクさせると効果的な演出が作れると思います。

 

ただ、やっぱり高価なアセットなので購入を躊う方もいるでしょうし、もっと情報が欲しい、と思う方もいらっしゃると思うので、次回もこの「Koreographer」についての記事を書きたいと思っています。

 

Particle System の「Simulation Space」って何?

愚痴から始まるストーリー

例えば電灯が一つあって、Onボタンを押すと点灯、Offボタンを押すと消灯しているプログラムがあって、それをボタンを一つにして一回押せば点灯、次に押せば消灯するようにしたい場合は一つのボタンに点灯と消灯の二つの機能を搭載しなくてはいけないのでプログラムの難易度が一段上がると思うのですが、何故かうちの営業は『ボタンが二つから一つに減る=作業工数が減る』と、馬鹿な計算しか出来ないので夏休み明けからイキナリ無理難題の作業スケジュールを渡されて、「だったらお前がやれよ」と言いたくなるのを何もいえなくて・・・夏、みたいな、気候的には猛暑は過ぎた感じですが、別の意味の猛暑が続きそうな今日この頃。

 

そんな感じなので手短に

早速本題。

現在制作中の『Dull Things No Life』は所謂避けゲーなので、ユーザライクがモットーのカバンさんは、障害物が迫ってくる前に「右に避けて」「左に避けて」みたいな矢印を表示しようと思い立ったわけです。

とはいえ、普通にスプライトやイメージを表示するのはつまらないので、アセットストアでそれっぽいParticleを購入して使用することに。

だがしかし、矢印のパーティクルを移動する障害物の子オブジェクトにして使用すると、障害物の前に一瞬表示されてその後は障害物と一緒に動いてしまう。

だからといって矢印と障害物とは無関係にしてしまうと、矢印の表示タイミングの制御とか面倒くさい。

どうしたものかと悩んでいたらこんな記事を発見

【Unity】Particle System の「Simulation Space」|株式会社トリサン

 

なるほど、Simulation Spaceをいじれば良いのか。

 

Simulation SpaceをWorldに

f:id:Karvan:20180822000707p:plain

矢印のパーティクルを障害物の子オブジェクトにてして、かつParicleSystemのインド人を右に、違う、Simulation SpaceをWorldに、これで障害物が移動してもパーティクルはその場に残ってくれるみたい。

 

結果、こんな感じになるよ

ドーン

f:id:Karvan:20180822001122g:plain

 

『Dull Things No Life』鋭意製作中です!!

 

 

次回作タイトル発表

溶ける・・・

東日本の方はどうか知りませんが、私の住んでいる地方では最高気温が体温以上になる猛暑日が連日続いているので体が溶けそうです。いや、実際に体が溶けた人がいるとは思いませんがそれぐらい暑いので、せっかくの夏休み期間でも一歩も外に出ていません。

 

で、暑くなくても外に出ないだろう、とかいう無粋な突っ込みは横に置いといて、今年の夏休み期間は有難くも一週間あるので、これはゲーム制作の進捗を進める絶好チャンス!じゃないですか、今作っているのは比較的単純なエンドレスランゲームなので一週間もあればメインの機能の大半は作れる!とか思っていたわけです。

 

それはまるで・・・

夏休みが始まる前の小学生が「最初の一週間で宿題を全部終わらせて後は遊ぶ!」とか無謀な計画を立てるのと同じように、私も休みが始まる前までは「一日10時間ぐらい制作する!!」とか意気込んでいました。

 

吾輩はやれば出来る子である。
    ∩∩
   (´・ω・)
   _| ⊃/(___
 / └-(____/
  ̄ ̄ ̄ ̄ ̄ ̄ ̄

 

ただ、まだやる気がでない

   ⊂⌒/ヽ-、__
 /⊂_/____ /

 

 

 ・・・ハイ、といわけで4日経ちましたー。

ちょっとだけ、と思ってPS4起動したら4日経ってた、夏の暑さのせいですね。

 

それでもなんとか・・・

一応タイトル画というか、アプリのアイコン用の画像だけは作ったのでタイトルと共に載せてみたいと思います。

 

ドン!

f:id:Karvan:20180814154147p:plain

タイトルは『Dull Things No Life』です!

ちょっとサイバーな感じのエンドレスランゲーム。多分、英語的にはメチャクチャですが、響きが気に入っているので変える気はないです。

 

以前の記事でも書いた気がしますが、ゲーム内容はありきたりな内容なので、ちょっと演出を凝ってみたいなと思っています。

基本的な部分は今月中には作ってしまいたいなー・・・

(といいつつ、PS4を起動してしまう)

 

 

 

 

 

Google Adwordsで広告出稿したよの巻

いきなり本題

3月末にGooglePlayにリリースしたCutie Circuitですが悲しいほどダウンロードされないので、6中旬あたりからGoogle Adwordsを利用して広告出稿しています。

 

広告出稿をするということは当然費用が掛かるということで、特にブルジョアでも特権階級でもない人間としては費用対効果が気になるところです。なので今回は広告出稿による費用とその効果(ダウンロード数)について纏めてみたいと思います。

 

全期間(6/23~8/7)の集計

広告出稿を始めてから本日(8/7)までの全46日間で集計をとった結果が

  • 総広告表示回数:95,300回
  • 総費用:32,600円
  • 総ダウンロード数:312DL
  • ダウンロード単価:104.5円

 

ダウンロード単価:104.5円は予想よりちょっと高い結果ですが、ただ全46日間でも広告出稿の予算額によって広告表示回数なりダウンロード数なりが大きく異なる結果になっています。

 

自作のアプリをGoogle Adwordsで宣伝する場合は、全て『ユニバーサル アプリ キャンペーン』を利用することになるので、費用的なことで利用者側が決定することは以下の二点。

  • 1日の平均予算
  • 目標インストール単価

どちらも随時変更できるので、表示回数と総費用を比べながら何回か変更しました。

 

1回目の予算(6/23~6/30)

  • 1日の予算=500円
  • インストール単価=50円

 

なにぶん初めてのことなので、よく分からずに以下のリンクを参考にして予算を決定。
一ヶ月15,000円ぐらいかぁ。。まぁ、なんとかなりそう。。みたいな甘い考えで、動画広告とかも未作成。

 

www.furimuke.com

で、その結果が

  • 広告表示回数:194回(1日あたり24回)
  • 費用:150円
  • ダウンロード数:0DL

あかん!!

まったく広告表示されないためダウンロードは0、なんじゃそりゃ、Adwords使った意味が全然ないし。

 

ということで予算アップを決定、それに併せてGooglePlayで表示する動画広告も作成

 

2回目の予算(7/1~7/12)

  •  1日の予算=1,000円 
  • インストール単価=200円

 

一日の予算500円ではまったく効果がでないので倍の1,000円に、あわせてインストール単価も一気に200円にあげる。
これなら多少は改善するだろうと思った結果

 

  • 広告表示回数:3,370回(1日あたり280回)
  • 費用:3450円(1日あたり287.5円)
  • ダウンロード数:9DL
  • ダウンロード単価:383.3円

 

本当に多少の改善で広告表示回数は10倍に増えたけれど、ダウンロード数はそれに比例せず全然伸びてくれない(9DL) 。
これではまるであかん、もっと費用を掛けないと意味がないなぁ、と予算の増額を決定。動画広告も二つ目を作成

 

3回目の予算(7/13~7/19)

  • 1日の予算=2,000円
  • インストール単価=200円

 

普通なら一月に60,000円掛かる計算になりますが、予算1,000円でも1日当たりは300円弱しか掛からなかったし平気、平気、と軽い考えで予算額を倍増、でその結果

 

  • 広告表示回数:25,200回(1日あたり3,600回)
  • 費用:14,200円(1日あたり2,028円)
  • ダウンロード数:36DL
  • ダウンロード単価:394.4 円

 

あかん!あかん!これはもっとあかん!破綻する!

一日の予算を2,000円にした途端、本当に一日2,000円消費するとか聞いてないし!(←自業自得)
ダウンロード単価が394.4 円と400円近くになり、安月給の身としてはちょっと耐え切れないと判断して今度は減額をすることに。

 

4回目の予算(7/20~現在)

  • 1日の予算=800円
  • インストール単価=150円

 

前回の予算期間で予定した費用を食いつぶしてしまったので、予算を大きく減らさざえるをえず、もうこれでダウンロード数が伸びなくても仕方ない、とあきらめていたのですが。。。

 

  • 広告表示回数:66,600回(1日あたり3,505回)
  • 費用:14,800円(1日あたり779円)
  • ダウンロード数:267DL
  • ダウンロード単価:55.4円

 

それまでとは打って変わって大幅改善!!ちょうど夏休みに突入した効果なのかダウンロード数が一気に伸び、それに伴ってダウンロード単価がガクンと下がってくれました!

 

広告の表示回数も2,000円のときと大きく変わらないのはネット全体の閲覧回数が増えているからだと思われます。夏休みの効果ってすごいですね!

 

ありがとう夏休み!VIVA!夏休み!

 

これまでを振り返って

初めてGoogle Adwordsを利用するにあたり色々と想定外のこともあったのですが、これまでを振り返ると

  • 自分で決めた予算内に納めてくれる
  • 予算に応じてそれなりに広告を表示してくる

ということが分かりました。広告が表示されないからと言って焦って予算を大きくしたらダメ、とうことも身をもって分かりました。

 

ただ、ダウンロード数を増やそうとするなら広告の表示回数だけに頼るのではなく、いつ広告を出稿するか、といったタイミングを見極めることも必要で、それこそ予算が限られている個人開発者にすれば闇雲に広告を打っても効果がない、ということも知ることができました。

 

最後に

これまでの結果を表に纏めてみました。これからGoogle Adwordsを利用しようと考えている方の参考になれば幸いです。

(表の中の数字は全て一日当たりの数字になります)

 

予算額 目標単価 表示回数 クリック数 費用 インストール数
500 100 24.3 0.3 18.8 0
1000 200 280.8 5.1 287.5 0.8
2000 200 3600 155.7 2028.6 5.1
800 150 3505.3 231.1 778.9 14.1

 

一番下の行が夏休み期間の数字です。費用対効果が通常時と全然違いますね。

 

 

 

 

 

次回作のお知らせを・・・

雑感だけを書こうと思った

コンサートを観にいって、前後の列は女性だらけなのに何故か自分席の周りだけ奇人変人大集合みたいな人間に取り囲まれるとか、そんな経験をした昨今、いや、「女子率高いよ」との言葉に騙されて鼻の下を伸ばしながらアイドルコンサートに出かけた私が悪いんです。特攻服とか久しぶりに見た。

 

で、CutieCircuitのiOS版が容量の問題で頓挫しているので、最近ちょっと話題になった下のリンク先の話について雑感を書いてみたら、結構クサくなってバッサリ削りました。どうせ誰も読まないし。

 

www.gamecast-blog.com

次回作を作っているよ

なので、現在制作中のゲーム画面を載せることにしました。

 

f:id:Karvan:20180801005528g:plain

 

みんな大好きエンドレスランゲームです。既にジャンルとして確立しているので制作側も作りやすいですしね。


ありきたりな感じもしますが、キャラをバイクにして、後はエフェクトいっぱいつけて誤魔化すかなぁ・・・

 

 

C# JobSystemを使ってみよう2

賢人の言葉

宮沢賢治氏の代表作「雨ニモマケズ」では「雨ニモマケズ風ニモマケズ・・」の次に「雪ニモ夏ノ暑サニモマケヌ・・・」という言葉がありますが、ここ最近はさすがの賢治氏もその節を曲げそうな暑さが続いていおり、私の場合は負けっぱなしでエアコン全開の日々が続いています。もう部屋から出られんし。

 

夏休みシーズンに入ったこともあり私の周りでも、フェスだ!、花火だ!、お祭りだ!、みたいなワードが飛び交っていますが、私としてはこの酷暑の中、一歩も外に出る気はありません。そもそも誘われない、という話は悲しくなるので置いといて、この夏は部屋から出ることなくゲーム制作に専念したいと思っています、うん、だから泣いてません。

 

C# JobSystemの続き

前回の記事でUnityのJobSystemについて取り上げましたが、今回はその続きです。他にネタがなかったとか、そういうわけでは決してありません、うん、たぶん、きっとそう。

 

前回では複数のモデルに公転と自転の運動をさせていましたが、今回は公転運動に収縮の動きを加えてみようと思います。つまり公転半径が周期的に変わるってやつ。

f:id:Karvan:20180724225803p:plain

 

公転運動に収縮運動を加える

公転運動のロジックに収縮分の計算を加えれば出来そうな気もしますが、今回は公転運動を計算した後、収縮運動を計算してオブジェクトのTransformに反映しようと思います。これにより前回作成した公転運動のロジック部分には一切手を入れなくて済みます。

 

f:id:Karvan:20180724225856p:plain

 

収縮運動の移動

収縮運動の移動量にはAnimationCurveを使用しました。Main側でカーブの位置を取得して、変数「SpreadPower」に渡して収縮運動計算で使用しています。

Main側

    // Update is called once per frame
    void Update () {
        // 公転用JOB領域設定
        RevolutionMotionUpdate revolutionJob = new RevolutionMotionUpdate()
        {
            Accessor = this._revolutionStructs,
            DeltaTime = Time.deltaTime,
        };

        // 自転用JOB領域設定
        RotationMotionUpdate rotationJob = new RotationMotionUpdate()
        {
            Accessor = this._rotationStructs,
            DeltaTime = Time.deltaTime,
        };

        //カーブ位置取得
        if (_curveRate == 1f)
        {
            _curveRate = 0;
        }
        else
        {
            _curveRate = Mathf.Clamp(_curveRate + _spreadPower, 0f, 1f);
        }

        // 収縮運動用JOB領域設定
        ContractionMotionUpdate contractionJob = new ContractionMotionUpdate()
        {
            Accessor = this._spreadStructs,
            SpreadPower = contPower * _jumpCurve.Evaluate(_curveRate)
        };

        this._jobRevolutionHandle.Complete();
        this._jobRotatioHandle.Complete();
        this._jobSpreadHandle.Complete();

        this._jobRotatioHandle = rotationJob.Schedule(this._planetTransformAccessArray);
        this._jobRevolutionHandle = revolutionJob.Schedule(this._planetTransformAccessArray);
        this._jobSpreadHandle = contractionJob.Schedule(this._planetTransformAccessArray, _jobRevolutionHandle);
        JobHandle.ScheduleBatchedJobs();
    }

 

収縮運動側

    /// <summary>
    /// 収縮運動のJOB
    /// </summary>
    struct ContractionMotionUpdate : IJobParallelForTransform
    {
        public NativeArray<RotCalStruct> Accessor;
        public float SpreadPower;

        // JobSystem側で実行する処理
        public void Execute(int index, TransformAccess transform)
        {
            RotCalStruct accessor = this.Accessor[index];
            transform.localPosition = Spread(accessor, transform);
            this.Accessor[index] = accessor;
        }

        // 収縮運動
        Vector3 Spread(RotCalStruct data, TransformAccess transform)
        {
            Vector3 nowPos = transform.localPosition;

            Vector3 radiusVec = new Vector3(nowPos.x - data.RevOri_X,
                                            nowPos.y - data.RevOri_Y,
                                            nowPos.z - data.RevOri_Z);

            Vector3 modVec = (SpreadPower + 1.0f) * data.RevRadius * radiusVec.normalized;

            Vector3 retPos = new Vector3(data.RevOri_X + modVec.x,
                                         data.RevOri_Y + modVec.y,
                                         data.RevOri_Z + modVec.z);

            return retPos;
        }
    }

 

公転運動後に収縮運動を行う

並列処理を行うためのJobSystemですが、今回の場合は公転運動⇒収縮運動の順に処理を行わなくてはいけません。これを実現させるためにJobSystemでは各Jobの依存関係を設定することができます。

 

上のソースのMain側で

       this._jobRevolutionHandle = revolutionJob.Schedule(this._planetTransformAccessArray);
        this._jobSpreadHandle = contractionJob.Schedule(this._planetTransformAccessArray, _jobRevolutionHandle);
        JobHandle.ScheduleBatchedJobs();

と記載している箇所があります。

収縮運動側のScheduleの第二引数に公転運動側のジョブハンドルを指定することで、公転運動の処理が完了したら収縮運動の処理が即時に実行させるようになります。

公転運動にも収縮運動にも同じtransformArrayを渡しているので、収縮運動の処理には公転運動分の移動が終ったtransformに収縮運動分の移動を反映させることができます。

 

実際の動作

これらの処理を使って実際に動作させた結果が以下の動画です。

どーん

f:id:Karvan:20180724231410g:plain

 

スクエア上に並んだ球体が自転+公転しながら収縮運動もしていることが分かります。

 

JobSystemは難しくない

前回も述べましたがJobSystemの実装自体は難しくありません。ただ、JobSystemでどのように実装するか、という設計の見極めは十分な考慮が必要かもしれません。今回の場合だと、自転と公転はそれぞれ並列処理でよいけど、収縮は公転⇒収縮の順じゃないと駄目、みたいな。まぁ、色々試してみるのが一番ですね。

 

 

Unity2018のC# JobSystemを使ってみよう

三連休

海の日だからって皆が海に行くとは思うなよ!ということで三連休は部屋に閉じこもる平常運転な休日だったのですが、連休中にやろうと思っていたCutieCircuitのiOS版のリリースはTextureを小さくしてもipaサイズの問題解消とはいかず、いよいよAssetBundleに手をだすしかないことが確定したので心折れて、現実逃避の意味も含めてちょっと違うことにチャレンジしました。

 

C# JobSystem

Unity 2018.1の目玉機能のひとつに「C# JobSystem」(以下 JobSystem)というものがあります。

 

JobSystemとは、ざっくり言うと「Unityで並列処理を可能とする機能」です。

 

もともとC#自体にはThread処理機能がありますが、JobSystemではUnity側が内部で使用しているWorkerThreadをC#の(ユーザ側の)スクリプト処理にも開放することで並列処理を可能としている機能なので、Unity側が使いやすいようにAPIを用意してくれていて、C#のThread処理を使うよりはロジックの実装がかなり楽ちん、らしいです。

 

ここら辺の話は以下のリンク先で詳しい解説が記載されています。

tsubakit1.hateblo.jp

せっかくだから使ってみよう

ロジックの実装がかなり楽、とはいえ、幾つか決まりがあります。

JobSystemを実装する場合は、この決まりに沿って処理を構築する必要があります。

 

  1. 並列処理で処理する関数(構造体)で使用する領域(メモリ)を事前に確保する
  2. 入力用の領域に並列処理用関数(構造体)へ渡すデータを設定する
  3. Sceduleを実行して、JobSystemにジョブを発行依頼
  4. Completeで処理が終了まで待つ
  5. 処理結果を出力用の領域から取得する
  6. 1.で確保した領域を解放する

 

手順が多くて難しそうな印象を受けるかもしれませんが、一つ一つの作業はそれほど難しくありません。むしろ一回覚えれば簡単に作れてしまいます。

 

ただ、メモリの確保にはNativeContainerと呼ばれるコンテナを使う必要があることや、処理が終ったら必ずメモリを解放しなければいけないこと、メモリの確保の仕方が何種類かあること、などは慣れないと戸惑うかもしれませんが、最初のうちは「おまじないみたいなもの」として捉えて、とりあえず見よう見まねで実装しておけばよいです。

 

詳しいことは全て先ほどのリンク先に書かれているのでそちらを参考してもらうとして、このJobSystemを使って実際私が作ったものがこちら

 

ドーン 

f:id:Karvan:20180717232612g:plain

3つの球体がそれぞれ違う速度で自転と公転をしています。
JobSystemを使って自転と公転それぞれの回転をフレーム毎に並列で計算しています。

 

公転の計算と移動

各球体の回転と移動はIJobParallelForTransformを継承したジョブ実行用の構造体にて行います。

 

ジョブを実行すると各球体毎にIJobParallelForTransformのExecute関数が呼ばれます。
このExecute関数にはTransformAccessという引数が渡ってくるので、このTransformAccessを経由して球体を移動させます。


なので並列処理側の動作としては

  1. 1フレーム分の角度回転した移動量を計算する
  2. TransformAccessのpositionから現在位置を取得し、移動量分を加算する
  3. 加算した位置をTransformAccessのpositionに設定する

という流れになります。

ちなみに、IJobParallelForTransformではTransform.RotateAroundと言ったAPIを使うことができないようなので、行列計算を使い自力で公転の移動量を計算しています。

 

これは以下のリンクのロジックを参考にしました。

 

qiita.com

実際のソースはこんな感じ

/// <summary>
/// 公転運動
/// </summary>
struct RevolutionMotionUpdate : IJobParallelForTransform
{
    public NativeArray<RotCalStruct> Accessor;
	
    // JobSystem側で実行する処理
    void IJobParallelForTransform.Execute(int index, TransformAccess transform)
    {
        RotCalStruct accessor = this.Accessor[index];
		
        // 計算結果をTransformに反映
        ApplyPosition(accessor.Matrix, transform, accessor);
		
        // 結果保持
this.Accessor[index] = retAccessor; } /// <summary> /// 公転位置の設定 /// </summary> /// <param name="matrix"></param> /// <param name="transform"></param> /// <param name="data"></param> void ApplyPosition (Matrix4x4 matrix, TransformAccess transform, RotCalStruct data) { Vector3 RevOriPos = new Vector3(data.RevOri_X,data.RevOri_Y,data.RevOri_Z); Vector3 TargetPos = transform.position; // 原点位置からの差分ベクトル Vector3 TempPos = TargetPos - new Vector3(data.RevOri_X, data.RevOri_Y, data.RevOri_Z); // 行列計算による移動量算出 Vector3 CalResPos = matrix.MultiplyPoint3x4(TempPos); // 元の位置に移動量を加算して設定 transform.position = RevOriPos + CalResPos; } }

 

ここで、最初のほうに記述されている

     public NativeArray<RotCalStruct> Accessor;
この部分が入力・出力用の領域(メモリ)となります。


RotCalStructは自前の回転計算用データで、メイン側で1フレーム毎の回転角度や公転原点を設定し、並列処理側でそれを参照する形で使用しています。

 

自転の計算と移動

こちらも公転と同様にIJobParallelForTransformを継承したジョブ実行用の構造体にて行います。

/// <summary>
/// 自転運動
/// </summary>
struct RotationMotionUpdate : IJobParallelForTransform
{
    public NativeArray<RotCalStruct> Accessor;

    // JobSystem側で実行する処理
    public void Execute(int index, TransformAccess transform)
    {
        RotCalStruct accessor = this.Accessor[index];
        
        // 角度を加算更新
        accessor.CurrentRot += accessor.CurrentAngle;
        
        // オブジェクトの向きを更新
        transform.rotation = Quaternion.AngleAxis(accessor.CurrentRot, Vector3.up);
        
        // 更新したデータをメイン側へ返却する
        this.Accessor[index] = accessor;
    }
}

こちらの場合は、Executeが呼ばれる度にCurrentRotを加算更新しメイン側へ返却、メイン側はそれを保持して次回のコール時に使用して球体を回転させています。

 

メイン側の処理

メイン側では前述した回転角度や公転原点を設定のほかに、TransformAccessArrayの配列を定義しています。


そこに動作対象となる3つの球体のtransformを設定することで3つの球体それぞれの並列処理(IJobParallelForTransform)が呼ばれる仕組みとなっています。

/// <summary>
/// 回転計算用データ
/// </summary>
struct RotCalStruct
{
    // 1フレーム毎の回転角度
    public float CurrentAngle;
    // 現在の回転角度
    public float CurrentRot;
    // 算出した回転情報を保持
    public Matrix4x4 Matrix;
    // 公転原点
    public float RevOri_X;
    public float RevOri_Y;
    public float RevOri_Z;

    public RotCalStruct(float currentAngle)
    {
        this.CurrentAngle = currentAngle;
        this.CurrentRot = 0f;
        this.Matrix = new Matrix4x4();
        this.RevOri_X = 0;
        this.RevOri_Y = 0;
        this.RevOri_Z = 0;
    }
}


// Jobの終了待ち等を行うHandle
JobHandle _jobRevolutionHandle;	// 公転運動
JobHandle _jobRotatioHandle;	// 自転運動

// Job用の回転計算用データ
NativeArray<RotCalStruct> _revolutionStructs;
NativeArray<RotCalStruct> _rotationStructs;

// JobSystem側で実行する際に用いるTransfromの配列
TransformAccessArray _planetTransformAccessArray;

void Start()
{
    // 回転計算用データのメモリ確保
    this._revolutionStructs = new NativeArray<RotCalStruct>(planetArray.Length, 
                                                            Allocator.Persistent);

    this._rotationStructs = new NativeArray<RotCalStruct>(planetArray.Length, 
                                                          Allocator.Persistent);

    for (int i = 0; i < planetArray.Length; i++)
    {
        //-------------------//
        // 公転用情報設定      //
        //-------------------//
        // 公転用情報生成
        RotCalStruct acsess = new RotCalStruct(jobSetting[i].revolAngle);

        // 公転Matrixの生成
        acsess.Matrix = Rotate(acsess);

        // 公転原点
        acsess.RevOri_X = originPlanet.transform.position.x;
        acsess.RevOri_Y = originPlanet.transform.position.y;
        acsess.RevOri_Z = originPlanet.transform.position.z;

        // 公転用Acesserに設定
        this._revolutionStructs[i] = acsess;

        //-------------------//
        // 自転用情報設定      //
        //-------------------//
        // 自転用情報生成
        RotCalStruct acsess2 = new RotCalStruct(jobSetting[i].rotatAngle);

        // 自転軸設定
        acsess2.RodAxis = (int)jobSetting[i].RotAxis;

        // 軸の傾き設定
        acsess2.AxisAngle = jobSetting[i].RotAxisAngle;

        this._rotationStructs[i] = acsess2;
    }

    // IJobParallelForTransform用データ
    this._planetTransformAccessArray = new TransformAccessArray(planetArray.Length);
    this._planetTrses = new Transform[planetArray.Length];
    for (int i = 0; i < planetArray.Length; ++i)
    {
        var trs = planetArray[i].transform;
        this._planetTransformAccessArray.Add(trs);
        this._planetTrses[i] = trs;
    }
}

/// <summary>
/// 回転マトリックスの取得
/// </summary>
/// <param name="deltaTime"></param>
/// <param name="data"></param>
/// <returns></returns>
static Matrix4x4 Rotate(RotCalStruct data)
{
    // マトリックスの初期化
    Matrix4x4 m = Matrix4x4.identity;
    float x = 0f, y = 0f, z = 0f;
    m.SetTRS(new Vector3(x, y, z), Quaternion.identity, Vector3.one);

    // 原点設定
    float oriX = data.RevOri_X;
    float oriY = data.RevOri_Y;
    float oriZ = data.RevOri_Z;

    // 回転角度
    float rot = data.CurrentAngle * Mathf.Deg2Rad;
    float sin = Mathf.Sin(rot);
    float cos = Mathf.Cos(rot);

    // 任意の原点周りにY軸回転を行う
    m.m00 = cos;
    m.m01 = -1 * (oriX * cos) + oriX - oriZ * sin;
    m.m02 = sin;
    m.m20 = -1 * sin;
    m.m21 = oriX * sin + oriZ - oriZ * cos;
    m.m22 = cos;

    return m;
}

void Update()
{
    RevolutionMotionUpdate revolutionJob = new RevolutionMotionUpdate()
    {
        Accessor = this._revolutionStructs,
    };
    
    RotationMotionUpdate rotationJob = new RotationMotionUpdate()
    {
        Accessor = this._rotationStructs,
    };
    
    this._jobRevolutionHandle.Complete();
    this._jobRotatioHandle.Complete();

    this._jobRevolutionHandle = revolutionJob.Schedule(this._planetTransformAccessArray);
    this._jobRotatioHandle = rotationJob.Schedule(this._planetTransformAccessArray);
    JobHandle.ScheduleBatchedJobs();
}

void OnDestroy()
{
    // 解放/破棄など
    this._jobRevolutionHandle.Complete();
    this._jobRotatioHandle.Complete();
    this._revolutionStructs.Dispose();
    this._rotationStructs.Dispose();
    this._planetTransformAccessArray.Dispose();
}

 

Update()内にて公転のJob、自転のJobをそれぞれ発行し、Completeで待ち合わせています。
各JobはScheduleで発行しますが、実際の動作開始はScheduleBatchedJobsになります。

 

ちなみにUpdateの先頭でCompleteを行っているのは、JOB発行⇒完了待ち(同じフレーム)、よりも JOB発行⇒次フレーム⇒完了待ち、のほうがより高速化するからです。(・・・と資料に書いてあった)

 

実際に作ってみて

JobSystemの実装自体は難しいものではありませんでした、どちらかといえばY軸周りの公転用マトリックスを作るのに時間が掛かったぐらいです。(久しぶりに自力で行列計算した)

 

JobSystemを使えば弾幕系シューティングのように大量のオブジェクトを動かすときなどに効果を発揮すると思います。


ただ、JobSystemではUnityAPが殆ど使えないので、複雑な動きをさせたい場合などは今回のように自力でロジックを組む必要がありそうで、使いどころの見極めが重要になると感じました。

 

最後に動かす球体を70個ぐらいに増やしたバージョンも作ったので見てやってください。

 

ド、ドーン

 

 

f:id:Karvan:20180718000756g:plain

 

 

 

 

◇プライバシーポリシー

●個人情報の利用目的

当ブログでは、メールでのお問い合わせ、メールマガジンへの登録などの際に、名前(ハンドルネーム)、メールアドレス等の個人情報をご登録いただく場合がございます。

これらの個人情報は質問に対する回答や必要な情報を電子メールなどをでご連絡する場合に利用させていただくものであり、個人情報をご提供いただく際の目的以外では利用いたしません。

●個人情報の第三者への開示

当サイトでは、個人情報は適切に管理し、以下に該当する場合を除いて第三者に開示することはありません。

・本人のご了解がある場合
・法令等への協力のため、開示が必要となる場合

個人情報の開示、訂正、追加、削除、利用停止
ご本人からの個人データの開示、訂正、追加、削除、利用停止のご希望の場合には、ご本人であることを確認させていただいた上、速やかに対応させていただきます。

アクセス解析ツールについて

当サイトでは、Googleによるアクセス解析ツール「Googleアナリティクス」を利用しています。

このGoogleアナリティクスはトラフィックデータの収集のためにCookieを使用しています。このトラフィックデータは匿名で収集されており、個人を特定するものではありません。
この機能はCookieを無効にすることで収集を拒否することが出来ますので、お使いのブラウザの設定をご確認ください。

●免責事項

当サイトからリンクやバナーなどによって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。

当サイトのコンテンツ・情報につきまして、可能な限り正確な情報を掲載するよう努めておりますが、誤情報が入り込んだり、情報が古くなっていることもございます。

当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。

●プライバシーポリシーの変更について

当サイトは、個人情報に関して適用される日本の法令を遵守するとともに、本ポリシーの内容を適宜見直しその改善に努めます。

修正された最新のプライバシーポリシーは常に本ページにて開示されます。