피곤핑
코딩일탈
피곤핑
전체 방문자
오늘
어제
  • 분류 전체보기
    • Kotlin & Java
    • Spring
      • Spring Security
      • Spring
    • 네트워크
    • JavaScript & Node js
    • Docker
    • Python3
    • Unity
    • 딥러닝
    • 객체지향프로그래밍
    • Error 보고서
    • 나의 이야기 & 회고
    • HTML & CSS
    • Archive
    • 독서

블로그 메뉴

  • 홈
  • 방명록

공지사항

인기 글

태그

  • 개발자취업
  • 코딩테스트준비
  • JavaScript
  • 오블완
  • Client
  • TiL
  • 99클럽
  • nodejs
  • 티스토리챌린지
  • 항해99

최근 댓글

hELLO · Designed By 정상우.
피곤핑

코딩일탈

0708 Game AI Behaviour Tree
Unity

0708 Game AI Behaviour Tree

2019. 7. 8. 15:40

 - 복잡한 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  (1) 2019.07.04
0703 Game RTS  (0) 2019.07.04
    'Unity' 카테고리의 다른 글
    • 0709 Shader Graph
    • 0705 Path Following 02
    • 0705 Path Follow
    • 0704 GameRTS
    피곤핑
    피곤핑

    티스토리툴바