GJ2016에서 만든 게임에서 BehaviorTree를 사용해 AI를 탑재한 사연입니다.
44303 단어 AIUnitybehaviortreenavmeshtech
この記事はQiitaからエクスポートしたままの記事です。
https://qiita.com/kakunpc/items/6717433ca058bb789c18
最終更新日(2016年02月12日)から5年以上が経過しています。
GJ가 끝나고 넣어본 AI에 관한 말이다.
글로벌 게임 잼(GGJ)에 참가한 니코니코 회의장에서 제작한 게임'Born To Beans'에 AI를 탑재해 외톨이 아이들도 여러 명 대결을 전제로 한 게임을 할 수 있도록 하는 기술에 관한 말이다.
AI용 물건
NavMesh
NavMesh는 Unity에 탑재된 경로 탐색 시스템이다.
사용 방법은 매우 간단하다.
우선, Navigation으로 지형 정보를 태운다.
도구 모음에서 Window→Navigation을 선택하고 NavMesh 설정 항목을 출력합니다.
NavMesh는 Static 속성의 Mesh를 태울 수 있기 때문에 MeshRender를 선택하여 Hierrarchy의 내용을 축소합니다
객체를 한 번에 선택
Navigation Static 설정 후
Bake 태그에서 AI 통행 허용 범위 설정
그냥 케이크에 구울 뿐이야!
아주 간단하죠!
파란 곳은 판단할 수 있는 곳이다.그 외에는 통상 갈 수 없는 곳이다.
또 배치
Cube
는 아이템의 출현 위치와 유저의 출현 위치의 대상이다.Navigation 구이에 푹 빠졌어요.
네, 잘 구워져서 다음 무대를 태우고 싶어요.
여기 문제는 이거예요.
산 위의 대상이 사라져도 산 위의 무대의 베이커 메시지는 아직 남아 있다
왜 이렇게 됐지?
Navigation의 규격에 따라 한 장면을 얼마나 태워야 하는지, 하나하나 태워서는 안 된다.
같은 장면에서 네이비게이션이 타버린 무대는 몇 개 없었다는 것이다.
해결책
무대마다 장면이 있다.
마침 Unity 5.3.2를 사용해 장면의 추가 읽기가 수월해졌기 때문에 이번 추가 읽기가 대응했다.
NavMesh를 통한 탐색 경로
자신의 위치와 가고 싶은 곳의 위치를'NavMesh.CalculatePath'에 맡기면 갈 코스를 얻을 수 있다.
샘플 소스NavMesh.CalculatePath의 소스입니다.
ShowGoldenPath.cs
using UnityEngine;
using System.Collections;
public class ShowGoldenPath : MonoBehaviour {
public Transform target;
private NavMeshPath path;
private float elapsed = 0.0f;
void Start () {
path = new NavMeshPath();
elapsed = 0.0f;
}
void Update () {
// Update the way to the goal every second.
elapsed += Time.deltaTime;
if (elapsed > 1.0f) {
elapsed -= 1.0f;
NavMesh.CalculatePath(transform.position, target.position, NavMesh.AllAreas, path);
}
for (int i = 0; i < path.corners.Length-1; i++)
Debug.DrawLine(path.corners[i], path.corners[i+1], Color.red);
}
}
이렇게 하면 path.corners
기점에서 종점까지의 경로를 얻을 수 있다.순조롭게 얻지 못하면
NavMesh.CalculatePath
의 반환값은 false
로 돌아간다.그래서 그곳부터 가려면 불가능하다.
그리고 이걸 AI의 컨트롤러 부분에 조립하면 가고 싶은 곳으로 가는 AI로 만들 수 있다.
BehaviorTree
BehaviorTree는 AI 알고리즘에서 자주 사용하는 사고방식이다.
Unreal Engine 4는 표준으로 탑재된 것으로 조사됐다.
이름에 트리가 있다는 걸 상상하면 사고 회로의 흐름은 나무와 같다.
하나하나의 요소를 노드라고 하는데 하나하나의 순서에 따라 집행되는 절차다.
노드 정보
하나둘씩 차례대로 진행하면 AI라고 할 수 없고 일만 되풀이하고 있다.
노드에도 종류를 두고, 실행과 불실행 등 분기하는 일을 AI로 만든다.
다음은 대표적인 노드를 하나하나 설명한다.
ActionNode
특징.
DecoratorNode
특징.
SelectorNode
특징.
SequenceNode
특징.
이번 AI.
PlanetUML로 AI의 흐름을 귀결해보면 이렇게 되는 느낌.
네, 한 번 보면 잘 모르겠어요.
내가 설명할게.
AI의 생각은 이런 느낌이다.
그럼 이건 우리가 실제로 사용하는 소스야.
AI_Strong.cs
using GGJ.AI.BehaviorTree;
using GGJ.GameManager;
using GGJ.Player;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
namespace GGJ.AI
{
public class AI_Strong : MonoBehaviour, IPlayerInput
{
private const float LevelMaxGettingUpToTime = 30f; // 最大のレベルになるまでの時間
private const float AttackDistance = 3f; // 攻撃開始までの距離
private const float StopDistance = 1.5f; // 止まるまでの距離
private const float GetItemDistance = 10f; // アイテムを取り行く距離
private Subject<bool> onAttackButtonSubject = new Subject<bool>();
/// <summary>
/// 攻撃ボタンが押されているかどうか
/// </summary>
public IObservable<bool> OnAttackButtonObservable
{
get { return onAttackButtonSubject.AsObservable(); }
}
private Subject<Vector3> moveDirectionSubject = new Subject<Vector3>();
/// <summary>
/// プレイヤの移動方向
/// </summary>
public ReadOnlyReactiveProperty<Vector3> MoveDirection
{
get { return moveDirectionSubject.ToReadOnlyReactiveProperty(); }
}
// Use this for initialization
private void Start()
{
var waitPositonTime = 0f;
GameObject attackPlayer = null;
var isMoveTerget = false;
var moveTerget = Vector3.zero;
GameObject itemObject = null;
var coolTime = 0f;
// 行き先に向かって移動する
this.UpdateAsObservable()
.Where(_ => isMoveTerget)
.Select(_ =>
{
var path = new NavMeshPath();
isMoveTerget = NavMesh.CalculatePath(transform.position, moveTerget, NavMesh.AllAreas, path);
return path;
})
.Where(x => x.corners.Length >= 2)
.Do(_ => waitPositonTime = 0f) // 行き先を見つけた
.Select(x => (x.corners[1] - x.corners[0]).normalized)
.Select(x => AI_Extension.CalcDoc(x))
.Subscribe(moveDirectionSubject);
// 止まっている時間のカウント
this.UpdateAsObservable()
.Where(_ => isMoveTerget == false)
.Subscribe(_ => { waitPositonTime += Time.deltaTime; });
// 止まっている時間が一定数立ってしまったらとりあえずターゲットに向かって歩く
this.ObserveEveryValueChanged(_ => waitPositonTime)
.Where(_ => isMoveTerget == false)
.Where(x => x > 1f)
.Where(_ => (Vector3.Distance(moveTerget, transform.position) > StopDistance * (1f - LevelMaxGettingUpToTimeRate))) // 近くにすでにいるなら動かない
.Select(_ => (moveTerget - transform.position).normalized)
.Select(x => AI_Extension.CalcDoc(x))
.Subscribe(moveDirectionSubject);
// 行きたい位置についたら移動処理を停止させる
this.UpdateAsObservable()
.Where(_ => isMoveTerget)
.Select(_ => Mathf.Abs(Vector3.Distance(moveTerget, transform.position)))
.Where(x => x < StopDistance * (1f - LevelMaxGettingUpToTimeRate))
.Subscribe(_ => isMoveTerget = false);
// 敵の近くによったら攻撃する
this.UpdateAsObservable()
.Where(_ => attackPlayer != null)
.Where(_ => (Vector3.Distance(attackPlayer.transform.position, this.transform.position) <= AttackDistance))
.Subscribe(_ =>
{
onAttackButtonSubject.OnNext(true);
onAttackButtonSubject.OnNext(false);
});
// アイテム生成通知
StageSpawner.Instance.OnSpowenItemAsObservable()
.Where(x => Vector3.Distance(x.transform.position, this.transform.position) < GetItemDistance) // 自分の近くに生成された
.Subscribe(x =>
{
// アイテムが存在していることを入れる
itemObject = x;
// 交戦中のプレイヤーを消しておく(取りに行くため)
attackPlayer = null;
});
// - ここからBehaviorTree ------------------------------------------------------
// アイテム存在チェック
var checkNearItemState =
new ActionNode("CheckItem", _ =>
{
if (itemObject != null) return NodeStatusEnum.Success;
return NodeStatusEnum.Failure;
});
// アイテムに近づくAI
var goItemObject = new DecoratorNode("GoItemCheck",
new ActionNode("GoItem", _ =>
{
// 取れた取れてないにかかわらず終了
if (itemObject == null)
{
isMoveTerget = false;
return NodeStatusEnum.Success;
}
// ターゲットを設定
moveTerget = itemObject.transform.position;
moveTerget.y = transform.position.y;
isMoveTerget = true;
return NodeStatusEnum.Running;
})
, () => (itemObject != null));
// 近くにアイテムがスポーンしたなら優先的に取り行くAI
var checkItem = new SequenceNode("SearchItem",
checkNearItemState // 生成してる
, goItemObject // 取りに行く
);
// 近くのプレイヤーを選択するAI
var nearPlayer = new ActionNode("SearchPlayer",
_ =>
{
// 生きているプレイヤー取得
var players = PlayerManager.Instance.GetAlivePlayers();
if (players.Count <= 1)
{
isMoveTerget = false;
return NodeStatusEnum.Failure;
}
// ソート(近い順)
players.Sort((x, y) =>
{
var xDist = Vector3.Distance(x.transform.position, this.transform.position);
var yDist = Vector3.Distance(y.transform.position, this.transform.position);
if (xDist == yDist)
return 0;
if (xDist > yDist)
return 1;
else
return -1;
});
// 一番近いプレイヤーを選択
attackPlayer = players[1].gameObject;
moveTerget = AI_Extension.RandomPosition(attackPlayer.transform.position,
(1f - LevelMaxGettingUpToTimeRate)); // 時間経過で性格射撃になる
moveTerget.y = transform.position.y;
isMoveTerget = true;
// 思考を停止するクールタイムを設定
coolTime = Random.Range(0.5f, 2f);
coolTime *= (1f - LevelMaxGettingUpToTimeRate);// 時間経過で思考停止クールタイムがなくなる
return NodeStatusEnum.Success;
}
);
var aiCoolTime = new ActionNode("CoolTime", _ =>
{
coolTime -= Time.deltaTime;
if (coolTime < 0f)
{
// ここでSuccessを返すとNearPlayerが呼ばれなくなってしまいので無理やり失敗にさせる
return NodeStatusEnum.Failure;
}
return NodeStatusEnum.Running;
});
// AI初期化
BehaviorTreeComponent.RegsterComponent(this.gameObject, new SelectorNode("name",
checkItem,
aiCoolTime,
nearPlayer));
// - ここまでBehaviorTree ------------------------------------------------------
}
/// <summary>
/// 時間経過によるレベルの割合
/// </summary>
/// <returns>0~1 1Max</returns>
private float LevelMaxGettingUpToTimeRate
{
get
{
if (LevelMaxGettingUpToTime <= 0f) return 1f;
return TimerManager.Instance.OnGameTimer.Value / LevelMaxGettingUpToTime;
}
}
}
}
이런 느낌으로 AI를 설치해봤어요.끝맺다
Aiming 개발자 블로그의 BehaviorTree에 대한 글은 매우 참고 가치가 있다.
또 BehaviorTree에 대한 이해가 얕아 노드의 처리가 실제와 다를 수 있다.
다른 실가라면 지정해 주세요.
BehaviorTree를 활용한'Behave 2 for Unity'도 있는데 그걸 사용할 수 있다. AssetStore
AI의 소스 코드를 일부 GiitHub로 높였습니다.BorntoBeans_AI
Reference
이 문제에 관하여(GJ2016에서 만든 게임에서 BehaviorTree를 사용해 AI를 탑재한 사연입니다.), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/kakunpc/articles/79b47dbba83e771354e3텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)