일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- 멋쟁이사자처럼
- 드로잉
- pixel art
- 서포터즈
- 노하우
- Aseprite
- layer
- Pixelart
- 픽셀 아트
- 반환원정대
- 자원순환보증금관리센터
- menu
- 스마일게이트
- 채색
- 드로잉 연습
- 포토샵
- 모작
- 도트공부
- 에이세프라이트
- 픽셀아트
- 애니메이션
- 도트
- 장학팀
- 연습
- 인디게임 개발
- photoshop
- 개발
- TOOL
- 기초
- COSMO
- Today
- Total
소소한 나의 하루들
2d 플랫포머(6) - 몬스터 AI 구현하기 본문
출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared
이번에는 인공지능에 의한 몬스터 AI를 만들어본다. 복잡한 것은 나중에하고, 간단한 것만 해본다.
정말 AI라고 할정도로 복잡한 것은 나중에 해보도록 한다.
#1. 준비하기
전에 했던 것처럼 Enemy 오브젝트에도 Animation으로 할 프레임을 다중선택해서 Enemy_Idle과 Enemy_Walk 애니메이션을 추가해준다.
(Player 오브젝트 애니메이션의 경우 ‘Player_’라고 앞에 입력함)
앞서 했던 것처럼 애니메이션 추가>Animator에서 Parameter추가>Transition으로 각 State 연결>Condition에서 Parameter 설정 및 Has Exit Time 해제 + Transition Duration 0으로 설정 한다.
이제 애니메이션 정리가 끝났으면, 스크립트를 추가해서 Parameter의 값을 설정해준다. (EnemyMove 스크립트 추가)
추가한 스크립트를 우선 Enemy 오브젝트에 반영한다.
#2. 기본 이동
Enemy 오브젝트도 물리 기반으로 움직이도록 한다.
그러면 RigidBody 컴포넌트를 가져와야한다.
함수 진입 전, 변수 선언 → Awake에서 변수 초기화
이번에는 AI이기 때문에, 키 입력에 따라 이동하는 것이 아니라 알아서 움직인다. 일단 앞으로만 쭉 이동하도록 해본다. (FixedUpdate에 작성)
어떻게? 속력을 써 넣어서.
rigid.velocity = new Vector2(-1, rigid.velocity.y);
왼쪽으로 이동하도록, -1 그리고 y는 그대로 입력한다.
Player 오브젝트에서 카메라도 꺼내고, 나머지 오브젝트(Item/Finish/Player)는 비활성화한다.
Enemy 오브젝트에 Rigidbody 2D 컴포넌트를 추가해야 이동한다.
실행해보면 Enemy가 알아서 움직인다.
이제 AI가 스스로 생각해서 방향도 바꾸고, 잠깐 멈춰있기도 하는 모습을 보고싶다.
#3. 행동설정
인공지능 AI의 행동을 짤 때는 어떤 행동을 할지 구상을 하고 들어가는 것이 좋다.
오늘 할 것은 간단하다. 오른쪽 이동, 왼쪽 이동, 정지 3가지이다.
행동지표를 결정할 변수 하나를 생성한다. (nextMove)
그리고 nextMove를 바꿔줄, 행동지표를 바꿔줄 함수 하나를 생성한다. (Think 함수)
왼쪽으로 이동중인지(-1)/정지해있는지(0)/왼쪽으로 이동중인지(1)
어떤 때는 왼쪽, 어떤 때는 오른쪽 어떤 때는 가만히 움직이도록 랜덤하게 움직임을 발생시킨다.
Random: 랜덤 수를 생성하는 로직 관련 클래스
Range(): 최소~최대 범위의 랜덤 수를 생성하는 함수 (※최대 제외)
매개변수로 (최소값, 최대값)이 들어간다. [최소값~(최대값-1)까지] : 0, 1, -1이 나오도록 할 것이므로 인수로 (0, 2)를 넣는다.
저장 후 실행해보면, 실행할 때마다 스크립트 Enemy Move 컴포넌트의 Next Move 값이 -1, 0, 1 중 랜덤하게 바뀌면서 Enemy 오브젝트가 랜덤하게 이동하는 것을 확인할 수 있다.
실행할 때 딱 한번만 랜덤값이 정해지는데, 이제 게임이 실행되는 동안 계속 꾸준하게 랜덤값이 일정 주기로 실행되어야한다.
만약 nextMove의 값을 랜덤하게 정해주는 Think 함수가 FixedUpdate나 Update에 작성된다면, 계속 호출되어서 과부하가 발생할 것이다.
따라서 Think 함수를 재귀함수로 작성해야한다. 재귀함수는 자신을 스스로 호출하는 함수이다.
일단 시작은 Awake( )에서 함수를 선언하는 것은 맞다. 그런데 딜레이없이/종료 조건 없이 재귀함수를 사용하는 것은 아주 위험하다.
이럴 때는 유니티 c#에서 제공해주는 Invoke( ) 함수를 사용하는 것이다.
Invoke(): 주어진 시간이 지난 뒤, 지정된 함수를 실행하는 함수
매개변수는 (호출할 함수, 딜레이 시간)이다.
그리고 이 Invoke 함수를 재귀함수 Think 함수 안에다가 또 입력하면 된다.
실행해보면 5초 간격으로 이동방향이 바뀌거나 정지하거나, 그대로 계속 이동한다.
#4. 지능 높이기
그런데 실행하다보면 지형이 끝나는 지점에 다다르면, 떨어지게된다.
따라서 빠지지 않는 방법을 구현해본다.
전에 Player가 바닥에 닿았을 때 Jump1 애니메이션을 중단하도록 했던 것처럼, RaycastHit를 써서 현재 바닥에 닿아있는지, 아닌지 알아보도록 한다.
우리가 알아야할 위치가 Player의 경우와는 다르다. Enemy는 이동하는 경로의 상태를 예측해야 하므로, 앞을 체크해야한다.
왼쪽으로 가면 현재위치 -1 / 오른쪽으로 가면 현재위치 + 1
현재 왼쪽으로 또는 오른쪽으로 이동할 때 가산해줄 값은 nextMove가 갖고있다.
이렇게 현재 이동방향에서 현재위치보다 앞 1칸의 위치를 frontVec 성분으로 설정했다.
이제 frontVec에서 아래로 빔을 쐈을 때 Platform 레이어의 지형 오브젝트가 맞는지 여부에 따라 rayHit의 초기화 여부가 결정된다. (null인지 아닌지)
지금 상황에서는 이동하는 앞칸에 지형이 있어야하므로 rayHit은 null이 아니여야 한다. 따라서 rayHit == null일 때, 뒤로 돌아야한다.
실행했을 때, 오브젝트가 앞으로 구른다면, Rigidbody 2D 컴포넌트의 Freeze Rotation Z를 체크해준다.
이제 Enemy가 앞이 낭떠러지인지 인식하게 되었다.
바로 앞이 낭떠러지라면, 방향을 바꿔야한다. 따라서 nextMove에다가 -1을 곱해준다.
방향이 정 반대가 되면서 다시 Invoke가 5초 시간을 딜레이시켜주면 좋을 것 같다.
CancelInvoke( ): 현재 작동 중인 모든 Invoke 함수를 멈추는 함수
그리고 다시 또 Invoke 함수를 실행하면 된다.
Vector2 frontVec = new Vector2(rigid.position.x + nextMove * 0.5f, rigid.position.y);
그런데 Enemy와 초록 빔(Raycast)의 거리 간격이 너무 긴 것 같다면, 간격을 줄여주면 된다.
이렇게 절대 낭떠러지에서 떨어지지 않는 몬스터 AI가 만들어졌다.
지금 Invoke 딜레이 시간이 5초로 고정되어있는데, 이 딜레이 시간도 랜덤으로 줄 수 있다.
#5. 애니메이션
앞에서 생성한 Animator의 Parameter를 삭제하고, 이번에는 Int타입의 Parameter를 생성한다. 그리고 이름은 ‘WalkSpeed’라고 짓는다.
이제 Enemy_Walk와 Enemy_Idle 사이 Transition 화살표의 Condition에다가 Parameter를 설정해주는데, Int 타입 Parameter는 Greater / Less / Equals / NotEqual 중 선택할 수 있다.
WalkSpeed가 0이면 가만히 정지, WalkSpeed가 0이 아니면 움직이는 것으로 판단한다.
이렇게 설정이 끝났으면 다시 스크립트로 돌아가서 코드를 작성한다.
Enemy의 이동을 조절하는 nextMove 변수는 Think 함수에서 조절할 수 있으니까, WalkSpeed도 Think에서 컨트롤한다.
0인지, 0이 아닌지.
그리고 문워크하는 것도 고쳐준다. *Flip관련 스크립트 코드 작성 전, 먼저 기본 sprite 방향이 어디인지 flip 체크 여부에 따라 어느 방향을 보는지 확인해야한다.
선언과 Awake에서 GetComponent로 초기화까지 해주어야 한다.
애니메이션 방향 전환은 오브젝트의 Sprite Renderer에서 Flip옵션이다. X축을 체크하면 된다. (게임 시점에 맞게)
현재 Flip을 체크 안하면 Enemy 방향은 왼쪽/체크하면 방향은 오른쪽이니까
오른쪽 방향으로 이동할 때 Flip X를 활성화해줘야한다.
flipX는 bool 타입이다.
※여기서 문제는, 멈추게되면 무조건 왼쪽을 보게된다. 서 있을 때는 방향을 바꿀 필요가 없다.
따라서 “가만히 있을때(=nextMove가 0일 때)’는 조건문으로 걸러줘야한다.
그리고 통상적으로는 재귀함수 부분은 제일 아래에 작성하는 것이 일반적이다.
이제 문제는 지형 끝에 몬스터가 도달하고 다시 이동 방향을 반대로 바꿀 때 문워크를 한다는 것이다.
using System.Collections;
using System.Collections.Generic;
using System.Runtime;
using UnityEngine;
public class EnemyMove : MonoBehaviour
{
Rigidbody2D rigid;
Animator anime;
SpriteRenderer sprite;
public int NextMove;
float NextThinkTime;
void Awake()
{
rigid = GetComponent<Rigidbody2D>();
anime = GetComponent<Animator>();
sprite = GetComponent<SpriteRenderer>();
NextThinkTime = Random.Range(2.0f, 5.0f);
Invoke("Think", NextThinkTime);
}
void FixedUpdate()
{
//Move
rigid.velocity = new Vector2(NextMove, rigid.velocity.y);
//Platform Check
Vector2 frontVec = new Vector2(rigid.position.x + NextMove * 0.5f, 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) //낭떠러지를 만났을 때(앞에 Platform 오브젝트가 없을때)
{
Turn();
}
}
//재귀 함수(Recursive)
void Think()
{
//Set Next Active
NextMove = Random.Range(-1, 2);
//Sprite Animation
anime.SetInteger("WalkSpeed", NextMove);
//Flip Sprite
if (NextMove != 0)
sprite.flipX = NextMove == 1;
//재귀함수 : 맨 마지막에 작성
Invoke("Think", NextThinkTime);
}
void Turn()
{
NextMove = NextMove * -1; //낭떠러지에서 반대쪽으로 이동
sprite.flipX = NextMove == 1; // 오른쪽(1)으로 이동중이라면 flipX 활성화
CancelInvoke();
Invoke("Think", NextThinkTime);
}
}
이동방향이 왼쪽(-1)일 때, 바로 앞이 낭떠러지라면 nextMove는 -1이 곱해져서 1이되고 CancelInvoke 후 5초 딜레이 다음 Think함수가 작동한다. 이때 flipX로직이 중간에 없기 때문에 flip되지 않아, 문제가 생기는 것이다.
같은 로직이 2개 이상 작성되니까 따로 함수로 빼서 만든다. (Turn 함수)
아까 지형 끝에서 이동방향을 전환할 때 flip되지 않던 문제를 따로 함수를 빼서 할 수 있도록 구현했다. 그것이 Turn 함수의 역할이다.
문제 상황 - 피드백
1. Enemy가 중간에 움직이지 않고 제자리 뛰기를 하는 경우가 있다.
: 이때 스크립트 로직 상에도 문제는 없어서 콜라이더의 모양을 Box Collider 2D 컴포넌트에서 Capsule Collider 2D 컴포넌트로 변경해주었더니 문제가 해결되었다.
[블럭 단위의 플랫폼의 경계에 걸리기 때문이다]
'개발 > 유니티' 카테고리의 다른 글
2d 플랫포머(8) - 스테이지를 넘나드는 게임 완성하기 (0) | 2024.01.25 |
---|---|
2d 플랫포머(7) - 플레이어 피격 이벤트 구현하기 (0) | 2024.01.24 |
2d 플랫포머(5) - 타일맵으로 플랫폼 만들기 (0) | 2024.01.23 |
2d 플랫포머(4) - 플레이어 점프 구현하기 (0) | 2024.01.23 |
2d 플랫포머(3) - 플레이어 이동 구현하기 (0) | 2024.01.22 |