당신은 주제를 찾고 있습니까 “유니티 3d 몬스터 ai – 3D 쿼터뷰 액션게임 – 목표를 추적하는 AI 만들기 [유니티 기초 강좌 B48]“? 다음 카테고리의 웹사이트 you.experience-porthcawl.com 에서 귀하의 모든 질문에 답변해 드립니다: you.experience-porthcawl.com/blog. 바로 아래에서 답을 찾을 수 있습니다. 작성자 골드메탈 이(가) 작성한 기사에는 조회수 23,726회 및 좋아요 389개 개의 좋아요가 있습니다.
유니티 3d 몬스터 ai 주제에 대한 동영상 보기
여기에서 이 주제에 대한 비디오를 시청하십시오. 주의 깊게 살펴보고 읽고 있는 내용에 대한 피드백을 제공하세요!
d여기에서 3D 쿼터뷰 액션게임 – 목표를 추적하는 AI 만들기 [유니티 기초 강좌 B48] – 유니티 3d 몬스터 ai 주제에 대한 세부정보를 참조하세요
유니티에서 제공하는 인공지능 AI
Navigation에 대해서 다루어보는 강좌입니다.
📖 챕터 :
01 00:00 오브젝트 생성
02 02:58 네비게이션
03 10:56 애니메이션
#유니티기초 #유니티강좌 #유니티3D #쿼터뷰
유니티 3d 몬스터 ai 주제에 대한 자세한 내용은 여기를 참조하세요.
[Unity 3D] 적 캐릭터 제작 : 인공지능 (AI) 구현 – 네이버 블로그
▷ 몬스터의 상태는 주로 휴면, 추적, 공격, 사망으로 정의를 합니다. 몬스터가 생성됐을 때 휴면 애니메이션을 수행하다가 플레이어가 접근해서 추적 …
Source: m.blog.naver.com
Date Published: 1/18/2022
View: 8167
AA(21) – 플레이어 추격하는 몬스터 AI 구현 – 일기
유니티/AA(3D 쿼터뷰 액션 게임). AA(21) – 플레이어 추격하는 몬스터 AI 구현 … 기본 몬스터로 사용할 오브젝트를 생성합니다.
Source: namjuhyung.tistory.com
Date Published: 1/14/2022
View: 1168
Chapter 13-8. 미니 RPG : 몬스터 AI – 평생 공부 블로그
인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다.
Source: ansohxxn.github.io
Date Published: 2/22/2021
View: 5494
#10 적 AI 구현하기
애니메이션이 완전히 실행되고 상태가 전환되어 부자연스러워서 체크해제한다. #유니티 트랜지션. https://docs.unity3d.com/kr/530/Manual/- …
Source: flowtree.tistory.com
Date Published: 10/8/2022
View: 5356
유니티 Nav Mesh Agent 이용해서 AI 만들기 (플레이어 따라 …
안녕하세요 ~ RPG 게임이나 FPS 게임에 PVE를 보면 특정 플레이어나 적을 따라다니면서 공격하는 AI들이 존재합니다. 오늘은 그런 AI들처럼 특정 타겟 …
Source: solution94.tistory.com
Date Published: 2/14/2022
View: 1136
[Unity] AI의 시야각(FieldOfView) 구현하는 법
안녕하세요 극꼼입니다. 오늘은 젤다 모작을 만들며 사용한 몬스터의 시야각을 구현하는 방법을 알아보겠습니다. 참고로 제가 만든 모작은 3D게임 …
Source: geukggom.tistory.com
Date Published: 1/8/2022
View: 8791
6. 플레이어를 추적하는 몬스터 스크립트 – 앤글 블로그
몬스터 AI를 제작하는 건 어느 정도가 적당한지 항상 고민이 된다. 제작하다 보면 굳이 필요 없는 기능까지 만들어야 되는지 고민하게 되는데 …
Source: angliss.cc
Date Published: 5/10/2021
View: 7662
[Unity2D] 적 몬스터 구현하기 – IagreeBUT
[유니티 기초 – B18] 몬스터 AI 구현하기 … 인공지능을 통해 자동으로 움직이는 것과 플레이어가 몬스터와 싸우는 모션까지 한번 해보도록 …Source: iagreebut.tistory.com
Date Published: 11/13/2022
View: 2768
주제와 관련된 이미지 유니티 3d 몬스터 ai
주제와 관련된 더 많은 사진을 참조하십시오 3D 쿼터뷰 액션게임 – 목표를 추적하는 AI 만들기 [유니티 기초 강좌 B48]. 댓글에서 더 많은 관련 이미지를 보거나 필요한 경우 더 많은 관련 기사를 볼 수 있습니다.
주제에 대한 기사 평가 유니티 3d 몬스터 ai
- Author: 골드메탈
- Views: 조회수 23,726회
- Likes: 좋아요 389개
- Date Published: 2020. 9. 1.
- Video Url link: https://www.youtube.com/watch?v=FBY_cmtCNHw
[Unity 3D] 적 캐릭터 제작 : 인공지능 (AI) 구현
▶ 몬스터의 상태를 지속적으로 체크하는 로직은 코루틴(Coroutine) 함수라는 것을 이용합니다. Update() 함수에서 구현하는 것이 잘못된 방법은 아니지만, 매 프레임마다 체크하는 것과 매 주기마다 체크하는 것은 과부하에 차이가 확연하기 때문입니다. 코루틴 함수는 IEnumerator형을 반환하게 되있습니다. (이 뜻은 IEnumerator형 함수는 코루틴 함수라고 이해하시면 됩니다)
yield return new WaitForSeconds(0.2f) 코드는 0.2초동안 기다렸다가 다음 내용들이 실행된다는 뜻입니다. 즉, 인자로 전달된 초 만큼 메인루프를 실행하도록 양보합니다. 좀 더 쉽게 설명을 드리자면 yield return 구문 이하의 로직을 대기시키는 효과를 가지고 있습니다. 일종의 Sleep 기능이라고 보시면 됩니다. 대체적으로 이러한 코루틴 함수는 일정한 주기에 맞추어 발생시켜야할 로직이 존재할 때 자주 사용합니다.
Vector3.Distance(Vector3 a, Vector3 b) 함수는 a, b 사이의 거리를 측정해서 반환하는 함수로 우리가 작성한 코드와 같은 경우에는 플레이어의 현재 위치와 몬스터의 현재 위치를 가지고 왔습니다.
플레이어 추격하는 몬스터 AI 구현
728×90
반응형
기본 몬스터로 사용할 오브젝트를 생성합니다.
리지드바디와 박스콜라이더, 에너미 스크립트를 추가합니다.
mat = GetComponentInChildren
().material; 매쉬랜더러가 오브젝트의 하위 오브젝트에 스킨매쉬랜더러로 있기 때문에 Enemy 스크립트에서 수정해줍니다.
몬스터 오브젝트의 태그와 레이어를 Enemy로 수정하고 Nav Mesh Agent 컴포넌트를 추가합니다.
public Transform target; //추적할 대상의 좌표 NavMeshAgent nav; void Awake() { nav = GetComponent
(); } void Update() { nav.SetDestination(target.position);//도착할 목표 위치 지정 함수 } Enemy스크립트에 using UnityEngine.AI;를 선언한 뒤 스크립트를 추가해줍니다.
타깃에 플레이어를 드래그&드롭합니다.
추적을 위한 네비매쉬를 생성합니다.
몬스터가 플레이어를 추적하여 다가옵니다.
오류는 기존 피격 테스터에서 생기는 오류입니다.
이제 필요 없으니 지워줍니다.
void FreezeVelocity() { rigid.velocity = Vector3.zero; rigid.angularVelocity = Vector3.zero; } void FixedUpdate() { FreezeVelocity(); }
물리력이 NavAgent 이동을 방해하지 않도록 로직을 추가합니다.
애니메이션을 세팅해줍니다.
파라메터와 트랜지션을 세팅하여 애니메이터를 완성합니다.
실행해보면 이동과 사망 애니메이션, 추적 모두 정상적으로 이루어집니다.
728×90
반응형
Chapter 13-8. 미니 RPG : 몬스터 AI
인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
Chapter 13. 미니 RPG 만들기
🚀 몬스터 AI
📜BaseController 👉 플레이어와 몬스터의 공통적인 속성과 기능 모음 📜PlayerController 📜MonsterController
📜MonsterController와 📜PlayerController의 공통적인 함수 및 멤버들은 📜BaseController로 옮겨주었음. 몬스터 애니메이션 컨트롤러의 애니메이션 클립의 이름들도 플레이어 애니메이션 컨트롤러와 동일하게.
📜MonsterController
몬스터 오브젝트에 붙여준다.
📜PlayerController 와 상당수 비슷하다. 여기에 없는건 📜BaseController로부터 상속 받음.
using System.Collections ; using System.Collections.Generic ; using UnityEngine ; using UnityEngine.AI ; public class MonsterController : BaseController { Stat _stat ; [ SerializeField ] float _scanRange = 10 ; [ SerializeField ] float _attackRange = 2 ; public override void Init () { _stat = gameObject . GetComponent < Stat >(); if ( gameObject . GetComponentInChildren < UI_HPBar >() == null ) Managers . UI . MakeWorldSpace < UI_HPBar >( transform ); }
게임이 시작되면 UI_HPBar 를 몬스터에게 붙인다. 📜BaseController로부터 이 Init을 실행시키는 Start 를 상속 받음
를 몬스터에게 붙인다.
protected override void UpdateIdle () { Debug . Log ( “Monster UpdateIdle” ); GameObject player = GameObject . FindGameObjectWithTag ( “Player” ); if ( player == null ) return ; float distance = ( player . transform . position – transform . position ). magnitude ; if ( distance <= _scanRange ) { _lockTarget = player ; State = Define . State . Moving ; return ; } } UpdateIdle 👉 몬스터가 Idle 상태일 때 매프레임 실행할 일 “Player”태그를 가진 오브젝트를 찾아 player 에 할당. 플레이어 오브젝트 찾기. 플레이어가 사정거리내에 존재하면 _lockTarget 에 플레이어 오브젝트 할당하고 이제 플레이어 쫓아가야 하니까 상태를 Moving 으로 변경 상태일 때 매프레임 실행할 일 protected override void UpdateMoving () { Debug . Log ( "Monster UpdateMoving" ); // 플레이어가 내 사정거리보다 가까우면 공격 if ( _lockTarget != null ) { _destPos = _lockTarget . transform . position ; float distance = ( _destPos - transform . position ). magnitude ; if ( distance <= _attackRange ) { State = Define . State . Skill ; return ; } } // 길 찾기 이동 Vector3 dir = _destPos - transform . position ; if ( dir . magnitude < 0.1f ) { State = Define . State . Idle ; } else { NavMeshAgent nma = gameObject . GetOrAddComponent < NavMeshAgent >(); nma . SetDestination ( _destPos ); nma . speed = _stat . MoveSpeed ; transform . rotation = Quaternion . Slerp ( transform . rotation , Quaternion . LookRotation ( dir ), 20 * Time . deltaTime ); } }
UpdateMoving 👉 몬스터가 Moving 상태일 때 매프레임 실행할 일 ‘시야’ 사정거리보다 가까우면 UpdateIdle 를 통해 _lockTarget 에 플레이어 들어있는 상태 플레이어를 향해 _destPos 업뎃하고 플레이어가 ‘공격’ 사정거리보다 가까우면 공격. 그리고 길 찾을 필요 없으니 return 길 찾기 도착했다면 Idle 상태로 돌아가기 아니라면 플레이어 향해 바라보며 쫓아가야 함..
상태일 때 매프레임 실행할 일
protected override void UpdateSkill () { Debug . Log ( “Monster UpdateSkill” ); if ( _lockTarget != null ) { Vector3 dir = _lockTarget . transform . position – transform . position ; Quaternion quat = Quaternion . LookRotation ( dir ); transform . rotation = Quaternion . Lerp ( transform . rotation , quat , 20 * Time . deltaTime ); } }
UpdateSkill 👉 몬스터가 Skill 상태일 때 매프레임 실행할 일 공격 중에 플레이어 바라보고 공격하게끔
상태일 때 매프레임 실행할 일
void OnHitEvent () { Debug . Log ( “Monster OnHitEvent” ); if ( _lockTarget != null ) // 플레이어 타겟팅 중 { // 체력 Stat targetStat = _lockTarget . GetComponent < Stat >(); int damage = Mathf . Max ( 0 , _stat . Attack – targetStat . Defense ); targetStat . Hp -= damage ; if ( targetStat . Hp > 0 ) { float distance = ( _lockTarget . transform . position – transform . position ). magnitude ; if ( _attackRange >= distance ) State = Define . State . Skill ; else State = Define . State . Moving ; } else { State = Define . State . Idle ; } } else // 플레이어 타겟팅 중이 아닐 땐 { State = Define . State . Idle ; } } }
OnHitEvent 👉 몬스터의 공격 애니메이션 중 발생하는 이벤트 플레이어의 체력 깎기 플레이어가 아직 안 죽었다면 공격 사정거리 이내라면 다시 공격 아니라면 다시 쫓기 플레이어가 죽었다면 정지
✈ 이동시 밀리는 현상
📜MonsterController : 몬스터가 이동시 플레이어를 미는 현상
protected override void UpdateMoving () { Debug . Log ( “Monster UpdateMoving” ); // 플레이어가 내 사정거리보다 가까우면 공격 if ( _lockTarget != null ) { _destPos = _lockTarget . transform . position ; float distance = ( _destPos – transform . position ). magnitude ; if ( distance <= _attackRange ) { NavMeshAgent nma = gameObject . GetOrAddComponent < NavMeshAgent >(); nma . SetDestination ( transform . position ); State = Define . State . Skill ; return ; } }
if ( distance <= _attackRange ) { NavMeshAgent nma = gameObject . GetOrAddComponent < NavMeshAgent >(); nma . SetDestination ( transform . position );
공격할 때도 계속 짧은 새 마다 플레이어를 쫓지 않도록(밀지 않도록), 공격 사정 거리 내에 있으면 그냥 제자리에 있도록 nma.SetDestination(transform.position)
📜PlayerController : 플레이어가 이동시 몬스터를 미는 현상
protected override void UpdateMoving () { // 이동 Vector3 dir = _destPos – transform . position ; if ( dir . magnitude < 0.1f ) { State = Define . State . Idle ; } else { Debug . DrawRay ( transform . position + Vector3 . up * 0.5f , dir . normalized , Color . green ); if ( Physics . Raycast ( transform . position + Vector3 . up * 0.5f , dir , 1.0f , LayerMask . GetMask ( "Block" ))) { if ( Input . GetMouseButton ( 0 ) == false ) // State = Define . State . Idle ; return ; } float moveDist = Mathf . Clamp ( _stat . MoveSpeed * Time . deltaTime , 0 , dir . magnitude ); transform . position += dir . normalized * moveDist ; transform . rotation = Quaternion . Slerp ( transform . rotation , Quaternion . LookRotation ( dir ), 20 * Time . deltaTime ); } } NavMeshAgent 의 nma.Move 함수로 이동하지 않고 직접 플레이어의 위치를 업뎃시켜 해결하였다. transform.position += dir.normalized * moveDist; NavMeshAgent를 붙여서 이동하는 방식은 기본적으로 Agent들은 서로 피해가도록 되어 있어 너무 인접하게 붙으면 의도치 않게 상대를 밀치기도 한다. Obstacle Avoidance 속성 때문이다. 이를 해결하는 방법 중 하나는 NavMeshAgent 를 사용하지 않고 레이저를 쏴서 이동 가능한지를 확인 한 후 일반적인 플레이어 위치 세팅으로 이동을 하는 것이다. -출처 : Rookiss님 답변- https://stackoverflow.com/questions/23451983/how-to-avoid-two-navmeshagent-push-away-each-other-in-unity 🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄 맨 위로 이동하기
#10 적 AI 구현하기
반응형
1.구현 목적
적 AI를 왜 구현해야 할까?(적 AI 기획의도)
= 게임 플레이가 재미있었으면…!
>장르가 도대체 뭐냐? 슈팅? 생존? 아케이드? = 아케이드
>단순 조작으로 재미가 있어야 하니까
>전투가 재미있었으면!
>적을 쉽게 잡는 재미, 적의 공격을 피하는 재미, 적절한 긴장감을 주는 적 AI 필요
구현할 적 AI 타입 정리
적을 쉽게 잡는 재미 제공 = 단순한 공격을 하고 잡기 쉬운 AI = 근접형 AI
적의 공격을 피하는 재미 제공 = 피할 수 있는 원거리 공격을 하는 AI = 원거리형 AI
적절한 긴장감 제공 = 강한 데미지를 가하고 예상치 못한 공격을 가하는 AI = 순간이동형 AI
#여담-추가적으로 생각할 요소, 나중에 마나 시스템 추가하기
마나 파밍 방법에 따라서 AI 난이도와 비율을 조절하기
1.적 사망 후 마나 드롭만으로 마나 파밍을 할지
2.마나 파밍 + 일정 시간이 지나면 조금씩 차오르게 할지
3.마나 파밍 + 일정 시간 마다 플레이어 주변에 마나가 드롭되게 할지
고민 중…
우선 적 3가지 타입을 구현 후 마나 시스템을 추가하고 테스트하며 수정하기로 결정
(고민만 하는 것보다 시간 절약 & 직접 테스트하며 원하는 기획의도를 확인해보는 게 낫다)
마나 수급량에 대한 고민
적을 너무 쉽게 잡으면 마나 파밍이 쉬워지면서 스킬 난사가 가능해진다.
그러면 플레이가 너무 쉬워지고 지루해진다.
스킬 난사를 방지하기 위해서 적 AI 난이도에 따라서 드롭하는 마나량을 구분했다.
적 AI를 쉬움, 보통, 어려움으로 구분했고, 잡기 쉬울 수록 소량의 마나를 드롭한다.
적의 난이도와 스폰 비율에 따라서 플레이 난이도와 마나 수급량이 정해지므로
일단 약간 느린 템포로 스킬을 사용하도록 스폰 비율은 쉬움 7 : 보통 3 : 어려움 1로 정했다.
이러한 적 AI와 마나 수급량으로 플레이 시 적절한 전투의 재미와 긴장감, 스킬 사용의 재미 밸런스를 조절할 것이다.
2.적 AI 간단 소개
사용한 적 리소스
비주얼 통일성: 적들의 외형과 분위기가 각각 다르면 이질감을 줄 것 같았다. 다른 타입의 적들이 같은 공간에서 등장해도 자연스럽게 느껴지도록 하나의 에셋에서(동일한 그래픽 분위기) 적 리소스를 선별했다.
SYNTY STUDIOS의 POLYGON – Dungeons Pack을 구매해서 이용했다.
근접형 AI
스켈레톤
외형 한손검을 장비한 스켈레톤 체력 적음 데미지 보통 공격 속도 빠름 마나 드롭량 소량 스폰 비율(10) 다수(7) 전투 난이도 쉬움 전투 스타일 1.플레이어를 추적하며 공격 사거리안에서 근접 공격을 한다. 2.공격 속도가 빨라서 한 번 붙으면 연속 공격을 받기 때문에 플레이어에게 일정 거리를 유지하게 한다. 설명 1.주 마나 파밍 요소: 거리만 유지한다면 쉽게 잡을 수 있어서 주 마나 파밍용 적이다. 2.노력형 요소: 적을 많이 잡아서 소량의 마나를 계속 저장하여 스킬을 사용하는 노력형 요소이다.
원거리형 AI
고블린 샤먼
외형 1.원시 주술을 사용하기 때문에 해골 지팡이를 장비함 2.원시 주술사 느낌으로 가면과 가죽 갑옷을 입은 고블린 원래는 맵 컨셉을 봉인된 던전으로 하여 스켈레톤 타입 적으로 통일성을 주려고 했는데 관련 리소스를 구하기 힘들어서(특히 애니메이션) 고블린 샤먼이 스켈레톤과 고스트를 조종한다는 설정으로 변경했다. 체력 보통 데미지 높음 공격 속도 보통 마나 드롭량 중간량 스폰 비율(10) 소수(2) 전투 난이도 보통 전투 스타일 공격 사거리 안에서 멈춰서 직선형 원거리 마법을 플레이어를 향해 발사한다. 설명 1.원거리 마법을 통해 플레이어의 이동과 공격을 견제한다. 2.원거리 마법이 플레이를 까다롭게 하기 때문에 플레이어에게 회피 또는 원거리 적 제거의 플레이 우선순위를 선택하게 한다.(플레이 우선순위 판단 요소) 3.회피(까다로운 공격 감수, 회피하는 재미) vs 원거리 적 제거(마나 보상 & 원활한 플레이) 4.적 제거를 한다면 괜찮은 마나 드롭량을 통해 보다 빠르게 스킬을 사용할 수 있게 한다.
순간이동형 AI
고스트
외형 전체가 반투명한 맨손의 유령 병사 초기 컨셉으로 점프 도약 공격(눈으로 식별할 수 있는 공격을 통해 플레이어가 대응 & 회피할 수 있게)을 하는 스켈레톤으로 기획했으나 해당 그래픽 리소스와 애니메이션을 찾을 수 없어서 고민했으나 유령의 경우 순간이동, 투과하는 특징이 있어서 유령 타입으로 변경했다. 이 후 점프 도약과 비슷한 기능을 하는 순간이동 기능을 추가했다. 아쉽겠도 순간이동의 경우 눈으로 식별하기 어려운 단점이 있다. 순간이동을 이용하기 전에 준비 동작?준비 효과?를 (몸이 안개로 변하는?) 추가해서 플레이어가 식별해서 대응할 수 있게 하고 싶다.(미래의 일…) 체력 낮음 데미지 매우 높음 공격 속도 느림 마나 드롭량 대량 스폰 비율(10) 극소수(1) 전투 난이도 어려움 전투 스타일 1.느린 이동 속도로 적에게 접근하여 강한 공격을 한다. 2.일정 시간 마다 플레이어 근처로 순간이동을 할 수 있어 예상치 못한 공격을 한다.(긴장감 유발 & 회피 강요) 3.플레이어에게 적과 일정 거리를 유지하며 제거하는 플레이를 유도한다. 설명 1.느린 이동속도로 플레이어를 방심하게 하나 순식간에 접근하여 공격할 수 있기 때문에 주시해야할 적이다. 2.하이리스크 하이리턴: 예상치 못한 공격과 매우 높은 데미지를 감수하고 적을 제거한다면 대량의 마나를 쉽게 파밍할 수 있어서 이 후 스킬 사용을 통해 더 많은 이익을 얻을 수 있다.
3.구현된 영상
4.구현할 기능
LivingEntity 클래스
적과 플레이어는 모두 비슷한 기능을 가진다. 체력이 있다. 피격받는다. 사망한다. 이런 공통 기능들은 모아서 부모 클래스인 LivingEntity에 먼저 구현했다. 유니티 게임 프로그래밍 에센스책의 내용을 이용했다.
필요한 데이터
기본 체력: 해당 타입의 기본 체력
기본 마나: 해당 타입의 기본 마나
현재 체력: 해당 타입의 게임 플레이 시 현재 체력
현재 마나: 해당 타입의 게임 플레이 시 현재 마나
현재 사망 상태(bool): 해당 타입의 사망 상태를 결정하는 데이터
필요한 기능
리셋 기능: 해당 타입이 활성화될 때 상태를 리셋, 사망하지 않은 상태로 시작, 시작 시 현재 체력을 기본 체력으로 설정
피격 기능: 피격 시 효과와 데미지만큼 현재 체력을 감소하는 기능(데미지 계산식 X, 단순 계산)
사망 기능: 현재 체력이 0이하 시 해당 타입을 사망 처리, 제거하는 기능
근접형 AI
근접형 AI는 플레이어를 추적하고 AI는 공격 사거리까지 플레이어에게 접근하여 공격한다. AI에 필요한 기본적인 기능들은 근접형 AI에서 구현했고 다른 타입을 구현할 때 이용했다.
#여담-클래스를 구현 못해서…
기본 타입을 부모 클래스로 해서 파생 타입을 자식 클래스로 구현하고 싶었지만 C#을 잘 모르기 때문에 기본 타입 스크립트를 복제해서 일일이 파생 타입을 구현했다.
구현하고 싶은 것들은 유튜브나 인터넷 검색으로 어느정도 따라해서 구현할 수 있지만 관련 자료가 없으면 구현이 어렵다. 정말 내 머릿 속 기획을 구현하고 싶다면 C#의 기본을 알아야함을 느낀다.
그래서 이번에 “이것이 C# 이다” 책을 구매했다. 진득하게 처음부터 공부하고싶지만 내 계획에 맞춰서 게임을 개발하고 포트폴리오를 준비하려면 불가능하다. 그래서 모르는 내용이 있을때 C# 책을 사전처럼 사용하기로 했다.
시중의 유니티 게임 개발 책은 유니티 게임 개발 입문으로 프로그래밍에 대한 진입 장벽을 낮춰줄 뿐이지, 그것만으로 부족하다. 누군가 개발한 것을 따라해서 만드는 것은 한계가 분명있다.
내 경험과 생각, 기획의도를 게임과 특정 기능으로 구현하고 싶다면 프로그래밍 지식(나는 C#)은 정말 필수인 것 같다. 만드는 방법을 알아야지 스스로 게임 개발이 가능하다.
근데, 유니티 게임 프로그래밍 에센스책은 설명이 정말 잘 되어있다. 잘 읽어보면 입문 실력한테 정말 큰 도움이 된다.(탈룰라 무엇?)
필요한 데이터
추적 대상: 적이 플레이어를 추적 시 필요한 추적 대상
기본 체력: 적의 기본 체력
추적 대상과의 거리: 실시간으로 추적 대상과 적의 거리를 계산하여 추적 대상이 공격 사거리 안에 있는지 비교하기 위한 데이터
공격력: 적의 공격력
공격 사거리: 적이 공격할 수 있는 공격 사거리
공격 속도(공격 딜레이): 적의 공격 속도
마지막 공격 시점: 마지막 공격 시점을 갱신 및 검사하여 공격 딜레이를 적용
이동 속도: 적의 이동 속도
마나 드롭량: 적이 사망 시 드롭할 마나의 량(미구현)
점수: 적이 사망 시 게임매니저에 제공할 점수(미구현)
필요한 기능
탐지 기능: 플레이어를 탐지하고 추적 대상으로 설정하는 기능
추적 기능: 추적 대상과의 거리를 계산하고 추적하는 기능
공격 기능: 추적 대상과의 거리와 적의 공격 사거리를 검사하고 공격 + 데미지를 입히는 기능
사망 기능: 적의 체력이 0 이하가 되면 적을 사망 처리하고 제거하는 기능(미구현)
마나 드롭 기능(미구현)
점수 업데이트 기능(미구현)
원거리형 AI
원거리형 AI는 공격 사거리 안에 플레이어가 있으면 멈춰서 원거리 마법을 발사한다. 근접형 AI에서 공격 기능을 수정하고 원거리 마법 발사 기능을 추가하면 된다. 근접형 AI를 기본으로 만들기 때문에 중복되는 정보는 제외했다.
필요한 데이터
기본 체력
공격 사거리
데미지
공격 속도(공격 딜레이)
이동 속도
마나 드롭량(미구현)
점수(미구현)
필요한 기능
공격 기능(마법구 발사)
공격 기능(마법구 발사) 마법구 이동 기능
순간이동형 AI
순간이동형 AI는 느리게 걸으며 근접 공격을 한다. 순간이동은 일정 시간마다 이용할 수 있고 한 번씩 사용할 수 있다. 쿨타임이 지나면 다시 이용할 수 있다. AI의 순간이동 사거리 안에 플레이어가 있으면 AI는 순간이동을 할 수 있다. AI가 순간이동 시 플레이어 캐릭터 주변의 랜덤한 위치로 순간이동을 한다.
필요한 데이터
기본 체력
데미지
공격 사거리
공격 속도(공격 딜레이)
순간이동 거리
순간이동 쿨타임
순간이동 횟수
마나 드롭량(미구현)
점수(미구현)
필요한 기능
순간이동 기능(쿨타임, 횟수 카운트, 순간이동 위치 계산, 순간 이동)
5.구현 과정
5.1.LivingEntity 스크립트
using System.Collections; using System.Collections.Generic; using System; using UnityEngine; //생명체로 동작할 게임 오브젝트들의 뼈대를 제공 //체력, 피해받음, 사망 기능, 사망 이벤트 제공 public class LivingEntity : MonoBehaviour//, IDamageable 적용 예정 { public float startingHealth = 100f; //시작 체력 public float startingMana = 0f; //시작 마나 public float health { get; protected set;} //현재 체력 public float mana { get; protected set; } //현재 마나 public bool dead { get; protected set;} //사망 상태 public event Action onDeath; //사망 시 발동할 이벤트 //생명체가 활성화될 떄 상태를 리셋 protected virtual void OnEnable() { //사망하지 않은 상태로 시작 dead = false; //체력을 시작 체력으로 초기화 health = startingHealth; mana = startingMana; } //피해를 받는 기능 public virtual void OnDamage(float damage) { //데미지만큼 체력 감소 health -= damage; // health = health – damage; //체력이 0 이하 && 아직 죽지 않았다면 사망 처리 실행 if (health <= 0 && !dead) { Die(); } } //체력을 회복 하는 기능은 책에 있는데 나는 플레이어 스텟 스크립트에서 적용할 것임 //사망 처리 public virtual void Die() { //onDeath 이벤트에 등록된 메서드가 있다면 실행 if (onDeath != null) { onDeath(); } dead = true; } } 5.2.근접형 AI 5.2.1.오브젝트 준비하기 1.하이어라키 창에서 빈 게임 오브젝트를 생성 > Enemys로 이름을 변경
2.Enemys는 모든 AI 오브젝트를 모아둘 게임 오브젝트
3.Enemys에서 빈 게임 오브젝트를 생성하고 Enemy_Skeleton로 이름 변경
4.Enemy_Skeleton는 적 모델링과 타겟팅UI을 담는다.
5.Enemy_Skeleton 오브젝트에 프로젝트에 있는 적 모델링(Character_Skeleton_Solider_02)을 추가
6.스켈레톤이 검을 장비하도록 적 모델링 > 오른손 오브젝트 > 사용할 무기 오브젝트를 넣어준다.
(검 위치 미세조정은 애니메이션 설명에서)
7.플레이어에게 적이 타겟팅됐을 경우 타겟팅UI를 보여주기 위해서 이전에 사용한 타겟팅UI(Canvas)를 Enemy_Skeleton 오브젝트에 넣어준다.
8.타겟팅UI는 적 모델링 정중앙 바닥에 위치하도록 위치를 조정한다.
9.타겟팅UI의 인스펙터를 체크해제하고 FieldOfView 스크립트에서 컨트롤한다.(FieldOfView 글 참고)
5.2.2.컴포넌트 추가하기
1.AI의 움직임을 제어할 Animator 컴포넌트 추가하기
2.애니메이터 컴포넌트의 Apply Root Motion 체크해제하기
3.AI의 충돌을 감지할 Capsule Collider 컴포넌트 추가하기
4.캡슐 콜라이더의 크기를 AI의 크기게 맞게 변경한다. Center Y는 1, Height는 2로 변경
5.AI가 플레이어를 탐지하고 추적할 수 있게 해주는 Nav Mesh Agent 컴포턴트를 추가하기
5.2.3.애니메이션 설정하기
1.프로젝트 창 > 에셋 > 마우스 오른쪽 클릭 > Create > Animator Controller 클릭하여 애니메이터를 생성한다.
2.애니메이터의 이름을 Enemy Skeleton으로 변경하여 구분하기 쉽게한다.
3.애니메이터를 더블 클릭해서 애니메이터 창을 연다.
4.AI의 Idle, Move, Attack1 상태를 구현한다.
5.Base Layer에서 마우스 오른쪽 클릭 > Create State > Empty를 클릭하여 빈 상태를 생성한다.
6.상태 설명
Idle: 대기하고 있는 상태, 대기하고 있는 애니메이션을 실행
Move: 이동하는 상태, 이동 애니메이션을 실행
Attack1: 공격하는 상태, 공격 애니메이션을 실행
7.상태들을 만들고 상태별 전환을 위해 Bool 타입 Parameters들을 추가한다.
bool타입 파라미터 CanMove: Move 상태 전환 스위치
bool타입 파라미터 CanAttack: Attack 상태 전환 스위치
8.스위치 값에 상태가 전환될 수 있게 Make Transition생성하여 상태들을 연결한다.
9.생성된 Transition을 선택하고 Conditions에서 사용할 Parameters와 Parameters의 값을 설정한다.
10.상태 변환 조건
상태 전환 파라미터 값 Idle > Move CanMove true Move > Idle false Idle > Attack1 CanAttack true Attack1 > Idle false
11.모든 Transitions들은 연결 후 Has Exit Time을 해제한다. Has Exit Time을 체크하면 실시간으로 애니메이션 변경이 안된다. 애니메이션이 완전히 실행되고 상태가 전환되어 부자연스러워서 체크해제한다.
#유니티 트랜지션
https://docs.unity3d.com/kr/530/Manual/class-Transition.html
12.Enemy Skeleton 애니메이터를 완성 후 Enemy Skeleton 게임 오브젝트의 애니메이터 컴포넌트에 할당한다.
13.플레이를 눌러서 애니메이션이 잘 작동하는 지 확인한다.
14.적에게 장비 시켜준 무기가 이상한 위치와 방향이라면 조절한다.
15.Play 버튼 옆에 있는 Pause 누르고 무기를 선택하고 E를 눌러서 Rotate Tool을 선택한다.
16. 무기의 위치와 방향을 자연스럽게 변경한다.
17.현재의 위치 정보를 복사한다. 인스펙터 창 > Transform 컴포넌트 > 톱니바퀴 > Copy Component
18. 플레이 정지 후 무기를 선택하고 Paste Component Values를 클릭하면 쉽게 무기 위치와 방향을 변경할 수 있다.
5.2.4.근접형 AI 플로우 차트
이 게임은 아케이드 게임처럼 단순한 조작으로 전투를 하기 때문에 AI 또한 단순하게 구현했다. 물론 짜임새 있는 AI는 전투를 즐겁게 해주지만 내가 그걸 구현할 실력도 안되고 시간도 부족해서 적 AI 한테 꼭 필요한 기능만 구현했다.
AI가 단순하다는 단점은 새로운 패턴/기능이 있는 파생형 타입 AI를 추가해서 보완했다. 만약에 게임을 지속적으로 업데이트를 하게 된다면 새로운 패턴/기능이 있는 파생형 AI들을 계속 추가할 것 같다.
근접형 AI는 탐지 > 추적 > 공격의 단계로 행동한다. AI의 행동이 끊김없이 자연스럽게 행동하길 원해서 코루틴으로 0.25초 간격으로 AI를 실행했다.
1.탐지
AI는 AI를 중심으로 탐지 범위가 있으며, 탐지 범위 안에서 살아 있는 플레이어를 탐지할 수 있다.
플레이어를 탐지했을 경우 해당 플레이어를 추적 대상으로 지정한다.
탐지 범위 안에 플레이어가 없을 경우 제자리에 정지하고 계속해서 탐지 범위를 체크한다.
2.추적
추적 대상이 있으면 AI의 공격 사거리까지 대상을 추적한다.
3.공격
AI가 공격 사거리까지 추적 대상을 추적했을 경우 이동을 정지한다.
AI는 추적 대상 방향으로 바라본다. (공격 애니메이션을 실행 시 공격 방향을 맞추기 위해서)
공격을 하기 전 공격 딜레이를 체크해서 공격이 가능한지 판단한다.
공격을 할 수 있으면 공격 애니메이션을 실행한다.
공격 애니메이션의 특정 프레임에서(유니티 애니메이션 이벤트 이용) 플레이어에게 데미지를 적용한다.
5.2.5.기능 구현
탐지 기능
1.읽기 전용 프로퍼티로 추적 대상 존재 유무를 구현한다.
2.탐지 기능은 Coroutine로 0.25초마다 실행한다. 최적화 + 끊김없는 행동을 위해서
3.while문을 이용해서 단순하고 특정 조건일 때만 탐지 기능을 실행한다.
추적 기능
1.Enemy_Skeleton 오브젝트에 Nav Mesh Agent 컴포넌트를 추가하기
2.Enemy 스크립트에서 Nav Mesh Agent를 제어하기
1.경로 계산 AI 에이전트 변수 선언 //AI 에이전트 private NavMeshAgent pathFinder; //추적대상 private LivingEntity targetEntity; 2.AI 에이전트 정지 제어 pathFinder.isStopped = true/false; 3.AI 목적지 정하기 pathFinder.SetDestination(추적대상.transform.position); pathFinder.SetDestination(targetEntity.transform.position);
공격 기능
1.추적 대상이 공격 사거리 안에 있으면 공격 메서드 실행하기
2.추적 대상이 공격 사거리 밖에 있으면 계속 추적하기
3.추적 대상 방향으로 바라보기 (transform.LookAt)
4.공격 딜레이: 최근 공격 시점+공격 딜레이 만큼 지나면 공격 가능
//추적 대상과의 거리에 따라 공격 실행 public virtual void Attack() { //자신이 사망X, 추적 대상과의 거리이 공격 사거리 안에 있다면 if (!dead && dist < attackRange) { //공격 반경 안에 있으면 움직임을 멈춘다. canMove = false; //추적 대상 바라보기 this.transform.LookAt(targetEntity.transform); //최근 공격 시점에서 attackDelay 이상 시간이 지나면 공격 가능 if (lastAttackTime + attackDelay <= Time.time) { canAttack = true; } //공격 반경 안에 있지만, 딜레이가 남아있을 경우 else { canAttack = false; } } //공격 반경 밖에 있을 경우 추적하기 else { canMove = true; canAttack = false; //계속 추적 pathFinder.isStopped = false; //계속 이동 pathFinder.SetDestination(targetEntity.transform.position); } } 공격 기능 - 데미지 적용하기 기능 대상을 공격하는 순간에 데미지를 적용하기 위해서 유니티 애니메이션 이벤트에서 사용할 메서드를 작성했다. 공격 애니메이션이 특정 프레임에서 데미지 적용 메서드가 실행되어 자연스럽게 데미지 적용을 할 수 있다. //유니티 애니메이션 이벤트로 공격하는 모션일 때 데미지 적용시키기 public void OnDamageEvent() { //공격 대상을 지정할 추적 대상의 LivingEntity 컴포넌트 가져오기 LivingEntity attackTarget = targetEntity.GetComponent
(); //공격 처리 attackTarget.OnDamage(damage); //최근 공격 시간 갱신 lastAttackTime = Time.time; } 5.2.6.스크립트
Enemy
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; //AI, 네비게이션 시스템 관련 코드 가져오기 public class Enemy : LivingEntity { public LayerMask whatIsTarget; //추적대상 레이어 private LivingEntity targetEntity;//추적대상 private NavMeshAgent pathFinder; //경로 계산 AI 에이전트 /*public ParticleSystem hitEffect; //피격 이펙트 public AudioClip deathSound;//사망 사운드 public AudioClip hitSound; //피격 사운드 */ private Animator enemyAnimator; //private AudioSource enemyAudioPlayer; //오디오 소스 컴포넌트 public float damage = 20f; //공격력 public float attackDelay = 1f; //공격 딜레이 private float lastAttackTime; //마지막 공격 시점 private float dist; //추적대상과의 거리 public Transform tr; private float attackRange = 2.3f; //추적 대상이 존재하는지 알려주는 프로퍼티 private bool hasTarget { get { //추적할 대상이 존재하고, 대상이 사망하지 않았다면 true if (targetEntity != null && !targetEntity.dead) { return true; } //그렇지 않다면 false return false; } } private bool canMove; private bool canAttack; private void Awake() { //게임 오브젝트에서 사용할 컴포넌트 가져오기 pathFinder = GetComponent
(); enemyAnimator = GetComponent (); //enemyAudioPlayer = GetComponent (); } //적 AI의 초기 스펙을 결정하는 셋업 메서드 public void Setup(float newHealth, float newDamage, float newSpeed) { //체력 설정 startingHealth = newHealth; health = newHealth; //공격력 설정 damage = newDamage; //네비메쉬 에이전트의 이동 속도 설정 pathFinder.speed = newSpeed; } void Start() { //게임 오브젝트 활성화와 동시에 AI의 탐지 루틴 시작 StartCoroutine(UpdatePath()); tr = GetComponent (); } // Update is called once per frame void Update() { enemyAnimator.SetBool(“CanMove”, canMove); enemyAnimator.SetBool(“CanAttack”, canAttack); if (hasTarget) { //추적 대상이 존재할 경우 거리 계산은 실시간으로 해야하니 Update() dist = Vector3.Distance(tr.position, targetEntity.transform.position); } } //추적할 대상의 위치를 주기적으로 찾아 경로 갱신 private IEnumerator UpdatePath() { //살아 있는 동안 무한 루프 while(!dead) { if (hasTarget) { Attack(); } else { //추적 대상이 없을 경우, AI 이동 정지 pathFinder.isStopped = true; canAttack = false; canMove = false; //반지름 20f의 콜라이더로 whatIsTarget 레이어를 가진 콜라이더 검출하기 Collider[] colliders = Physics.OverlapSphere(transform.position, 20f, whatIsTarget); //모든 콜라이더를 순회하면서 살아 있는 LivingEntity 찾기 for (int i = 0; i < colliders.Length; i++) { //콜라이더로부터 LivingEntity 컴포넌트 가져오기 LivingEntity livingEntity = colliders[i].GetComponent (); //LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아 있다면 if (livingEntity != null && !livingEntity.dead) { //추적 대상을 해당 LivingEntity로 설정 targetEntity = livingEntity; //for문 루프 즉시 정지 break; } } } //0.25초 주기로 처리 반복 yield return new WaitForSeconds(0.25f); } } //추적 대상과의 거리에 따라 공격 실행 public virtual void Attack() { //자신이 사망X, 추적 대상과의 거리이 공격 사거리 안에 있다면 if (!dead && dist < attackRange) { //공격 반경 안에 있으면 움직임을 멈춘다. canMove = false; //추적 대상 바라보기 this.transform.LookAt(targetEntity.transform); //최근 공격 시점에서 attackDelay 이상 시간이 지나면 공격 가능 if (lastAttackTime + attackDelay <= Time.time) { canAttack = true; } //공격 반경 안에 있지만, 딜레이가 남아있을 경우 else { canAttack = false; } } //공격 반경 밖에 있을 경우 추적하기 else { canMove = true; canAttack = false; //계속 추적 pathFinder.isStopped = false; //계속 이동 pathFinder.SetDestination(targetEntity.transform.position); } } //유니티 애니메이션 이벤트로 휘두를 때 데미지 적용시키기 public void OnDamageEvent() { //공격 대상을 지정할 추적 대상의 LivingEntity 컴포넌트 가져오기 LivingEntity attackTarget = targetEntity.GetComponent (); //공격 처리 attackTarget.OnDamage(damage); //최근 공격 시간 갱신 lastAttackTime = Time.time; } //데미지를 입었을 때 실행할 처리 public override void OnDamage(float damage) { /*사망하지 않을 상태에서만 피격 효과 재생 if (!dead) { //공격 받은 지점과 방향으로 피격 효과 재생 hitEffect.transform.position = hitPoint; hitEffect.transform.rotation = Quaternion.LookRotation(hitNormal); hitEffect.Play(); //피격 효과음 재생 enemyAudioPlayer.PlayOnShot(hitSound); } */ //피격 애니메이션 재생 enemyAnimator.SetTrigger(“Hit”); //LivingEntity의 OnDamage()를 실행하여 데미지 적용 base.OnDamage(damage); } //사망 처리 public override void Die() { //LivingEntity의 DIe()를 실행하여 기본 사망 처리 실행 base.Die(); //다른 AI를 방해하지 않도록 자신의 모든 콜라이더를 비활성화 Collider[] enemyColliders = GetComponents (); for (int i = 0; i < enemyColliders.Length; i++) { enemyColliders[i].enabled = false; } //AI추적을 중지하고 네비메쉬 컴포넌트를 비활성화 pathFinder.isStopped = true; pathFinder.enabled = false; //사망 애니메이션 재생 enemyAnimator.SetTrigger("Die"); /*//사망 효과음 재생 enemyAudioPlayer.PlayOnShot(deathSound); */ } } 5.3.원거리형 AI 기본적인 AI 플로우는 근접형 AI를 따르며, 근접 공격에서 원거리 공격으로 변경하여 구현했다. 이 원거리 공격 기능(마법구)은 플레이어 캐릭터의 매직 미사일 기능과 똑같아서 그대로 구현했다. 오브젝트, 컴포넌트 준비 과정은 근접형 AI 준비과정과 비슷하니 생략한다. 5.3.1.원거리 공격 기능 구현 AI의 마법구 생성 기능 1.공격 대상 방향을 바라보고 공격하기 2.공격할 때 유니티 애니메이션 이벤트로 마법구 생성 메서드 실행 public void ShamanFire() { magicMissile = Instantiate(magicMissilePrefab, firePoint.transform.position, firePoint.transform.rotation); //Instatiate()로 매직 미사일 프리팹을 복제 생성한다. } AI의 마법구 이동 기능 1.생성된 마법구는 전방으로 날아간다.(유도 기능X, 플레이어가 날아오는 마법구를 보고 피할 수 있다.) 2.오브젝트와 충돌했을 때 충돌한 오브젝트가 플레이어면 데미지 적용 5.3.2.스크립트 EnemyShaman(원거리형 AI 스크립트) using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class EnemyShaman : LivingEntity { public LayerMask whatIsTarget; //추적대상 레이어 private LivingEntity targetEntity;//추적대상 private NavMeshAgent pathFinder; //경로 계산 AI 에이전트 private float dist; //적과 추적대상과의 거리 /*public ParticleSystem hitEffect; //피격 이펙트 public AudioClip deathSound;//사망 사운드 public AudioClip hitSound; //피격 사운드 */ //스태프 public GameObject firePoint; //매직미사일이 발사될 위치 public GameObject magicMissilePrefab; //사용할 매직미사일 할당 public GameObject magicMissile; //Instantiate()메서드로 생성하는 매직미사일을 담는 게임오브젝트 private Animator enemyAnimator; //private AudioSource enemyAudioPlayer; //오디오 소스 컴포넌트 public float damage = 30f; //공격력 public float attackDelay = 2.5f; //공격 딜레이 private float lastAttackTime; //마지막 공격 시점 public Transform tr; private float attackRange = 10f; //추적 대상이 존재하는지 알려주는 프로퍼티 private bool hasTarget { get { //추적할 대상이 존재하고, 대상이 사망하지 않았다면 true if (targetEntity != null && !targetEntity.dead) { return true; } //그렇지 않다면 false return false; } } private bool canMove; private bool canAttack; private void Awake() { //게임 오브젝트에서 사용할 컴포넌트 가져오기 pathFinder = GetComponent (); enemyAnimator = GetComponent (); //enemyAudioPlayer = GetComponent (); } //적 AI의 초기 스펙을 결정하는 셋업 메서드 public void Setup(float newHealth, float newDamage, float newSpeed) { //체력 설정 startingHealth = newHealth; health = newHealth; //공격력 설정 damage = newDamage; //네비메쉬 에이전트의 이동 속도 설정 pathFinder.speed = newSpeed; } void Start() { //게임 오브젝트 활성화와 동시에 AI의 탐지 루틴 시작 StartCoroutine(UpdatePath()); tr = GetComponent (); //추적 대상과의 멈춤 거리 랜덤하게 설정하기(7~10사이), 적이 뭉쳐있는 것보다 산개된 모습을 주기 위해서 pathFinder.stoppingDistance = Random.Range(7, 11); } // Update is called once per frame void Update() { //추적 대상의 존재 여부에 따라 다른 애니메이션 재생 enemyAnimator.SetBool(“CanMove”, canMove); enemyAnimator.SetBool(“CanAttack”, canAttack); if (hasTarget) { //추적 대상이 존재할 경우 거리 계산은 실시간으로 해야하니 Update() dist = Vector3.Distance(tr.position, targetEntity.transform.position); } } //추적할 대상의 위치를 주기적으로 찾아 경로 갱신, 대상이 있으면 공격한다. private IEnumerator UpdatePath() { //살아 있는 동안 무한 루프 while (!dead) { if (hasTarget) { Attack(); } else { //추적 대상이 없을 경우, AI 이동 정지 pathFinder.isStopped = true; canAttack = false; canMove = false; //반지름 20f의 콜라이더로 whatIsTarget 레이어를 가진 콜라이더 검출하기 Collider[] colliders = Physics.OverlapSphere(transform.position, 20f, whatIsTarget); //모든 콜라이더를 순회하면서 살아 있는 LivingEntity 찾기 for (int i = 0; i < colliders.Length; i++) { //콜라이더로부터 LivingEntity 컴포넌트 가져오기 LivingEntity livingEntity = colliders[i].GetComponent (); //LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아 있다면 if (livingEntity != null && !livingEntity.dead) { //추적 대상을 해당 LivingEntity로 설정 targetEntity = livingEntity; //for문 루프 즉시 정지 break; } } } //0.25초 주기로 처리 반복 yield return new WaitForSeconds(0.25f); } } //적과 플레이어 사이의 거리 측정, 거리에 따라 공격 메서드 실행 public virtual void Attack() { //자신이 사망X, 최근 공격 시점에서 attackDelay 이상 시간이 지났고, 플레이어와의 거리가 공격 사거리안에 있다면 공격 가능 if (!dead && dist <= attackRange) { //공격 반경 안에 있으면 움직임을 멈춘다. canMove = false; this.transform.LookAt(targetEntity.transform); //공격 딜레이가 지났다면 공격 애니 실행 if (lastAttackTime + attackDelay <= Time.time) { canAttack = true; lastAttackTime = Time.time; //최근 공격시간 초기화 } //공격 반경 안에 있지만, 딜레이가 남아있을 경우 else { canAttack = false; } } //공격 반경 밖에 있을 경우 추적하기 else { //추적 대상이 존재 && 추적 대상이 공격 반경 밖에 있을 경우, 경로를 갱신하고 AI 이동을 계속 진행 canMove = true; canAttack = false; pathFinder.isStopped = false; //계속 이동 pathFinder.SetDestination(targetEntity.transform.position); } } //유니티 애니메이션 이벤트로 지팡이를 앞으로 휘두를 떄 메서드 실행 public void ShamanFire() { magicMissile = Instantiate(magicMissilePrefab, firePoint.transform.position, firePoint.transform.rotation); //Instatiate()로 매직 미사일 프리팹을 복제 생성한다. } /*미사일에서 데미지 처리하기 //유니티 애니메이션 이벤트로 휘두를 때 데미지 적용시키기 public void OnDamageEvent() { //상대방의 LivingEntity 타입 가져오기 LivingEntity attackTarget = targetEntity.GetComponent (); //공격 처리 attackTarget.OnDamage(damage); } */ //데미지를 입었을 때 실행할 처리 public override void OnDamage(float damage) { /*사망하지 않을 상태에서만 피격 효과 재생 if (!dead) { //공격 받은 지점과 방향으로 피격 효과 재생 hitEffect.transform.position = hitPoint; hitEffect.transform.rotation = Quaternion.LookRotation(hitNormal); hitEffect.Play(); //피격 효과음 재생 enemyAudioPlayer.PlayOnShot(hitSound); } */ //피격 애니메이션 재생 enemyAnimator.SetTrigger(“Hit”); //LivingEntity의 OnDamage()를 실행하여 데미지 적용 base.OnDamage(damage); } //사망 처리 public override void Die() { //LivingEntity의 DIe()를 실행하여 기본 사망 처리 실행 base.Die(); //다른 AI를 방해하지 않도록 자신의 모든 콜라이더를 비활성화 Collider[] enemyColliders = GetComponents (); for (int i = 0; i < enemyColliders.Length; i++) { enemyColliders[i].enabled = false; } //AI추적을 중지하고 네비메쉬 컴포넌트를 비활성화 pathFinder.isStopped = true; pathFinder.enabled = false; //사망 애니메이션 재생 enemyAnimator.SetTrigger("Die"); /*//사망 효과음 재생 enemyAudioPlayer.PlayOnShot(deathSound); */ } } EnemyMagicMissileMove(AI의 매직 미사일 이동 스크립트) using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyMagicMissileMove : MonoBehaviour { public LivingEntity targetEntity;//공격 대상 public float speed = 15f; public float hitOffset = 0f; public bool UseFirePointRotation; public Vector3 rotationOffset = new Vector3(0, 0, 0); public GameObject hit; public GameObject flash; private Rigidbody rb; private SphereCollider sphCollider; public GameObject[] Detached; private float lastCollisionEnterTime; private float collisionDealy = 0.1f; //매직 미사일 고정 데미지, public float damage; void Start() { rb = GetComponent (); sphCollider = GetComponent (); EnemyShaman enemyShaman = GameObject.Find(“Enemy_Goblin Shaman”).GetComponent (); damage = enemyShaman.damage; if (flash != null) { var flashInstance = Instantiate(flash, transform.position, Quaternion.identity); //Quaternion.identity 회전 없음 flashInstance.transform.forward = gameObject.transform.forward; var flashPs = flashInstance.GetComponent (); if (flashPs != null) { Destroy(flashInstance, flashPs.main.duration); //ParticleSystem의 main.duration, 기본 시간인듯, duration은 따로 값을 정할 수 있음 } else { var flashPsParts = flashInstance.transform.GetChild(0).GetComponent (); Destroy(flashInstance, flashPsParts.main.duration); } } Destroy(gameObject, 5); } //매직 미사일 이동 기능 void FixedUpdate() { if (speed != 0) { rb.velocity = transform.forward * speed; //타겟팅 대상이 없을 때 매직 미사일은 전방으로 날아간다 } } private void Update() { OnSphereCollider(); } void OnCollisionEnter(Collision collision) //매직미사일이 충돌했을 경우 { if (collision.gameObject.layer == LayerMask.NameToLayer(“Player”)) { rb.constraints = RigidbodyConstraints.FreezeAll; speed = 0; //상대방의 LivingEntity 타입 가져오기, 데미지를 적용하기 위한 준비 LivingEntity attackTarget = collision.gameObject.GetComponent (); Debug.Log(“충돌한 오브젝트의 레이어” + collision.gameObject.layer + “충돌한 시간” + lastCollisionEnterTime); ContactPoint contact = collision.contacts[0]; Quaternion rot = Quaternion.FromToRotation(Vector3.up, contact.normal); Vector3 pos = contact.point + contact.normal * hitOffset; if (hit != null) { var hitInstance = Instantiate(hit, pos, rot); if (UseFirePointRotation) { hitInstance.transform.rotation = gameObject.transform.rotation * Quaternion.Euler(0, 180f, 0); } else if (rotationOffset != Vector3.zero) { hitInstance.transform.rotation = Quaternion.Euler(rotationOffset); } else { hitInstance.transform.LookAt(contact.point + contact.normal); } var hitPs = hitInstance.GetComponent (); if (hitPs != null) { Destroy(hitInstance, hitPs.main.duration); } else { var hitPsParts = hitInstance.transform.GetChild(0).GetComponent (); Destroy(hitInstance, hitPsParts.main.duration); } } foreach (var detachedPrefab in Detached) { if (detachedPrefab != null) { detachedPrefab.transform.parent = null; } } Destroy(gameObject); //데미지 처리 attackTarget.OnDamage(damage); Debug.Log(“현재 데미지” + damage); } else if(collision.gameObject.layer == LayerMask.NameToLayer(“Obstacle”)) { rb.constraints = RigidbodyConstraints.FreezeAll; speed = 0; Debug.Log(“충돌한 오브젝트의 레이어” + collision.gameObject.layer + “충돌한 시간” + lastCollisionEnterTime); ContactPoint contact = collision.contacts[0]; Quaternion rot = Quaternion.FromToRotation(Vector3.up, contact.normal); Vector3 pos = contact.point + contact.normal * hitOffset; if (hit != null) { var hitInstance = Instantiate(hit, pos, rot); if (UseFirePointRotation) { hitInstance.transform.rotation = gameObject.transform.rotation * Quaternion.Euler(0, 180f, 0); } else if (rotationOffset != Vector3.zero) { hitInstance.transform.rotation = Quaternion.Euler(rotationOffset); } else { hitInstance.transform.LookAt(contact.point + contact.normal); } var hitPs = hitInstance.GetComponent (); if (hitPs != null) { Destroy(hitInstance, hitPs.main.duration); } else { var hitPsParts = hitInstance.transform.GetChild(0).GetComponent (); Destroy(hitInstance, hitPsParts.main.duration); } } foreach (var detachedPrefab in Detached) { if (detachedPrefab != null) { detachedPrefab.transform.parent = null; } } Destroy(gameObject); } else { sphCollider.enabled =false; Debug.Log(“충돌한 오브젝트의 레이어” + collision.gameObject.layer + “충돌한 시간” + lastCollisionEnterTime); } } void OnSphereCollider() { if (lastCollisionEnterTime + collisionDealy < Time.time) { sphCollider.enabled = true; lastCollisionEnterTime = Time.time; Debug.Log("콜라이더 켜짐"); } } } 5.4.순간이동형 AI 근접형 AI에서 순간이동 기능을 추가하여 구현했다. 근접형 AI 플로우 차트에서 순간이동 기능을 추가했다. 5.4.1.순간이동형 AI 플로우 차트 5.4.2.기능 구현 순간이동 기능 구글에서 순간이동, teleportation 키워드로 검색하다보니 외국 사이트의 댓글에서 Random.insideUnitCircle 기능을 알게됐다. 이 기능은 자신의 위치에서 반경 1안의 원 안에서 랜덤한 위치로 이동하는데 기준을 상대방으로 바꿔서 순간이동을 구현했다. https://docs.unity3d.com/kr/530/ScriptReference/Random-insideUnitCircle.html //Teleportation 메서드 private void Teleportation() { if(bTeleportation) { //추적대상 근처 랜덤 위치 계산 //Random.insideUnityCircle은 x, y값만 계산해서(2차원) y값을 z값에 더함(2차원에서 y값은 3차원의 z값), y값은 높이라서 그냥 y값으로 함 tpPos = Random.insideUnitCircle * 1.5f; tpPos.x += targetEntity.gameObject.transform.position.x; tpPos.z = tpPos.y + targetEntity.gameObject.transform.position.z; tpPos.y = targetEntity.gameObject.transform.position.y; tr.position = tpPos; //시간 갱신 curTpTime = Time.time; //순간이동 가능 여부 false로 변경 bTeleportation = false; Debug.Log("순간이동 스킬을 사용했습니다" + Time.time); } } 순간이동 가능 여부 계산 기능 게임 시작 시 Start()에서 최초 1회 현재시간을 순간이동 사용 시간으로 기록하고 Update()에서 매 프레임마다 순간이동 사용 여부를 계산한다. 순간이동 사용 시간 + 순간이동 쿨타임 시간이 현재 시간보다 작을 경우 쿨타임이 지났으니 사용 여부를 true로 변경한다. private void CheckTpCooldown() { if(curTpTime + tpCooldown <= Time.time) { bTeleportation = true; } } 5.4.3.스크립트 EnemyGhost using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class EnemyGhost : LivingEntity { public LayerMask whatIsTarget; //추적대상 레이어 private LivingEntity targetEntity;//추적대상 private NavMeshAgent pathFinder; //경로 계산 AI 에이전트 private float dist; //적과 추적대상과의 거리 public float tpRange = 10f; /*public ParticleSystem hitEffect; //피격 이펙트 public AudioClip deathSound;//사망 사운드 public AudioClip hitSound; //피격 사운드 */ private Animator enemyAnimator; //private AudioSource enemyAudioPlayer; //오디오 소스 컴포넌트 public float damage = 50f; //공격력 public float attackDelay = 2.5f; //공격 딜레이 private float lastAttackTime; //마지막 공격 시점 public Transform tr; private float attackRange = 1.5f; private float curTpTime; //teleportation 쿨다운 계산을 위한 현재 시간 저장 public float tpCooldown = 10f; //teleportation 쿨다운 private bool bTeleportation; //teleportation 가능 여부 private Vector3 tpPos; //텔레포트에 사용할 좌표, 랜덤 좌표를 담음 //추적 대상이 존재하는지 알려주는 프로퍼티 private bool hasTarget { get { //추적할 대상이 존재하고, 대상이 사망하지 않았다면 true if (targetEntity != null && !targetEntity.dead) { return true; } //그렇지 않다면 false return false; } } private bool canMove; private bool canAttack; private void Awake() { //게임 오브젝트에서 사용할 컴포넌트 가져오기 pathFinder = GetComponent (); enemyAnimator = GetComponent (); //enemyAudioPlayer = GetComponent (); } //적 AI의 초기 스펙을 결정하는 셋업 메서드 public void Setup(float newHealth, float newDamage, float newSpeed) { //체력 설정 startingHealth = newHealth; health = newHealth; //공격력 설정 damage = newDamage; //네비메쉬 에이전트의 이동 속도 설정 pathFinder.speed = newSpeed; } void Start() { //게임 오브젝트 활성화와 동시에 AI의 탐지 루틴 시작 StartCoroutine(UpdatePath()); tr = GetComponent (); curTpTime = Time.time; //텔레포트 쿨다운 계산을 위해서 시작 시간을 저장 } // Update is called once per frame void Update() { //실시간으로 이동 애니메이션 변수 값을 읽고, 애니메이터에 전달 enemyAnimator.SetBool(“CanMove”, canMove); enemyAnimator.SetBool(“CanAttack”, canAttack); if (hasTarget) { //추적 대상이 존재할 경우 거리 계산은 실시간으로 해야하니 Update() dist = Vector3.Distance(tr.position, targetEntity.transform.position); } CheckTpCooldown(); } //추적할 대상의 위치를 주기적으로 찾아 경로 갱신 private IEnumerator UpdatePath() { //살아 있는 동안 무한 루프 while (!dead) { //탐지 반경 안에 있는 적이 있어서 이동과 공격이 가능 if (hasTarget) { Attack(); } else { //추적 대상이 없을 경우, AI 이동 정지 pathFinder.isStopped = true; canAttack = false; canMove = false; //추적 대상 찾고, 대상으로 지정하기 //반지름 20f의 콜라이더로 whatIsTarget 레이어를 가진 콜라이더 검출하기 Collider[] colliders = Physics.OverlapSphere(transform.position, 20f, whatIsTarget); //모든 콜라이더를 순회하면서 살아 있는 LivingEntity 찾기 for (int i = 0; i < colliders.Length; i++) { //콜라이더로부터 LivingEntity 컴포넌트 가져오기 LivingEntity livingEntity = colliders[i].GetComponent (); //LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아 있다면 if (livingEntity != null && !livingEntity.dead) { //추적 대상을 해당 LivingEntity로 설정 targetEntity = livingEntity; //for문 루프 즉시 정지 break; } } } //0.25초 주기로 처리 반복 yield return new WaitForSeconds(0.25f); } } //추적대상과의 거리에 따라 공격 실행 //현재 이동 가능 상태 public virtual void Attack() { //자신이 사망X, 추적 대상과의 거리이 공격 반경 안에 있다면 if (!dead && dist < attackRange) { //공격 반경 안에 있으면 움직임을 멈춘다. canMove = false; //추적 대상 바라보기 this.transform.LookAt(targetEntity.transform); //최근 공격 시점에서 attackDelay 이상 시간이 지나면 공격 가능 if (lastAttackTime + attackDelay <= Time.time) { canAttack = true; } //공격 반경 안에 있지만, 딜레이가 남아있을 경우 else { canAttack = false; } } //공격 반경 밖에 있을 경우 추적하기 else { canMove = true; canAttack = false; //계속 추적 pathFinder.isStopped = false; //계속 이동 pathFinder.SetDestination(targetEntity.transform.position); //추적대상과의 거리가 tpRange 이하일 경우 && 텔레포트가 가능할 경우 if(dist <= tpRange && bTeleportation) { Teleportation(); } } } //유니티 애니메이션 이벤트로 휘두를 때 데미지 적용시키기 public void OnDamageEvent() { //상대방의 LivingEntity 타입 가져오기 LivingEntity attackTarget = targetEntity.GetComponent (); //공격 처리 attackTarget.OnDamage(damage); //최근 공격 시간 갱신 lastAttackTime = Time.time; } //데미지를 입었을 때 실행할 처리 public override void OnDamage(float damage) { /*사망하지 않을 상태에서만 피격 효과 재생 if (!dead) { //공격 받은 지점과 방향으로 피격 효과 재생 hitEffect.transform.position = hitPoint; hitEffect.transform.rotation = Quaternion.LookRotation(hitNormal); hitEffect.Play(); //피격 효과음 재생 enemyAudioPlayer.PlayOnShot(hitSound); } */ //피격 애니메이션 재생 enemyAnimator.SetTrigger(“Hit”); //LivingEntity의 OnDamage()를 실행하여 데미지 적용 base.OnDamage(damage); } //사망 처리 public override void Die() { //LivingEntity의 DIe()를 실행하여 기본 사망 처리 실행 base.Die(); //다른 AI를 방해하지 않도록 자신의 모든 콜라이더를 비활성화 Collider[] enemyColliders = GetComponents (); for (int i = 0; i < enemyColliders.Length; i++) { enemyColliders[i].enabled = false; } //AI추적을 중지하고 네비메쉬 컴포넌트를 비활성화 pathFinder.isStopped = true; pathFinder.enabled = false; //사망 애니메이션 재생 enemyAnimator.SetTrigger("Die"); /*//사망 효과음 재생 enemyAudioPlayer.PlayOnShot(deathSound); */ } //teleportation 쿨다운 계산 메서드 private void CheckTpCooldown() { if(curTpTime + tpCooldown <= Time.time) { bTeleportation = true; } } //Teleportation 메서드 private void Teleportation() { if(bTeleportation) { //추적대상 근처 랜덤 위치 계산 //Random.insideUnityCircle은 x, y값만 계산해서 y값을 z값에 더함, y값은 그냥 y값으로 함 tpPos = Random.insideUnitCircle * 1.5f; tpPos.x += targetEntity.gameObject.transform.position.x; tpPos.z = tpPos.y + targetEntity.gameObject.transform.position.z; tpPos.y = targetEntity.gameObject.transform.position.y; tr.position = tpPos; //시간 갱신 curTpTime = Time.time; //순간이동 가능 여부 false로 변경 bTeleportation = false; Debug.Log("순간이동 스킬을 사용했습니다" + Time.time); } } } 5.5.네비 메쉬 굽기 적 AI(네비 메쉬 에이전트)가 게임 월드에서 이동하려면 네비 메쉬가 필요하다. 네비 메쉬는 정적 게임 오브젝트를 대상으로 생성가능하다. 1.적 AI(네비 메쉬 에이전트)가 이동할 영역을 굽기위해서 발판과 장애물을 선택한다. 2.인스펙터창에서 이름 오른쪽의 Static를 체크한다. 3.유니티의 Windows > AI > Navigation를 선택해서 Navigation 창을 연다. 4.네비게이션 창에서 Bake 탭을 선택한다.
5.필요에 따라서 Agent Radius와 Agent Height를 수정한다.(나는 기본값 이용)
6.Agent Radius = 에이전트의 반경 / Agent Height = 에이전트의 크기이며 이것을 조절하여 AI가 다닐 수 있는 영역을 조절할 수 있다.
7.우하단의 Bake 버튼을 클릭한다.
8.구워진 네비 메쉬는 파란색 영역으로 표시된다.
9.파란색 영역은 적 AI(네비 메쉬 에이전트)가 이동할 수 있는 영역이다.
#확장 네비게이션 기능 활용 – 이재현 마스터의 내비게이션 활용법
현재는 고정된 영역에서 적 AI가 이동할 수 있게 네비 메쉬를 미리 구웠지만 나중에 절차적 맵 생성으로 랜덤한 맵을 생성할 때에는 확장 네비게이션 기능을 이용해서 네비 메쉬를 구어볼 생각이다.
다음 구현 목표
1.적 HPbar UI
2.캐릭터 스킬 구현(파이어볼, 썬더, 텔레포트)
3.UI 버튼과 일반 공격 연결
여담…
최근 편도선염? 장염이 번갈아가며 걸려서 너무 힘들다. 장염은 대응 가능하나 편도선염의 경우 몇 일간 호전 악화를 반복한다. 열이 오르면 집중을 할 수 가 없어서 개발에 진전이 없다. 특히 블로그에 글을 작성하는 게 너무 힘들다. 나름 대응으로 목 따뜻하게 하고 따뜻한 물 많이 마시고, 생강차사서 먹어보고 있다.
생강차가 조금 효과는 있는 것 같다. 개발할 땐 몸 건강이 필수다. 빨리 나았으면 쉽지만 휴식해도 회복이 잘 안되니 디버프 상태에서 조금씩 할 일을 할 수 밖에 없다.
반응형
유니티 Nav Mesh Agent 이용해서 AI 만들기 (플레이어 따라다니는 적)
반응형
안녕하세요 ~ RPG 게임이나 FPS 게임에 PVE를 보면 특정 플레이어나 적을 따라다니면서 공격하는 AI들이 존재합니다. 오늘은 그런 AI들처럼 특정 타겟을 따라다니는 오브젝트를 만들어 볼게요 !
먼저 직접 조작할 수 있는 Player 오브젝트 (초록색)과 플레이어를 따라 다니는 Enemy 오브젝트 (빨간색)를 생성했습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 using System .Collections; using System .Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { void Update () { float h = Input.GetAxis( “Horizontal” ); float v = Input.GetAxis( “Vertical” ); transform.Translate( new Vector3 (h, 0 , v) * 5f * Time.deltaTime); } } Colored by Color Scripter cs
먼저 플레이어 오브젝트를 조작할 수 있는 스크립트를 작성합니다.
많이들 보셨던 스크립트니까 설명은 생략할게요 ~
[설명은 http://solution94.tistory.com/9?category=672286 게시글을 참조하시면 됩니다 ~]실행해보면 WASD 키로 이동이 가능한 상태입니다 !
다음은 적에게 추적 기능을 넣어줄 차례입니다. Nav Mesh Agent 라는 컴포넌트를 추가시켜줍니다.
window탭에 Navigation을 눌러서 창을 추가 시켜주세요.
그러면 이미지 우측에 보이시는 것 처럼 Navigation 창이 생겨납니다.
그리고 Bake 탭에서 Bake를 눌러주시면 되는데 여기서 중요한건
해당 오브젝트가 지나갈 수 있거나 지나갈 수 없거나 인식하지 않는 오브젝트들을 나눠주어야 합니다.
쉽게 다시 말하면 적 오브젝트가 걸어다닐 수 있는 길, 통과하지 못하는 장애물 등을 지정해줘야하는데 아주 간단합니다.
장애물, 길 오브젝트를 Static 시켜주면 됩니다.
Nav Mesh Agent 컴포넌트 설정에 보시면 오를 수 있는 경사의 각도, 오브젝트의 크기 등을 설정할 수 있습니다.
다시 Navigation 탭에 Bake탭에서 Bake를 눌러주시면 보시는 것 처럼 이동할 수 있는 공간을 색상으로 표현해줍니다.
만약 장애물로 만들 오브젝트를 Static화 시키면 저런 형태로 됩니다. 가운데 있는 큐브의 지면은 파란색으로 색칠이 되어 있지 않은 상태 즉, Nav Mesh Agent 컴포넌트를 추가시킨 오브젝트 (적)이 지나갈 수 없게 되어 장애물이 되는 겁니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 using System .Collections; using System .Collections.Generic; using UnityEngine; using UnityEngine.AI; public class EnemyAI : MonoBehaviour { NavMeshAgent nav; GameObject target; // Use this for initialization void Start () { nav = GetComponent < NavMeshAgent > (); target = GameObject.Find( “Player” ); } // Update is called once per frame void Update () { if (nav.destination ! = target.transform.position) { nav.SetDestination (target.transform.position); } else { nav.SetDestination (transform.position); } } } Colored by Color Scripter cs
그리고 Enemy 오브젝트에 추가시켜 줄 스크립트를 작성합니다. 혹여나 컴포넌트 선언 부에서 NavMeshAgent가 라이브러리에서 보이지 않는다면 using UnityEngine.AI; 이걸 추가 안하셨기 때문입니다 ! 추가하고 나시면 정상적으로 라이브러리에 나올꺼에요 ~
스크립트는 아주아주 간단합니다. nav 변수에 Nav Mesh Agent 컴포넌트를 넣어주고 target 게임 오브젝트 변수에 플레이어를 찾아내게 만들어줍니다.
그리고 ‘만약 타겟으로 설정된 오브젝트와 목적지가 같지 않다면 타겟의 위치로 계속 이동하고 그렇지 않다면 그자리에 서라’ 라는 조건문을 적어주면 끝입니다.
그리고 실행시키면 플레이어를 따라다니는 적을 볼 수 있습니다 !
이렇게만 하셔도 플레이어를 따라다니는 적을 아주 쉽고 간단하게 구현할 수 있습니다 ~ 공포게임이나 RPG의 적이나 펫, FPS 등에도 이용할 수 있으니 다양하게 사용해보세요 ~
반응형
[Unity] AI의 시야각(FieldOfView) 구현하는 법
반응형
안녕하세요 극꼼입니다.
오늘은 젤다 모작을 만들며 사용한 몬스터의 시야각을 구현하는 방법을 알아보겠습니다.
참고로 제가 만든 모작은 3D게임이지만 지형이 입체적이거나 하지는 않기 때문에, 평면적인(y축 값 변동이 없는) 시야각을 사용했습니다.
구현한 기능을 미리보기로 보면 다음과 같습니다.
이와 같은 기능을 위해 3가지 단계를 거쳤습니다.
1. 필요한 변수 적절하게 선언하기
2. 시야각의 범위를 OnDrawGizmos를 통해 표시하기
3. 시야각의 범위 내에 들어온 적 또는 장애물 인지하기
1. 필요한 변수 선언하기
[SerializeField] bool DebugMode = false; [Range(0f, 360f)] [SerializeField] float ViewAngle = 0f; [SerializeField] float ViewRadius = 1f; [SerializeField] LayerMask TargetMask; [SerializeField] LayerMask ObstacleMask;먼저 다음과 같이 변수들을 선언해줍니다. 그러면 이와 같이 인스펙터 창에서 각도 등을 조절할 수 있게 됩니다.
2. 시야각 범위 OnDrawGizmos를 통해 표시하기
먼저 ViewAngle에 따른 시야각을 보여주는 선을 OnDrawGizmos를 통해 그어줄건데요, ViewRadius 범위를 그려주는 원을 그어줍니다.
if (!DebugMode) return; Vector3 myPos = transform.position + Vector3.up * 0.5f; Gizmos.DrawWireSphere(myPos, ViewRadius);
그 다음 시야각을 그어주는 선을 그어주기 이전에, 각도를 벡터값으로 바꿔주는 함수를 만들어줍니다.
Vector3 AngleToDir(float angle) { float radian = angle * Mathf.Deg2Rad; return new Vector3(Mathf.Sin(radian), 0f, Mathf.Cos(radian)); }
이 함수를 이용해 시야각을 그어줍니다.
float lookingAngle = transform.eulerAngles.y; //캐릭터가 바라보는 방향의 각도 Vector3 rightDir = AngleToDir(transform.eulerAngles.y + ViewAngle * 0.5f); Vector3 leftDir = AngleToDir(transform.eulerAngles.y – ViewAngle * 0.5f); Vector3 lookDir = AngleToDir(lookingAngle); Debug.DrawRay(myPos, rightDir * ViewRadius, Color.blue); Debug.DrawRay(myPos, leftDir * ViewRadius, Color.blue); Debug.DrawRay(myPos, lookDir * ViewRadius, Color.cyan);
이제 인스펙터 창에서 이와 같이 조정할 수 있습니다.
3. 범위 내에 들어온 적 또는 장애물 인지하기
먼저 범위 내로 들어오는 오브젝트들의 Collider를 담을 list 변수와 raycast에 맞은 물체를 담을 변수인 hit를 선언합니다.
List
hitTargetList = new List (); RaycastHit hit; 계산 순서는 다음과 같습니다.
1. 범위 내로 들어온 특정 layer의 오브젝트 인식
2. 해당 오브젝트의 위치와 시야각 각도 비교
3. 해당 오브젝트와 나 사이에 장애물이 없는지 판단
hitTargetList.Clear(); Collider[] Targets = Physics.OverlapSphere(myPos, ViewRadius, TargetMask); if (Targets.Length == 0) return; foreach(Collider EnemyColli in Targets) { Vector3 targetPos = EnemyColli.transform.position; Vector3 targetDir = (targetPos – myPos).normalized; float targetAngle = Mathf.Acos(Vector3.Dot(lookDir, targetDir)) * Mathf.Rad2Deg; if(targetAngle <= ViewAngle * 0.5f && !Physics.Raycast(myPos, targetDir, ViewRadius, ObstacleMask)) { hitTargetList.Add(EnemyColli); if (DebugMode) Debug.DrawLine(myPos, targetPos, Color.red); } } 이제 움직이는 적을 위와 같이 인식합니다. 코드를 응용해서 적을 공격하도록 할 수도 있습니다! 반응형
6. 플레이어를 추적하는 몬스터 스크립트 – 앤글 블로그
몬스터 AI를 제작하는 건 어느 정도가 적당한지 항상 고민이 된다. 제작하다 보면 굳이 필요 없는 기능까지 만들어야 되는지 고민하게 되는데 프로그래머의 입장으로서는 이 몬스터의 주변 맵이 어떻게 생겨먹은 지 예측하기가 힘들기 때문이다.
예를 들면 미로에 있는 적이면 좁은 통로를 잘 다닐 수 있는 이동 코드를 짜야 하는데 대충 만들면 벽을 향해 걸어 다니기 때문이다.
어쨌든 정확한 사정을 모르면 기본 기능에 충실하게 만들어야 되는데 물론 나도 그렇게 만들었다. 나중에 추가할 사항이 있다면 간단하게 짜인 코드가 알아보기도 쉽고 수정하기도 쉽다.
나는 OverlapSphere 메서드를 사용하여 플레이어가 저 범위안에 들어선다면 플레이어를 추적하게 만들었다. 벗어난다면 추적을 종료하고 Idle 또는 Moving 상태로 전환한다.
이런 식으로 OverlapSphere를 활용할 수 있지만 매 순간 플레이어가 범위 안에 있는지 없는지 체크를 해야 한다. 코드를 잘못 짜면 OutOfRange 오류가 지속해서 출력되므로 플레이어가 범위 안에 들어왔을 때 조건을 잘 부여하는 게 중요하다.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MonsterMove : MonoBehaviour { public float speed; private Vector3 velocity; public float stateChangeTime; private float inputX; private float inputZ; public bool stateChange;//State 바꾸기용 불변수 public bool idle; public bool moving; public bool tracking; public Vector3 directionVec; private Rigidbody rigidbody; public int rayDistance; //Target()용 변수들 public Collider[] target; public LayerMask whatIsLayer; public float overlapRadius; enum State { Idle, Moving, Tracking } private State state { set { switch(value) { case State.Idle: idle = true; moving = false; tracking = false; break; case State.Moving: moving = true; idle = false; tracking = false; break; case State.Tracking: tracking = true; idle = false; moving = false; break; } } } void Start() { rigidbody = GetComponent
(); rayDistance = 1; state = State.Idle; stateChange = false; } void Update() { Debug.DrawRay(transform.position, directionVec * rayDistance); } void FixedUpdate() { /* 테스트를 위해 일단 주석처리 * if(idle) * { * Idle(); * } * else if(moving) * { * Moving(); * } * else if(tracking) * { * Tracking(); * } */ if(tracking) { Tracking(); } if(idle)//제대로 작동 하는가? { Debug.Log(“Current State:idle”); } else if(moving) { Debug.Log(“Current State:moving”); } else if(tracking) { Debug.Log(“Current State:tracking”); } target = Physics.OverlapSphere(transform.position, overlapRadius, whatIsLayer); if(target.Length > 0) { state = State.Tracking; } else { //추적 종료 시 Idle, Moving 상태로 랜덤하게 돌입 state = (State)Random.Range(0, 2); stateChange = false; return; } } public void Idle() { if(!stateChange) StartCoroutine(StateChange()); inputX = 0; inputZ = 0; velocity = new Vector3(inputX, 0, inputZ); transform.position += velocity * speed * Time.deltaTime; Direction(); } public void Moving() { if(!stateChange) StartCoroutine(StateChange()); //float fallSpeed = rigidbody.velocity.y; //캐릭터가 움직이는 코드 velocity = new Vector3(inputX, 0, inputZ); transform.position += velocity * speed * Time.deltaTime; Direction(); } public void Tracking() { /* *이건 예시임 OverlapSphere(Vector3.point, float radius, int layerMask, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal) *target = Physics.OverlapSphere(transform.position, overlapRadius, whatIsLayer); */ velocity = new Vector3(Mathf.Clamp(target[0].transform.position.x – transform.position.x, -1.0f, 1.0f), 0, Mathf.Clamp(target[0].transform.position.z – transform.position.z, -1.0f, 1.0f)); transform.position += velocity * speed * Time.deltaTime; Debug.Log(“velocity: ” + velocity); } public void Direction() { if (inputX == -1 && inputZ == 1)//UpLeft { //left = true; //up = true; directionVec = new Vector3(-1.14f, 0, 1.14f); //down = false; //right = false; } if (inputX == 0 && inputZ == 1)//Up { //up = true; directionVec = new Vector3(0, 0, 2.0f); //down = false; //right = false; //left = false; } if (inputX == 1 && inputZ == 1)//UpRight { //right = true; //up = true; directionVec = new Vector3(1.14f, 0, 1.14f); //left = false; //down = false; } if (inputX == -1 && inputZ == 0)//Left { //left = true; directionVec = new Vector3(-2.0f, 0, 0); //right = false; //up = false; //down = false; } if (inputX == 0 && inputZ == 0)//Idle { //up = false; //down = false; //right = false; //left = false; directionVec = new Vector3(0, 0, 0); } if (inputX == 1 && inputZ == 0)//Right { //right = true; directionVec = new Vector3(2.0f, 0, 0); //up = false; //down = false; //left = false; } if (inputX == -1 && inputZ == -1)//DownLeft { //left = true; //down = true; directionVec = new Vector3(-1.14f, 0, -1.14f); //up = false; //right = false; } if (inputX == 0 && inputZ == -1)//Down { //down = true; directionVec = new Vector3(0, 0, -2.0f); //up = false; //right = false; //left = false; } if (inputX == 1 && inputZ == -1)//DownRight { //right = true; //down = true; directionVec = new Vector3(1.14f, 0, -1.14f); //up = false; //left = false; } } IEnumerator StateChange() { stateChange = true; inputX = Random.Range(-1, 2); inputZ = Random.Range(-1, 2); yield return new WaitForSeconds(stateChangeTime); //State.Idle = 0, State.Moving = 1, State.Tracking = 2 //0과 1까지만 대입 state = (State)Random.Range(0, 2); stateChange = false; } void OnDrawGizmosSelected() { //충돌범위 기즈모 Gizmos.color = Color.yellow; Gizmos.DrawSphere(transform.position, overlapRadius); } } 위 코드는 현재 Tracking 함수만 발동되는 코드이다. 기획자님이 일단 추적기능만 제작해 달라고 하여 Idle과 Moving 기능은 작동하지 않는다.
나는 Physics.OverlapSphere의 반환값이 Collider[]이므로 Collider[].Length가 0 이상일 때 플레이어가 범위 안에 들어왔다는 조건을 주었다. 이렇게 작성하면 오류는 일어나지 않지만 이게 좋은 방법인 것 같지는 않다.
이 글을 작성하면서 떠오른 방법인데 추적 기능을 갖는 몬스터 게임 오브젝트에게 Collider 컴포넌트를 연결하고 Collider size를 원하는 추적 범위만큼 늘린 다음 플레이어가 트리거 충돌 했을 경우 추적하는 기능을 만들어도 될 것 같다는 생각이 떠올랐다.
왜 처음에는 이런 생각이 안 났을까..? 나중에 추적 기능이 문제가 생긴다면 위 방법으로 바꾸는 걸 고려해 봐야겠다.
[Unity2D] 적 몬스터 구현하기
728×90
[유니티 기초 – B18] 몬스터 AI 구현하기 [유니티 기초 – B19] 플레이어 피격 이벤트 구현하기영상을 보고 쓰여진 게시글 입니다.
이번시간에는 드디어 몬스터에 대한 구현을 한번 해보도록 할탠데요
인공지능을 통해 자동으로 움직이는 것과 플레이어가 몬스터와 싸우는 모션까지 한번 해보도록 하겠습니다.
사실 AI가 거창하게 들리지만 스스로 생각해서 판단내릴 수만 있으면 AI라고 하는? 뭐 여튼 범위가 엄청 넓을 겁니다
롤에서도 AI모드라고 하지않나요 봇전? 걔들..멍청하죠.. 물론 코드는 복잡하겠지만
엄청 어렵고 복잡한 AI를 사용하면 알파고마냥 우리보다 잘하겠지만 게임에서 봇들도 다 AI라고 한다고 합니다.
기초이기 때문에 매우 기본적인 AI를 이용하기(움직임과 멈춤만 스스로 판단)로 합니다.
몬스터 AI구현하기
준비하기
몬스터 애니메이션을 먼저 넣도록 합시다
Monster_Walk로 애니메이션 폴더에 저장해주고 (이미 Idle은 넣은 상태라 생략)
애니메이터에 들어가서
마찬가지로 isWalking변수를 만들고 Idle 과 Monster사이의 트렌지션을 추가한 후
Condition / Has Exit Time / 간격을 조절해줍니다.
또한 일단 저번 코드에서 카메라를 캐릭터와 분리해주시고, 몬스터 이동에 필요한 충분한 플랫폼을 구성해 주신뒤
몬스터가 잘 보이도록 카메라를 배치 후 시작하시는 것이 좋습니다.
몬스터 기본이동
MonsterMove.cs 스크립트를 생성하고 Monster에 적용시켜줍니다.
일단 속력을 주어야 하므로 Rigidbody2D변수를 사용하고
우리가 키보드 입력을 통하여 적을 움직이는게 아니므로 일단 한쪽방향으로 이동하도록 했습니다.
Rigidbody2D rigid; // Start is called before the first frame update private void Awake() { rigid = GetComponent
(); } // Update is called once per frame void FixedUpdate() { rigid.velocity = new Vector2(-1,rigid.velocity.y); //단순 왼쪽방향 이동 } 이렇게 되면 왼쪽방향으로 쭉 음직이는 것을 볼 수 있습니다.
행동설정 (인공지능 부여)
우리는 한쪽방향이 아닌 스스로 어떤 구간을 왕복하거나 중간에 쉬거나하는 AI를 만들어야 하므로
행동을 설정해주어야 합니다.
그 전에 기획단계를 거쳐
몬스터가 어떤 행동을 어떤 타이밍에 하는지
어떤 구간을 돌아다니는지에 대하여 기획을 해주셔야합니다.
여기서는 단순하게 오른쪽 이동 / 왼쪽 이동 / 멈춤 정도로 설정하도록 합니다.
nextMove라는 변수를 주어 랜덤함수(Random.Range(min,max))를 이용해
-1 / 0 / 1 의 난수를 생성해서 이를 Vector2의 x값으로 부여하면 각각 왼쪽, 정지, 오른쪽 으로 이동 할 것입니다.
사용함수
// min<= 수
(); Invoke(“Think”, 5); // 초기화 함수 안에 넣어서 실행될 때 마다(최초 1회) nextMove변수가 초기화 되도록함 } // Update is called once per frame void FixedUpdate() { rigid.velocity = new Vector2(nextMove,rigid.velocity.y); //nextMove 에 0:멈춤 -1:왼쪽 1:오른쪽 으로 이동 } void Think(){//몬스터가 스스로 생각해서 판단 (-1:왼쪽이동 ,1:오른쪽 이동 ,0:멈춤 으로 3가지 행동을 판단) //Random.Range : 최소<= 난수 <최대 /범위의 랜덤 수를 생성(최대는 제외이므로 주의해야함) nextMove = Random.Range(-1,2); //Think(); : 재귀함수 : 딜레이를 쓰지 않으면 CPU과부화 되므로 재귀함수쓸 때는 항상 주의 ->Think()를 직접 호출하는 대신 Invoke()사용 Invoke(“Think”, 5); //매개변수로 받은 함수를 5초의 딜레이를 부여하여 재실행 } } 그런데 이렇게 되면 만약에 5초가 지난 이후에도 오른쪽 방향으로 계속 이동하게되면 결국
맵 밖으로 탈출하고 바닥밑으로 떨어지게 될 수도 있으니 이를 막아주어야 합니다.
지능 높이기
RayHit를 이동하여 자신의 앞이 낭떨어지인지 지형을 체크해보도록 만들어봅시다
낭떨어지를 만나면 Debug.Log로 경고문구를 출력하는 코드를 FixedUpdate()에 작성하여 움직일때마다 측정하도록 합니다.
void FixedUpdate() { //Move rigid.velocity = new Vector2(nextMove,rigid.velocity.y); //nextMove 에 0:멈춤 -1:왼쪽 1:오른쪽 으로 이동 //Platform check(맵 앞이 낭떨어지면 뒤돌기 위해서 지형을 탐색) //자신의 한 칸 앞 지형을 탐색해야하므로 position.x + nextMove(-1,1,0이므로 적절함) Vector2 frontVec = new Vector2(rigid.position.x + nextMove, rigid.position.y); //한칸 앞 부분아래 쪽으로 ray를 쏨 Debug.DrawRay(frontVec, Vector3.down, new Color(0,1,0)); //레이를 쏴서 맞은 오브젝트를 탐지 RaycastHit2D raycast = Physics2D.Raycast(frontVec, Vector3.down,1,LayerMask.GetMask(“Platform”)); //탐지된 오브젝트가 null : 그 앞에 지형이 없음 if(raycast.collider == null){ Debug.Log(“경고! 이 앞은 낭떨어지”); } }
이렇게 Ray를 이용하여 탐지되면 경고 문구가 출력되었네요
이제 경고문구 출력대신 우리가 직접 방향을 바꾸어 떨어지지 않도록 해봅시다
-1을 곱하면 되겠네요
//탐지된 오브젝트가 null : 그 앞에 지형이 없음 if(raycast.collider == null){ nextMove= nextMove*(-1); //우리가 직접 방향을 바꾸어 주었으니 Think는 잠시 멈추어야함 CancelInvoke(); //think를 잠시 멈춘 후 재실행 Invoke(“Think”,5); }
직접 방향을 멈추었으니 잠시 스스로 생각하여 방향을 정하는 것을 멈춰주어야 할 것입니다.
캔슬을 통해 잠시 중지해 준 후 다시시작하면 됩니다
낭떠러지 탐지 거리 줄이기
또한 ray가 1칸앞을 내다보는 것이 너무 멀다고 생각될 경우
//자신의 한 칸 앞 지형을 탐색해야하므로 position.x + nextMove(-1,1,0이므로 적절함) Vector2 frontVec = new Vector2(rigid.position.x + nextMove*0.4f, rigid.position.y);
한칸앞이 아닌 0.4정도 앞을 탐지하도록 적절한 수를 곱해주시면 해결됩니다.
1보단 훨씬 가까이를 탐지하게 되네요
추가적으로
5초 마다 생각하기 보다는 생각하는 시간 또한 랜덤으로 부여하면 더욱 더 똑똑해보이는 인공지능이 될 것입니다.
void Think(){//몬스터가 스스로 생각해서 판단 (-1:왼쪽이동 ,1:오른쪽 이동 ,0:멈춤 으로 3가지 행동을 판단) //Random.Range : 최소<= 난수 <최대 /범위의 랜덤 수를 생성(최대는 제외이므로 주의해야함) nextMove = Random.Range(-1,2); float time = Random.Range(2f, 5f); //생각하는 시간을 랜덤으로 부여 //Think(); : 재귀함수 : 딜레이를 쓰지 않으면 CPU과부화 되므로 재귀함수쓸 때는 항상 주의 ->Think()를 직접 호출하는 대신 Invoke()사용 Invoke(“Think”, time); //매개변수로 받은 함수를 time초의 딜레이를 부여하여 재실행 }
이런식으로 변경하면 됩니다.
전체 코드를 한번 쓰고 이제 애니메이션으로 넘어가도록 하겠습니다.
MonsterMove.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MonsterMove : MonoBehaviour { Rigidbody2D rigid; public int nextMove;//다음 행동지표를 결정할 변수 // Start is called before the first frame update private void Awake() { rigid = GetComponent
(); Invoke(“Think”, 5); // 초기화 함수 안에 넣어서 실행될 때 마다(최초 1회) nextMove변수가 초기화 되도록함 } // Update is called once per frame void FixedUpdate() { //Move rigid.velocity = new Vector2(nextMove,rigid.velocity.y); //nextMove 에 0:멈춤 -1:왼쪽 1:오른쪽 으로 이동 //Platform check(맵 앞이 낭떨어지면 뒤돌기 위해서 지형을 탐색) //자신의 한 칸 앞 지형을 탐색해야하므로 position.x + nextMove(-1,1,0이므로 적절함) Vector2 frontVec = new Vector2(rigid.position.x + nextMove*0.4f, rigid.position.y); //한칸 앞 부분아래 쪽으로 ray를 쏨 Debug.DrawRay(frontVec, Vector3.down, new Color(0,1,0)); //레이를 쏴서 맞은 오브젝트를 탐지 RaycastHit2D raycast = Physics2D.Raycast(frontVec, Vector3.down,1,LayerMask.GetMask(“Platform”)); //탐지된 오브젝트가 null : 그 앞에 지형이 없음 if(raycast.collider == null){ nextMove= nextMove*(-1); //우리가 직접 방향을 바꾸어 주었으니 Think는 잠시 멈추어야함 CancelInvoke(); //think를 잠시 멈춘 후 재실행 Invoke(“Think”,5); } } void Think(){//몬스터가 스스로 생각해서 판단 (-1:왼쪽이동 ,1:오른쪽 이동 ,0:멈춤 으로 3가지 행동을 판단) //Random.Range : 최소<= 난수 <최대 /범위의 랜덤 수를 생성(최대는 제외이므로 주의해야함) nextMove = Random.Range(-1,2); float time = Random.Range(2f, 5f); //생각하는 시간을 랜덤으로 부여 //Think(); : 재귀함수 : 딜레이를 쓰지 않으면 CPU과부화 되므로 재귀함수쓸 때는 항상 주의 ->Think()를 직접 호출하는 대신 Invoke()사용 Invoke(“Think”, time); //매개변수로 받은 함수를 time초의 딜레이를 부여하여 재실행 } } 애니메이션
이제 애니메이션을 변경해 줄 차례입니다.
변수생성 / 초기화
애니메이션 넣을때 Animator / sprite Renderer(방향전환flipX) 를 사용한 것이 기억이 나실겁니다
일단 두개의 변수를 만들어 초기화해줍니다.
//변수 생성 Animator animator; SpriteRenderer spriteRenderer; // Start is called before the first frame update private void Awake() { //초기화 animator = GetComponent
(); spriteRenderer = GetComponent (); } 애니메이터 설정
걷고있는지 판정에 Bool 형이 아닌 WalkSpeed(int형) 구조를 사용해서 판단해보도록 합시다
원래 만들었던 isWalking을 제거한 후
2개의 Transition에 전부 Condition을 다시 추가해주셔야합니다.
WalkSpeed가 0이면(Equal) , Walk -> Idle로
WalkSpeed가 0이 아니면(NotEqual) , Idle -> Walk로 변경해줍시다
스크립트
이제 애니메이션 상태는 완료했으니 코드로 WalkSpeed에 따른 방향전환을
걷기
Think함수 내에서 애니메이터 안의 WalkSpeed변수를 NextMove와 동일하게 set해주는 코드를 작성합니다.
nextMove는 어쩌피 0,-1,1이므로 0인 정지상태가 아니면 Walk애니메이션이 실행됩니다.
방향전환
nextMove==0일때는 굳이 방향전환의 필요가 없으므로 제외하고
nextMove가 1일때만(그림은 왼쪽으로 가는 그림이므로 오른쪽으로 전환하면 flip임) flipX를 true로 변경하는 코드를 작성합니다
void Think(){//몬스터가 스스로 생각해서 판단 (-1:왼쪽이동 ,1:오른쪽 이동 ,0:멈춤 으로 3가지 행동을 판단) //Set Next Active //Random.Range : 최소<= 난수 <최대 /범위의 랜덤 수를 생성(최대는 제외이므로 주의해야함) nextMove = Random.Range(-1,2); //Sprite Animation //WalkSpeed변수를 nextMove로 초기화 animator.SetInteger("WalkSpeed",nextMove); //Flip Sprite if(nextMove != 0) //서있을 때 굳이 방향을 바꿀 필요가 없음 spriteRenderer.flipX = nextMove == 1; //nextmove 가 1이면 방향을 반대로 변경 //Recursive (재귀함수는 가장 아래에 쓰는게 기본적) float time = Random.Range(2f, 5f); //생각하는 시간을 랜덤으로 부여 //Think(); : 재귀함수 : 딜레이를 쓰지 않으면 CPU과부화 되므로 재귀함수쓸 때는 항상 주의 ->Think()를 직접 호출하는 대신 Invoke()사용 Invoke(“Think”, time); //매개변수로 받은 함수를 time초의 딜레이를 부여하여 재실행 }
다음 코드를 실행하면 하나의 오류가 발생합니다.
바로 이 부분에서는 Think함수가 Invoke되는 것이 Cancle되어 방향전환이 이루어지지 않는 버그입니다.
//탐지된 오브젝트가 null : 그 앞에 지형이 없음 if(raycast.collider == null){ nextMove= nextMove*(-1); //우리가 직접 방향을 바꾸어 주었으니 Think는 잠시 멈추어야함 CancelInvoke(); //think를 잠시 멈춘 후 재실행 Invoke(“Think”,5); }
이런 경우 if문 내부에 방향전환을 해주는 것보다는
앞에 지형이 없는 특수한경우에 해당하는 방향전환을 위한 Turn함수를 작성해 주는 것이 좋습니다.
void Turn(){ nextMove= nextMove*(-1); //우리가 직접 방향을 바꾸어 주었으니 Think는 잠시 멈추어야함 spriteRenderer.flipX = nextMove == 1; CancelInvoke(); //think를 잠시 멈춘 후 재실행 Invoke(“Think”,2);// }
이를 작성 한 후 if문 내부에서 실행시키면
//탐지된 오브젝트가 null : 그 앞에 지형이 없음 if(raycast.collider == null){ Turn(); }
오류가 해결됩니다.
마지막으로 코드 전문
MonsterMove.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MonsterMove : MonoBehaviour { Rigidbody2D rigid; public int nextMove;//다음 행동지표를 결정할 변수 Animator animator; SpriteRenderer spriteRenderer; // Start is called before the first frame update private void Awake() { animator = GetComponent
(); spriteRenderer = GetComponent (); rigid = GetComponent (); Invoke(“Think”, 5); // 초기화 함수 안에 넣어서 실행될 때 마다(최초 1회) nextMove변수가 초기화 되도록함 } // Update is called once per frame void FixedUpdate() { //Move rigid.velocity = new Vector2(nextMove,rigid.velocity.y); //nextMove 에 0:멈춤 -1:왼쪽 1:오른쪽 으로 이동 //Platform check(맵 앞이 낭떨어지면 뒤돌기 위해서 지형을 탐색) //자신의 한 칸 앞 지형을 탐색해야하므로 position.x + nextMove(-1,1,0이므로 적절함) Vector2 frontVec = new Vector2(rigid.position.x + nextMove*0.4f, rigid.position.y); //한칸 앞 부분아래 쪽으로 ray를 쏨 Debug.DrawRay(frontVec, Vector3.down, new Color(0,1,0)); //레이를 쏴서 맞은 오브젝트를 탐지 RaycastHit2D raycast = Physics2D.Raycast(frontVec, Vector3.down,1,LayerMask.GetMask(“Platform”)); //탐지된 오브젝트가 null : 그 앞에 지형이 없음 if(raycast.collider == null){ Turn(); } } void Think(){//몬스터가 스스로 생각해서 판단 (-1:왼쪽이동 ,1:오른쪽 이동 ,0:멈춤 으로 3가지 행동을 판단) //Set Next Active //Random.Range : 최소<= 난수 <최대 /범위의 랜덤 수를 생성(최대는 제외이므로 주의해야함) nextMove = Random.Range(-1,2); //Sprite Animation //WalkSpeed변수를 nextMove로 초기화 animator.SetInteger("WalkSpeed",nextMove); //Flip Sprite if(nextMove != 0) //서있을 때 굳이 방향을 바꿀 필요가 없음 spriteRenderer.flipX = nextMove == 1; //nextmove 가 1이면 방향을 반대로 변경 //Recursive (재귀함수는 가장 아래에 쓰는게 기본적) float time = Random.Range(2f, 5f); //생각하는 시간을 랜덤으로 부여 //Think(); : 재귀함수 : 딜레이를 쓰지 않으면 CPU과부화 되므로 재귀함수쓸 때는 항상 주의 ->Think()를 직접 호출하는 대신 Invoke()사용 Invoke(“Think”, time); //매개변수로 받은 함수를 time초의 딜레이를 부여하여 재실행 } void Turn(){ nextMove= nextMove*(-1); //우리가 직접 방향을 바꾸어 주었으니 Think는 잠시 멈추어야함 spriteRenderer.flipX = nextMove == 1; CancelInvoke(); //think를 잠시 멈춘 후 재실행 Invoke(“Think”,2);// } } 여기까지가 몬스터 AI구현하기였습니다.
이제 몬스터가 플레이어를 공격하도록 만들어봅시다
사실 공격은 아니고.. 닿으면 플레이어가 피해를 입는 것으로..
플레이어 피격 이벤트 구현하기
함정추가
몬스터와 더불어 플레이어에게 타격을 입힐 수 있는 함정 맵을 추가해봅시다
골드메탈님이 가시를 만들어 놓으셨네요
다시 타일맵으로 돌아가서 타일팔레트에 가시를 추가 한 후 그려주면됩니다(생략)
또한 가시도 콜라이더 물리모양Physics Shape도 수정해주셔야 합니다.
이 때 주의할 점은 지형Platform과 가시Spike가 동일한 취급을 받아서는 안되므로
Tilemap을 하나 더 추가하여 분리해주어야합니다.
Spike로 하나 더 추가하여 가시는 여기에 그려주었습니다.
물론 새로 추가한 Spike 타일맵에도 TileMap Collider 2D 컴포넌트를 추가해야합니다.
기본 설정(레이어/태그)
둘이 닿으면 발생하기 때문에 누구랑 닿았는지를 구분하기위해서는
Tag / Layer를 사용할탠데
일단 Enamy라는 태그와 레이어를 둘다 만들어서 적용시켜 주도록 합시다.
Tag Layer
가시가 들어간 Tilemap에 적용해주는데 Monster에도 적용해주세요
그리고 플레이어에도 레이어를 추가합시다
Player / PlayerDamaged라는 두개의 Layer를 만들어줍니다
플레이어는 레이어를 2개 사용할 예정입니다.
레이어 설정
– 몬스터 끼리의 물리충돌 해제
보통 몬스터는 함정(가시)장애물 같은 것을 그냥 지나칩니다
물리 효과를 받지 않는데, 지금은 실행해보면 몬스터가 가시를 지나갈때 열심히 기어올라가서 지나가는 모습입니다.
몬스터 Enamy레이어 끼리는 서로를 무시하도록 만들어봅시다
Edit -> Project Setting ->Physics2D
맨아래 보시면 물리 충돌을 관리하는 Matrix가 있는데 Enamy끼리의 충돌을 꺼줍시다
이제 장애물을 그냥 통과하게 되는데
몬스터가 더 앞쪽으로 지나가게 하려면 몬스터의 z축을 앞쪽(-1로 변경)으로 변경하면됩니다.
이제 Enamy끼리는 물리 충돌이 발생하지 않고, 몬스터가 장애물보다 앞쪽을 지나가는 것을 볼 수 있습니다.
– 플레이어가 피격을 받았을 때 1~2초의 무적타임을 부여
그러기 위하여 PlayedDamaged 레이어는 플레이어와 닿지 않게 체크를 해제해줍니다
(이렇게 해제되면 OncollisionEnter도 먹지않음)
스크립트
” 충돌이 발생하면 플레이어가 튕겨나가짐 + 무적상태에 돌입 ” 하는 코드를 작성해 봅시다
충돌이 발생하는 것을 감지하는 함수는 OnCollsionEnter2D이고, 그 대상이 Enamy여야 합니다
해당 충돌이 발생하면 여러가지 이벤트가 발생되므로 충돌시 호출할 함수 OnDamaged()를 만들어줍니다.
– 무적상태에 돌입한 레이어로 변경
– 무적 상태에 돌입했음을 알려줄 시각효과(투명해짐)
– 튕겨져 나가기 (addForce) -> 튕겨져 나가는 방향을 계산해야함
private void OnCollisionEnter2D(Collision2D collision) { if(collision.gameObject.tag == “Enamy”){ OnDamaged(collision.transform.position); //현재 충돌한 오브젝트의 위치값을 넘겨줌 } } void OnDamaged(Vector2 tartgetPos){ gameObject.layer = 11; //playerDamaged Layer number가 11로 지정되어있음 spriteRenderer.color = new Color(1,1,1,0.4f); //투명도를 0.4로 부여하여 지금이 무적시간으로 변경되었음을 보여줌 //맞으면 튕겨나가는 모션 int dirc = transform.position.x-tartgetPos.x > 0 ? 1 : -1; //튕겨나가는 방향지정 -> 플레이어 위치(x) – 충돌한 오브젝트위치(x) > 0: 플레이어가 오브젝트를 기준으로 어디에 있었는지 판별 //> 0이면 1(오른쪽으로 튕김) , <=0 이면 -1 (왼쪽으로 튕김) rigid.AddForce(new Vector2(dirc,1)*7, ForceMode2D.Impulse); // *7은 튕겨나가는 강도를 의미 } 일정시간이 지나면 무적상태가 해제되도록 수정해봅시다 무적상태 해제를 해주는 함수를 OffDamaged()로 제작하여 Invoke함수를 통해 일정 시간이 지나면 실행되도록 만듭시다 void OnDamaged(Vector2 tartgetPos){ gameObject.layer = 11; //playerDamaged Layer number가 11로 지정되어있음 spriteRenderer.color = new Color(1,1,1,0.4f); //투명도를 0.4로 부여하여 지금이 무적시간으로 변경되었음을 보여줌 //맞으면 튕겨나가는 모션 int dirc = transform.position.x-tartgetPos.x > 0 ? 1 : -1; //튕겨나가는 방향지정 -> 플레이어 위치(x) – 충돌한 오브젝트위치(x) > 0: 플레이어가 오브젝트를 기준으로 어디에 있었는지 판별 //> 0이면 1(오른쪽으로 튕김) , <=0 이면 -1 (왼쪽으로 튕김) rigid.AddForce(new Vector2(dirc,1)*7, ForceMode2D.Impulse); // *7은 튕겨나가는 강도를 의미 Invoke("OffDamaged",2); //2초의 딜레이 (무적시간 2초) } void OffDamaged(){ //무적해제함수 gameObject.layer = 10; //플레이어 레이어로 복귀함 spriteRenderer.color = new Color(1,1,1,1); //투명도를 1로 다시 되돌림 } 애니메이션 튕겨나가는 모션에 대한 스프라이트가 따로 없으므로 점프를 재활용합니다. Damaged 라고 이름을 지어서 만들었습니다. 일단 loop를 해제해주세요 애니메이션을 열어서 앞에꺼는 하나 지우고 뒤에 꺼 하나를 늘려서 0.3초 정도로 설정해주고 애니메이터에 들어가서 다음과 같이 연결해 준 후 AnyState -> ? -> Exit: 는 현재 상태에 상관없이 실행 후 복귀되는 애니메이션입니다
충돌이벤트는 다른 walk / idle / jump등과 상관없이 발생할 수 있고 후에 복귀되므로 다음과 같이 연결합니다.
Trigger라는 타입의 변수로 doDamaged를 만들어줍니다.
Trigger : 방아쇠 역할의 매개변수, 값이 없음
AnyState -> Damaged로 가는 Transition
다음과 같이 설정해주시고
Damaged->Exit 는 건들이지 않으셔도됩니다.
이제 Enamy와 충돌 시 Trigger변수를 스크립트에서 조작해주어야합니다.
void OnDamaged(Vector2 tartgetPos){ //Animation animator.SetTrigger(“doDamaged”); }
다음과 같이 한 줄 추가해주시면됩니다.
완성되었습니다.
흠..근데 강의랑 똑같이했는데도 애니메이션에 오류가 좀 나는 것 같네요.. 뭐지 어디가 틀렸나…
되긴되는데 가끔 먹혀요
<참고영상>
youtu.be/7MYUOzgZTf8
youtu.be/epZFE5Hpbdc
728×90
키워드에 대한 정보 유니티 3d 몬스터 ai
다음은 Bing에서 유니티 3d 몬스터 ai 주제에 대한 검색 결과입니다. 필요한 경우 더 읽을 수 있습니다.
이 기사는 인터넷의 다양한 출처에서 편집되었습니다. 이 기사가 유용했기를 바랍니다. 이 기사가 유용하다고 생각되면 공유하십시오. 매우 감사합니다!
사람들이 주제에 대해 자주 검색하는 키워드 3D 쿼터뷰 액션게임 – 목표를 추적하는 AI 만들기 [유니티 기초 강좌 B48]
- 유니티
- 유니티강좌
- 유니티기초
- 유니티기초강좌
- 유니티개발
- 유니티개발강좌
- 유니티3D
- 유니티3D강좌
- 유니티3D기초
- 유니티2D
- 유니티2D강좌
- 유니티2D기초
- 도트
- 도트게임
- 도트게임만들기
- 도트게임개발
- 픽셀아트
- 픽셀아트게임
- 픽셀아트게임만들기
- 픽셀아트게임개발 게임개발
- 게임개발툴
- 프로그래밍
- 게임프로그래밍
- 게임그래픽
- 슈팅게임
- 슈팅게임만들기
- 슈팅게임개발
- unity
- unity3d
- game
- gamedevelopment
- gamedev 유니티에셋
- 유니티에셋
- 에셋스토어
- 튜토리얼에셋
- 쿼터뷰
- 쿼터뷰 게임 만들기
3D #쿼터뷰 #액션게임 #- #목표를 #추적하는 #AI #만들기 #[유니티 #기초 #강좌 #B48]
YouTube에서 유니티 3d 몬스터 ai 주제의 다른 동영상 보기
주제에 대한 기사를 시청해 주셔서 감사합니다 3D 쿼터뷰 액션게임 – 목표를 추적하는 AI 만들기 [유니티 기초 강좌 B48] | 유니티 3d 몬스터 ai, 이 기사가 유용하다고 생각되면 공유하십시오, 매우 감사합니다.