몬스터를 사냥하고 강해지기위한, 퀘스트를 위한 아이템으로 성장
RPG게임이면 몬스터와 아이템은 존재한다.
기존에 만들어 둔 CharactorFSM, CharactorInfo를 상속받아서 몬스터를 만들거다.
그리고 아이템을 만들고 죽으면 드랍하도록 만들자
몬스터 만들기
몬스터는 캐릭터와 다르게 입력이 없이 스스로 움직인다.
랜덤한 방향으로 움직이고, 공격해야할 상황이 오면 적에게 다가가서 공격한다.
그렇기에 결국 랜덤한 움직임이 몬스터에겐 자연스러움 이라고 볼 수 있다.
우선 몬스터는 플레이어의 위치가 근처에 오면 공격을 한다.
플레이어가 뒤에 있으면 발견못하는 게임도 있지만 구현하지않았다.
그리고 플레이어를 circleCast등으로 근처에 있는지 확인하는 방법도 있지만,
게임의 규모가 커지고 모든 몬스터가 플레이어를 찾기위해 Cast를 사용하는건 엄청난 비효율이다.
그래서 몬스터가 플레이어의 위치만 가지고 있고, 자신과의 거리만 계산해서 근처에 오는지 판단하도록 만들었다.
public class MonsterFSM : CharacterFSM
{
private Transform player;
private float playerDistance => Vector3.Distance(transform.position, player.position); // 플레이어와 거리
[SerializeField] private Vector3 moveDir; // 이동 방향
[Space(10f)]
[Header("상태 전환 확률")]
[SerializeField] int moveChangeValue; // 해당 확률로 움직임 상태로 전환
[SerializeField] int IdleChangeValue;
}
상태 전환 확률은 매 프레임마다 1/chageValue 의 확률로 상태전환을 한다. 500을 하니까 가장 적당했다.
playerDistance는 값을 읽을때 마다 플레이어와 거리차이를 계산한다.
각 움직임은 Player와 똑같이 상태를 정의하면 된다.
MonsterFSM
// 앞으로 이동하기
private void MoveToDir(Vector3 dir)
{
// 공중유무 체크
if (!characterController.isGrounded)
{
dir += new Vector3(0, myInfo.gravity * Time.deltaTime, 0);
}
else
{
dir.y = 0;
}
characterController.Move(Time.deltaTime * myInfo.moveSpeed * dir);
}
// 대기
private IEnumerator Idle()
{
while (true)
{
yield return null;
if (Random.Range(0, moveChangeValue) == 0) // 일정 확률로 Move상태로 전환 or 적이 있으면
{
transform.Rotate(new Vector3(0, Random.Range(0, 360), 0)); //랜덤한 방향으로 회전
ChangeState(State.Move);
}
//적이 가까이 있다면 이동or공격
if (playerDistance < 1f)
{
ChangeState(State.Attack);
}
if (playerDistance < 5f)
{
ChangeState(State.Move);
}
}
}
// 이동
private IEnumerator Move()
{
while (true)
{
yield return null;
MoveToDir(transform.forward);
if (playerDistance < 5f) // 플레이어와 거리가 가까우면 적을 향해 이동
{
transform.LookAt(new Vector3(player.position.x, transform.position.y, player.position.z)); //적이 있는 방향으로 회전
if (playerDistance <= 1f) //적과 가까워지면 공격
{
ChangeState(State.Attack);
}
}
else if (Random.Range(0, IdleChangeValue) == 0) // 일정 확률로 Idle상태로 전환
{
ChangeState(State.Idle);
}
}
}
// 공격
private IEnumerator Attack()
{
float timer = 0;
bool giveDamage = false;
while (true)
{
yield return null;
timer += Time.deltaTime;
if (timer > 1f)
{
if (playerDistance < 1f)
{
ChangeState(State.Idle);
}
else
{
ChangeState(State.Move);
}
}
// 공격 모션 0.55초에 데미지를 줌
else if (timer > 0.55f)
{
if (!giveDamage)
{
giveDamage = true;
Hit(transform);
}
}
}
}
// 공격 당함
public IEnumerator GetHit()
{
float timer = 0;
while (true)
{
timer += Time.deltaTime;
if (timer >0.7f)
{
ChangeState(State.Idle);
}
yield return null;
}
}
private IEnumerator Die()
{
characterController.enabled = false;
while (true)
{
yield return new WaitForSeconds(1f);
// 오브젝트풀에 의한 삭제
break;
}
}
Die의 오브젝트풀은 만들지않아서 놔뒀다.
플레이어는 Input으로 움직였다면, Monster는 확률 변수로 움직이면 된다.
while문 안에 yield return null이 player랑 다르게 썼다.
player는 while문 끝에 yield를 썼는데 몬스터는 먼저 썼다.
플레이어와 몬스터가 딱 붙어있으면 상태전환이 한 프레임에 여러번 바껴버리게 된다.
그러면 스택오버플로우가 발생하거나 애니메이션이 실행되지않았다.
그래서 yield를 먼저써서 해결했다.
평상시엔 랜덤하게 움직이다가, 플레이어가 가까이가면 공격한다.
MonsterInfo는 죽으면 드랍할 아이템정보와 관련있기 때문에 아이템을 먼저 만들어야 한다.
아이템
아이템은 각각의 아이템이 있고, 그 아이템들이 모여서 보관되거나 드랍된다.
그래서 ItemData와 Item을 구분해서 만들었다.
ItemData는 각 아이템의 정보를 가지고 있다.
그리고 Item은 ItemData를 가지고있고, 그 아이템이 몇개인지 개수를 가지고있다.
ItemData
[CreateAssetMenu(fileName = "ItemData", menuName = "Inventory/ItemData")]
public class ItemData : ScriptableObject
{
public int id; // 고유 번호
public string Name; // 이름
[TextArea(2,4)]
public string toolTip; // 설명
public Sprite sprite; // 스프라이트
}
아이템의 정보를 들고있다.
이름 설명 이미지가 있고, 고유번호를 가지고 있다.
관리하기 쉽도록 ScriptableObject로 만들었다.
Item
public class Item
{
public ItemData data;
public int amount;
public Item(ItemData _data, int _amount = 1)
{
data = _data;
amount = _amount;
}
}
ItemData와 개수를 가지고 있다.
생성자는 ItemData와 개수를 지정해서 아이템을 생성가능하다.
amount를 적지않으면 1개로 인식한다.
몬스터의 아이템
이렇게 만든 아이템을 몬스터에게 넣어주면 된다.
몬스터가 죽으면 아이템을 떨어트리는데, 아이템의 개수가 랜덤으로 떨어지도록 만들려 한다.
그러면 결국 Item을 몬스터가 들고있는것이 아니라, ItemData를 가지고있고,
죽으면서 드랍확률과 개수에 맞춰서 Item이 생기는 것이 자연스러울 것이다.
/// <summary> MonsterInfo에서 각 몬스터가 드랍할 아이템을 리스트로 담아둠 </summary>
[System.Serializable]
public class MonsterDroppableItem
{
public ItemData itemData; // 드랍 아이템 데이터
public float percentage; // 드랍 확률
public int maxDropCount; // 최대 드랍 개수
}
몬스터가 떨어트릴 아이템에 대한 정보를 가지고있는 클래스다.
유니티 인스펙터에서 보이게 하기위해 Serializable을 썼고,
이 클래스를 배열로 만들어도 인스펙터에서 볼 수 있다.
(그리고 가장위에 ///<summary>는 해당 클레스에 마우스를 올리면 summary의 내용이 보이는데
일반적인 IDE가 아닌 vscode를 사용중이고 다른 IDE에선 가능한지 모르겠다.)
MonsterInfo
public class MonsterInfo : CharacterInfo
{
[SerializeField] private List<MonsterDroppableItem> droppableItems; // (보상으로 줄 아이템, 확률)
protected override void Awake()
{
base.Awake();
}
//보상 주기
public void GiveItem()
{
// 아이템 떨어트리기
}
public override void DieEvent()
{
base.DieEvent();
GiveItem();
}
}
위 MonsterDroppableItem를 배열로 만들어서 몬스터가 떨어트릴 수 있는 아이템들을 넣으면 된다.
보통 몬스터가 죽을때, 여러종류의 아이템을 떨어트릴 수 있기 떄문에 이렇게 만들었다.
GiveItem은 죽으면서 아이템을 떨어트리기 위함이다.
아이템을 떨어트리는건 몬스터가 죽을때외에도, 플레이어가 인벤토리에서 아이템을 버릴 수도 있고,
채집, 제작 등 여러가지로 아이템이 떨어질 수 있다.
아이템이 떨어질 일이 생각보다 많았다.
그래서 아이템을 떨어트리는 것을 관리하는 Manager를 만들었다.
아이템 떨어트리기
아이템이 생성되면 아이템이 위로 뿌려지듯이 나오는 연출을 하고싶었다.
그래서 주울수 있는 아이템을 PickUpItem으로 별개로 만들었다.
public class PickUpItem : MonoBehaviour
{
public Item item;
private Rigidbody rigid;
private SpriteRenderer spriteRenderer;
private void Awake()
{
rigid = GetComponent<Rigidbody>();
spriteRenderer = GetComponent<SpriteRenderer>();
}
private void Start()
{
rigid.AddForce(new Vector3(Random.Range(-100f, 100f), 200, Random.Range(-100f, 100f)));
}
void Update()
{
transform.Rotate(Vector3.up * 100f * Time.deltaTime);
}
public void SetItem(ItemData _data, int _amount = 1)
{
item = new Item(_data, _amount);
spriteRenderer.sprite = item.data.sprite;
}
}
아이템은 처음 생성될때 랜덤한방향과 위로 튕겨나온다.
그리고 습득하기 전까지 아이템이 회전하도록 만들었다.
떨어지는 아이템은 만들었지만 아이템을 떨어트리는것은 아직 없다.
위에 말했듯 아이템이 떨어질 일은 많기때문에 Manager로 관리하기로 했다.
떨어지는 아이템은 모두 튕겨나오고 회전하도록 만들었다.
그리고 아이템의 이미지가 회전을 하는데, 뿌려진 아이템을 보고 자세한 정보가 표시되는건 아니다.
그러니 아이템이 정보를 들고있더라도 아이템의 이미지만 표시되면 아무런 문제가 없다.
DropItemManager에서 모든 Item프리팹을 가질필요 없이, 하나의 Item프리팹만 있으면 된다.
이미지가 없는 프리팹에 PickUpItem을 컴포넌트로 넣기만 하면 쓸 수 있다.
이 프리팹에 이미지만 변경해서 떨어지는 아이템으로 바꿔버리면 된다.
DropItemManager
public class DropItemManager : Singleton<DropItemManager>
{
[SerializeField] private GameObject p_Item;
[SerializeField] private Transform droppableItems; //떨어진 아이템을 담아둘 변수
// 몬스터가 아이템을 떨어트림
public void DropItemOnMonster(List<MonsterDroppableItem> _mdi, Transform _transform)
{
for (int i = 0; i < _mdi.Count; i++)
{
MonsterDroppableItem droppableItem = _mdi[i];
for (int j = 0; j < droppableItem.maxDropCount; j++)
{
if (Random.Range(0, 100) < droppableItem.percentage)
{
DropItem(_transform, droppableItem.itemData);
}
}
}
}
// 아이템을 떨어트림
public void DropItem(Transform _transform, ItemData _itemData, int _amount = 1)
{
GameObject cloneItem = Instantiate(p_Item, _transform.position, _transform.rotation);
cloneItem.transform.SetParent(droppableItems);
PickUpItem pickUpItem = cloneItem.GetComponent<PickUpItem>();
pickUpItem.SetItem(_itemData, _amount);
}
}
아이템을 떨어트리는 DropItem은 프리팹을 instantiate로 객체로 만들고, 아이템정보를 넣어주면 끝이다.
부모설정을 해두는건 나중에 오브젝트풀으로 만들기위해서다.
몬스터가 아이템을 떨어트리는건 앞에 정했던 최대개수와 확률에 맞춰서 아이템을 떨어트리는 것이다.
DropItemOnMonster 메소드를 MonsterInfo에 GiveItem에서 사용하면 끝이다.
MonsterInfo
public void GiveItem()
{
DropItemManager.Instance.DropItemOnMonster(droppableItems, transform);
}
완성본
체력은 당연히 일부러 낮춰놨다.
몬스터가 죽으면서 아이템이 뿜어져나온다.
끝!
'유니티_일기 > 3D_RPG!' 카테고리의 다른 글
3D RPG 만들기! (8) 인벤토리 만들기 (0) | 2023.10.19 |
---|---|
3D RPG 만들기! (7) 싱글톤 (0) | 2023.08.18 |
3D RPG 만들기! (5) 공격 만들기 - 레이어마스크, 애니메이션 레이어 (0) | 2023.08.09 |
3D RPG 만들기! (4) 대화 구현하기 (0) | 2023.08.07 |
3D RPG 만들기! (3) 이동 구현하기 (0) | 2023.08.06 |
댓글