Humility

아무리 노력해도 최고가 되지 못할 수 있다⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀그럼에도 노력하는자가 가장 겸손한 것 아닌가

공부하는 블로그

유니티/기능구현

[Unity] 유니티 C#) 메트로배니아 미니맵과 전체맵 ( 심화편 )

새벽_글쓴이 2024. 12. 23. 23:49
반응형
이번엔 맵 이동과 이동했을때 맞춰 업데이트 되는 맵을 구현해보겠다

0. 준비

다른맵을 같은 형식으로 만들어 주었다 ( Terrain 타일맵과 미니맵로 사용할 타일맵 )

 

1. 플레이어

간단하게 캡슐로 만들어주고 리지드바디와 콜라이더를 붙여주었다

회전만 막아주었다

 

플레이어의 자식으로 빈게임오브젝트를 하나 만들자

Sprite Renderer만 추가하고 레이어를 미니맵으로 변경해주면

미니맵에서 플레이어의 위치를 표시해줄 수 있다

 

플레이어 부분 코드는 아래쪽에서 한꺼번에 작성하였습니다

 

2. 카메라

유니티 레지스트리에서 시네머신 다운로드

 

빈게임 오브젝트에 폴리곤 콜라이더로 시네머신이 돌아다닐 수 있는 범위 설정

폴리곤 콜라이더 컴포넌트의 Points 값을 조정하여 반듯한 모양을 만들 수 있다

 

버츄얼 카메라 생성 후 세팅

플레이어를 따라가야 하니 Follow에 플레이어를 지정해준다
그후 Confiner 라는 컴포넌트를 추가하여 카메라가 일정 범위를 벗어나지 않게 만든다

 

하지만 이 상태로 씬이 변경되면 카메라의 제한 범위를 잃어버리게 되므로 스크립트를 간단하게 만들었다

폴리곤 콜라이더로 만든 카메라 제한범위의 태그를 변경해줘야 함
using Cinemachine;
using UnityEngine;
using UnityEngine.SceneManagement;

public class CamRange : MonoBehaviour
{
    private CinemachineConfiner2D cineCam;

    private void OnEnable()
    {
        SceneManager.sceneLoaded += onSceneLoaded;
        UpdateCameraConfiner();
    }

    private void OnDisable()
    {
        SceneManager.sceneLoaded -= onSceneLoaded;
    }

    void onSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        UpdateCameraConfiner();
    }

    void UpdateCameraConfiner()
    {
        cineCam = GetComponent<CinemachineConfiner2D>();
        GameObject cameraRangeObj = GameObject.FindGameObjectWithTag("CameraRange");
        Collider2D cameraRange = cameraRangeObj.GetComponent<Collider2D>();
        cineCam.m_BoundingShape2D = cameraRange;
    }
}

 

3. 정리

몇 가지 요소가 추가되고 씬 이동을 대비하여 몇가지를 정리해보았다

 

하이어라키

 

Don't Destroy ( 빈게임오브젝트 )

빈게임 오브젝트에 이 스크립트를 부착하고

씬이 이동되도 파괴되지 말아야 될 요소들을 모두 자식으로 만들었다

using UnityEngine;

public class DontDestroy : MonoBehaviour
{
    public static DontDestroy instance;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(this);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

 

맵컨트롤러, 맵 매니저 → 플레이어 컨트롤러

최적화가 되어있다거나 깔끔한 코드는 아니지만 이러한 행동들을 한꺼번에 묶는 편이 적합하다 판단하였다

using UnityEngine;
using UnityEngine.UI;

public class PlayerController : MonoBehaviour
{
    // 플레이어 컨트롤러
    float moveSpeed = 7f;
    Rigidbody2D rigid;
    int jumpCount = 1;
    bool canUsePotal = false;

    // 맵 컨트롤러
    public RawImage minimapImage;
    public RawImage fullmapImage;

    private KeyCode toggleMapKey = KeyCode.M;

    public Camera mapCamera;
    private Vector2 mapSize;

    // 전체맵 줌인 줌아웃
    private float zoomSpeed = 3f;
    private float oriZoom = 5f;

    // 드래그
    private Vector3 dragOrigin;
    private bool isDragging = false;
    private float currentZoom;

    private void Awake()
    {
        minimapImage.gameObject.SetActive(true);
        fullmapImage.gameObject.SetActive(false);
    }

    void Start()
    {
        rigid = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        PlayerMove();
        MapKey();
    }

    void PlayerMove()
    {
        float moveDir = 0f;

        if (Input.GetKey(KeyCode.LeftArrow))
            moveDir = -1f;

        if(Input.GetKey(KeyCode.RightArrow))
            moveDir = 1f;

        if (Input.GetKey(KeyCode.UpArrow) && canUsePotal)
            Potal.Instance.Interact();

        if (Input.GetKey(KeyCode.Space) && jumpCount == 0)
        {
            rigid.velocity = Vector2.up * 7;
            jumpCount++;
        }

        if (rigid.velocity.y == 0f)
            jumpCount = 0;

        transform.position += Vector3.right * moveDir * moveSpeed * Time.deltaTime;
    }

    void MapKey()
    {
        if (Input.GetKeyDown(toggleMapKey))
            ToggleMap();

        if (fullmapImage.gameObject.activeSelf)
        {
            CalculateMapSize();
            HandleMapZoom();
            HandleMapDrag();
        }
        else
        {
            mapCamera.orthographicSize = oriZoom;
            currentZoom = oriZoom;
            mapCamera.transform.position = Camera.main.transform.position;
        }
    }

    private void ToggleMap()
    {
        bool isMiniMapActive = minimapImage.gameObject.activeSelf;

        minimapImage.gameObject.SetActive(!isMiniMapActive);
        fullmapImage.gameObject.SetActive(isMiniMapActive);
    }

    // 맵 크기 계산
    private void CalculateMapSize()
    {
        // Raw Image의 실제 크기 계산
        Rect rect = fullmapImage.rectTransform.rect;
        Vector2 size = new Vector2(rect.width, rect.height);
        Vector2 worldSize = size / fullmapImage.canvas.scaleFactor;
        mapSize = worldSize / 2f;
    }


    // 맵 줌
    private void HandleMapZoom()
    {
        // 마우스 휠 값
        float scrollDelta = Input.mouseScrollDelta.y;
        if (scrollDelta != 0)
        {
            if (currentZoom < oriZoom)
            {
                currentZoom = oriZoom;
            }
            currentZoom -= scrollDelta * zoomSpeed;
            mapCamera.orthographicSize = currentZoom;
        }
    }

    // 맵 드래그
    private void HandleMapDrag()
    {
        if (Input.GetMouseButtonDown(0))
        {
            isDragging = true;
            dragOrigin = mapCamera.ScreenToWorldPoint(Input.mousePosition);
        }

        if (Input.GetMouseButtonUp(0))
        {
            isDragging = false;
        }

        if (isDragging)
        {
            Vector3 difference = dragOrigin - mapCamera.ScreenToWorldPoint(Input.mousePosition);
            mapCamera.transform.position += difference;
        }
    }

    // 포탈에 닿았을때
    private void OnTriggerEnter2D(Collider2D collision)
    {
        canUsePotal = true;
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        canUsePotal= false;
    }

}

 

맵 컨테이너와 미니맵으로 사용하는 타일맵

맵 매니저 스크립트를 부착함

 

미니맵에는 맵 데이터를 부착함

 

맵 매니저

using UnityEngine;
using UnityEngine.SceneManagement;

public class MapManager : MonoBehaviour
{
    public static MapManager instance;

    private MapData[] mapDatas;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }

        mapDatas = GetComponentsInChildren<MapData>(true);
    }

    public void RevealRoom()
    {
        string newLoadedScene = SceneManager.GetActiveScene().name;

        for (int i = 0; i < mapDatas.Length; i++)
        {
            if (mapDatas[i].roomScene.SceneName == newLoadedScene && !mapDatas[i].HasBeenRevealed)
            {
                mapDatas[i].gameObject.SetActive(true);
                mapDatas[i].HasBeenRevealed = true;

                return;
            }
        }
    }
}

 

맵 데이터

using UnityEngine;

public class MapData : MonoBehaviour
{
    public SceneField roomScene;
    public bool HasBeenRevealed { get; set; }
}

 

미니맵마다 데이터를 만들고 해당하는 맵으로 이동하면 true값을 받아
미니맵이 켜지는 방법으로 만들었다

 

4. 포탈

 

빈게임오브젝트를 만들어 박스콜라이더를 붙여주었다

이것은 마찬가지로 다른 맵에도 만들어 주어야 한다

 

포탈 스크립트

스크립트 내용

각 포탈마다 이동할 씬, 이동할 포탈 번호, 현재포탈번호를 지정해준다

씬을 이동하면 해당하는 씬과 포탈을 찾고

플레이어를 해당하는 포탈로 이동

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Potal : MonoBehaviour
{
    public static Potal Instance;

    public enum Door
    {
        None,
        One,
        Two,
    }

    private GameObject player;
    private Vector3 playerSpawnPosition;
    private Collider2D doorCollider;
    private Collider2D playerCollider;

    [SerializeField] private SceneField toLoad;
    [SerializeField] private Door toSpawnDoor;

    public Door currentDoor;

    private void Awake()
    {
        if(Instance == null)
        {
            Instance = this;
        }

        player = GameObject.FindGameObjectWithTag("Player");
        playerCollider = player.GetComponent<Collider2D>();
    }

    private void OnEnable()
    {
        SceneManager.sceneLoaded += OnSceneLoded;
    }

    private void OnDisable()
    {
        SceneManager.sceneLoaded -= OnSceneLoded;
    }

    public void Interact()
    {
        SwapScene(toLoad, toSpawnDoor);
    }

    public static void SwapScene(SceneField toScene, Potal.Door toPotal)
    {
        Instance.StartCoroutine(Instance.ChangeScene(toScene, toPotal));
    }

    private IEnumerator ChangeScene(SceneField toScene, Potal.Door toPotal = Potal.Door.None)
    {
        yield return null;
        SceneManager.LoadScene(toScene);
    }

    private void OnSceneLoded(Scene scene, LoadSceneMode mode)
    {
        FindDoor(toSpawnDoor);
        player.transform.position = playerSpawnPosition;
        MapManager.instance.RevealRoom();
    }

    private void FindDoor(Door potalNum)
    {
        Potal[] potals = FindObjectsOfType<Potal>();

        for (int i = 0; i < potals.Length; i++)
        {
            if (potals[i].currentDoor == potalNum)
            {
                doorCollider = potals[i].gameObject.GetComponent<Collider2D>();
                CalculateSpawnPosition();
                return;
            }
        }
    }

    private void CalculateSpawnPosition()
    {
        float colliderHeight = playerCollider.bounds.extents.y;
        playerSpawnPosition = doorCollider.transform.position - new Vector3(0f, colliderHeight, 0f);
    }
}
이동 중간에 페이드인과 페이드 아웃 효과를 추가하면 더욱 훌륭한 연출이 될 것이다

 

SceneField

원래는 string으로 씬을 찾아야 했으나, 간편한 코드를 발견하여 이것으로 대체하였다

using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

[System.Serializable]
public class SceneField
{
    [SerializeField]
    private Object m_SceneAsset;

    [SerializeField]
    private string m_SceneName = "";
    public string SceneName
    {
        get { return m_SceneName; }
    }

    // makes it work with the existing Unity methods (LoadLevel/LoadScene)
    public static implicit operator string(SceneField sceneField)
    {
        return sceneField.SceneName;
    }
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(SceneField))]
public class SceneFieldPropertyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect _position, SerializedProperty _property, GUIContent _label)
    {
        EditorGUI.BeginProperty(_position, GUIContent.none, _property);
        SerializedProperty sceneAsset = _property.FindPropertyRelative("m_SceneAsset");
        SerializedProperty sceneName = _property.FindPropertyRelative("m_SceneName");
        _position = EditorGUI.PrefixLabel(_position, GUIUtility.GetControlID(FocusType.Passive), _label);
        if (sceneAsset != null)
        {
            sceneAsset.objectReferenceValue = EditorGUI.ObjectField(_position, sceneAsset.objectReferenceValue, typeof(SceneAsset), false);

            if (sceneAsset.objectReferenceValue != null)
            {
                sceneName.stringValue = (sceneAsset.objectReferenceValue as SceneAsset).name;
            }
        }
        EditorGUI.EndProperty();
    }
}
#endif

 

5. 완성

반응형