- 복잡한 Game AI를 구현할 때 많이 사용하는 기법
- Helo시리즈, Sims 시리즈 다양한 게임의 AI에 사용
- 언리얼에서는 기본 AI로 탑재
- 트리 구조
- 개발 유지 보수가 편리
* 앞부분은 FSMprogramming 과 비슷한데 (모델 만드는 부분) 소스코드가 다름
<Node 타입>
1. Leat Node
1) condition (transition에 사용하는 condition과 같음 - 조건이 충족되었는지의 여부 체크 (SUCCESS, FAILURE 두가지)
2) action - 에이전트 상태를 변경하는 계산수행, 사운드 재생, NPC 행동처리, 조명켜기등 (state 라고 생각하면 됨)
- 실제적으로 로직을 제일 많이 구현함 (디테일한부분)
2. Composite Node
1) select (둘중에 하나를 선택) - 여러개의 자식노드들중 하나만이라도 SUCCESS가 뜨면 부모도 SUCCESS를 갖게됨
2) sequence - 하나라도 FAILURE이면 FAILURE
3) parallel - 동시에 모든 chile tick 처리 (그렇게 많이 쓰이진 않음)
3. Decorator Node - 개발자가 설계하기나름
- 기본적으로 select와 sequence로 대부분 처리
<Node 리턴값> | ||
1 | SUCCESS | 성공적으로 완료 |
2 | FALURE | 완료되지 못함 |
3 | RUNNING | 현재 진행중인 Action |
4 | INVALID | 방문한 노드가 아닐 때 최초 상태 값 |
5 | ERROR | 예기치 않은 오류가 트리에 발생했을 때 |
* 상태가 너무 많아지만 FSM Programming 에서는 감당하기가 힘듦 -> 이럴 때 Tree사용
- sequence라면 하나의 그룹으로 생각하면 됨
1. Scene 생성하기
1) "Groun" 생성 (100 1 100)
2) "mGround" material 만들고 오브젝트와 연결
2. Player 만들기
1) capsule로 "Player" 생성
2) transition을 초기화 한 상태에서 Y:1
3) "mPlayer" material 만들고 오브젝트와 연결
4) "Rigidbody" 컴포넌트 추가
5) Freeze Rotation 체크로 축 고정하기
6) player 태그 달기
3. 방향 메시 만들기
1) Player의 chile로 cylinder를 만들고 위치를 reset한뒤 설정
2) capsule collider 박스를 체크 해제 (방향만 나타낼 것이기 때문에 부딪히면 안되서!)
4. 스크립트 만들기
1) "PlayerController.cs" 스크립트 만들고 플레이어 오브젝트와 연결
2) 기본적인 코드 작성
- 키보드 Input, 회전, 월드좌표이동 등
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float _fSpeedPower = 10.0f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float fHorizontal = Input.GetAxis("Horizontal");
float fVertical = Input.GetAxis("fVertical");
if (fHorizontal == 0.0f && fVertical == 0.0f)
return;
Vector3 vMovement = new Vector3(fHorizontal, 0.0f, fVertical);
// 회전
transform.rotation = Quaternion.LookRotation(vMovement);
// 월드 좌표 이동
transform.position = transform.position + (vMovement.normalized * _fSpeedPower * Time.deltaTime);
}
}
5. 카메라 설정바꾸기
1) 메인카메라 설정
2) "CameraController.cs" 스크립트 만들고 메인카메라에 연결
3) 코드 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraController : MonoBehaviour
{
public GameObject _myPlayer;
private Vector3 _vPositionOffset;
// Start is called before the first frame update
void Start()
{
_vPositionOffset = transform.position - _myPlayer.transform.position;
}
private void LateUpdate()
{
transform.position = Vector3.Lerp(transform.position, _myPlayer.transform.position + _vPositionOffset, Time.deltaTime * 2.0f);
}
// Update is called once per frame
void Update()
{
}
}
4) public GameObject 변수와 Player 오브젝트 연결
5. 적 캐릭터 만들기
1) capsule > "Enemy"
2) 위치 설정
3) "mEnemy" material 만들고 오브젝트와 연결
4) 사용자 임의 설정
5) "Rigidbody" 컴포넌트 추가 (rotation 축 체크)
6) enemy에도 방향메시를 확인하기위해 cylinder를 자식으로 생성하고 palyer와 같이 설정
6. Tree 스크립트 만들기
1) "btBehaviour.cs" 만들고 코드 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace myBehaviourTree
{
// 리턴값
public enum enStatus
{
EBH_Invalid,
EBH_Success,
EBH_Failure,
EBH_Running,
EBH_Aborted,
};
// 어떤 노드인지 역할을 알려줌
public enum enNodeType
{
Root,
Selector,
Sequence,
Paraller,
Decorator,
Condition,
Action,
};
// 부모 클래스
public class btBehavior
{
// 아래 두개가 중요함
private enStatus _enMyStatus;
private enNodeType _enMyNodeType;
private int _iIndex;
private btBehavior _btParent;
public btBehavior()
{
_enMyStatus = enStatus.EBH_Invalid;
}
public bool IsTerminated() { return _enMyStatus == enStatus.EBH_Success | _enMyStatus == enStatus.EBH_Failure; }
public bool IsRunning() { return _enMyStatus == enStatus.EBH_Running; }
public void SetParent(btBehavior btNewParent) { _btParent = btNewParent; }
public btBehavior GetParent() { return _btParent; }
public enStatus GetStatus() { return _enMyStatus; }
public void SetStatus(enStatus enNewStatus) { _enMyStatus = enNewStatus; }
public enNodeType GetNodeType() { return _enMyNodeType; }
public void SetNodeType(enNodeType enNewType) { _enMyNodeType = enNewType; }
public int GetIndex() { return _iIndex; }
public void SetIndex(int iIndex) { _iIndex = iIndex; }
virtual public void Reset() { _enMyStatus = enStatus.EBH_Invalid; }
public virtual void Initialize() { }
public virtual enStatus Update()
{
return enStatus.EBH_Success;
}
public virtual void Terminate() { }
public virtual enStatus Tick()
{
if (_enMyStatus == enStatus.EBH_Invalid)
{
Initialize();
_enMyStatus = enStatus.EBH_Running;
}
_enMyStatus = Update();
if (_enMyStatus != enStatus.EBH_Running)
{
Terminate();
}
return _enMyStatus;
}
}
}
2) "btRoot.cs" 스크립트 생성후 코드 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace myBehaviorTree
{
public class btRoot : btBehavior
{
private btBehavior _Child;
public btRoot()
{
SetNodeType(enNodeType.Root);
SetParent(null);
}
public void AddChild(btBehavior newChild)
{
_Child = newChild;
_Child.SetParent(this);
}
public btBehavior GetChile() { return _Child; }
public override void Terminate()
{
_Child.Terminate();
base.Terminate();
}
public override enStatus Tick()
{
if (_Child == null)
return enStatus.EBH_Invalid;
else if(_Child.GetStatus() == enStatus.EBH_Invalid)
{
_Child.Initialize();
_Child.SetStatus(enStatus.EBH_Running);
}
SetStatus(_Child.Update());
// child로 설정 변경
_Child.SetStatus(GetStatus());
if (GetStatus() != enStatus.EBH_Running)
Terminate();
return GetStatus();
}
}
}
3) "btComposite.cs" 스크립트 만들고 코드 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace myBehaviorTree
{
public class btComposite : btBehavior
{
protected List<btBehavior> _listChild;
public btComposite()
{
_listChild = new List<btBehavior>();
}
public override void Reset()
{
for (int i = 0; i < GetChildCount(); ++i)
{
GetChild(i).Reset();
}
}
public btBehavior GetChild(int iIndex)
{
return _listChild[iIndex];
}
public int GetChildCount()
{
return _listChild.Count;
}
public void AddChild(btBehavior newChild)
{
_listChild.Add(newChild);
// 인덱스 취득
newChild.SetIndex(_listChild.Count - 1);
// 부모 설정
newChild.SetParent(this);
}
}
}
4) "btCondition.cs" 스크립트 만들고 코드 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace myBehaviorTree
{
//------------------------------------------------------------------------------
// Left Node
// EBH_Running 존재하지 않음 : EBH_Success or EBH_Failure
// Initialize(), Terminate() 사용 안함 : 의미 없는 함수 - 조건만 체크하기때문에
//------------------------------------------------------------------------------
// 시퀀스 노드에서 제일 처음 접하는 노드
public class btCondition : btBehavior
{
public btCondition()
{
SetNodeType(enNodeType.Condition);
}
public override enStatus Tick()
{
SetStatus(Update());
if (GetStatus() == enStatus.EBH_Running)
{
//error
}
// !!! (최적화 필요) : 이전 Action node를 참조하는 필드 변수로 만들어 사용
if (GetStatus() == enStatus.EBH_Success)
{
// 이전에 다른 노드의 값들을 초기화 하기위해서 - 반드시 들어가는 조건문 필요
TerminateRunningStatusByOtherAction();
}
return GetStatus();
}
public void TerminateRunningStatusByOtherAction()
{
btBehavior btFindRoot = null;
int IErrorCount = 0;
// find root
btFindRoot = GetParent();
if (btFindRoot != null)
{
while (IErrorCount < 100)
{
btFindRoot = btFindRoot.GetParent();
if (btFindRoot.GetParent() == null)
break;
++IErrorCount;
}
}
// find running action
if (btFindRoot != null)
{
if (btFindRoot.GetStatus() == enStatus.EBH_Running)
{
btBehavior btRunningAction = FindRunningAction(((btRoot)btFindRoot).GetChild());
if (btRunningAction != null)
{
// 만일 this Condition과 Running Action이 같은 부모를 가졌고 해당 부모가 Sequence가 아니라면 Terminate호출
if (GetParent() != btRunningAction.GetParent() || GetParent().GetNodeType() != enNodeType.Sequence)
btRunningAction.Terminate();
}
}
}
}
public btBehavior FindRunningAction(btBehavior btChild)
{
btBehavior btRunningAction = null;
if (btChild != null)
{
if (btChild.GetNodeType() == enNodeType.Selector || btChild.GetNodeType() == enNodeType.Sequence)
{
for (int i = 0; i < ((btComposite)btChild).GetChildCount(); ++i)
{
btRunningAction = FindRunningAction(((btComposite)btChild).GetChild(i));
if (btRunningAction != null)
return btRunningAction;
}
}
if (btChild.GetNodeType() == enNodeType.Action && btChild.GetStatus() == enStatus.EBH_Running)
return btChild;
}
return btRunningAction;
}
}
}
5) "btAction.cs" 스크립트 생성후 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace myBehaviorTree
{
//------------------------------------------------------------------------------
// Left Node
// Actor의 상태를 처리하는 클래스
// * 해당 Action Node가 EBH_Success 상태고 재방문 시 skip
//------------------------------------------------------------------------------
public class btAction : btBehavior
{
public btAction()
{
SetNodeType(enNodeType.Action);
}
public override void Initialize() { }
public override void Terminate() { }
public override void Reset()
{
SetStatus(enStatus.EBH_Invalid);
}
public override enStatus Tick()
{
if(GetStatus() == enStatus.EBH_Invalid)
{
Initialize();
SetStatus(enStatus.EBH_Running);
}
SetStatus(Update());
if (GetStatus() != enStatus.EBH_Running)
Terminate();
return GetStatus();
}
}
}
10) "btSelector.cs" 스크립트 생성 후 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace myBehaviorTree
{
public class btSeletor : btComposite
{
public btSeletor()
{
SetNodeType(enNodeType.Selector);
}
public override enStatus Update()
{
for(int i=0; i<GetChildCount(); ++i)
{
enStatus enCurrentStatus = GetChild(i).Tick();
if(enCurrentStatus != enStatus.EBH_Failure)
{
ClearChild(i);
return enCurrentStatus;
}
}
return enStatus.EBH_Failure;
}
// 자식 중 EBH_Success or EBH_Running를 발견하면 모든 자식을 초기화(이때, 기존의 EBH_Running이 있으면 Terminatied()함수 호출)
protected void ClearChild(int iSkipIndex)
{
for(int i=0; i<GetChildCount(); ++i)
{
if(i != iSkipIndex)
{
GetChild(i).Reset();
}
}
}
}
}
11) "btSequence.cs"
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace myBehaviorTree
{
public class btSequence : btComposite
{
public btSequence()
{
SetNodeType(enNodeType.Sequence);
}
public override enStatus Update()
{
enStatus enCurrentStatus = enStatus.EBH_Invalid;
for(int i=0; i<GetChildCount(); i++)
{
// 우선 status 값은 취득 해야함
enCurrentStatus = GetChild(i).GetStatus();
if (GetChild(i).GetNodeType() != enNodeType.Action || GetChild(i).GetStatus() != enStatus.EBH_Success)
enCurrentStatus = GetChild(i).Tick();
if (enCurrentStatus != enStatus.EBH_Success)
return enCurrentStatus;
}
return enStatus.EBH_Success;
}
}
}
7. 적용하기
1) AI 이동을 위한 스크립트인 "EnemyState_Patro_Rotation.cs" 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using myBehaviorTree;
public class EnemyState_Patrol_Rotation : btAction
{
private GameObject _myOwner;
private float _fIntervalTime = 1.0f;
private Vector3 _vDirection;
public EnemyState_Patrol_Rotation(GameObject myOwner)
{
_myOwner = myOwner;
}
public override void Initialize()
{
InitDirection();
SetStateColor();
}
private void SetStateColor()
{
_myOwner.GetComponent<MeshRenderer>().material.color = Color.green;
}
public override void Terminate()
{
base.Terminate();
}
public override enStatus Update()
{
OnRotationByDir();
return enStatus.EBH_Running;
}
private void InitDirection()
{
float fX = Random.Range(-1.0f, 1.0f);
float fZ = Random.Range(-1.0f, 1.0f);
_vDirection = new Vector3(fX, 0.0f, fZ);
_vDirection.Normalize();
}
public void OnRotationByDir()
{
_fIntervalTime = _fIntervalTime * Time.deltaTime;
if(_fIntervalTime < 0.0f)
{
float fX = Random.Range(-1.0f, 1.0f);
float fZ = Random.Range(-1.0f, 1.0f);
// 예외처리
if (fX == 0.0f && fZ == 0.0f)
fX = 1.0f;
_vDirection = new Vector3(fX, 0.0f, fX);
// interval 재설정
_fIntervalTime = Random.Range(0.5f, 3.0f);
}
// 회전
_myOwner.transform.rotation = Quaternion.Slerp(_myOwner.transform.rotation, Quaternion.LookRotation(_vDirection), Time.deltaTime);
}
}
2) "EnemyAI.cs" 스크립트 만들고 코드 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using myBehaviorTree;
public class EnemyAI : MonoBehaviour
{
private btRoot _btAIState;
// Start is called before the first frame update
void Start()
{
CreateBehaviorTreeAIState();
}
// Update is called once per frame
void Update()
{
// 자식노드들이 체크하면서 돌아감
_btAIState.Tick();
}
void CreateBehaviorTreeAIState()
{
_btAIState = new btRoot();
btSeletor btMainSelector = new btSeletor();
//patrol
btSequence btPatrol = new btSequence();
EnemyState_Patrol_Rotation statePatrol_Rotation = new EnemyState_Patrol_Rotation(gameObject);
btPatrol.AddChild(statePatrol_Rotation);
//main selector
btMainSelector.AddChild(btPatrol);
// 최종 root에 attach
_btAIState.AddChild(btMainSelector);
}
}
3) "Enemytate_Patrol_WayPoint.cs" 스크립트 생성후 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using myBehaviorTree;
public class EnemyState_Patrol_WayPoint : btAction
{
private GameObject _myOwner;
private float _fSpeedPower = 3.0f;
//waypoint
private List<Vector3> _listWayPoint = new List<Vector3>();
private int _iCurrentWayPoint = 0;
private float _fWayPointRadius = 2.0f;
public EnemyState_Patrol_WayPoint(GameObject myOwner)
{
_myOwner = myOwner;
}
public override void Initialize()
{
AddWayPoint();
}
public override void Terminate()
{
}
public override enStatus Update()
{
OnMove();
return enStatus.EBH_Running;
}
private void AddWayPoint()
{
_listWayPoint.Add(new Vector3(-10.0f, 0.0f, 20.0f));
_listWayPoint.Add(new Vector3(10.0f, 0.0f, 20.0f));
}
private void OnMove()
{
Vector3 vWayPoint = _listWayPoint[_iCurrentWayPoint];
float fDistance = Vector3.Distance(vWayPoint, _myOwner.transform.position);
// next way point
if(fDistance < _fWayPointRadius)
{
if (++_iCurrentWayPoint >= _listWayPoint.Count)
_iCurrentWayPoint = 0;
vWayPoint = _listWayPoint[_iCurrentWayPoint];
}
Vector3 vDir = vWayPoint - _myOwner.transform.position;
//회전
_myOwner.transform.rotation = Quaternion.Slerp(_myOwner.transform.rotation, Quaternion.LookRotation(vDir), Time.deltaTime * 4.0f);
//이동
_myOwner.transform.Translate(Vector3.forward * _fSpeedPower * Time.deltaTime);
}
}
* 실행하면 적이 회전하다가 wayPoint로 맞춰서 왔다갔다하는지 확인
4) "EnemyState_Chase_IsEnemy.cs" 스크립트 생성후 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using myBehaviorTree;
public class EnemyState_Chase_IsEnemy : btCondition // 무엇을 상속받는지 꼭 확인하고 기억할것!
{
private GameObject _myOwner;
public EnemyState_Chase_IsEnemy(GameObject myOwner)
{
_myOwner = myOwner;
}
public override enStatus Update()
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player)
{
float fDistance = Vector3.Distance(player.transform.position, _myOwner.transform.position);
if(fDistance < 10.0f)
{
return enStatus.EBH_Success;
}
}
return enStatus.EBH_Failure;
}
}
5) "EnemyState_Chase_LookAt.cs" 스크립트 생성후 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using myBehaviorTree;
public class EnemyState_Chase_LookAt : btAction
{
private GameObject _myOwner;
public EnemyState_Chase_LookAt(GameObject myOwner)
{
_myOwner = myOwner;
}
public override void Initialize()
{
SetStateColor();
}
public override void Terminate()
{
}
private void SetStateColor()
{
_myOwner.GetComponent<MeshRenderer>().material.color = Color.yellow;
}
public override enStatus Update()
{
OnLookAt();
return enStatus.EBH_Running;
}
private void OnLookAt()
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player)
{
Vector3 vDir = player.transform.position - _myOwner.transform.position;
//회전
_myOwner.transform.rotation = Quaternion.Slerp(_myOwner.transform.rotation, Quaternion.LookRotation(vDir), Time.deltaTime * 4.0f);
}
}
}
- "EnemyAI.cs" 에 코드 추가 (//chase 부분)
void CreateBehaviorTreeAIState()
{
_btAIState = new btRoot();
btSeletor btMainSelector = new btSeletor();
//chase
btSequence btChase = new btSequence();
EnemyState_Chase_IsEnemy stateChase_IsEnemy = new EnemyState_Chase_IsEnemy(gameObject);
btChase.AddChild(stateChase_IsEnemy);
EnemyState_Chase_LookAt stateChase_LookAt = new EnemyState_Chase_LookAt(gameObject);
btChase.AddChild(stateChase_LookAt);
//patrol
btSequence btPatrol = new btSequence();
EnemyState_Patrol_Rotation statePatrol_Rotation = new EnemyState_Patrol_Rotation(gameObject);
btPatrol.AddChild(statePatrol_Rotation);
EnemyState_Patrol_WayPoint statePatrol_WayPoint = new EnemyState_Patrol_WayPoint(gameObject);
btPatrol.AddChild(statePatrol_WayPoint);
//main selector
btMainSelector.AddChild(btPatrol);
// 최종 root에 attach
_btAIState.AddChild(btMainSelector);
}
6) "EnemyState_Chase_Chase.cs" 스크립트 생성후 작성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using myBehaviorTree;
public class EnemyState_Chase_Chase : btAction
{
private GameObject _myOwner;
private float _fSpeedPower = 5.0f;
public EnemyState_Chase_Chase(GameObject myOwner)
{
_myOwner = myOwner;
}
public override void Initialize()
{
SetStateColor();
}
private void SetStateColor()
{
_myOwner.GetComponent<MeshRenderer>().material.color = Color.cyan;
}
public override void Terminate()
{
}
public override enStatus Update()
{
OnChase();
return enStatus.EBH_Running;
}
private void OnChase()
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player)
{
Vector3 vDir = player.transform.position - _myOwner.transform.position;
//회전
_myOwner.transform.rotation = Quaternion.Slerp(_myOwner.transform.rotation, Quaternion.LookRotation(vDir), Time.deltaTime * 4.0f);
//이동
_myOwner.transform.Translate(Vector3.forward * _fSpeedPower * Time.deltaTime);
}
}
}
+ 다른 코드들도 조금씩 추가
("EnemyState_Chase_LookAt.cs", "EnemyAI.cs" 등)
##과제
animator controller 연결해보기
* animator = _myOwner.GetComponentInChildren();
'Unity' 카테고리의 다른 글
0709 Shader Graph (0) | 2019.07.09 |
---|---|
0705 Path Following 02 (0) | 2019.07.05 |
0705 Path Follow (0) | 2019.07.05 |
0704 GameRTS (0) | 2019.07.04 |
0703 Game RTS (0) | 2019.07.04 |