見出し画像

コードの読めない人間がVRChatでNPCワールドを作ってみた


はじめに

みなさんはじめまして、VRChat楽しんでますか?いいですよね、VRChat。可愛いアバターを着て色んなワールドを観光するだけでも結構楽しいものです。(まあ最近はUnityやBlenderを触る頻度が増えてVRChatを起動することが減ってきましたが…)

そんな私ですがワールド巡りをしていて感動したワールドがいくつかあります。それはNPCシステムが実装されているワールドです。NPCシステムが実装されているワールドで有名なのは「NPC Cat toy - ねこじゃらし遊び」や「Trance's Hotel with NPCs」などがありますね。特に後者のワールドはNPCの種類が豊富で非常に素晴らしいワールドだと思います。

そして、こういったNPCワールドで遊んでいるうちに私もNPCワールドを作りたくなってしまったので作ってみることにしました。というわけで今回はタイトルの通りVRChatでNPCワールドを作ってみたので備忘録として記事を書いていきたいと思います。

(あんまりこの手の記事を書いたことないので駄文になると思いますがお許しください。また、我流での実装になるのでより効率的な方法があるかもしれません、っていうかあると思います。あくまで私はこういう実装方法をしたよ、っていう内容です。)

今回使用するアバターのショコラッタちゃんです
ぐるぐる目、いいですよね


自分にできること、できないこと

NPCワールドを作ると決めた私でしたが、実際のところ自分にそんなことができるかは半信半疑でした。一応私はSDK2時代からUnityを触っていたのでアバターの改変やギミックの作成、そして簡単なワールドの作成などはある程度できましたが、今回のような本格的なギミックを実装したワールドを作るのは初めてでした。

そういったこともあり、まずは先駆者の方々が残した情報を参考にすることにしました。が、なんということでしょう、先駆者の方々の多くは自分でNPCシステムのスクリプトを書いてしまう優秀な人ばかりなのです。残念ながら私はC#が書けませんし、そもそも読むこともできません。詰みです。

いきなり詰んでしまったわけですが、解決策はありました。Unity公式が配布しているいたStandardAssets(2018)の中にNPCシステム関係のスクリプトがあるので、それを流用すればとりあえずNPCを動かすことができるのです。

(残念ながら現在はStandardAssets(2018)をアセットストアから新規にダウンロードすることはできないようです。私の場合は過去にダウンロードしていたので今でもダウンロードすることができましたが…一応まだ正規の方法で入手する方法はあるみたいなので調べてみるといいかもしれません。勿論自己責任ではありますが。)

なぜか持っていたStandardAssetsくん

しかし、このスクリプトだけだとただ目標を追従するだけのNPCとなってしまい少し寂しいです。せっかくならプレイヤーのことを目で追いかけてくれたり、定期的に笑ってくれたり、一緒に椅子に座ったり、ベッドで添い寝したりしたいですよね。

先駆者の方々はこういうこともスクリプトで管理していたわけですが、頑張ればUnityのコンポーネントやアニメーター、そしてVRChatのUdonでなんとかなりそうでした。これだったら私でもできるんじゃないか?って思えてきたので諦めずに続けてみることにしました。



システムの構造(基礎部分)

今回作ったNPCシステムは色々なスクリプトやUdonギミックを組み合わせて作られています。それぞれ見ていきましょう。

NavMeshとNavMeshAgent

まずNPCシステムの基礎としてUnityのNavMesh(+NavMeshAgent)を使っています。NavMeshは指定したエリア内をNPCが自由に移動できるようにする機能で、事前に歩けるエリアと歩けないエリアを設定しておくことで障害物を避けながら目標に向かうNPCを作ることができます。移動できるNPCを作るときの定番らしいです。

(ここで少し罠があって、ワールド作成時のUnityのバージョンは2022だったので私は最初に最新版のNavMeshを使用したのですが、これをVRChatに持っていくと正常に動作しませんでした。どうやら最新版のNavMeshはVRChatでは使えないようで、旧版のNavMeshを使う必要がありました。一応VRChat開発も最新版のNavMeshを使えるようにする予定らしいのでみなさんがこの記事を読んでいる頃には使えるようになってるかもしれませんね。)

ベイクされたNavMesh
水色で塗られているエリアがNPCの歩けるエリア

AICharacterControlとThirdPersonCharacter

さらにNavMeshだけではNPCを動かすことはできないのでStandardAssetsから2つのスクリプトを持ってきて使用しています。

1つ目のスクリプトはAICharacterControlです。これは目標のオブジェクトを設定してあげることでNPCが自動で目標まで移動してくれるスクリプトです。NavMeshの情報を元に移動してくれるので、ちゃんと障害物を避けながら目標に移動してくれます。

しかし、このままだとNPCがT字ポーズのままスライド移動してしまうので、ここで2つ目のスクリプトであるThirdPersonCharacterを使います。これはNPCの移動速度などの情報から、付属するアニメーター(ThirdPersonAnimatorController)で再生するアニメーションを選択してくれるスクリプトです。これによってNPCが歩いたり走ったりしてくれます。

AICharacterControlとThirdPersonCharacter
TargetPositionについては後述(視線追従ギミックの項目で)

StandardAssetsに付属しているデフォルトのThirdPersonAnimatorControllerにはGround、Crouching、AirBorneの3つのStateがあります。Groundは立ち姿勢、Crouchingはしゃがみ姿勢、AirBorneは空中姿勢です。ぶっちゃけGround以外使わないので今回は削除しています。CrouchingとAirBorne関係のアニメーションパラメーターは使わないので消してもいいかもしれませんが、バグが怖いので私は残してます。多分消していい。

生き残ったGroundくん
使うパラメーターはForwardとTurnだけに

また、Groundステート内のアニメーションが全体的にゴツくて気に入らなかったので待機アニメーションや移動アニメーションをBoothで配布されていた可愛いものに変更しています。

BlendTree内のアニメーションを半分ぐらい変更
変更した待機時のアニメーション

というわけでNavMeshAICharacterControlThirdPersonCharacter、この3つだけでも目標に向かって自動で移動するNPCシステムができちゃいます。

注意点として、StandardAssetsのスクリプトは古いUnity向けのものなので、今回使用したスクリプトは問題ありませんが一部のスクリプトはエラーを起こすことがあります。エラーの修正方法は検索すれば出てくると思います。

また、これらのスクリプトはC#のままだと使用できないのでUdonSharpに直してあげる必要があるのですが、基本的にコピペで動いてくれます。(そもそもコピペで動いてくれないと私がそこで詰みます)UdonSharpに直してあげないとUnity上では動いてもVRChatに持っていくと動かなくなってしまうので注意です。

C#で作られたAICharacterControlとThirdPersonCharacterをUdonSharpに移植したもの

そしてこれはおま環かもしれませんが、私の環境ではプレイモード時にAICharacterControlとThirdPersonCharacterを同時に動かすとエラーを吐いてしまうので、AICharacterControlだけ後から動かすようにギミックを組んでいます。

AICharacterControlのスクリプトだけ事前にコンポーネントをOFFにしておいて
ゲーム開始時にコンポーネントをONにする処理(CyanTriggerで作成)
(CyanTriggerについては「機能を追加するために①②」の項目で)

NPCの同期について

とりあえずの精神でVRCObjectSyncコンポーネントをNPCにつけています。つけるだけでオブジェクトの位置や回転を同期してくれる凄いやつです。ただし、結構負荷が高いらしいのであんまりたくさんは使えないみたいです。

結構便利なObjectSyncくん


システムの構造(追加部分)

機能を追加するために①

さてNPCシステムの基礎部分はできたわけですが、今のままでは無表情でこちらに向かってくるだけのNPCとなってしまい少し寂しいです。なので色々と機能を追加していきます。

Unity標準機能のコンポーネントやアニメ―ターは便利ですが、より深く色々なことをするためにはやっぱりUdonをこねる必要があります。UdonはUdonGraphUdonSharpのニ種類があります。UdonGraphはノードを繋いでプログラミングをするもので、UdonSharpはC#みたいにコードを書いてプログラミングするものです。

私の場合、後者はC#が書けない読めないわからないので論外だとして、前者も後者よりは初心者向けみたいですが全く触ったことがないのでムリムリムリムリかたつむり状態です。SDK2のVRCTriggerは触ったことがあるので、そんな感じに組めたら楽なんですが、そんな便利なものがあるわけが…





ありました

救世主CyanTrigger様

CyanTriggerというものがあります。CyanTriggerはVRCTriggerのように項目を選んでギミックを組んでいくことができます。しかもUdonの機能もちゃんと使えちゃうという神ツールですね。(実は先ほどちょろっと出ていました。)

欠点もないわけではなく、基本的に情報が英語圏ぐらいにしかなく、またその量もUdonGraphやUdonSharpと比べると圧倒的に少ないです。私もUdonGraphやUdonSharp向けの解説を参考にCyanTriggerでギミック組んだりしたので結構遠回りなやり方をしていると思います。私はCyanTriggerが使いやすいのでこれを使っていますが、初めてUdonをこねる人は大人しくUdonGraphやUdonSharpを使った方がいいかもしれません。



機能を追加するために②

ここからはCyanTriggerを多用するので、事前にCyanTriggerの機能を軽く説明したいと思います。

・SyncSettings(同期設定)
一番上の項目ですがここの説明は後から書きました。最初は説明しなくてもいいかなって思ってたんですけどこれのせいで数日溶ける事件が発生したのでちゃんと説明を書いておきます。

この項目では同期方法について設定ができる項目です。選択できる項目はContinuous(連続的)、Manual(手動)、ManualWithAutoRequestNone(同期なし)の4つあります。ContinuousとManualはUdonを触ってる人ならお馴染みかもしれませんね。Continuousは変数の値を頻繁に更新してくれるもので、値が変更されなくても更新されるのでそこそこ重いらしいです。また、同期のタイミングも自分で選ぶことができないみたいです。ManualはRequestSerializationを使うことで変数の値を同期するもので、手動で同期させる必要こそあれど処理が早く、同期のタイミングも自分で選べるそうです。ManualWithAutoRequestはCyanTrigger独自のやつみたいです。これはManualを改良したものでRequestSerializationを使った変数の同期処理とかをCyanTriggerが勝手にやってくれるらしいです。便利ですね。Noneは文字通り同期なしです。

選べる4つの同期方法

そしてCyanTriggerにはAutoSetSyncModeというものがあって、これはCyanTriggerが自動でギミックにふさわしい同期処理を組んでくれるというものです。便利ですね。

AutoSetSyncModeを有効にしているとCyanTriggerが自動で同期方法を設定してくれる

しかし、たまーに使ってほしくない同期方法が選択されることがあり、それが原因でギミックがちゃんと動いてくれないことがあります。私もなぜか変数が同期されずに数日悩んでいたら、CyanTriggerくんが勝手にManualに設定していたということがありました。ギミックの組み方を間違えたのかと思って何度も何度も確認していただけあって気がついた時は脱力しましたね…そういうのを避けたければちゃんと自分で同期方法を選んだ方がいいかもしれません。

・Variables(変数)
CyanTriggerは様々な変数を設定することができます。よくあるbool、int、floatだけではなく、Vector3やGameObjectなど様々なものを入れることができるので頻繁に使うものは入れておくと便利です。ギミックのコピペとかもやりやすくなります。また、Syncの項目から変数の同期を行うかどうかも設定できます。

様々な変数を設定することができる

・Event
Eventは処理の開始条件とかを設定できます。よく使うEventはStart、Update、OnPickUpDown、OnPlayerTriggerEnterなどでしょうか。それぞれゲームスタート時に処理を開始、毎フレームごとに処理を開始、ピックアップしている状態でUseしたら処理を開始、Playerがトリガーの中に入ったら処理を実行、っていう感じです。

ここからEventを追加
ジャンルごとにEventが細かくわかれている
たとえばPickupジャンルの中にはOnPickUpDownあったりする

・EventHeader
これはStartをEventとして設定した時の画面です。この色々な項目があるところをEventHeaderと呼ぶらしいです。さて、Startの下にAnyoneという項目とLocalという項目がありますね。それぞれ見ていきましょう。

親の顔より見た画面

・EventGate
Anyoneの項目の正式名称はEventGateというもので、公式ドキュメントによると「Who can activate」、誰がEventを有効化できるかを設定できるようです。よく使うのはAnyone、Master、AllowListとかですかね。
Anyoneは誰でも、Masterはインスタンスのマスターだけが、AllowListはホワイトリストに入れた人だけが、それぞれEventを有効化することができます。

EventGateの項目

・EventBroadcast
Localの項目の正式名称はEventBroadcastというもので、公式ドキュメントによると「Who will activate」、誰がEventを実行できるかを設定できるようです。LocalはEventを有効化したローカルユーザーだけが、Send to Ownerはオブジェクトのオーナーだけが、Send to Allは全員が、それぞれEventを実行できます。

EventBroadcastの項目

・Action
Actionは実際に行う処理とかを設定できます。よく使うActionはGameObject.SetActive、Animator.SetBool(integer,float)、Transform.Setpositionなどがありますかね。それぞれGameObjectのON/OFF、アニメーター内のパラメーターにBool/Int/Floatを代入、オブジェクトの位置を設定、といった処理ができます。また、IFなどの処理もできるので便利です。Actionは非常に種類が多いので使いこなすのは結構大変だったりします。

+マークからActionを追加
ジャンル数は少ないが、その中に大量のActionが眠っている
Start時にGameObjectを有効化する処理


視線追従ギミック①

ここからは本格的に追加ギミックの説明をしていきます。

NPCがプレイヤーのことを目で追いかけてくれると、まるで本当に生きているみたいで良いですよね。ということで、まずは視線追従機能を作っていきます。

視線追従は主にAimConstraintコンポーネントを使って実装しています。AimConstraintコンポーネントにはオブジェクトを指定した目標の方向に向かせることができる機能があります。機能自体は簡易版のLookAtConstraintでも代用できると思います。

注意点としては、目標の方向を常に向いてしまうという性質上、デフォルトの設定では後ろに立っても胸や頭を180度回転させてこちらを見てしまいます。完全にホラーですね。そうならないためにもウェイトの値を調整することで可動域に制限をかける必要があります。私の場合、Chest:0.25、Head:0.5、LeftEye:0.75、RightEye:0.75で設定しています。

Chest、Head、LeftEye、RightEyeにそれぞれAimConstraintコンポーネントを設定

追従先のターゲットであるTargetPositionオブジェクトはAICharacterControlのターゲットでもありましたね。つまり視線追従の目標でもあり、NPCの移動目標でもあります。TargetPositionにはParentConstraintコンポーネントを設定しており、コンポーネント内のソースの値を弄ることで追従先を変更できるようにしています。ここでは2つのソースが存在していますが、下の方は後で説明します。

PHTがプレイヤーへの視線追従(今話している方)
PTがピックアップオブジェクトへの視線追従(PTについては後述)

まずは上の方から見ていきましょう。PHTはPlayerHeadTrackerの略で主にプレイヤーへの視線追従に使います。オブジェクト名が酷いことになってますが、これは視線追従のギミックを作成している時に色々と試行錯誤した時の名残なので気にしないでください。

CTで作ったPlayerHeadTrackerにCTで作ったDelay機能を加えた、という意味
試行錯誤していた時にCTを使わないPlayerHeadTrackerを作ったり、
DynamicBoneを使ったDelay機能を作ったりしたのでこんな名前に

PlayerHeadTrackerはその名の通りプレイヤーのHead位置をトラッキングするCTギミックです。処理としては、まずStart時にNetworking.GetLocalPlayerでローカルのプレイヤー情報を取得してPlayerData変数に代入し、さらにFixedUpdateで定期的にVRCPlayerAPI.GetBonePositionでPlayerDataからプレイヤーのHead位置を取得して、最後にTransform.SetpositionでPlayerHeadTrackerをHead位置に移動させています。

プレイヤーのHead位置をPlayerHeadTrackerが追従して、PlayerHeadTrackerをTargetPositionが追従して、AimConstraintでTargetPositionの方向を向かせる形で視線追従を実装しているわけですね。

PlayerHeadTrackerのCT処理

普通に視線追従をさせるだけならこれでいいのですが、今のままでは目標が高速移動したとしても問題なく視線追従ができてしまいます。普通、人がなにかを見つめていてその対象が動いた時、その対象に再び視線を合わせるまでには若干のラグが生じます。というわけで、視線追従に若干の遅延を加えたいと思います。

PHT_FollowObjectはPlayerHeadTrackerに遅延をしながら追従するCTギミックです。このPHT_FollowObjectをTargetPositionが追従することで視線追従のラグを表現しています。

処理はちょっと複雑です。まず3つのvector3タイプの変数を設定します。TargetPosition(v3)にはPlayerHeadTrackerの座標を、DelayTargetPosition(v3)にはPHT_FollowObjectの座標を、ResultPosition(v3)には計算結果の座標をそれぞれ代入していきます。

次にFixedUpdateで定期的にTargetPosition(v3)とDelayTargetPosition(v3)にそれぞれの座標を代入してあげます。

PHT_FollowObjectのCT処理①

そしてVector3.SmoothDamp_CTを使って、現在位置(current)であるDelayTargetPosition(v3)から目的地(target)であるTargetPosition(v3)まで0.25秒(smoothTime)で到達する座標を結果(results)としてResultPosition(v3)に代入しています。多分。そのはず。現在速度(currentVelocity)は内部の計算用で現在位置と目的地の距離によって変動するようです。最後にTransform.SetpositionでResultPosition(v3)の座標にPHT_FollowObjectを移動させます。これを繰り返すことでPHT_FollowObjectをPlayerHeadTrackerに遅延させながら追従させることができます。

PHT_FollowObjectのCT処理②
どうやってこれを見つけたのか本当に謎

正直に言うと、詳しい原理は自分でもわかってないです。上記の説明が間違っている可能性も普通にあります。そもそもVector3.SmoothDampじゃなくてVector3.SmoothDamp_CT(CyanTrigger用の別のやつ)を使っている理由もあんまり覚えてないです。もしかしたらCTじゃない方でもできるかもしれません。できないからそうしたのかもしれません。謎です。

追記:発生原因は不明なのですが、今回の方法で視線追従機能を実装すると、インスタンスマスター以外がNPCを見た時に凄い荒ぶったりします。

実際はもっと激しく荒ぶっている
(ちなみに頭の上にあるオブジェクトはデバッグ用)

この問題の修正方法を色々調べていたのですが完全に解決する方法は見つかりませんでした。しかし、症状を緩和する方法はあったので書いていきます。

これもまた原理は不明なのですが、TargetPositionの視線追従先であるPHT_FollowObjectの下の階層に何かしらのメッシュ付きのオブジェクトを配置すると荒ぶりが緩和されるようです。メッシュが表示されている状態であれば目に見えないほど大きさを小さくしても問題ないみたいです。

多分CubeでもSphereでも何でもいいはず
めちゃくちゃ小さくしたCube
コライダーは削除した

恐らくメッシュが入っていないGameObjectを追従先にするとこうなってしまうのかもしれません。ですが、この対策をしていも特定の条件でNPCが再び荒ぶったりするので正直よくわかりません。



視線追従ギミック②

NPCがプレイヤーを見つめてくれるようになりましたが、どうせなら他のオブジェクトも見つめてくれたら面白いですよね。というわけでさっき無視した下の方を見ていきましょう。

PTがピックアップオブジェクトへの視線追従(今から話す方)
PTはPickupableTargetの略

PickupableTargetはその名の通りピックアップ可能なCTギミック付きのオブジェクトです。このオブジェクトをピックアップ中にUseをするとNPCの視線追従目標がプレイヤーからピックアップオブジェクトに変更されます。具体的にはTargetPositionオブジェクトのParentConstraintのソース値が変更されることでPlayerHeadTrackerからPickupableTargetに追従目標が変わるということですね。正確には同じ階層にあるFollowObjectに追従します。

処理を見ていきましょう。まず、TargetSystemとPickableTarget_boolという2つの変数を設定します。

PickupableTargetの変数
TargetSystemとPickableTarget_bool(ん?)

TargetSystemはAnimatorタイプの変数で、名前の通りアニメーターを代入して使います。

TargetSystemアニメーターの中身
追従目標を切り替えるアニメーションの処理をしている

PickableTarget_boolはboolタイプの変数で、ピックアップ中のUseのON/OFFの管理に使います。このCTの変数PickableTarget_boolとアニメーターのパラメーターのPickableTargetはリンクさせて使うことになります。ちなみにPickableTarget_boolは同期変数です。

アニメーション切り替え用のBoolパラメーター(ん?)
これがTrueの時はPickupableTargetがONになる

(これを書いていて気が付きました、Pickableって普通にスペルミスですね…正しくはPickupableです。まあここではこのまま使っていきます。)

最初にピックアップしてUseした時の処理を作っていきます。ここではOnPickupUseUpを使っていますがOnPickupUseDownでも同じことができると思います。

OnPickupUseUpではUseした時にIf+ElseIfの処理を行っています。このIf処理はConditionの条件を満たせばConditionBodyの処理を実行し、そうでなければ処理を行わないというものです。ですが、今回はIfと一緒にElseIfも使っているので、Ifの条件を満たさなかった場合にElseIfの処理が行われます。ElseIfも条件を満たさなかった場合に初めて何も処理が行われないわけですね。

そしてこの処理はAnyone、誰でも有効化することができ、その処理はSendToAll、全員が処理することになります。

PickupableTargetのCT処理①全体図

まずはIfの処理を見ていきましょう。IfのConditionの処理ではPickupableTargetがOFFだった時の処理を行っています。bool.EqualsでPickableTarget_boolの値がFalseと等しいのであればcondition_bool1を出力しています。このcondition_bool1が出力されればConditionの条件が満たされてConditionBodyの処理が実行されます。condition_bool1が出力されなければElseIfの方に処理が流れていきます。

PickupableTargetのCT処理①A1

IfのConditionBodyの処理ではbool.SetでPickableTarget_boolをTrueに設定し、Animator.SetBoolでアニメーターパラメーターのPickableTargetをTrueに設定し、GameObject.SetActiveでSphereオブジェクトを表示しています。

簡単に説明すると、PickupableTargetをUseした時にPickableTarget_boolがFalseのときは当然PickupableTargetがOFFの状態なので、PickableTarget_boolとPickableTargetをそれぞれTrueに設定してあげてPickupableTargetをONにしてあげてるわけですね。

PickupableTargetのCT処理①A2

SphereオブジェクトはPickupableTargetのON/OFFをわかりやすくするために存在しており、PickupableTargetがOFFのときは白いSphereが表示され、反対にONのときは赤いSphereが表示されます。白いSphereはもともと表示されているのでここで操作されるのは赤いSphereの方です。

デフォルトの状態
ピックアップ時にUseした時の状態
実は白いSphereよりも大きい赤いSphereを表示しているだけ

次にElseIfのConditionの処理ではPickupableTargetがONだった時の処理を行っています。bool.EqualsでPickableTarget_boolの値がTrueと等しいのであればcondition_bool2を出力しています。このcondition_bool2が出力されればConditionの条件が満たされてConditionBodyの処理が実行されます。condition_bool2が出力されなければこのまま処理が終了します。

PickupableTargetのCT処理①B1

ElseIfのConditionBodyの処理ではbool.SetでPickableTarget_boolをFalseに設定し、Animator.SetBoolでアニメーターパラメーターのPickableTargetをFalseに設定し、GameObject.SetActiveでSphereオブジェクトを非表示にしています。まあ、まんまIfの時の反対の処理ですね。

PickupableTargetのCT処理①B2

さらに他のプレイヤーが後からJoinしてきた時に状態が同期されるような処理を作る必要があります。全体的な処理の流れは基本的にさっきと大体同じです。

ですが、EventHeaderは結構変わっているので注意です。この処理はStartで呼び起こし全員がローカルで処理します。Startの処理はたまにうまく処理されないことがあるのでDelayInSecondsで2秒の遅延を与えています。こんぐらい遅延させれば大体動いてくれます。

PickupableTargetのCT処理②全体図

まずIfのConditionの処理ではbool.EqualsでPickableTarget_boolがTrueかどうかをチェックし、条件を満たしていればcondition_bool1を出力します。

そしてConditionBodyの処理ではAnimator.SetBoolでTargetSystemアニメ―ターのPickableTargetパラメーターにTrueを代入します。また、GameObject.SetActiveを使ってSphereを有効化します。

PickupableTargetのCT処理②A

次にElseIfのConditionの処理ではbool.EqualsでPickableTarget_boolがFalseかどうかをチェックし、条件を満たしていればcondition_bool2を出力します。

そしてConditionBodyの処理ではAnimator.SetBoolでTargetSystemアニメ―ターのPickableTargetパラメーターにFalseを代入します。また、GameObject.SetActiveを使ってSphereを無効化します。

PickupableTargetのCT処理②B

最後にPHT_FollowObjectの時と同じようにPT_FollowObjectに遅延処理を追加しておきましょう。基本的にコピペで大丈夫ですが、Transformで使っているオブジェクトは再設定してあげる必要があります。(今思えばTransformを変数にしておけばコピペが楽でしたね…)

PT_FollowObjectのCT処理①
PT_FollowObjectのCT処理②

これでPickupableTargetをピックアップ中にUseするとNPCの追従目標を変更できるようになりました。今回はピックアップ中にUseした時に追従目標が変更されるようにしましたが、OnPickupとOnDropを使えばピックアップしている間だけ追従目標が変更されるようにもできたりします。このあたりはお好みで。



視線追従ギミック(補足)

地味に書くのを忘れていましたが、PHT_FollowObjectPickupableTargetにはVRCObjectSyncをつけてあげてください。これをやらないとNPCが凄い荒ぶります。PT_FollowObjectじゃなくてPickupableTargetにVRCObjectSyncをつけるのは、ピックアップオブジェクトにはVRCObjectSyncをつけないと動きが同期しないので、既にもうVRCObjectSyncがついているからですね。PHT_FollowObjectにVRCObjectSyncをつけると、自然とインスタンスマスターがNPCの初期追従目標になるようです。PickupableTargetを使えば追従目標を変更できますが、PickupableTargetをOFFにしたらインスタンスマスターに再び追従します。追従目標のプレイヤーを変更したい場合はインスタンスマスターがワールドを抜ける必要があります。次のインスタンスマスターが選ばれる条件はいまいちわかりませんが、インスタンスマスターの次に長くワールドにいたプレイヤーが選ばれるんじゃないでしょうか。もしかしたらランダムかもしれませんが。任意のプレイヤーに追従目標を変えるためには、追加で何かギミックを作らないと無理だと思います。



足音ギミック

人によっては邪魔に感じるかもしれませんが、もしもNPCが歩いた時に足音がしたらより生活感が出ますよね。というわけで足音ギミックを作っていきます。

NPCが歩いたら足音を鳴らすわけですが、「歩いたら」というのをどうやって検知すればいいか調べるので結構沼りました。NPCが一定距離移動したら足音を鳴らすとかでも良かったんですが、よく考えたらStandardAssetsnのThirdPersonCharacterってNPCの移動速度を検知してそれをアニメーターのfloat型のForwardパラメーターとして出力してくれているので普通にそれを使えばいいですね。Forwardの値が一定以上になったら足音を鳴らすようにすればよさそうです。

今回は2種類の足音を鳴らせるようにしてみました。WalkとRunです。といっても、設定が悪いせいかNPCを動かすと基本的に全速力で移動してしまってRunばっかりが再生されてしますので、わざわざ足音を2つに分けなくても良いかもしれません。

音源

NPCSoundは足音の音源を再生するためのCTギミックです。

ここではNPCSound用に3つの変数を設定します。NPCはAnimatorタイプの変数で文字通りNPCに付けられているアニメーターを設定しています。NPCSoundも同じくAnimatorタイプの変数で足音を鳴らすアニメーションの処理を担当しているアニメーターを設定しています。Forward_floatはfloatタイプの変数で2つのアニメーター間でForwardパラメーターを同期させるときに使用します。

NPCSoundの変数

処理を見ていきましょう。FixedUpdateで定期的に以下の処理をします。Animator.GetFloatでNPCアニメーターからForwardパラメーターの値を取得し、それをForward_floatに代入します。そして、Animator.SetFloatでNPCSoundアニメーター内のForwardパラメーターにForward_floatの値を代入します。これでNPCアニメーターとNPCSound内のForwardパラメーターの値が定期的に同期されることになります。

この処理は全員がローカルで実行します。

NPCSoundのCT処理①

後はNPCSoundアニメーター内でForwardパラメーターが一定以上であればWalkの音源を、さらに一定以上であればRunの音源をそれぞれ再生できるようにStateを組めば完成です。音源によってはAudioSourceコンポーネントから音源のピッチなどを調整してあげたほうがいいかもしれませんね。

2つのアニメーターで使用されるForwardパラメーター
NPCSoundアニメーターの処理

NPC用の足音ギミックができたのでプレイヤー用の足音ギミックも作っちゃおうと思ったんですが、自分以外のプレイヤーにも足音を付与するのが結構面倒だったので大人しくBoothで配布されていたものを使用することにしました。これめちゃくちゃ楽に設定できて便利です。神。



ランダム表情変更ギミック

せっかく可愛いNPCがいるのに無表情のままだと寂しいですよね。どうせならこちらを見つめて笑ってもらいたいです。いくつか表情の種類があるとさらにいいですね。というわけでランダム表情変更ギミックを作っていきましょう。

これを
こんな感じにしたい

ランダム表情変更ギミックを作るにあたって一番沼ったのはどのようなタイミングで表情を変更するかでした。私が当初やろうとしていたのは一定時間ごとにCustomEventを送ってランダムで表情を選択して再生するというものでした。ですが、CustomEventからCustomEventを送ろうとしたらUdonに怒られちゃったので別の方法を選ぶことにしました。

私が採用したのはOnPlayerTriggerEnterを使う方法でした。NPCの頭にTriggerOnlyのコライダーを設置して、その範囲にプレイヤーが侵入したら表情変更用のCustomEventを発生させ、ランダムに表情が変更されるようにしました。プレイヤーが近づいた時に笑顔になったりするので、結果的に最初に考えていた方法よりも自然な感じになりました。

Headの前方にTriggerOnlyのコライダーを設置する

しかし、NPC自体にはThirdPersonCharacter用のカプセルコライダーが既に設置されているので、NPCと同じ階層にGameObjectを作りそこにCTコンポーネントとボックスコライダーコンポーネントをまとめて設置することにしました。GameObjectの名前はNPCRandomFaceで表情の変更を処理するCTギミックです。

NPC用のアバターと同じ階層にCT用のGameObjectを設置する
名前はNPCRandomFace
コライダーをHeadに追従させるためにParentConstraintを使用している

ThirdPersonCharacterで使っているThirdPersonAnimatorControllerに追加の表情用のレイヤーを作成します。今回使う表情は0~7までの8種類です。表情の管理にはint型のFaceStateパラメーターを使用しています。

0のIdleStateにはデフォルトの表情と瞬きアニメーションが入っています。瞬きがあるとより生きている感が出ていいですよね。

表情管理用のレイヤーの中身
表情管理用のパラメーター

ここで注意なのですが、今回のギミックはSyncSettings(同期設定)をContinuous(連続的)にしておいた方がいいかもしれません。同期方法の自動設定だとManualWithAutoRequestが推奨だったのですが、あんまり上手く変数を同期してくれなかったのでめちゃくちゃくちゃ困りました。

ここの設定に気がつくまでに数日溶けました

さて、ここでは4つの変数を設定します。NPCはAnimatorタイプの変数で今回は表情変更用に使います。NPCHeadColliderはColliderタイプの変数でNPCRandomFace用の頭に追従するコライダーを設定してあげます。FaceState_intはFaceStateパラメーターのCT処理用の変数です。FaceState_intは同期変数でプレイヤー間の表情の同期にも使用します。見切れていますがRandom_Fcae_Probabilityはfloatタイプの変数で表情を変化させるかどうかを判定するために使用します。

NPCRandomFaceの変数

処理の大まかな流れは以下の通りです。

まずStart時にFaceStateパラメーターとFaceState_intの値を同期させます。

そしてOnPlayerTriggerEnterでプレイヤーがコライダーの範囲内に侵入したらStart_Random_Face_Master、Start_Random_Face_Sync、End_Random_Face_Master、End_Random_Face_Syncの4つのCustomEventを発生させます。_MasterのCustomEventはインスタンスマスターだけが処理を実行し、_SyncのCustomEventは全員が処理を実行します。

Start_Random_Face_MasterではIf処理とRandom_Fcae_Probabilityを使って1/2の確率で表情を変化させるか判定し、条件を満たしていた場合はランダム生成した値をFaceState_intに代入します。条件を満たしていなかった場合はElseを使って0の値をFaceState_intに代入します。さらにNPCHeadColliderを非表示にします。(確率で表情を変えるようにしているのは表情変更の頻度を減らすためです。表情がコロコロ変わるのも可愛いですけどちょっと変ですからね。もしも毎回確実に表情を変化させたいのであればこのIf処理を無くしても大丈夫です。)

Start_Random_Face_SyncではFaceState_intの値をFaceStateパラメーターに代入します。FaceState_intをアニメーター内のFaceStateパラメーターに代入することで初めて表情が変わります。

End_Random_Face_MasterではNPCHeadColliderを再表示します。

End_Random_Face_SyncではFaceState_intの値をリセットし、その値をFaceStateパラメーターに代入します。これで表情が初期状態に戻ります。

途中でNPCHeadColliderを非表示/再表示しているのは、こうすることで一度プレイヤーがコライダーの外にでなくてもプレイヤーが再びコライダーの中に入ったという判定を出せるので、プレイヤーがコライダーの中にいる間はギミックを継続させることができるからですね。

NPCRandomFaceのCT処理の大まかな流れ
(表情はExpressionが正しいが、ここではFaceで)

具体的な処理を見ていきましょう。まずStart時にAnimator_GetIntegerでFaceStateパラメーターの値をFaceState_intに代入して値を同期させます。

FaceState_intの値は同期されているので、この処理はインスタンスマスターだけがすれば大丈夫です。

NPCRandomFaceのCT処理①

次にOnPlayerTriggerEnter.LocalPlayerを使って、インスタンスマスターがNPCRandomFace内のコライダーに侵入した時に4つのCustomEventを以下の条件で発生させます。

・Start_Random_Face_Master
UdonBehaviour.SendCustomEventを使用して瞬時に発生させる
・Start_Random_Face_Sync
UdonBehaviour.SendCustomEventDelayedSecondsを使用して2秒後に発生させる
・End_Random_Face_Master
UdonBehaviour.SendCustomEventDelayedSecondsを使用して8.5秒後に発生させる
・End_Random_Face_Sync
UdonBehaviour.SendCustomEventDelayedSecondsを使用して8.5秒後に発生させる

この処理を有効化するのはインスタンスマスターだけですが、処理は全員が実行します。

NPCRandomFaceのCT処理②A1
NPCRandomFaceのCT処理②A2

CustomEventのStart_Random_Face_Masterでは以下の処理を実行します。

まずCollider.SetenabledでNPCHeadColliderを非表示にします。

次にIfの処理です。IfのConditionの処理ではRandom.Getvalueで0~1の範囲でランダム生成したfloat型の値をRandom_Fcae_Probabilityに代入し、float.GreaterThanOrEqualでその値が0.5以上であるかを判定し、その条件を満たしていればcondiion_bool_aを出力するという処理をしています。

IfのConditionBodyの処理ではRandom.Rangeを使用して1以上8未満のint型の値をランダムで出力してそれをFaceState_intに代入します。
(地味に沼ったんですが、floatは以上以下、intは以上未満らしいです)

そして念の為にElseの処理を追加しておきます。int.SetでFaceState_intに0を代入するようにしておきます。

このCustomEventの処理はインスタンスマスターだけが実行します。FaceState_intは同期される変数ですし、NPCHeadColliderはインスタンスマスターしかアクセスできませんから、インスタンスマスターだけがこの処理を実行すればいいわけですね。

NPCRandomFaceのCT処理③A1
NPCRandomFaceのCT処理③A2

CustomEventのStart_Random_Face_SyncではAnimator.SetIntegerでFaceState_intの値をNPCアニメーターのFaceStateパラメーターに代入します。

このCustomEventの処理は全員が実行します。

NPCRandomFaceのCT処理④

CustomEventのEnd_Random_Face_MasterではCollider.SetenabledでNPCHeadColliderを再表示します。

この処理はインスタンスマスターだけが実行します。

NPCRandomFaceのCT処理⑤

CustomEventのEnd_Random_Face_Syncではint.SetでFaceState_intの値を0にリセットして、Animator.SetIntegerでFaceState_intの値をNPCアニメーターのFaceStateパラメーターに代入します。

このCustomEventの処理は全員が実行します。

NPCRandomFaceのCT処理⑥

これでNPCの表情がランダムに変更されるギミックが完成しました。変数のおかげでアニメーターさえ事前に用意しておけば簡単に使い回せます。これはさっそく次のギミックでも使っていきます。



アニメーション再生ギミック①

色々と機能が増えてそれっぽいNPCになってきましたが、今のところプレイヤーかピックアップオブジェクトに追従して、視線を追従するか表情を変えるぐらいしかしてくれないんですよね。

どうせならベッドに入ったら添い寝してくれるとか、ソファーに座ったら隣に座ってくれるとか、鏡の前に立ったら鏡に向かってポーズするとか、そういう追従以外の行動もしてもらいたいです。

というわけで次は特定の位置でアニメーションを再生するギミックを作ってきましょう。

アニメーション再生ギミックを作るにあたって沼ったのはどうやってNPCにアニメーションを再生させるかでした。当初の予定では動き回るNPCが指定の位置に移動したらそのNPCにアニメーションを再生させるというやり方を考えていましたが、それだとNPCのアニメーターとギミックがめちゃくちゃ複雑になりそう、っていうか実際にそうなったのでその方法は諦めることにしました。

私が採用した方法は動き回るNPCとアニメーションを再生するNPCを分離させるやり方でした。(以下動き回るNPCを「NPC」、アニメーションを再生するNPCを「StaticNPC」とします)

NPCが指定の位置に移動した時にギミックを発動させ、NPCを非表示にしてStaticNPCを表示するわけですね。NPCとStaticNPCを切り替える時にNPCが突然消えたように見える(実際消えてますが)という欠点はありますが、この方法はギミックを作るのが凄い楽だったので今後も使っていくと思います。

ベッドで横になるショコラッタちゃん
ソファーで隣に座ってくれるショコラッタちゃん
テーブル越しに座っているショコラッタちゃん
バスタブの中に入っているショコラッタちゃん
鏡の前でポーズをするショコラッタちゃん

さて、ここからはギミックの説明をしていきます。アニメーションを再生するエリアは合計で5つあり、それぞれBed、Sofa、TableAndChair、Bath、Mirrorです。このそれぞれに専用のStaticNPCが存在しており、そのStaticNPCと同じ階層にStaticNPCAnimationというCTギミックを作っていきます。一部にはNPCRandomFaceもつけていきますが、これは後で説明します。

アニメーションを再生する場所
場所ごとにグループ分けしている
Mirrorの中には以下のオブジェクトがある
StaticNPC(Chocolatta_lt)
P1(StaticNPCの位置調整用)
StaticNPCAnimation(CTギミック)

StaticNPCのアニメーターは基本的に動かしたいアニメーションを入れたStateを置くだけで大丈夫です。NPCRandomFaceをつける場合は表情用のLayerも必要になりますが、こういった凝ったことをやるのはStaticNPCAnimationの説明が終わってからにします。

Mirror用のStaticNPCのアニメーター

StaticNPCAnimationには以下の4つの変数を設定します。NPCIsInAreaはboolタイプの同期変数でNPCがエリアの中にいるかを判定します。PlayerIsInAreaはboolタイプの同期変数でプレイヤーがエリアの中にいるかを判定します。NPCはGameObjectタイプの変数で動き回る方のNPCを指定してあげます。StaticNPCはGameObjectタイプの変数でアニメーション再生用のNPCを指定してあげます。

StaticNPCAnimationの変数

処理の大まかな流れは以下の通りです。結構Eventの数が多くなっちゃいましたが、処理自体はそんなに難しくありません。

OnTriggerStay.GameObjectではNPCがエリア内にいるかを判定し、条件を満たしていた場合にはNPCIsInAreaにTrueを代入します。

OnTriggerExit.GameObjectではNPCがエリアの外に出たかを判定し、条件を満たしていた場合にはNPCIsInAreaにFalseを代入します。

OnPlayerTriggerStay.LocalPlayerではインスタンスマスターがエリア内にいるかを判定し、条件を満たして場合にはPlayerIsInAreaにTrueを代入します。

OnPlayerTriggerEnter.LocalPlayerではインスタンスマスターがエリア内に入ったかを判定し、条件を満たしていた場合にはCustomEventのStart_Switch_NPC_Syncを発生させます。_SyncのCustomEventは全員が処理を実行します。

OnPlayerTriggerExit.LocalPlayerではインスタンスマスターがエリアの外に出たかを判定し、条件を満たしていた場合にはCustomEventのEnd_Switch_NPC_Syncを発生させます。そして、NPCIsInAreaとPlayerIsInAreaにFalseを代入します。

CustomEventのStart_Switch_NPC_SyncではIf処理を使ってNPCとStaticNPCのの切り替えをしています。NPCを非表示にしてStaticNPCを表示します。さらにElseIfを使ってIf処理が正常に動かなかった時の処理もしています。

CustomEventのEnd_Switch_NPC_SyncではStart_Switch_NPC_Syncで行った切り替えの反対の処理を行います。NPCを表示にしてStaticNPCを非表示します。

StartではIf処理を使ってNPCIsInAreaとPlayerIsInAreaがTrudであった場合にCustomEventのStart_Switch_NPC_Syncを発生させます。これは後からワールドに入った人向けの処理ですね。

StaticNPCAnimationのCT処理の大まかな流れ

具体的な処理を見ていきましょう。OnTriggerStay.GameObjectはInputで指定したGameObjectがTrigger内に滞在している時に発動するEventです。ここではNPCが指定されており、NPCがTrigger内で滞在している間はbool.setでNPCIsInAreaにTrueが代入されます。

この処理はインスタンスマスターだけが実行します。

StaticNPCAnimationのCT処理①
緑色の線で囲まれているのがSofaのTrigger

OnTriggerExit.GameObjectはInputで指定したGameObjectがTrigger外に出た時に発動するEventです。ここではNPCが指定されており、NPCがTrigger外に出た瞬間にbool.setでNPCIsInAreaにTrueが代入されます。

この処理はインスタンスマスターだけが実行します。

StaticNPCAnimationのCT処理②

OnPlayerTriggerStay.LocalPlayerはプレイヤーがTriggerの中で滞在している時に発動するEventで、その間はbool.SetでPlayerIsInAreaにTrueが代入されます。

この処理はインスタンスマスターだけが実行します。

StaticNPCAnimationのCT処理③

OnPlayerTriggerEnter.LocalPlayerはプレイヤーがTrigger内に入った時に発動するEventで、SendCustomEventDelayedSecondsでCustomEventのStart_Switch_NPC_Syncを3秒後に発生させます。

ここで3秒の遅延を設定しているのはStart_Switch_NPC_SyncのIf処理でNPCIsInAreaとPlayerIsInAreaがTrueになっている必要があるからです。プレイヤーとNPCでTrigger内に入るタイミングは違いますから、念の為に遅延をかけているわけですね。

この処理を有効化できるのはインスタンスマスターだけですが、処理は全員が実行します。

StaticNPCAnimationのCT処理④

OnPlayerTriggerExit.LocalPlayerはプレイヤーがTriggerの外に出た時に発動するEventで以下の処理を実行します。まずbool.SetでNPCIsInAreaとPlayerIsInAreaにFalseを代入します。そしてSendCustomEventDelayedSecondsでCustomEventのEnd_Switch_NPC_Syncを2秒後に発生させます。この遅延は単純に見栄えのための遅延です。プレイヤーがTriggerの外に出た瞬間NPCが立ち上がってくるのもなんか怖いので。

この処理を有効化できるのはインスタンスマスターだけですが、処理は全員が実行します。

StaticNPCAnimationのCT処理⑤

CustomEventのStart_Switch_NPC_SyncはIf処理を使って条件を満たせばNPCを非表示にしてStaticNPCを表示します。ElseIfはIfの条件を満たさなかった場合にもう一度Start_Switch_NPC_Syncを発生させます。3秒の遅延で間に合わなかった時用ですね。

このCustomEventの処理は全員が実行します。

StaticNPCAnimationのCT処理⑥全体図

IfのConditionではbool.EqualsでPlayerIsInAreaがTrueだった時にob1を出力し、さらにNPCIsInAreaがTrueだったらob2を出力し、bool.ConditionalAndでob1とob2の値が両方Trueであればcondition_bool_a1を出力します。

StaticNPCAnimationのCT処理⑥A1

IfのConditionBodyではGameObject.SetActiveでNPCを非表示にし、StaticNPCを表示しています。

StaticNPCAnimationのCT処理⑥A2

ElseIfのConditionではbool.EqualsでPlayerIsInAreaがTrueだった時にob1を出力し、さらにNPCIsInAreaがFalseだった時にob2を出力し、bool.ConditionalAndでob1とob2の値が両方TrueであればCondition_bool_a2を出力します。

StaticNPCAnimationのCT処理⑥B1

ElseIfのConditionBodyではSendsCustomEventDelayedSecondsを使って10秒後に再びCustomEventのStart_Switch_NPC_Syncを発生させるようにしています。

StaticNPCAnimationのCT処理⑥B2

CustomEventのEnd_Switch_NPC_SyncではGameObject.SetActiveでNPCを表示し、StaticNPCを非表示にしています。

このCustomEventの処理は全員が実行します。

StaticNPCAnimationのCT処理⑦

Startでは後からワールドに入ったプレイヤーにもStaticNPCAnimationの処理を同期させる処理をしています。DelayInSecondsの設定を2秒にしているのはStartの処理がうまく処理されないことがあるからですね。PickupableTargetのStart処理の時も同じことをしたと思います。

StaticNPCAnimationのCT処理⑧全体図

IfのConditionではbool.EqualsでPlayerIsInAreaがTrueだった時にob1を出力し、さらにNPCIsInAreaがTrueだったらob2を出力し、bool.ConditionalAndでob1とob2の値が両方Trueであればcondition_bool_startを出力します。

StaticNPCAnimationのCT処理⑧A

IfのConditionBodyではSendCustomEventでCustomEventのStart_Switch_NPC_Syncを発生させています。

StaticNPCAnimationのCT処理⑧B


アニメーション再生ギミック②

これでプレイヤーが特定のエリアに入った時にStaticNPCがアニメーションを再生してくれるようになりました。

ですが、StaticNPCとNPCは別個体なので視線追従や表情変更ギミックは搭載されていません。なのでStaticNPCにもNPCと同じようにギミックを追加してあげる必要がありますね。

アニメーションを再生するエリアとしてBed、Sofa、TableAndChair、Bath、Mirrorの5つを用意しましたが、この中でBedとMirrorは視線追従や表情変更ギミックが必要ないので、ここではSofa、TableAndChair、Bathの3つエリアで実装していきます。Bedの表情アニメーションは目を瞑っているだけですし、Mirrorはアニメーションに合わせた専用の表情を設定しているので表情変化をさせる必要がないですからね。

プレイヤーを笑顔で見てくれているショコラッタちゃん

まずは視線追従ギミックから実装していきましょう。必要な設定は視線追従ギミックの項目で既にやってあるので、ここでやるのはStaticNPCにConstraintを追加してあげるだけです。

視線追従ギミックの項目ではAimConstraintを使いましたが、LookAtConstraintでも普通に動いたのでなんとなくこっちを使っています。どっちの処理が軽いとかはわからないです。

Chest、Head、LeftEye、RightEyeにそれぞれLookAtConstraintを追加する

表情変更ギミックの方はちょっとやることが増えます。まずランダム表情変更ギミックの項目でやったようにStaticNPCのアニメーターに表情変更用Layerを作ってあげます。

表情変更用のLayer
表情変更用のInt

そしてStaticNPCやStaticNPCAnimationと同じ階層にNPCRandomFaceを持ってきてあげます。

このNPCRandomFaceはコピペで持ってきただけなので、変数の参照先を直してあげる必要があります。NPCはStaticNPCのアニメーターを設定し、NPCHeadColliderはStaticNPCと同じ階層にあるNPCRandomFace、つまり今弄っているこれを設定してあげます。

CTギミックをコピペしても参照元はそのままなので注意


アニメーション再生ギミック③

このまま終わっても良かったのですが、Bedのアニメーションが一種類だとちょっと寂しかったのでニ種類にしてみました。

ポーズ1
ポーズ2

Bed用のStaticNPCAnimationの下にSleepPoseというCTギミックを作成します。これは一定確率でベッド上のStaticNPCのアニメーションを変更するCTギミックです。

SleepPose
Bed用のアニメーター
2つのアニメーションが存在している
睡眠アニメーション管理用のboolであるSleepPose
SleepPoseのTriggerの範囲はStaticNPCAnimationと同じにする

具体的な処理を見ていきましょう。SleepPoseには以下の3つの変数を設定します。BedはAnimatorタイプの変数でStaticNPCのアニメーターを設定します。SleepPose_boolはパラメーターのSleepPoseとリンクする変数で、同期変数です。Random_Floatは睡眠アニメーションを変更するかどうかを確率で判定するために使用します。

SleepPoseの変数

処理の大まかな流れは以下の通りです。

OnPlayerTriggerEnter.LocalPlayerではインスタンスマスターがエリア内にいるかを判定し、条件を満たしていた場合にCustomEventのRandom_SleepPoseとRandom_SleepPose_Syncを発生させます。

Random_SleepPoseではIf処理とRandom_Floatを使って睡眠アニメーションを変更するかどうかを処理します。結果によってSleepPose_boolがTrueになるかFlaseのままか決まります。

Random_SleepPose_SyncではSleepPose_boolの値によってSleepPoseパラメーターに代入する値が変わります。TrueであればTrueが代入されます。

StartではRandom_SleepPose_Syncと同じ処理をします。後からワールドに入ってきたプレイヤー用にSleepPoseの処理を同期する処理ですね。

SleepPoseのCT処理の大まかな流れ

OnPlayerTriggerEnter.LocalPlayerはインスタンスマスターがTrigger内に侵入した時にCustomEventのRandom_SleepPoseとRandom_SleepPose_Syncを発生させます。

この処理を有効化できるのはインスタンスマスターだけですが、処理は全員が実行します。

SleepPoseのCT処理①

CustomEventのRandom_SleepPoseはIf処理を使って条件を満たせば睡眠アニメーションを変更します。条件を満たさない場合はElseでアニメーションを変更しません。

この処理はインスタンスマスターだけが実行します。SleepPose_boolは同期変数なのでインスタンスマスターが値を変更すれば同期されるからですね。

SleepPoseのCT処理②の全体図

IfのConditionではRandom.Getvalueでランダム生成した値をRandom_Floatに代入し、float.GreaterThanOrEqualでその値が0.5以上であればcondition_bool_aを出力します。

IfのConditionBodyではbool.SetでSleepPose_boolにTrueを代入します。

Elseではbool.SetでSleepPose_boolにFalseを代入します。

SleepPoseのCT処理②

CustomEventのRandom_SleepPose_SyncはIf処理を使って条件を満たせばTrueの値をSleepPoseパラメーターに代入します。条件を満たさない場合はElseでFalseの値をSleepPoseパラメーターに代入します。

SleepPoseのCT処理③全体図

IfのConditionではbool_EqualsでSleepPose_boolがTrueであればcondition_bool_bを出力します。

IfのConditionBodyではAnimator.SetBoolでBedアニメーターのSleepPoseパラメーターにTrueを代入します。

ElseではAnimator.SetBoolでBedアニメーターのSleepPoseパラメーターにFalseを代入します。

SleepPoseのCT処理③

Startでは後からワールドに入ったプレイヤーにもSleepPoseの処理を同期させる処理をしています。DelayInSecondsの設定を5秒にしているのはStartの処理がうまく処理されないことがあるのと、同じ理由でStaticNPCAnimationのStartも2秒の遅延を加えているからですね。StaticNPCAnimationのStart処理の後にSleepPoseのStart処理を実行したいのでこうしています。

SleepPoseのCT処理④全体図

IfのConditionではbool_EqualsでSleepPose_boolがTrueであればcondition_bool_startを出力します。

IfのConditionBodyではAnimator.SetBoolでBedアニメーターのSleepPoseパラメーターにTrueを代入します。

ElseではAnimator.SetBoolでBedアニメーターのSleepPoseパラメーターにFalseを代入します。

SleepPoseのCT処理④

これで一定確率で睡眠アニメーションが変更されるようになりました!



おわりに

というわけでコードが読めない書けないわからない私でもなんとかNPCワールドを作ることができました。よかったら見に来てみてください。殺風景なワールドではありますがショコラッタちゃんが可愛いので問題ありません。

このワールドは空き時間にちょっとずつ作業を進めたり、休日に頑張ったりして大体1週間ぐらいで完成しました。試行錯誤の連続だったこともあり、結構大変でした。

この記事を書くのも1週間ぐらいかかっていて、本格的に文章を書くのが久しぶりだったので結構疲れました。大した文量はないのですが、ギミックを修正しながら作業していたのでこんな時間かかっちゃったのかなと思います。

こうして文章にまとめてみると理解が深まったり新しい発見があったりして良かったです。まあこの記事を書いている時により良いギミックの組み方を見つけたりして半分ぐらい書き直すことになったりもしていますが…(嬉しいような嬉しくないような…)ランダム表情変更ギミックとかアニメーション再生ギミックとかが結構曲者で何回もギミックを作り直すことになりました。後は全体的に同期が面倒すぎました。一生和解できない気がします。

将来的には製品版ショコラッタちゃんを使って、ショコラッタちゃんと一緒にお部屋で過ごせるワールドとかを作りたいですね。その時にはもっとギミックを追加したいです。例えば頭をなでたり胸を触ったらリアクションをとってくれるギミックとか、プレイヤーの頭をナデナデしてくれるギミックとか、自動徘徊をしてくれるギミックとか、お風呂に入ったらスク水とかバスタオル姿に着替えてくれるギミックとか、お菓子とかを食べさせてあげられるギミックとか、とにかく色々と実装したいギミックがありますね。

ここまで私の記事を読んでいただきありがとうございました。駄文のせいで読みづらかった思いますが、皆様がNPCワールドを作成する時の参考になったら嬉しいです。



アセットリスト

最後にこのワールドのアセットリストを載せて終わりにしたい思います。

【アバター】
・オリジナル3Dモデル「ショコラッタ」Quest対応版

【ギミック】
・CyanTrigger
・【VRChat】YamaPlayer - VRChat向け動画プレイヤー【Video Player】
・【VRChat】VRC Hand Menu ハンドメニュー【Udonギミック】
・VRCPlayersOnlyMirror (背景なし No background)
・FootStepSystem 無料版

【ペン】
・QvPen
・【無料】軽量化したQvPen差し替えモデル 【ワールド】

【シェーダー】
・lilToon

【オブジェクト】
・VRChat向け家具セット(1)~(4)

【アニメーション】
・VRChat用立ち・呼吸Idleアニメーション
・キャプチャーロコモーション Vol.01
・【無料】揺れ物チェックとかに使えそうなアニメーション
・VeryAnimation

【音声素材】
・スリッパで歩く 効果音ラボ
・スリッパで走る 効果音ラボ



いいなと思ったら応援しよう!