人間の視覚系には次のような特徴があります。
- 距離は限られています。近くはよく見えるが、遠くはよく見えない
- 簡単にわかりにくくなります。不透明な障害物は通過できません
- 視野角は約90度です。前面部分には色や詳細情報が豊富に含まれていることを認識し、外側部分には輪郭と動きの情報しかないことを認識します。
- 注意力には限界があります。特定の場所やオブジェクトに焦点を当てると、他の部分は無視されます。たとえば、マジックの目隠しは常に観客を欺きます。
AIビジョンのシミュレーションは上記の基本特性に基づいており、これに基づいてさまざまな実装アイデアが存在します。2 番目の特性から始めると、光線にも物体を通過しない特性があると考えられやすく、発射距離も設定できるのではないかと推測されます。最大の難点は視野が約90°であることで、3Dフリービューゲームでは視野が円錐形となるのに対し、トップダウンゲームでは視野が扇形になります。円錐領域も扇形領域も、光線、球面光線、ボックス光線などの単純な形状では直接シミュレートできないため、柔軟な解決策を見つける必要があります。
セクター領域をレイでシミュレートする問題について、複数のレイを使用して領域をシミュレートするというわかりやすい方法があります。
スクリプトとレンダリングは次のとおりです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AIEnemy : MonoBehaviour
{
public int viewRadius = 4; //视野距离
public int viewLines = 30; //射线数量
void Start(){ }
void Update()
{
FieldOfView();
}
void FieldOfView()
{
//获得最左边那条射线的向量,相对正前方,角度是-45°
Vector3 forward_left = Quaternion.Euler(0, -45, 0) * transform.forward * viewRadius;
//依次处理每条射线
for(int i = 0; i <= viewLines; i++)
{
Vector3 v = Quaternion.Euler(0, (90.0f / viewLines) * i, 0) * forward_left;
//角色位置+v,就是射线终点pos
Vector3 pos = transform.position + v;
//从玩家为之到pos画线段,只会在编辑器里看到
Debug.DrawLine(transform.position, pos, Color.red);
}
}
}
ただし、これは最初のステップにすぎず、実際に光線を放射する必要があります。光線が照射されて初めて障害物に遭遇したかどうかが判断でき、障害物に遭遇した場合は視線の終点が衝突点に当たります。FieldOfView コードを次のように変更します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AIEnemy : MonoBehaviour
{
public int viewRadius = 4; //视野距离
public int viewLines = 30; //射线数量
void Start(){ }
void Update()
{
FieldOfView();
}
void FieldOfView()
{
//获得最左边那条射线的向量,相对正前方,角度是-45°
Vector3 forward_left = Quaternion.Euler(0, -45, 0) * transform.forward * viewRadius;
//依次处理每条射线
for(int i = 0; i <= viewLines; i++)
{
Vector3 v = Quaternion.Euler(0, (90.0f / viewLines) * i, 0) * forward_left;
//角色位置+v,就是射线终点pos
Vector3 pos = transform.position + v;
//实际发射射线。注意RayCast的参数,重载很多容易搞错
RaycastHit hitInfo;
if(Physics.Raycast(transform.position,v,out hitInfo, viewRadius))
{
//碰到物体,终点改为碰到的点
pos = hitInfo.point;
}
//从玩家位置到pos画线段,只会在编辑器里看到
Debug.DrawLine(transform.position, pos, Color.red);
}
}
}
レンダリング:
変更後、何らかのオブジェクトを使用して光線をブロックすると、光線が実際にブロックされていることがわかります。
実際のゲームでは、レイがプレイヤーに焦点を合わせている場合、それは敵がプレイヤーを発見したことを意味するため、さらなる処理が必要になります。簡単に言うと、上記コードのifコード部分にロジックコードを挿入します。
上記の方法は論理的な機能を実現しますが、視野の範囲を明確に示すものではありません。古典的なステルス ゲームでは、プレイヤーに特定の敵の視野を思い出させるために、明らかな視覚的表現が追加されます。たとえば、「Honkai Impact 3」の一部のレベルでは、敵の視界を示すためにロボットの前に非常に誇張された赤いセクターがあります。
次にプロシージャルモデリングの手法を用いて視野を表示する準備作業は以下の通りです。
- 敵の新しい空のオブジェクトをサブオブジェクトとして作成し、ビューという名前を付け、位置を 0 に設定します。
- メッシュ レンダラー コンポーネントとメッシュ フィルター コンポーネントを追加します。
- 新しいマテリアルを作成し、レンダリング モードを透明に変更し、色を緑色に変更して、透明度を約 150 に変更します。マテリアル設定をテストするには、マテリアル設定を立方体上にドラッグしてテストし、透明度が適切かどうかを確認します。
- マテリアル ボールを新しいビュー オブジェクト上にドラッグします。ビュー オブジェクトには現在モデルがないため、表示できません。
視野は動的に変化するため、視野を表現するには扇形の平面モデルを構築し、それをビューオブジェクトに割り当てるコードを使用する必要があります。多くの変更が加えられたため、変更された完全なスクリプトを以下に示します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AIEnemy : MonoBehaviour
{
public int viewRadius = 4; //视野距离
public int viewLines = 30; //射线数量
public MeshFilter viewMeshFilter;
List<Vector3> viewVerts; //定点列表
List<int> viewIndices; //定点序号列表
void Start()
{
Transform view = transform.Find("view");
viewMeshFilter = view.GetComponent<MeshFilter>();
viewVerts=new List<Vector3>();
viewIndices = new List<int>();
}
void Update()
{
FieldOfView();
}
void FieldOfView()
{
viewVerts.Clear();
viewVerts.Add(Vector3.zero); //加入起点坐标,局部坐标系
//获得最左边那条射线的向量,相对正前方,角度是-45°
Vector3 forward_left = Quaternion.Euler(0, -45, 0) * transform.forward * viewRadius;
//依次处理每条射线
for(int i = 0; i <= viewLines; i++)
{
Vector3 v = Quaternion.Euler(0, (90.0f / viewLines) * i, 0) * forward_left;
//角色位置+v,就是射线终点pos
Vector3 pos = transform.position + v;
//实际发射射线。注意RayCast的参数,重载很多容易搞错
RaycastHit hitInfo;
if(Physics.Raycast(transform.position,v,out hitInfo, viewRadius))
{
//碰到物体,终点改为碰到的点
pos = hitInfo.point;
}
//将每个点的位置加入列表,注意转为局部坐标系
Vector3 p = transform.InverseTransformPoint(pos);
viewVerts.Add(p);
}
//根据顶点绘制模型
RefreshView();
}
void RefreshView()
{
viewIndices.Clear();
//逐个加入三角面,每个三角面都以起点开始
for(int i = 1; i < viewVerts.Count-1; i++)
{
viewIndices.Add(0);
viewIndices.Add(i);
viewIndices.Add(i+1);
}
//填写Mesh信息
Mesh mesh = new Mesh();
mesh.vertices= viewVerts.ToArray();
mesh.triangles = viewIndices.ToArray();
viewMeshFilter.mesh = mesh;
}
}
メッシュ情報は簡単に言うと「頂点」と「頂点番号」で構成されます。頂点は空間位置であるため、Vector3 で表されます。すべての頂点は大きな配列に配置され、各頂点は配列の添字に対応します。
頂点番号は配列の添字を指します。グリッドは三角形面で構成されているため、3 つの通し番号が 1 つのグループとなり、頂点の通し番号 3 と 3 を埋めると 1 つと 1 つの三角形面を表します。
三角形の正の辺と負の辺:
三角形面は3つの頂点番号で構成されており、その3つの点の順序はabcとacb、つまり右回りと左回りの2通りあります。デフォルトでは、一般的なマテリアルは片面でレンダリングされ、各面は一方の側からのみ表示され、もう一方の側からは表示されません。
abc=bca=cab のように右回りの特定点の順序が異なっていれば影響はなく、cba はその逆になります。
コードを書いているときに三角形が見えない場合は、裏面が表示されているかどうかを確認してください。3点のうち任意の2つの通し番号の順序を入れ替えることで、三角形の面を反転させることができます。
頂点リストと頂点シーケンス リストを準備したら、メッシュ オブジェクトを作成できます。Mesh オブジェクトのいくつかのプロパティは配列型であるため、リストを配列に変換するには List.ToArray メソッドを使用する必要があります。最後に、Mesh オブジェクトを Mesh FIlter コンポーネントに割り当てます。
上記の手順に従って実行すると、半透明の緑色のセクター モデルが得られます。また、緑色の範囲は、障害物によって遮られている場合でも、範囲を正しく表示できます。