2023. 7. 31. 00:24ㆍC# & Unity 공부
오늘은 몬스터 AI를 구현해보겠다.
준비하기
들어가기 앞서서 몬스터 애니메이션을 먼저 잡아주고 가자.
몬스터의 걷는 Sprite들을 끌어 몬스터에다가 넣어 애니메이션 파일을 만들고,
Make Transition을 통해 Enemy_Idle과 Enemy_Walk를 서로 연결 시켜주자.
Parameters에 들어가 isWalking이라는 bool 변수를 만들어준다.
그리고 화살표를 클릭하여 Has Exit Time을 해제하고
애니메이션의 겹치는 시간을 줄인뒤 Conditions를 추가해주면 된다.
반대쪽 화살표도 해준 뒤에, 몬스터의 행동을 결정,
즉 AI를 구현하기 위하여 스크립트를 만들어주자.
기본 이동
몬스터가 기본적으로 왼쪽으로 이동하는 것을 먼저 짜보자.
만든 EnemyMove 스크립트에다가 다음과 같은 코드를 짜보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyMove : MonoBehaviour
{
Rigidbody2D rigid;
void Awake()
{
rigid = GetComponent<Rigidbody2D>();
}
void FixedUpdate()
{
rigid.velocity = new Vector2(-1, rigid.velocity.y);
}
}
그리고 주의할 점이 있는데, 저번 시간에 배웠던
Freeze Rotation Z를 꼭 넣어주자.
아니면 몬스터가 걸어다니다가 혼자 구를 것이다.
행동 설정
우선 이 몬스터가 왼쪽으로 움직일 것만 할 것은 아니기 때문에,
코드로 작성하기 앞서 무슨 행동을 할 지 고민해보자.
오늘 할 것은 오른쪽 이동, 왼쪽 이동, 정지를 할 것이다.
아래 코드를 작성하여 몬스터가 랜덤으로 이동하게 만들어보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyMove : MonoBehaviour
{
Rigidbody2D rigid;
public int nextMove;
//행동 상태를 나타내는 변수
//왼쪽 이동은 -1, 정지는 0, 오른쪽 이동은 1로 나타낼 것이다.
void Awake()
{
rigid = GetComponent<Rigidbody2D>();
Think(); //오브젝트가 생성될 때, 함수가 실행되면서 랜덤값 결정
}
void FixedUpdate()
{
rigid.velocity = new Vector2(nextMove, rigid.velocity.y);
}
void Think() //행동지표를 바꿔줄 함수 생성
{
nextMove = Random.Range(-1, 2);
//Random : 랜덤 수를 생성하는 로직 관련 클래스
//Range() : 최소~최대 범위의 랜덤 수 생성 (최대 제외)
}
}
그리고 실행시켜 보면,
스크립트의 Think()함수에 의해 랜덤값으로 NextMove가 결정되며,
-1,0,1이 x의 속도가 되어서 왼쪽 이동, 정지 혹은 오른쪽 이동이 된다.
하지만 Think 함수가 한 번만 실행되면 안된다.
우리가 원하는 것은 게임처럼 조금씩 바뀌는 것이기 때문이다.
그럼 Think()함수를 Awake()안에다가 넣어주면 안될 것이고,
FixedUpdate()에다가 넣는다고 가정해보자.
1초에 50번 정도씩 돌 것이기 때문에 넣는다면 이상하게 될 것이다.
그래서 재귀함수를 쓸 것이다.
재귀함수란, 함수안에 같은 함수를 한번 더 호출하여 한번 더 실행하게끔 해준다.
그러나 이는 딜레이 타임을 잡아주어야 한다.
이를 생각하며 다음과 같이 코드를 짜보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyMove : MonoBehaviour
{
Rigidbody2D rigid;
public int nextMove;
//행동 상태를 나타내는 변수
//왼쪽 이동은 -1, 정지는 0, 오른쪽 이동은 1로 나타낼 것이다.
void Awake()
{
rigid = GetComponent<Rigidbody2D>();
//Think(); //오브젝트가 생성될 때, 함수가 실행되면서 랜덤값 결정
Invoke("Think", 5);
}
void FixedUpdate()
{
rigid.velocity = new Vector2(nextMove, rigid.velocity.y);
}
void Think() //행동지표를 바꿔줄 함수 생성
{
nextMove = Random.Range(-1, 2);
//Random : 랜덤 수를 생성하는 로직 관련 클래스
//Range() : 최소~최대 범위의 랜덤 수 생성 (최대 제외)
//Think();
//재귀함수
//딜레이 없이 재귀함수를 사용하는 것은 위험!!
Invoke("Think", 5);
//Invoke() : 주어진 시간이 지난 뒤, 지정된 함수를 실행하는 함수
}
}
그러면 5초마다 몬스터가 원하는 방향으로 이동하게 된다.
지능 높이기
몬스터가 원하는 방향으로 가는 건 좋으나, 낭떠러지로 떨어지는 것이 아쉽다.
이것을 한 번 고쳐볼 것이다.
앞에 낭떠러지라는 것을 인식해야하기 때문에, 저번 시간에 이용햇던 Raycast(Raycasthit)를 사용하여 보겠다.
저번 시간에 배웠었던 Raycast의 코드의 일부분을 복사해서 다음과 같이 수정해보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyMove : MonoBehaviour
{
Rigidbody2D rigid;
public int nextMove;
//행동 상태를 나타내는 변수
//왼쪽 이동은 -1, 정지는 0, 오른쪽 이동은 1로 나타낼 것이다.
void Awake()
{
rigid = GetComponent<Rigidbody2D>();
//Think(); //오브젝트가 생성될 때, 함수가 실행되면서 랜덤값 결정
Invoke("Think", 5);
}
void FixedUpdate()
{
//Move
rigid.velocity = new Vector2(nextMove, rigid.velocity.y);
//Platform Check
Vector2 frontVec = new Vector2(rigid.position.x + nextMove, rigid.position.y);
Debug.DrawRay(frontVec, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(frontVec, Vector3.down, 1, LayerMask.GetMask("Platform"));
if (rayHit.collider == null)
{
Debug.Log("경고! 이 앞은 낭떨어지!!");
}
}
void Think() //행동지표를 바꿔줄 함수 생성
{
nextMove = Random.Range(-1, 2);
//Random : 랜덤 수를 생성하는 로직 관련 클래스
//Range() : 최소~최대 범위의 랜덤 수 생성 (최대 제외)
//Think();
//재귀함수
//딜레이 없이 재귀함수를 사용하는 것은 위험!!
Invoke("Think", 5);
//Invoke() : 주어진 시간이 지난 뒤, 지정된 함수를 실행하는 함수
}
}
주의할 점은 rayHit가 나타내는 오브젝트가 땅(Platform)이 아닐 때,
즉 아무것도 가리키지 않을 때 밑의 내용들이 실행되어야 한다.
그리고 Raycast(빔)은 왼쪽으로 갈 때랑 오른쪽으로 갈 때랑 위치가 바뀌어야 하니
현재 위치에서 nextMove 변수를 이용해 유동적으로 바꿀 수 있도록 하자.
또한 이번에 새로 나온 Invoke()함수와 재귀함수의 개념은 많이 사용되니
잘 알아두도록 하자.
이제 메시지를 생성했을 때가 아닌, 반대로 돌아가는 모습을 짜보자.
또한 오브젝트와 Ray의 거리가 멀지 않게 0.5f 정도 nextMove에다가 곱해주자.
마지막으로, 5초마다 바뀌는 것이 아닌 랜덤 시간으로 이동방향을 정하게 해보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyMove : MonoBehaviour
{
Rigidbody2D rigid;
public int nextMove;
//행동 상태를 나타내는 변수
//왼쪽 이동은 -1, 정지는 0, 오른쪽 이동은 1로 나타낼 것이다.
void Awake()
{
rigid = GetComponent<Rigidbody2D>();
//Think(); //오브젝트가 생성될 때, 함수가 실행되면서 랜덤값 결정
Invoke("Think", 5);
}
void FixedUpdate()
{
//Move
rigid.velocity = new Vector2(nextMove, rigid.velocity.y);
//Platform Check
Vector2 frontVec = new Vector2(rigid.position.x + 0.5f*nextMove, rigid.position.y);
Debug.DrawRay(frontVec, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(frontVec, Vector3.down, 1, LayerMask.GetMask("Platform"));
if (rayHit.collider == null)
{
nextMove *= -1;
CancelInvoke();
//현재 작동중인 모든 Invoke 함수를 취소하는 함수
//닿으면 Invoke()를 멈춤
Invoke("Think", 5);
}
}
void Think() //행동지표를 바꿔줄 함수 생성
{
nextMove = Random.Range(-1, 2);
//Random : 랜덤 수를 생성하는 로직 관련 클래스
//Range() : 최소~최대 범위의 랜덤 수 생성 (최대 제외)
float nextThinkTime = Random.Range(2f, 5f);
//Think();
//재귀함수
//딜레이 없이 재귀함수를 사용하는 것은 위험!!
Invoke("Think", nextThinkTime);
//Invoke() : 주어진 시간이 지난 뒤, 지정된 함수를 실행하는 함수
}
}
애니메이션
자, 이제 남은 건 애니메이션 뿐이다.
걷는 애니메이션을 넣어볼 것이다.
우선 Animator에 들어있던 Parameters에 bool 변수였던 isWalking을 삭제하자.
다른 방법으로 시도할 것이다.
그리고 위 사진처럼 새로운 int형 변수를 만들어주자.
후에는 그 변수 이름을 WalkSpeed라고 해주고, 화살표를 클릭해
각 Condition에 변수를 넣어주도록 하자.
Conditions의 WalkSpeed 옆에를 누르면 밑의 사진과 같이 4가지가 나온다.
Idle(대기 상태)에서 Walk(걷기 상태)로 가는 것은 속도가 0이 아닐 때이므로, NotEqual을 선택해준다.
반대로 Walk에서 Idle로 가는 것은 속도가 0일 때이므로, Equals를 선택해준다.
그리고 이제 스크립트를 수정하면 된다.
Setinteger 함수를 통해 WalkSpeed의 값을 nextMove의 값으로 바꿔주면 되고,
조건식을 통해 문워크를 하지 않게 뒤집어주면 된다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyMove : MonoBehaviour
{
Rigidbody2D rigid;
Animator anim;
SpriteRenderer spriteRenderer;
public int nextMove;
//행동 상태를 나타내는 변수
//왼쪽 이동은 -1, 정지는 0, 오른쪽 이동은 1로 나타낼 것이다.
void Awake()
{
rigid = GetComponent<Rigidbody2D>();
anim=GetComponent<Animator>();
spriteRenderer = GetComponent<SpriteRenderer>();
//Think(); //오브젝트가 생성될 때, 함수가 실행되면서 랜덤값 결정
Invoke("Think", 5);
}
void FixedUpdate()
{
//Move
rigid.velocity = new Vector2(nextMove, rigid.velocity.y);
//Platform Check
Vector2 frontVec = new Vector2(rigid.position.x + 0.5f*nextMove, rigid.position.y);
Debug.DrawRay(frontVec, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(frontVec, Vector3.down, 1, LayerMask.GetMask("Platform"));
if (rayHit.collider == null)
{
nextMove *= -1;
spriteRenderer.flipX = nextMove == 1;
CancelInvoke();
//현재 작동중인 모든 Invoke 함수를 취소하는 함수
//닿으면 Invoke()를 멈춤
Invoke("Think", 5);
}
}
void Think() //행동지표를 바꿔줄 함수 생성
{
//Set Next Active
nextMove = Random.Range(-1, 2);
//Random : 랜덤 수를 생성하는 로직 관련 클래스
//Range() : 최소~최대 범위의 랜덤 수 생성 (최대 제외)
//Recursive
float nextThinkTime = Random.Range(2f, 5f);
//Think();
//재귀함수
//딜레이 없이 재귀함수를 사용하는 것은 위험!!
Invoke("Think", nextThinkTime);
//Invoke() : 주어진 시간이 지난 뒤, 지정된 함수를 실행하는 함수
//SpriteAnimation
anim.SetInteger("WalkSpeed", nextMove);
//Think()함수를 실행할 때마다, WalkSpeed를 nextMove로 바꾸어준다.
//Condition에서 설정해주었기 때문에 nextMove와 값이 같은
//WalkSpeed의 값에 따라 애니메이션이 바뀐다.
//Flip Sprite
if(nextMove!=0)
{
spriteRenderer.flipX = nextMove == 1;
}
//nextMove의 값이 0이 아니라면 => 움직이고 있다면
//nextMove의 값이 1이라면 flipX의 값에 1을 넣는다.
//if 조건식이 없다면 flipX가 1이였을 때 오른쪽을 봐라보고 있을텐데
//멈췄다고 nextMove가 0이 될 것이고 1이 아니여서 flipX가 0이되고
//정지했다고 뒤집히는 오류가 발생할 것이다
}
}
여기서 주의할 점은 Think 함수에다가는 움직이면서 오른쪽 이동이 아니라면,
flipX의 값을 1로 바꿔주어서 뒤집으라는 것인데,
FixedUpdate()에서 Ray의 Collider가 인식되지 않을 때 발동하는 것들 중
nextMove의 값에 -1이 곱해진다.
그러나 이것은 다음 Think()함수가 실행되기 전이라 방향만 바꾸고 애니메이션의
좌우는 바뀌지 않는다.
따라서 FixedUpdate()안에다가도 flipX와 관련된 명령문을 넣어 주어야한다.
'C# & Unity 공부' 카테고리의 다른 글
Unity 2D - 8. 스테이지를 넘나드는 게임 완성하기 (0) | 2023.08.05 |
---|---|
Unity 2D - 7. 플레이어 피격 이벤트 구현하기 (0) | 2023.07.31 |
Unity 2D - 5. 타일맵으로 플랫폼 만들기 (0) | 2023.07.30 |
Unity 2D - 4. 플레이어 점프 구현하기 (0) | 2023.07.28 |
Unity 2D - 3. 플레이어 이동 구현하기 (0) | 2023.07.21 |