Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

Unity:もうひとりじゃない!Boidsで動くNPCを神奈川に放つ

この記事は「3D都市モデル(Project PLATEAU)横浜市(2020年度) - データセット」(国土交通省)を使って作成されました。

🔗 目次

  1. まえがき
  2. 解決したい問題
  3. 完成品
  4. 環境紹介
  5. そもそもBoidsって何?
  6. 実装してみよう
  7. 実際に放流してみた
  8. あとがき

🔗まえがき

皆様こんにちは、BPSの協力会社として横浜を拠点に活動しております、株式会社ECNのFuseです。
昨日の記事では神奈川県のECN周辺部分をバーチャル空間に召還し、その周りをうろつくことで出勤した気になって寂しさを紛らわせようと思いました。

ですが…

↑目次に戻る

🔗 解決したい問題

一人ぼっちはやっぱり寂しいんです。

だだっ広い空間に一人きり、寂しくないわけがありません。
なので今回はBoids(鳥もどき)なるプログラムを使い、
バーチャル空間にNPCを放流し少しでも寂しさを紛らわせようと思います。

↑目次に戻る

🔗 完成品


↑目次に戻る

🔗 環境紹介

言語&エンジン バージョン
C# 11
.NET 7.0.101
unity 2021.3.5
ライブラリ&パッケージ バージョン
PLATEAU-SDK-for-Unity v1.1.6

↑目次に戻る

🔗そもそもBoidsって何?

Boidsとは、昔からある人工生命シミュレーションプログラムで、

  • 分離(仲間にぶつからないように動く)
  • 整列(群れ全体で進行方向を揃える)
  • 結合(近くの群れと合流する)

の3つのふるまいからなるプログラムで、単純なプログラムから鳥の性質をうまく再現することができます。
今回は、そのBoidsをスライムに実装してみます。

スライムの3Dモデルです。

▲今回使うスライムくん。0からBlenderで作りました。

↑目次に戻る

🔗 実装してみよう

基本的な部分の実装

まずはエージェントの基本的な部分を実装していきます。視界内にいるエージェントをリストを使って管理していきます。

using UnityEngine;
using System.Collections.Generic;
[RequireComponent(typeof(Rigidbody))]//RigidBody必須
public class NPCBrain : MonoBehaviour {
    [SerializeField] private float movespd = 10;
    [SerializeField] private float InnerSphere = 5; //パーソナルスペースの半径
    [SerializeField] private float S_power=1;//分離力
    [SerializeField] private float A_power=1;//整列力
    [SerializeField] private float C_power=1;//結合力
    private Rigidbody rb;
    private List<NPCBrain> outerlist=new();//認識した個体のリスト
    private Vector3 movevec;//加速ベクトル
    public Vector3 GetVec{get{return rb.velocity;}}//現在の速度ベクトル

    private void Awake() {
        this.transform.LookAt(Random.onUnitSphere,Vector3.up);//ランダムな方向を向く
        rb=GetComponent<Rigidbody>();
    }
    private void Update() {
        movevec=transform.forward;
        if(outerlist.Count>0){
            //TODO:進むべき方向を求める
        }
        rb.AddForce(movevec*movespd - rb.velocity);//最大速度を制限
        this.transform.LookAt(transform.position+rb.velocity.normalized,Vector3.up);//移動方向を向く
    }
    private void OnTriggerEnter(Collider other) {
        if(other.gameObject.TryGetComponent<NPCBrain>(out var b)){//エージェントなら
            if(!outerlist.Contains(b)){
                outerlist.Add(b);//登録
            }
        }
    }
    private void OnTriggerExit(Collider other) {
        if(other.gameObject.TryGetComponent<NPCBrain>(out var b)){
            if(outerlist.Contains(b)){
                outerlist.Remove(b);
            }
        }
    }
   //TODO:分離部分のプログラム
    //TODO:整列部分のプログラム
    //TODO:結合部分のプログラム
}

また、AddForceは何も考えずに使うと際限なく加速していくため、現在の速度を使って計算させることで最高速を制限しています。
これで雛型のようなものができたので、ここからBoidsの根幹を担う3つの関数を実装していきましょう。

分離

private Vector3 Separation(){//近くのエージェントと距離をとる
    Vector3 TargetVec=new();
    foreach(var v in outerlist){
        var diff=(transform.position-v.transform.position);
        if(diff.magnitude<=InnerSphere){//特定距離より近いなら
            TargetVec+=(transform.position-v.transform.position).normalized/diff.magnitude;//地下いほど影響大
        }
    }
    TargetVec.y=0;
    TargetVec/=outerlist.Count;
    TargetVec=TargetVec.normalized;
    return TargetVec;
}

Separation関数では認識しているエージェントの距離に応じて離れようとする力の平均を長さ1に丸めた力を返します。
これにより、距離が近すぎるエージェントが離れていく動きが作れます。

整列

private Vector3 Align(){//視界内のエージェントと向きを合わせる
    Vector3 TargetVec=new();
    foreach(var v in outerlist){
        TargetVec+=v.GetVec;
    }
    TargetVec.y=0;
    TargetVec/=outerlist.Count;
    TargetVec=TargetVec.normalized;
    return TargetVec;
}

Align関数では認識しているエージェントたちの移動ベクトルの平均を長さ1に丸めた力を返します。
これにより、群れ全体で移動方向を合わせようとする動きが作れます。

結合

private Vector3 Cohesion(){//視界内のエージェントの中心座標を目指す
    Vector3 TargetPos=new();
    foreach(var v in outerlist){
        TargetPos+=v.gameObject.transform.position;
    }
    TargetPos/=outerlist.Count;
    var TargetVec=(TargetPos-this.transform.position).normalized;
    TargetVec.y=0;
    TargetVec=TargetVec.normalized;
    return TargetVec;
}

Cohesion関数では認識しているエージェントたちの座標の平均をとり、平均座標へ長さ1の力を与えます。
これにより、群れ同士が1つに結合する動きを作ることができます。

完成品

完成したNPCBrainクラスはこのようになります。

using UnityEngine;
using System.Collections.Generic;
[RequireComponent(typeof(Rigidbody))]//RigidBody必須
public class NPCBrain : MonoBehaviour {
    [SerializeField] private float movespd = 10;
    [SerializeField] private float InnerSphere = 5; //パーソナルスペースの半径
    [SerializeField] private float S_power=1;//分離力
    [SerializeField] private float A_power=1;//整列力
    [SerializeField] private float C_power=1;//結合力
    private Rigidbody rb;
    private List<NPCBrain> outerlist=new();//認識した個体のリスト
    private Vector3 movevec;//加速ベクトル
    public Vector3 GetVec{get{return rb.velocity;}}//現在の速度ベクトル

    private void Awake() {
        this.transform.LookAt(Random.onUnitSphere,Vector3.up);//ランダムな方向を向く
        rb=GetComponent<Rigidbody>();
    }
    private void Update() {
        movevec=transform.forward;
        if(outerlist.Count>0){
            var TargetVec=(Separation()*S_power+Align()*A_power+Cohesion()*C_power+transform.forward).normalized;//進むべき向きを計算
            TargetVec.y=0;//縦方向への移動を防ぐ
            movevec=TargetVec;
        }
        rb.AddForce(movevec*movespd - rb.velocity);//最大速度を制限
        this.transform.LookAt(transform.position+rb.velocity.normalized,Vector3.up);//移動方向を向く
    }
    private void OnTriggerEnter(Collider other) {
        if(other.gameObject.TryGetComponent<NPCBrain>(out var b)){//エージェントなら
            if(!outerlist.Contains(b)){
                outerlist.Add(b);//登録
            }
        }
    }
    private void OnTriggerExit(Collider other) {
        if(other.gameObject.TryGetComponent<NPCBrain>(out var b)){
            if(outerlist.Contains(b)){
                outerlist.Remove(b);
            }
        }
    }
    private Vector3 Separation(){//近くのエージェントと距離をとる
        Vector3 TargetVec=new();
        foreach(var v in outerlist){
            var diff=(transform.position-v.transform.position);
            if(diff.magnitude<=InnerSphere){//特定距離より近いなら
                TargetVec+=(transform.position-v.transform.position).normalized/diff.magnitude;//地下いほど影響大
            }
        }
        TargetVec.y=0;
        TargetVec/=outerlist.Count;
        TargetVec=TargetVec.normalized;
        return TargetVec;
    }
    private Vector3 Align(){//視界内のエージェントと向きを合わせる
        Vector3 TargetVec=new();
        foreach(var v in outerlist){
            TargetVec+=v.GetVec;
        }
        TargetVec.y=0;
        TargetVec/=outerlist.Count;
        TargetVec=TargetVec.normalized;
        return TargetVec;
    }
    private Vector3 Cohesion(){//視界内のエージェントの中心座標を目指す
        Vector3 TargetPos=new();
        foreach(var v in outerlist){
            TargetPos+=v.gameObject.transform.position;
        }
        TargetPos/=outerlist.Count;
        var TargetVec=(TargetPos-this.transform.position).normalized;
        TargetVec.y=0;
        TargetVec=TargetVec.normalized;
        return TargetVec;
    }
}

あとは各種パラメータをInspectorから編集すれば、このような通行人っぽい動きの出来上がりです。


↑目次に戻る

🔗実際に放流してみた

それでは作ったエージェントたちをさっそく神奈川に放流してみましょう。

中々に癒される動きに仕上がったと思います。

↑目次に戻る

🔗あとがき

そんなわけで、今日はバーチャル世界に作られた神奈川県横浜市中区蓬莱町1丁目付近にスライムの群れを放流して和みました。次回は、話題を変えてGoogle Colabの力を借りて爆速で音声合成したり簡易的なボイスチェンジャーもどきを作ったりする記事を書く予定です。それでは、また明日会いましょう!
↑目次に戻る


株式会社ECNはPHP、JavaScriptを中心にお客様のご要望に合わせたwebサービス、システム開発を承っております。
ビジネスの最初から最後までサポートを行い
お客様のイメージに合わせたWebサービス、システム開発、デザインを行います。



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。