20200722のUnityに関する記事は5件です。

URP下でのSteamVR PluginV2を使用したVR開発メモ。

はじめに

これは毎回自分のPCの中をたどりながらVR開発をするのがめんどくさくなったがための開発覚書です。後々書き足す可能性有り。
-追記
タイトルにURP下でのと入れていますがコントローラーのシェーダーを変えることしかやってません、ごめんなさい

本題

  • もろもろのアセットを入れたらCameraRigを設置する。CameraRigの子のmodelのシェーダーはURP対応ではないので手動でLitシェーダーに変える。
  • SteamVR Inputからdefaultのjsonファイルを作る。コントローラーの割り当てを変えたい場合はOpen binding UI。

自分だけのコントローラーの設定を行いたい場合

  • 自分だけのコントローラーの設定を行いたい場合SteamVR Inputで設定を作ってあげる
  • そのあとOpen Binding UIからbindの設定を行う こんな感じ(アクションポーズ、Haptic、スケルトンも下から設定する。) image.png
  • デフォルトバインドの置換を押す(必要かはわからないけど押したところで支障はない。)
  • デフォルトであるコンポーネントのBehaviour_PoseのPose Actionを自分で作った設定に適用する。
  • またスクリプトでコントローラーにイベントを登録する際も自分で作った設定にすること。

    注意事項

  • Poseの設定は必須なのでアクションポーズの設定から左手未加工にPoseを割り当てる。

  • Skeltonの設定は一回ミラーを外してからスケルトンの設定で割り当てる。


  • これでPlayすると移動できないがコントローラーが見えるようになる。
  • Input系列のコードを書く
    以下使用例
using Valve.VR;
//押すボタン、Inspectorから選ぶ
public SteamVR_Action_Boolean actionButton;
//handtypeはInspectorから右か左かで選ぶ
public SteamVR_Input_Sources handtype;
//判定
if(actionButton.GetStateDown(handtype)){}

//コントローラーの動きをとる場合
SteamVR_Behaviour_Pose controllerPose;
//角度、方向
Vector3 vec3 = controllerPose.GetAngularVelocity();
//振っている力
Vector3 power = controllerPose.GetVelocity();

コードの例

Teleport

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;

public class Teleport : MonoBehaviour
{
    public SteamVR_Input_Sources handtype;
    public SteamVR_Behaviour_Pose controllerPose;
    public SteamVR_Action_Boolean teleportAction, triggerAction;

    public GameObject laserPrefab;
    private GameObject laser;
    private Transform laserTransform;
    private Vector3 hitPoint;
    public Transform cameraRigTransform;
    public GameObject teleportReticlePrefab;
    private GameObject reticle;
    private Transform teleportReticleTransform;
    public Transform headTransform;
    public Vector3 teleportReticleOffset;
    public LayerMask layerMask;
    private bool shouldTeleport;
    // Start is called before the first frame update
    void Start()
    {
        laser = Instantiate(laserPrefab);
        laserTransform = laser.transform;
        reticle = Instantiate(teleportReticlePrefab);
        teleportReticleTransform = reticle.transform;
    }

    // Update is called once per frame
    void Update()
    {
        if (teleportAction.GetState(handtype))
        {
            RaycastHit hit;

            if (Physics.Raycast(controllerPose.transform.position, transform.forward, out hit, 100, layerMask))
            {
                hitPoint = hit.point;
                ShowLaser(hit);
                reticle.SetActive(true);
                teleportReticleTransform.position = hitPoint + teleportReticleOffset;
                shouldTeleport = true;

                if (triggerAction.GetStateDown(handtype) && shouldTeleport)
                {
                    TeleportPlayer();
                }
            }
        }
        else
        {
            laser.SetActive(false);
            reticle.SetActive(false);
        }
    }

    private void ShowLaser(RaycastHit hit)
    {
        laser.SetActive(true);
        laserTransform.position = Vector3.Lerp(controllerPose.transform.position, hitPoint, 0.5f);
        laserTransform.LookAt(hitPoint);
        laserTransform.localScale = new Vector3(laserTransform.localScale.x, laserTransform.localScale.y, hit.distance);
    }

    private void TeleportPlayer()
    {
        shouldTeleport = false;
        reticle.SetActive(false);
        Vector3 difference = cameraRigTransform.position - headTransform.position;
        difference.y = 0;
        cameraRigTransform.position = hitPoint + difference;
    }
}

物をつかむGrabObject

  • この場合掴みたい物に対して投げた時に力を加えたい&&FixedJointで掴むものとをくっつけたいので、Rigidbodyを付与しておく
  • 掴むものにもRigidbodyを付けておく。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;

public class GrabObject : MonoBehaviour
{
    public SteamVR_Input_Sources handtype;
    public SteamVR_Behaviour_Pose contollerPose;
    public SteamVR_Action_Boolean grabAction;

    private GameObject collidingObject;
    private GameObject objectInHand;


    // Update is called once per frame
    void Update()
    {
        if (grabAction.GetStateDown(handtype))
        {
            if (collidingObject)
            {
                Grab();
            }
        }

        if (grabAction.GetStateUp(handtype))
        {
            if (objectInHand)
            {
                ReleaseObject();
            }
        }
    }

    void OnTriggerEnter(Collider other)
    {
        SetCollidingObject(other);
    }

    void OnTriggerStay(Collider other)
    {
        SetCollidingObject(other);
    }
    void OnTriggerExit(Collider other)
    {
        if (!collidingObject)
        {
            return;
        }
        collidingObject = null;
    }

    private void Grab()
    {
        objectInHand = collidingObject;
        collidingObject = null;
        var joint = AddFixedJoint();
        joint.connectedBody = objectInHand.GetComponent<Rigidbody>();
    }

    private void ReleaseObject()
    {
        if (GetComponent<FixedJoint>())
        {
            GetComponent<FixedJoint>().connectedBody = null;
            Destroy(GetComponent<FixedJoint>());
            objectInHand.GetComponent<Rigidbody>().velocity = contollerPose.GetVelocity();
            objectInHand.GetComponent<Rigidbody>().angularVelocity = contollerPose.GetAngularVelocity();
        }
        objectInHand = null;
    }

    private FixedJoint AddFixedJoint()
    {
        FixedJoint fixedJoint = this.gameObject.AddComponent<FixedJoint>();
        fixedJoint.breakForce = 20000;
        fixedJoint.breakTorque = 20000;
        return fixedJoint;
    }

    private void SetCollidingObject(Collider col)
    {
        if (collidingObject || !col.GetComponent<Rigidbody>())
        {
            return;
        }
        collidingObject = col.gameObject;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RiderにUnity Log Windowが出なくなった

概要

Riderのこのウィンドウです
image.png

Unityのコンソールと連動して表示してくれます
Unityみればいいじゃん、と思っていたのですが いなくなって大切さに気付きました…?

でなくなった原因

別バージョンのRiderをいれたりしたのが原因だと思っています

修復方法

Unity上、Preferences > External Toolsで設定します
image.png

Preferences > Riderを確認
image.png

以上です

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityEditor上でgitのヒストリーを表示させるUniGitHistoryViewerを作りました

経緯

チーム開発をしていると作業ファイルごとのgitの履歴を知りたくなる事があります。

特に非エンジニアがUnityプロジェクトに対して画像の追加やPrefabの修正などをした時です。

ソースコードであればRiderなどのエディタに履歴閲覧機能が備わっている場合があるので問題はありません。
しかし画像などのバイナリファイルを調査する時は少し時間がかかります。

  • シェルからgit logコマンドを実行して調査
  • SourceTreeなどのGUIツールからgit log相当の機能を実行して調査

など

Riderでもバイナリファイルのgit履歴は調査可能ですが、Riderがインストールされていない非エンジニアは困ります。非エンジニアにgit logさせるのもハードルが高め。。。

理想はUnityEditor上からgitの履歴をサクッと確認できる事です。

ということで非エンジニア向けの「UniGitHistoryViewer」というものを作りました。

UniGitHistoryViewerとは

UniGitHistoryViewerとはUnityEditor上でサクッと任意のファイルまたはディレクトリのgit履歴を閲覧するエディタ拡張です。

使い方

Tools > UniGitHistoryViewerから開きます。

プロジェクトウィンドウでファイルまたはディレクトリを選択してCheck Historyボタンをクリックします。
すると履歴が以下のgif動画のようにリストアップします。

unigithistoryviewer.gif

表示内容は以下です。

  • commit日時(年-月-日)
  • author
  • コミットメッセージ

Countに最大表示履歴数を設定でき、また、左端のCボタンをクリックするとコミットハッシュをクリップボードにコピーする事ができます。

インストール方法

Unity2019.3.4f1以降1の場合はUnityPackageManagerでインストール可能です。

  • UnityPackageManager(以下:UPM)からインストール
  • manifest.jsonを書き換えてインストール

Unity2019.3.4f1未満の場合は従来のunitypackageをインポートしてインストール出来ます。

  • unitypackageをインポート

このどれかを選んで頂ければと思います。

UPMからインストールする方法


Add Package Git URLを選択します。


以下のパッケージURLを入力します。
https://github.com/baobao/UniGitHistoryViewer.git?path=Assets/UniGitHistoryViewerを入力してAddボタンをクリックします。するとインストールが完了します。

manifest.jsonを書き換えてインストールする方法

UPMを使わない場合はmanifest.jsonのdependenciesに以下を追加してインストールしてください。

"info.shibuya24.uni-git-history-viewer": "https://github.com/baobao/UniGitHistoryViewer.git?path=Assets/UniGitHistoryViewer"

unitypackageをインポートしてインストールする方法

Unity2019.3.4f1未満の環境の場合はunitypackageReleaseページからダウンロードできますので、そちらをインポートしてインストールしてください。

UniGitHistoryViewerを使うメリット

このツールを使うメリットは複数の非エンジニアメンバーがUnityを触る場合に生まれます。
非エンジニア同士、誰がどのファイルを触ったかという事実は重要で、何かトラブルが発生した際に、非エンジニアメンバーのみで安全に調査できる情報(ここでは履歴情報)が増える事で、エンジニアが介入することなく原因の特定をする可能性が上がります。

僕はエンジニア、非エンジニアがそれぞれ並列して仕事できている状態がベストだと考えています(時と場合にもよりますが)。
その状態を促進させるツールの1つになれば良いなと思っています。

最後に

よろしければ使って頂きバグがあればPR、issueをお待ちしております。
https://github.com/baobao/UniGitHistoryViewer
よろしければStarをください。


  1. Unity2019.3.4f1以降でないとPackage URLのクエリパラメータに対応していないため 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityのPanda BTでシンプルにBehaviour Treeを実装する

前提

この記事では、UnityでシンプルにBehaviour Treeが実装できるPanda BTというアセットについて書きます。

banner2006.png

Panda BT Free
Panda BT Pro
Panda BTのドキュメント

Behaviour Treeとは

AIのふるまいをツリー構造に細かく分けていくことで実装するAI技術です。
例えば目玉焼きを作る場合、以下のように処理を細かく分けていってそのふるまいを記載することができます。

目玉焼きを作る
    フライパンを温める
        フライパンを持つ
        フライパンをコンロにかける
        コンロに火をつける
    卵を割る
    卵をフライパンに入れる
    五分間待つ

テキストの各行は処理のノードを表します。Tab文字による字下げによりノード間の親子関係を表します(字下げされると子ノードになる)。

Panda BTでは、上のような処理の手順を構造化ノードビルトインノードカスタムノードを組み合わせて書くことでAIのふるまいとして記述していきます(各ノードの説明については後から詳述します)。

Panda BTではBehaviour Treeがどのように実現されるのか

実際にPanda BT Freeに含まれるサンプルシーンを見ながら、Panda BTの使い方を解説していきます。無料でアセットストアからダウンロードできますので、自分でも動かしてみてください。

Panda BT Free

では、Panda BT Freeに含まれるPlayTagというサンプルシーンを見てみます。

これは、プレイヤーとコンピューター(AI)が鬼ごっこをするサンプルシーンです(Tagというのは英語で鬼ごっこという意味)。最初はプレイヤーが鬼です。プレイヤーがコンピューターに触ると、鬼を交代してコンピューターが鬼になります。下の画像を見れば分かるように、プレイヤーは丸〇、コンピューターは四角□です。鬼が赤追われる方は青です。プレイヤーはマウスで丸〇を動かし、コンピューターはPanda BTで四角□を動かしています。

ezgif.com-video-to-gif.gif

コンポーネントの構成

それでは、コンピューターのゲームオブジェクトにアタッチされているゲームオブジェクトを見てみましょう(下図参照)。

Mesh Filter、Mesh Renderer、Rigidbody、BoxColliderはごく普通のコンポーネントで、どんなキャラクターオブジェクトにもアタッチされている基本的なコンポーネントです。キャラクターの表示と接触判定を行うためにアタッチされています。

Panda BTでBehaviour Treeを実現するコンポーネントが残りの2つです。Panda Behaviourというコンポーネントと、Computerという自作スクリプトです。
Clipboard01.png
2つのコンポーネントのそれぞれを見てみます。

Panda Behaviourコンポーネント

まずPanda Behaviourコンポーネントですが、こちらでBehaviour Treeのロジックを記述します。記述はBTスクリプトという独自のフォーマットのテキストファイルで記述して、そのBTスクリプトをPanda Behaviourコンポーネントにアサインします。Panda Bahaviourコンポーネントには複数のBTスクリプトをアサインできます。今回は2つのBTスクリプトがアサインされています(Computer.BT.TxtとPlayTag.commented.BT.txtの2つ)。
Clipboard02.png

ちなみにUnityエディタのプレイモード中は、BTスクリプトの各ノードは以下の画像のように色付けされます。ノードの各色は、そのノードがどのような状態にあるかを表します。Runningが青色Succeededが緑色Failedが赤色、ノードが実行されていない場合灰色となります。ノードの状態(Running、Succeeded、Failed)については後述します。
Clipboard01.png

このようにPanda BTでは、インスペクタ上の字下げ表記だけでBehaviour Treeを表現します。実行時には、各ノードの状態は色で分かりますので動作状況も容易に把握できます。また、Pro版の機能からはインスペクタ上からのBTスクリプト編集、ブレークポイントの挿入などもできます。こういったアセットのコンパクトさのおかげで、とてもシンプルに開発・運用できるところがPanda BTの最大の魅力です。

tree("Root")の解説

Panda BTではまず初めにtree("Root")という構造化ノードから処理が始まります。なので、今回はComputer.BTの以下のtreeが最初に実行されます。

Computer.BT.txt
tree("Root")
    //Play Tag, check for collision and Talk at the same time.
    parallel
        repeat mute tree("PlayTag")
        repeat mute tree("CheckCollision")
        repeat mute tree("Talk")

treeという構造化ノードは、ツリーの一番上の親に必ず一つ書く決まりになっています。最初に実行されるtreeには"Root"という名前を付けます。処理はtreeノードから順番に子ノードへと移動します。子ノードはタブ文字による字下げで表現します。

// の行はコメント行なので無視されて、その次の行に移ります。

次の行はparallelノードです。ParallelノードはPanda BTに付属の構造化ノードで、全ての子ノードを並列に実行するという機能があります。今回の場合はtree("PlayTag")tree("CheckCollision")tree("Talk")という3つのツリーを並列に実行しています。

parallel : 子ノード全てを並列に実行する

最後にrepeat muteですが、repeatmuteも構造化ノードでそれぞれ以下の機能があります。

repeat:子ノードが失敗(Failed)するまで、子ノードの処理をリピートする

mute:子ノードの失敗(Failed)・成功(Succeeded)に関わらず、その結果を成功(Succeeded)に変更する。子ノードが実行中(Running)の場合は実行中(Running)を返す。

という意味があります。ここでFailed(失敗)Scceeded(成功)Running(実行中)という概念が出てきました。これは各ノードの状態を表すもので、BTスクリプト上で実行されたノードは必ずこの3つのどれかの状態を返します。

ではrepeat mute tree("PlayTag")を考えてみましょう。この行のrepeatmuteは(子ノードが必ず1つであるので)略式記載されていて、実際には以下のようなツリー構造となっています。

repeat
    mute
        tree("PlayTag")

まず、tree("PlayTag")からその実行状態が返ってきます(Failed、Succeeded、Runningのいずれか)。ですがmuteにより、その結果がFailedの場合でもSucceededに変更されます。muteの影響でrepeat以下の実行状態は常にSucceeded(またはRunning)なので、repeatは永遠にmute以下のツリー構造をrepeatし続けます。要するに、repeat mute treeと書けばtree以下の処理を無限に繰り返し実行するということです。

それでは、tree("Root")全体をもう一回見てみましょう。tree("Root")tree("PlayTag")tree("CheckCollision")tree("Talk")を並列に無限回繰り返す処理であることがわかります。コンピューターが、鬼ごっこをプレイする(Play)、衝突判定を行う(CheckCollision)、話す(Talk)という3つのタスクを並列処理していることが分かりますね。

tree("CheckCollision")の解説

では、3つのツリーtree("PlayTag")tree("CheckCollision")tree("Talk")を並列実行するということだったので、その中の一つで衝突判定を行っているtree("CheckCollision")を見てみましょう。

tree("CheckCollision")
    //Tag when we collide with the player.
    //Leave 1 sec of cooldown between each "Tag".
    sequence
        IsColliding_Player
        Tag
        Wait(1.0)

tree("CheckCollision")では、まずsequenceという構造化ノードが処理されます。sequenceは以下の機能があります。

sequence : 子ノードの処理が失敗するまで、子ノードを順次実行する(子ノードが成功したら、次の子ノードを実行する)。子ノードが実行中(Running)の場合は、sequenceもRunningの状態を返す。子ノードが失敗(Failed)した時点で、sequenceもFailedを返して処理を終了する。全ての子ノードが成功(Succeeded)したら、sequenceもSucceededを返して処理を終了する。

ですので処理の手順を日本語で説明すると、以下のようになります。

まずIsColliding_Playerノードで、コンピューターがPlayerと接触したかを判定します。

接触していない場合、IsColliding_PlayerノードはFailedを返します。子ノードがFailedを返したのでsequenceはそこで処理を終了して自らもFailedを返します。ですが、tree("CheckCollision")はrepeat muteされているので繰り返し処理されます。結果、次のフレームで再度IsColliding_Playerノードが実行されます(毎フレーム衝突判定を行うことになります)。

接触している場合、IsCollinding_PlayerノードはScucceededを返します。成功したので次のノードに移ります。Tagノードが実行されます。ここでは鬼のフラグ(IsIt)を反転させます。また青・赤の色も変更します。このノードは必ず成功します(スクリプトでそう記述しています)。Tagノードが成功したのでWait(1.0)ノードに移ります。WaitノードはPanda BTのビルトインノードでして、指定した秒数だけ処理を待ちます(待っている間はRunning、待ちが終了した状態でSucceededを返す)。Waitノードをここで実行することで、鬼の交代をした後の1秒間は衝突判定を行わないようになっています。Waitによる待ちを完了しsequenceを抜けたあとは、tree("CheckCollision")がrepeat muteされているため、再度sequenceの先頭から処理が開始されます。

このように、sequenceを用いれば子ノードの成功・失敗により、次のノードを実行するかどうかを選択するという、if文のような条件分岐が実現できます。

tree("PlayeTag")の解説

では次にtree("PlayTag")を見てみましょう。

tree("PlayTag")
    //Do whatever is appropriate:
    //Chase the player, Avoid the player or just Idle.
    fallback
        tree("ChasePlayer")
        tree("AvoidPlayer")
        tree("Idle")

tree("PlayTag")ではfallbackという構造化ノードを使っています。fallbackノードは以下のような機能があります。

fallback : 子ノードの処理が成功を返すまで、子ノードを順次実行する。子ノードの実行中(Running)の場合は、fallbackもRunningを返す。子ノードが成功(Succeeded)したら、fallbackもSucceededを返してそこで処理を終了する。子ノードが失敗したら次の子ノードを実行する。すべての子ノードが失敗(Failed)したら、fallbackもFailedを返して処理を終了する。

fallbackの子ノードは、tree("ChasePlayer")tree("AvoidPlayer")tree("Idle")の3つのtreeです。

sequenceは複数の成功を積み重ねて順次処理を行うようなものでしたが、fallbackの場合は子ノードは1つしか成功しません。今回の場合は、Playerを追いかける処理(ChasePlayer)が成功した場合は、Playerをよける処理(AvoidPlayer)とアイドル状態(Idle)は実行されない。ChasePlayerが失敗してAvoidPlayerが成功した場合は、Idleは実行されない。ChasePlayerとAvoidPlayerが失敗した場合は、Idleが実行される。のようになります。このようにfallbackを使うと、複数の状態を持つキャラクターコントローラのようなロジックが簡単に記述できます。

tree("Talk")の解説

次にtree("Talk")を見てみます。

tree("Talk")
    //Say something when changing.
    fallback
        while IsIt
            sequence
                Say("...I'm it.")
                Running

        while not IsIt
            sequence
                Say("Tag, you're it!")
                Running

fallbackは先程説明しました。子ノードのwhile IsItwhile not IsItのどちらかが実行されます。

whileノードという新しい構造化ノードが出てきました。whileノードは必ず2つの子ノードを持ちます。1つ目のノードが条件(condition)を表し、2つ目のノードがアクション(action)を表します。

while
    codition
    action

別の書き方として、以下のようにも書けます。

while codition
    action

whileノードには以下のような機能があります。

while : conditionノードがFailedになるまで、conditionノードとactionノードの処理を実行する(conditionノードがSucceededであることを確認後にactionノードは処理実行される)。whileノードは、conditionノードがSucceededかつactionノードがSucceededの場合Succeededを返す。conditionノードとactionノードのいずれかがFailedの場合Failedを返す。conditionノードとactionノードのいずれかがRunningの場合Runningを返す。

要するにC#プログラミングのwhileと同じような機能を果たします。

次にnotノードですが、こちら子ノードの結果を反転させます。

not 
    child

childノードがSucceededの場合はnotノードはFailedを返し、childノードがFailedの場合はnotノードはSucceededを返すわけです。notノードは(必ず子ノードが1つなので)以下のように簡略化できます。

not child

では、改めてtree("Talk")の内容を見てみます。

鬼かどうかを表すIsItノードがSucceededを返す場合、while IsItノード以下のsequenceノードが実行されます。sequenceノードでは、Say("...I'm it.")というカスタムノードで"...I'm it."というテキストメッセージを表示した後、ビルトインのRunningノードが実行されます。Runningノードは常に実行中(Running)の状態を返します。ですのでsequenceノードはそこで処理を実行中のままで保留します。この書き方はあるフラグがたったら一回だけ処理を実行したい場合に有効です。

余談ですがRunningノードと同様のノードにSucceedノード、Failノードがあります。Succeedノードは常に成功(Succeeded)を返し、Failノードは常に失敗(Failed)を返します。

while IsIt
    sequence                //子ノードRunningで実行中の状態でとまる(永遠にSucceededしない)
        Say("...I'm it.")   //IsItフラグが変化すると1回だけ処理されるノード
        Running             //常にRunningを返すビルトインノード

fallback+while+while notの書き方も大事です。このように書くと、フラグの状態により2つに分岐するツリーができます。これで、Behaviour Treeをステートマシーンのように扱うこともできます。

// フラグによるステートマシーンを実現する書き方
tree("StateMachine")
    fallback
        while condition
            action1
        while not condition
            action2

さて、解説していないtreeは残りtree("ChasePlayer")tree("AvoidPlayer")tree("Idle")だけとなりました。これらのtreeの説明は省略します。これらのtreeの内容は今まで説明した知識で読める内容になっていますので、実際にどのような動きをするかをご自分で解析し、理解してみてください。

カスタムノードを自作する

さて、今までの説明でPanda Behaviourコンポーネントの説明がほぼ終わったのですが、1つだけまだ説明していないことがあります。それがカスタムノードです。Panda BehaviourコンポーネントのBTスクリプトで登場したIsColliding_PlayerTagIsItSay("...I'm it.")などのことです。

このカスタムノードの中身はどこにあるでしょう?察しがついている方もいるかと思いますが、これらカスタムノードの内容はゲームオブジェクトにアタッチされているComputer.csという自作スクリプトに書いてあります。

それではスクリプトComputer.csを見てみましょう。

まず、IsItカスタムノードですが、スクリプトComputer.csでは以下のように書いてあります。

Computer.cs
    [Task]
    bool IsIt = true; // Whether the agent is "It".

非常に簡単です。bool変数に[Task]という属性を加えてあげるだけでPanda Behaviour上のカスタムノードとして機能します。bool変数がtrueの場合Succeededを返し、falseの場合Failedを返すわけです。

次にIsColliding_Playerカスタムノードです。以下のようにboolプロパティも[Task]属性でカスタムノード化出来ます。

Computer.cs
    [Task]
    bool IsColliding_Player
    {
        get
        {
            return _IsColliding_Player;
        }
    }

次はSayカスタムノードを見てみましょう。こちらは関数での実装になっています。boolを返り値とする関数に対して[Task]属性を付与するとカスタムノードとしてPanda Behaviourコンポーネント上から利用できます。関数がtrueを返すとカスタムノードはSucceededを返し、falseを返すとカスタムノードはFailedを返すわけです。引数には int/float/bool/string などが使えます(複数引数も可能です。GameObject、Transformなどは引数にできないようです。)

Computer.cs
    [Task]
    bool Say(string text)
    {
        //ここにカスタムノード内でやりたい処理を書く
        tagDialogue.SetText(text);
        tagDialogue.speaker = this.gameObject;
        tagDialogue.ShowText();

        //最後にbool値を返す
        return true; //常にSucceededを返す場合
    }

関数を使えば、Succeeded/Failedをカスタムノード上で決定できるようになります。

    [Task]
    bool SomeFunk(string text)
    {
        if(何かの条件)
            return true; //Succeeded
        else
            return false; //Failed
    }

関数を使ったカスタムノードはもう1つ作り方があります。Computer.csのMoveToDestination関数を見てください。

    [Task]
    void MoveToDestination()
    {
        var task = Task.current;
        var delta = destination - transform.position;

        if (transform.position != destination)
        {
            // 目的地に近づく処理
            // ...
            // ...
        }

        //目的地に到着したら実行される
        if (transform.position == destination)
            task.Succeed();
    }

このMoveToDestination関数の返り値はbool型でなくてvoid型です。void型の関数をカスタムノードとする場合、以下のルールによりそのカスタムノードの状態が変わります。

Task.current.Succeed()が呼び出されたとき、カスタムノードはSucceededを返す。Task.current.Fail()が呼び出されたとき、カスタムノードはFailedを返す。どちらも呼び出されなかったとき、カスタムノードはRunningを返す。

要するに、void型の関数でカスタムノードを作るとRunning状態を表現できます。上のコードの例で言うと、目的地に到達していない状態ではRunning状態にしながら目的地に近づいていき、目的地に到着したらTask.current.Succeed()を呼び出してSucceededとするなどの処理ができます。

もう1つカスタムノードを作るときに重要なTask.current.isStartingについて触れておきます。Task.current.isStartingは、カスタムノードに入った直後だけtrueを返します。カスタムノードに入った時に一度だけ初期化処理を行いたいときなどに便利です。下のコードはTask.current.isStartingの使用例です。

    // Task.current.isStartingの使用例
    // numberOfFramesのフレーム数だけRunning状態でWaitするカスタムノードの例

    private int counter;

    [Task]
    public void WaitFrames(int numberOfFrames)
    {
        var task = Task.current;

        if (task.isStarting)
        {
            // このカスタムノードに入った時に一回だけ実行される
            counter = 0;
        }

        // Running状態でカウンターをプラスしていく
        counter++;

        if (counter >= numberOfFrames)
        {
            // counterが10以上になったら
            // Succeededを返してこのカスタムノードを抜ける
            task.Succeed();
        }
    }

最後に、[Task]属性を使う場合はスクリプトの最初にusing Panda;と書くのを忘れないようにしてください。

using Panda;

ここまでのまとめ

さて、Panda BTのデモシーンを題材に説明をしてきましたがいかがだったでしょうか。Panda Behaviourコンポーネントとカスタムノード用のスクリプト1つで、シンプルかつ可読性がよくBehaviour Treeが実現できていることが分かっていただけたのではないでしょうか。

ここまでの話をまとめてみます。

  • ゲームオブジェクトにPanda Behaviourコンポーネントとカスタムノード用のスクリプトを1つアタッチすれば良い
  • Panda Behaviourコンポーネント用にBTスクリプトを書き、登録する
  • BTスクリプトは構造化ノード、ビルトインのノード、カスタムノードの3つで構成される
  • 構造化ノードの機能を理解してBehaviour Treeのロジックを組み立てる
  • 実際の処理はビルトインのノードの活用、カスタムノードの自作により実現する。
  • カスタムノードは、C#スクリプト上の変数、プロパティ、関数で表現できる。

ここまで話したことの詳細は、公式のドキュメントでより詳細に記載されていますのでご参考になさってください。

Panda BTのドキュメント

構造化ノードのまとめ

BTスクリプトでBehaviour Treeのロジックの要を担うのは構造化ノードです。
構造化ノードを把握しておくことはとても重要ですのでここでまとめておきます。
構造化ノードは全部で10種類あります。

  • tree
  • sequence
  • fallback
  • parallel
  • race
  • random
  • repeat
  • while
  • not
  • mute

以下に、それぞれの構造化ノードの機能を文章でまとめました。ご参照ください。

また、公式サイトに構造化ノードの動作をデモしたUnity WebGLがあります。
こちらもあわせて確認してみて下さい。→ Structual Nodes Demo

tree

  • treeノードはツリー構造の一番上に必ず必要(ツリー構造をまとめる意味がある)
  • treeノードはstringの引数を必ず一つとる。その引数がtreeの名前になる(ex. tree("Patrol")など)
  • tree("Root")は必ず必要。tree("Root")から処理が開始される。
  • treeノードは子ノードは必ず1つ。複数の子ノードを持つとエラーになる。
tree("Name")
    child
返り値 条件
Running 子ノードがRunningの時
Succeeded 子ノードがSucceededを返したとき
Failed 子ノードがFailedを返したとき

sequence

  • sequenceノードは子ノードを何個でも持てる。
  • sequenceノードは子ノードを上から順に順番に実行していく。
  • 順番に実行していく途中で、子ノードがFailedを返した時点でsequenceノードもFailedを返し処理は終了する。
  • 順番に実行していき、全ての子ノードがSucceededを返したら、sequenceノードもSucceededを返し処理は終了する。
sequence
    child_0
    child_1
    ...
    child_k
返り値 条件
Running 子ノードがRunningの時
Succeeded 全ての子ノードがSucceededを返したとき
Failed いずれかの子ノードがFailedを返したとき

fallback

  • fallbackノードは子ノードを何個でも持てる。
  • fallbackノードは子ノードを上から順に順番に実行していく。
  • 順番に実行していく途中で、子ノードがSucceededを返した時点でfallbackノードもSucceededを返し処理は終了する。
  • 順番に実行していき、全ての子ノードがFailedを返したら、fallbackノードもFailedを返し処理は終了する。
fallback
    child_0
    child_1
    ...
    child_k
返り値 条件
Running 子ノードがRunningの時
Succeeded いずれかの子ノードがSucceededを返したとき
Failed 全ての子ノードがFailedを返したとき

parallel

  • parallelノードは子ノードを何個でも持てる。
  • parallelノードは全ての子ノード並列に実行する。
  • 全ての子ノードがSucceededを返した時点でparallelノードもSucceededを返し処理は終了する。
  • いずれかの子ノードがFailedを返したら、parallelノードもFailedを返し処理は終了する。
parallel
    child_0
    child_1
    ...
    child_k
返り値 条件
Running 少なくとも1つの子ノードがRunningの時 かつ Failedを返した子ノードが存在しないとき
Succeeded 全ての子ノードがSucceededを返したとき
Failed いずれかの子ノードがFailedを返したとき

race

  • raceノードは子ノードを何個でも持てる。
  • raceノードは全ての子ノード並列に実行する。
  • 全ての子ノードがFailedを返した時点でraceノードもFailedを返し処理は終了する。
  • いずれかの子ノードがSucceededを返したら、raceノードもSucceededを返し処理は終了する。
race
    child_0
    child_1
    ...
    child_k
返り値 条件
Running 少なくとも1つの子ノードがRunningの時 かつ Succeededを返した子ノードが存在しないとき
Succeeded いずれかの子ノードがSucceededを返したとき
Failed 全てのの子ノードがFailedを返したとき

random

  • randomノードは子ノードを何個でも持てる。
  • randomノードは全ての子ノードの中から1つの子ノードをランダムに選び、実行する。
  • randomノードの状態は、ランダムに選択された子ノードの状態と同じなる。
  • randomノードの引数でランダム選択の確率的な重みを指定できる。例えば子ノードが4つで、それぞれの確率を10%、20%、30%、40%としたい場合、random(10.0, 20.0, 30.0, 40.0)とする(合計100にする必要はない)。
random(w_0, w_1, ... ,w_k) //ウェイトは省略可能
    child_0
    child_1
    ...
    child_k
返り値 条件
Running ランダム選択された子ノードがRunningの時
Succeeded ランダム選択された子ノードがSucceededを返したとき
Failed ランダム選択された子ノードがFailedを返したとき

repeat

  • repeatノードは整数の引数を1つ持ち、子ノードを引数の数だけリピート実行する。
  • 引数を省略すると子ノードを無限回リピート実行する。
  • repeatノードは、子ノードを必ず1つだけ持つ
  • repeatノードは、子ノードを引数回Succeededさせた場合にSucceededを返す(子ノードが引数以下の回数のSucceededであるならば、repeatノードはRunningを返す)(引数を省略した場合、repeatノードはSucceededを返すことはない。ずっとRunnging返す。Failedを返す場合はある。)
  • repeatノードは、子ノードがFailedを返した場合にFailedを返す。
repeat(n) //引数は省略可能
    child_0
返り値 条件
Running 子ノードのSucceeded回数が引数よりも小さいとき
Succeeded 子ノードのSucceeded回数が引数回数になった時(引数を省略した場合Succeededにはならない。)
Failed 子ノードがFailedを返したとき

while

  • whileノードは必ず2つの子ノードを持つ
  • 1つ目のノードは条件(condition)を表す。2つ目のノードはアクション(action)を表す。
  • conditionノードがFailedを返すまで、conditionノードとactionノードが繰り返し実行される。
  • actionノードは、conditionノードの成功判定後に実行される。
  • actionノードがSucceededを返した場合、whileノードもSucceededを返す。
  • conditionノードまたはactionノードがFailedを返した場合、whileノードもFailedを返す。
while
    condition
    action

// もう一つの書き方
while condition
    action
返り値 条件
Running いずれかの子ノードがRunning状態のとき(conditionがRunningでactionが未実行の場合とconditionがSucceededでactionがRunningの場合の2パターンがある)
Succeeded actionノードがSucceededを返したとき(conditionがSucceededでactionがSucceededの場合)
Failed conditionノードまたはactionノードがFailedを返したとき(conditionがFailedでactionが未実行の場合とconditionがSucceededでactionがFailedの場合の2パターンがある)

not

  • notノードは必ず1つの子ノードを持つ
  • notノードは子ノードの状態を反転させる
  • 子ノードがSucceededを返したとき、notノードはFailedを返す
  • 子ノードがFailedを返したとき、notノードはSucceededを返す
  • 子ノードがRunningを返したとき、notノードもRunningを返す
not
    child

// もう一つの書き方
not child
返り値 条件
Running 子ノードがRunning状態のとき
Succeeded 子ノードがFailedを返したとき
Failed 子ノードがSucceededを返したとき

mute

  • muteノードは必ず1つの子ノードを持つ
  • muteノードは子ノードがFailedの場合でもSucceededを返す(Failedには絶対にならなくなる)
  • 子ノードがSucceededを返したとき、muteノードはSucceededを返す
  • 子ノードがFailedを返したとき、muteノードはSucceededを返す
  • 子ノードがRunningを返したとき、muteノードもRunningを返す
mute
    child

// もう一つの書き方
mute child
返り値 条件
Running 子ノードがRunning状態のとき
Succeeded 子ノードがSucceededまたはFailedを返したとき
Failed 絶対にFailedにはならない

ビルトインのノードを少し紹介

Panda BTに最初から用意されている便利なノードについても少しまとめておきます。
完全なビルトインのノードのリストは以下で確認できます。

http://www.pandabehaviour.com/?page_id=23#Built-in_Tasks

Status系

単に状態を返すだけのノードがあります。たまに使います。

Succeed //Succeededを返すだけ
Fail    //Failedを返すだけ
Running //Runningを返すだけ

Wait系

一定時間処理をWaitする場合に使います。

Wait(3.0)           //3.0秒waitする
Wait(10)            //10フレームwaitする
RealtimeWait(3.0)   //3.0秒waitする。TimeScaleを無視する(Unscaled)

Debug系

Debug.Logを仕込むこともできます。

DebugLog("StringMessage") //コンソールにログを表示したときに利用する

アセットに含まれるサンプル紹介

最後にアセットに含まれるサンプルシーンをご紹介します。

一定時間ごとに色を変化させるサンプルです。
ezgif.com-video-to-gif (1).gif

直方体を自動的に動かすサンプルです。
ezgif.com-video-to-gif (2).gif

Hotlineマイアミのようなサンプルです。
敵キャラとプレイヤー両方ともPanda Behaviourコンポーネントを利用して実装されています。
ezgif.com-video-to-gif (3).gif

無料で全機能が使えます

Panda BTにはPro版とFree版があります。

Pro版では以下の3つの機能が強化されます。
1. BehaviourTree内にBreakPointが設定できる(デバッグ機能の向上です。)
2. インスペクタ上のドラッグ&ドロップでBTスクリプトを編集できる(編集機能の向上です。エディタで修正する手間が省けるので割と便利です。)
3. 全ソースコードが付属する(Free版はDllとして提供される)(アセットの保守、改造が自分で出来るようになります。)

Pro版では、Free版よりもデバッグ/編集/保守などのゲーム制作ワークフロー上の機能が強化されるだけです。なので、Free版とPro版の間で出来上がったゲームの性能に違いは生まれません。より効率的に作業したくなったらPro版の購入を検討しましょう。

UnityでシンプルにBehaviour Treeをやってみたい!と思う人はぜひPanda BTを使ってみてください!

f865cebe-b9cf-41e6-8078-61f9cc6dbbb2.png

Panda BT Free
Panda BT Pro
Panda BTのドキュメント

補足

この記事は、Unity アセット真夏のアドベントカレンダー 2020 Summer! 8月14日のために書かれた記事です。

http://assetstore.info/eventandcontest/adventcalendar/summer-advent-calendar-2020/
summer_Advent_banner_3_La-1-1024x593.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでのゲーム制作時、オブジェクトの移動処理が重い場合に行った事とその効果

筆者の環境:OS:Windows10, Unity 2019.3.5f1 使用言語:C#
利用Asset:DOTween,Destructible2D
筆者のプログラミング歴:2020年1月よりUnity上でC#の勉強を開始。

現在開発中のゲーム(2D)のメインが『落下してくるオブジェクトを切断する』といった内容なのですが、切断後に異常に動きが遅くなる現象が多発し、ゲーム性を著しく損なう為、対策を調べつつ対処した結果を、ざっくりではありますが、備忘録として残そうと思います。

まず、UnityのProfilerを起動し、どこの処理が重いのかを調べました。
結果、『Physics2D.FindNewContacts』という部分に多大な時間を取られていました(数百~千数百ms)。
調べてみると、どうやらRigidbody2Dが良くないようです。
しかし、色々と試しましたが、Rigidbody2Dを外したり、Kinematicにすると思った動きが出来なかった為、以下の事を試しました。
※筆者の環境で、効果が高かったと思われる順に表記。

●切断されるオブジェクトのSpriteのサイズを小さくした。
Spriteを選択した時のInspectorから、MaxSizeを小さく、Pixels Per Unitの値を大きくして調整しました。

●オブジェクトのGravity Scaleの値を小さくした。
これは、単純に動きが早すぎて切断し辛かったのでスピードを遅くする目的でやったのですが、カクカク感が軽減されました。

●Update関数をFixedUpdateに変更。
数値としては反映されているか分かりませんでしたが、見た目のカクカクを軽減する効果があったように感じました。

●Project SettingsのTime Fixed Timestepの値を”小さく”した。
処理を軽くする為には、値を大きくすべきだそうですが、大きくすると思ってもない動きをしたり、若干ワープしたような動きをするので、逆に小さくしてみると、スムーズに動くようになったので、小さくしてしまいました。

結果、目的がブレたような気もしますが、見た目のスムーズさは向上したのと、固まったかのような現象はほとんど起こらなくなったので、とりあえずはこのまま開発を進めようと思います。
新情報等見つけたら、再度試してこちらに追記します。

ここまで読んでいただき、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む