Development/Unity Engine

[Retro유니티] 아이템 생성기 및 Item 구현

사이바 미도리 2024. 12. 29. 14:28
  • 주기적으로 아이템 생성
  • 플레이어 근처 내비메시 위에서 랜덤한 한 점에 아이템 생성

 

EnemySpawner와 유사한 구조로 구현한다.

다만, NavMesh 위의 랜덤한 점을 선택하는 기능은 구현난이도가 높으니 ItemSpawner 스크립트에 미리 완성된 것을 사용하기로 한다.

 

생성할 Item Prefab을 GameObject 배열에 저장한다.

    public GameObject[] items; // 생성할 아이템들
    public Transform playerTransform; // 플레이어의 트랜스폼(위치)

 

private void Start() {
        // 생성 간격과 마지막 생성 시점 초기화
        timeBetSpawn = Random.Range(timeBetSpawnMin, timeBetSpawnMax);
        lastSpawnTime = 0;
    }

가장 먼저 실행되는 메서드이다.

생성간격을 최솟값~최댓값 사이에서 랜덤설정하고, 마지막 생성시점을 0으로 초기화한다.

 

Update 함수는 매 frame마다 현 시점이 Item 생성을 처리하는 Spawn을 실행할수있는지 체크하고, 가능하다면 Spawn을 실행한다.

    // 주기적으로 아이템 생성 처리 실행
    private void Update() {
        // 현재 시점이 마지막 생성 시점에서 생성 주기 이상 지남
        // && 플레이어 캐릭터가 존재함
        if (Time.time >= lastSpawnTime + timeBetSpawn && playerTransform != null)
        {
            // 마지막 생성 시간 갱신
            lastSpawnTime = Time.time;
            // 생성 주기를 랜덤으로 변경
            timeBetSpawn = Random.Range(timeBetSpawnMin, timeBetSpawnMax);
            // 아이템 생성 실행
            Spawn();
        }
    }

 

이 스크립트의 핵심함수는 Spawn()과 GetRandomPointOnNavMesh() 이다.

먼저 Spawn을 보자.

    // 실제 아이템 생성 처리
    private void Spawn() {
        // 플레이어 근처에서 내비메시 위의 랜덤 위치 가져오기
        Vector3 spawnPosition =
            GetRandomPointOnNavMesh(playerTransform.position, maxDistance);
        // 바닥에서 0.5만큼 위로 올리기
        spawnPosition += Vector3.up * 0.5f;

        // 아이템 중 하나를 무작위로 골라 랜덤 위치에 생성
        GameObject selectedItem = items[Random.Range(0, items.Length)];
        GameObject item = Instantiate(selectedItem, spawnPosition, Quaternion.identity);

        // 생성된 아이템을 5초 뒤에 파괴
        Destroy(item, 5f);
    }

 

아이템이 바닥에 딱 붙지 않도록 z축으로 0.5만큼 늘렸다.

생성될 item종류를 prefab 배열에서 랜덤선택하고, Instatiate 메서드로 해당 프리팹의 복제본을 생성한다.

그 후 Destroy로 파괴한다. Destroty의 2번재 인수는 수명이다.

 

주위의 랜덤한 위치에서 아이템이 생성되는 스크립트인 GetRandomPointOnNavMesh()이다.

    // 내비메시 위의 랜덤한 위치를 반환하는 메서드
    // center를 중심으로 distance 반경 안에서 랜덤한 위치를 찾는다
    private Vector3 GetRandomPointOnNavMesh(Vector3 center, float distance) {
        // center를 중심으로 반지름이 maxDistance인 구 안에서의 랜덤한 위치 하나를 저장
        // Random.insideUnitSphere는 반지름이 1인 구 안에서의 랜덤한 한 점을 반환하는 프로퍼티
        Vector3 randomPos = Random.insideUnitSphere * distance + center;

        // 내비메시 샘플링의 결과 정보를 저장하는 변수
        NavMeshHit hit;

        // maxDistance 반경 안에서, randomPos에 가장 가까운 내비메시 위의 한 점을 찾음
        NavMesh.SamplePosition(randomPos, out hit, distance, NavMesh.AllAreas);

        // 찾은 점 반환
        return hit.position;
    }

입력된 center를 중심으로 distance 반경 내이면서, NavMesh 위인 한 점을 찾아서 반환한다.

 

randomPos와 가장 가까운 내비메시 위의 한 점을 찾기 위해, NavMesh 샘플링이 필요하다.

NavMesh 샘플링의 실행결과는 RayCast처럼 별개의 정보저장용 변수에 할당된다.

해당 내비메시 샘플링 정보를 저장할 NavMeshhit 타입변수인 hit을 선언하고 할당했다.

그 후, 내비메시 위의 모든 영역(NavMesh.AllAreas)에 대해 randomPos에 가장 가까운 한 점을 찾는 샘플링은 아래 코드로 실행한다.

NavMesh.SamplePosition(randomPos, out hit, distance, NavMesh.AllAreas);

그 후, hit.position을 리턴한다.

 

ItemSpawner.cs 완성

using UnityEngine;
using UnityEngine.AI; // 내비메쉬 관련 코드

// 주기적으로 아이템을 플레이어 근처에 생성하는 스크립트
public class ItemSpawner : MonoBehaviour {
    public GameObject[] items; // 생성할 아이템들
    public Transform playerTransform; // 플레이어의 트랜스폼(위치)

    public float maxDistance = 5f; // 플레이어 위치로부터 아이템이 배치될 최대 반경

    public float timeBetSpawnMax = 7f; // 최대 시간 간격
    public float timeBetSpawnMin = 2f; // 최소 시간 간격
    private float timeBetSpawn; // 생성 간격

    private float lastSpawnTime; // 마지막 생성 시점

    private void Start() {
        // 생성 간격과 마지막 생성 시점 초기화
        timeBetSpawn = Random.Range(timeBetSpawnMin, timeBetSpawnMax);
        lastSpawnTime = 0;
    }

    // 주기적으로 아이템 생성 처리 실행
    private void Update() {
        // 현재 시점이 마지막 생성 시점에서 생성 주기 이상 지남
        // && 플레이어 캐릭터가 존재함
        if (Time.time >= lastSpawnTime + timeBetSpawn && playerTransform != null)
        {
            // 마지막 생성 시간 갱신
            lastSpawnTime = Time.time;
            // 생성 주기를 랜덤으로 변경
            timeBetSpawn = Random.Range(timeBetSpawnMin, timeBetSpawnMax);
            // 아이템 생성 실행
            Spawn();
        }
    }

    // 실제 아이템 생성 처리
    private void Spawn() {
        // 플레이어 근처에서 내비메시 위의 랜덤 위치 가져오기
        Vector3 spawnPosition =
            GetRandomPointOnNavMesh(playerTransform.position, maxDistance);
        // 바닥에서 0.5만큼 위로 올리기
        spawnPosition += Vector3.up * 0.5f;

        // 아이템 중 하나를 무작위로 골라 랜덤 위치에 생성
        GameObject selectedItem = items[Random.Range(0, items.Length)];
        GameObject item = Instantiate(selectedItem, spawnPosition, Quaternion.identity);

        // 생성된 아이템을 5초 뒤에 파괴
        Destroy(item, 5f);
    }

    // 내비메시 위의 랜덤한 위치를 반환하는 메서드
    // center를 중심으로 distance 반경 안에서 랜덤한 위치를 찾는다
    private Vector3 GetRandomPointOnNavMesh(Vector3 center, float distance) {
        // center를 중심으로 반지름이 maxDistance인 구 안에서의 랜덤한 위치 하나를 저장
        // Random.insideUnitSphere는 반지름이 1인 구 안에서의 랜덤한 한 점을 반환하는 프로퍼티
        Vector3 randomPos = Random.insideUnitSphere * distance + center;

        // 내비메시 샘플링의 결과 정보를 저장하는 변수
        NavMeshHit hit;

        // maxDistance 반경 안에서, randomPos에 가장 가까운 내비메시 위의 한 점을 찾음
        NavMesh.SamplePosition(randomPos, out hit, distance, NavMesh.AllAreas);

        // 찾은 점 반환
        return hit.position;
    }
}

 

 

ItemSpawner 컴포넌트 설정

 

각 Item들 뜯어보기

각 item prefab 은 공통적으로 아래 컴포넌트를 가진다.

  • 트리거설정된 Sphere Collider
  • Light
  • Rotator Script

Rotate 스크립트

using UnityEngine;

// 게임 오브젝트를 지속적으로 회전하는 스크립트
public class Rotator : MonoBehaviour {
    public float rotationSpeed = 60f;

    private void Update() {
        transform.Rotate(0f, rotationSpeed * Time.deltaTime, 0f);
    }
}

이 함수는 아무 반환형이 없는데, 어떻게 다른 item들이 회전할때 영향을 미치게되는건지 궁금했는데,

 

// 아이템 타입들이 반드시 구현해야하는 인터페이스
public interface IItem {
    // 입력으로 받는 target은 아이템 효과가 적용될 대상
    void Use(GameObject target);
}

별거없다.

Coin 스크립트

Coin Prefab은 Coin.cs와 Rotate.cs를 가진다.

Coin.cs는 아래와 같은데, 설령 Rotate.cs와 동일한 Update() 함수를 Coin.cs가 가진다고 하더라도, 충돌나지 않고 각자의 행동을 매 frame마다 수행한다고 한다.

using UnityEngine;

// 게임 점수를 증가시키는 아이템
public class Coin : MonoBehaviour, IItem {
    public int score = 200; // 증가할 점수

    public void Use(GameObject target) {
        // 게임 매니저로 접근해 점수 추가
        GameManager.instance.AddScore(score);
        // 사용되었으므로, 자신을 파괴
        Destroy(gameObject);
    }
}

AmmoPack 스크립트

using UnityEngine;

// 총알을 충전하는 아이템
public class AmmoPack : MonoBehaviour, IItem {
    public int ammo = 30; // 충전할 총알 수

    public void Use(GameObject target) {
        // 전달 받은 게임 오브젝트로부터 PlayerShooter 컴포넌트를 가져오기 시도
        PlayerShooter playerShooter = target.GetComponent<PlayerShooter>();

        // PlayerShooter 컴포넌트가 있으며, 총 오브젝트가 존재하면
        if (playerShooter != null && playerShooter.gun != null)
        {
            // 총의 남은 탄환 수를 ammo 만큼 더한다
            playerShooter.gun.ammoRemain += ammo;
        }

        // 사용되었으므로, 자신을 파괴
        Destroy(gameObject);
    }
}

HealthPack 스크립트

using UnityEngine;

// 체력을 회복하는 아이템
public class HealthPack : MonoBehaviour, IItem {
    public float health = 50; // 체력을 회복할 수치

    public void Use(GameObject target) {
        // 전달받은 게임 오브젝트로부터 LivingEntity 컴포넌트 가져오기 시도
        LivingEntity life = target.GetComponent<LivingEntity>();

        // LivingEntity컴포넌트가 있다면
        if (life != null)
        {
            // 체력 회복 실행
            life.RestoreHealth(health);
        }

        // 사용되었으므로, 자신을 파괴
        Destroy(gameObject);
    }

    private void Update() {
        // HealthPack의 Update 로직
        Debug.Log("HealthPack Update");
    }
}

테스트용으로 Update 함수를 넣어보았다. Rotate.cs의 Update() 함수와 충돌하지 않는다.

 

아이템이 스폰되는걸 확인할수있다.