프로젝트/프로젝트 A

# 023 Object Icon (with. Minimap Drag & Click)

효따 2025. 8. 29. 02:47

안녕하세요.😊

 

오늘은 크게 3가지를 작업하였습니다.

 

1. Enemy 관리.

2. 오브젝트 요소 아이콘 배치

3. 미니맵 드래그 및 클릭 이동

 

 

나름 어제보단 훨씬 볼만해진 것 같습니다.😖


 

우선 아이콘 배치는

 

넥서스와 터렛은 고정이기에, 미니맵 이미지 위에 배치하여 고정을 해주었습니다.

 

 


 

다음으로 Enemy 관리입니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyManager : MonoBehaviour
{
    public static EnemyManager Instance;

    public List<GameObject> minions = new List<GameObject>();

    void Awake()
    {
        Instance = this;
    }

    public void RegisterMinion(GameObject minion)
    {
        if (!minions.Contains(minion))
            minions.Add(minion);
    }

    public void UnregisterMinion(GameObject minion)
    {
        if (minions.Contains(minion))
            minions.Remove(minion);
    }

    public List<GameObject> GetMinions()
    {
        return minions;
    }
}

 

EnemyManager의 스크립트로 모든 Enemy들을 List로 관리해 주었습니다.

생성 : RegisterMinion

삭제 : UnregisterMinion

탐색 : GetMinions

 

현재 포탑이나 Minion  의 태그를 찾는 방식은 FindGameObjectWithTag를 사용하여

 

유니티 내 모든 오브젝트를 검사하여 Tag를 찾는데

 

이를 이용하여 쉽게 해당 Minion 들의 Tag만 검색해서 찾아 최적화를 하기 위함입니다.

 

    private void SpawnEnemy(GameObject prefab, float speed)
    {
        if (prefab == null || spawnPoints.Length == 0) return;

        Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
        GameObject enemy = Instantiate(prefab, spawnPoint.position, spawnPoint.rotation);
        EnemyManager.Instance.RegisterMinion(enemy.gameObject);

        NavMeshAgent agent = enemy.GetComponent<NavMeshAgent>();
        if (agent != null)
        {
            agent.speed = speed;
        }
        else
        {
            Debug.Log($"Nav check Go");
        }
    }

Enemy가 생성될 때

 

    public void TakeDamage(float damage)
    {
        enumyGoldResult = Random.Range(10, 20);
        enumyHp -= damage;
        enumyHpBar.HpVarUpdate(enumyHp, enumyMaxHp);
        if (enumyHp <= 0)
        {
            playerExpBar.ExpBarUpdate(enumyExpResult);
            getGoldText.GetGold(enumyGoldResult.ToString());
            EnemyManager.Instance.UnregisterMinion(this.gameObject);
            Destroy(this.gameObject);
        }
    }

 Enemy가 죽어서 삭제될 때


 

다음은 MinimapController에서 추가된 작업인

 

전체 Enemy들을 따라다닐 아이콘과

 

카메라가 현재 보고있는곳을 알리기 위해 따라다닐 이미지 관련 작업입니다.

 

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MinimapController : MonoBehaviour
{
    [SerializeField] private NavMeshAgent playerNav;
    [SerializeField] private Transform playerTransform;
    [SerializeField] private RectTransform minimapRawImage;
    [SerializeField] private RectTransform playerIcon;
    [SerializeField] private RectTransform pointIcon;
    [SerializeField] private GameObject ground;
    [SerializeField] private RectTransform cameraIcon;
    [SerializeField] private Camera mainCamera;
    private float worldMinX = 0f;
    private float worldMaxX = 0f;
    private float worldMinZ = 0f;
    private float worldMaxZ = 0f;

    [SerializeField] private GameObject minionIcon;
    [SerializeField] private GameObject enemyMinionIcon;


    private Dictionary<GameObject, RectTransform> minionIconDict = new Dictionary<GameObject, RectTransform>();


    void Start()
    {
        float groundSizeX = 10f * ground.transform.localScale.x;
        float groundSizeZ = 10f * ground.transform.localScale.z;

        worldMinX = -groundSizeX / 2f;
        worldMaxX = groundSizeX / 2f;
        worldMinZ = -groundSizeZ / 2f;
        worldMaxZ = groundSizeZ / 2f;
    }

    void Update()
    {

        float xNorm = Mathf.InverseLerp(worldMinX, worldMaxX, playerTransform.position.x);
        float zNorm = Mathf.InverseLerp(worldMinZ, worldMaxZ, playerTransform.position.z);

        float uiX = (xNorm - 0.5f) * minimapRawImage.rect.width;
        float uiZ = (zNorm - 0.5f) * minimapRawImage.rect.height;

        playerIcon.anchoredPosition = new Vector2(uiX, uiZ);

        if (pointIcon.gameObject.activeSelf)
        {
            if (!playerNav.pathPending && playerNav.remainingDistance <= playerNav.stoppingDistance)
            {
                pointIcon.gameObject.SetActive(false);
            }
        }

        if (Input.GetMouseButtonDown(1))
        {
            Vector2 localPoint;
            if (RectTransformUtility.ScreenPointToLocalPointInRectangle(minimapRawImage, Input.mousePosition, null, out localPoint))
            {
                if (localPoint.x >= -minimapRawImage.rect.width / 2f && localPoint.x <= minimapRawImage.rect.width / 2f &&
                    localPoint.y >= -minimapRawImage.rect.height / 2f && localPoint.y <= minimapRawImage.rect.height / 2f)
                {
                    float clickXNorm = (localPoint.x / minimapRawImage.rect.width) + 0.5f;
                    float clickZNorm = (localPoint.y / minimapRawImage.rect.height) + 0.5f;

                    float targetX = Mathf.Lerp(worldMinX, worldMaxX, clickXNorm);
                    float targetZ = Mathf.Lerp(worldMinZ, worldMaxZ, clickZNorm);

                    playerNav.isStopped = true;
                    playerNav.ResetPath();
                    playerNav.SetDestination(new Vector3(targetX, playerTransform.position.y, targetZ));

                    pointIcon.gameObject.SetActive(true);
                    pointIcon.anchoredPosition = new Vector2(localPoint.x, localPoint.y);
                }
            }
        }
        UpdateMinions();
    }

    void UpdateMinions()
    {
        UpdateCamImage();
        List<GameObject> minions = EnemyManager.Instance.minions;

        foreach (GameObject minion in minions)
        {
            if (minion == null) continue;

            if (!minionIconDict.ContainsKey(minion))
            {
                GameObject prefab = null;
                if (minion.CompareTag("EnemyMinion")) prefab = minionIcon;
                else if (minion.CompareTag("Enumy")) prefab = enemyMinionIcon;

                if (prefab != null)
                {
                    GameObject icon = Instantiate(prefab, minimapRawImage);
                    RectTransform rt = icon.GetComponent<RectTransform>();
                    minionIconDict[minion] = rt;
                }
            }

            if (minionIconDict.TryGetValue(minion, out RectTransform iconRect))
            {
                float xNorm = Mathf.InverseLerp(worldMinX, worldMaxX, minion.transform.position.x);
                float zNorm = Mathf.InverseLerp(worldMinZ, worldMaxZ, minion.transform.position.z);

                float uiX = (xNorm - 0.5f) * minimapRawImage.rect.width;
                float uiZ = (zNorm - 0.5f) * minimapRawImage.rect.height;

                iconRect.anchoredPosition = new Vector2(uiX, uiZ);
            }
        }

        List<GameObject> toRemove = new List<GameObject>();
        foreach (var kvp in minionIconDict)
        {
            if (!minions.Contains(kvp.Key) || kvp.Key == null)
            {
                Destroy(kvp.Value.gameObject);
                toRemove.Add(kvp.Key);
            }
        }
        foreach (var m in toRemove) minionIconDict.Remove(m);
    }
    private void UpdateCamImage()
    {
        if (cameraIcon != null && mainCamera != null)
        {
            float camXNorm = Mathf.InverseLerp(worldMinX, worldMaxX, mainCamera.transform.position.x);
            float camZNorm = Mathf.InverseLerp(worldMinZ, worldMaxZ, mainCamera.transform.position.z);

            float camUIX = (camXNorm - 0.5f) * minimapRawImage.rect.width;
            float camUIZ = (camZNorm - 0.5f) * minimapRawImage.rect.height;

            cameraIcon.anchoredPosition = new Vector2(camUIX, camUIZ);
        }
    }
}

 


 

 

1. 변수 선언부

    [SerializeField] private NavMeshAgent playerNav;
    [SerializeField] private Transform playerTransform;
    [SerializeField] private RectTransform minimapRawImage;
    [SerializeField] private RectTransform playerIcon;
    [SerializeField] private RectTransform pointIcon;
    [SerializeField] private GameObject ground;
    [SerializeField] private RectTransform cameraIcon;
    [SerializeField] private Camera mainCamera;
    private float worldMinX = 0f;
    private float worldMaxX = 0f;
    private float worldMinZ = 0f;
    private float worldMaxZ = 0f;

    [SerializeField] private GameObject minionIcon;
    [SerializeField] private GameObject enemyMinionIcon;


    private Dictionary<GameObject, RectTransform> minionIconDict = new Dictionary<GameObject, RectTransform>();

 

playerNav : 플레이어 이동 제어용 NavMeshAgent

playerTransform : 플레이어 위치 참조

minimapRawImage : 미니맵 UI

playerIcon : 미니맵 상의 플레이어 아이콘

pointIcon : 플레이어 목적지 아이콘

ground : 월드 지형 참조

cameraIcon : 미니맵 상의 카메라 위치 아이콘

mainCamera : 메인 카메라

minionIcon : 일반 미니언 아이콘 Prefab

enemyMinionIcon : 적 미니언 아이콘 Prefab

worldMinX/Z, wordMaxX/Z : 월드 좌표 최소/최대 값, 미니맵 좌표

minionIconDict : 미니언 오브젝트와 아이콘 매핑

 


 

 

2. Start() 월드 범위 초기화

    void Start()
    {
        float groundSizeX = 10f * ground.transform.localScale.x;
        float groundSizeZ = 10f * ground.transform.localScale.z;

        worldMinX = -groundSizeX / 2f;
        worldMaxX = groundSizeX / 2f;
        worldMinZ = -groundSizeZ / 2f;
        worldMaxZ = groundSizeZ / 2f;
    }

 

 

ground의 scale을 기반으로 월드 좌표 범위를 계산

미니맵 좌표 변환 시 월드 → 0~1 정규화에 사용

 

 

 


 

 

3. Update() : UI 갱신 및 클릭 이동

맵 배치 및 카메라 각도 조정

    void Update()
    {

        float xNorm = Mathf.InverseLerp(worldMinX, worldMaxX, playerTransform.position.x);
        float zNorm = Mathf.InverseLerp(worldMinZ, worldMaxZ, playerTransform.position.z);

        float uiX = (xNorm - 0.5f) * minimapRawImage.rect.width;
        float uiZ = (zNorm - 0.5f) * minimapRawImage.rect.height;

        playerIcon.anchoredPosition = new Vector2(uiX, uiZ);

 

월드 좌표 > 0~1 정규화 > UI 좌표 변환

(xNorm - 0.5f)는 UI 중심을 (0,0)으로 맞추기 위함


        if (pointIcon.gameObject.activeSelf)
        {
            if (!playerNav.pathPending && playerNav.remainingDistance <= playerNav.stoppingDistance)
            {
                pointIcon.gameObject.SetActive(false);
            }
        }

 

플레이어가 목적지에 도착하면 pointIcon 비활성화


        if (Input.GetMouseButtonDown(1))
        {
            Vector2 localPoint;
            if (RectTransformUtility.ScreenPointToLocalPointInRectangle(minimapRawImage, Input.mousePosition, null, out localPoint))
            {
                if (localPoint.x >= -minimapRawImage.rect.width / 2f && localPoint.x <= minimapRawImage.rect.width / 2f &&
                    localPoint.y >= -minimapRawImage.rect.height / 2f && localPoint.y <= minimapRawImage.rect.height / 2f)
                {
                    float clickXNorm = (localPoint.x / minimapRawImage.rect.width) + 0.5f;
                    float clickZNorm = (localPoint.y / minimapRawImage.rect.height) + 0.5f;

                    float targetX = Mathf.Lerp(worldMinX, worldMaxX, clickXNorm);
                    float targetZ = Mathf.Lerp(worldMinZ, worldMaxZ, clickZNorm);

                    playerNav.isStopped = true;
                    playerNav.ResetPath();
                    playerNav.SetDestination(new Vector3(targetX, playerTransform.position.y, targetZ));

                    pointIcon.gameObject.SetActive(true);
                    pointIcon.anchoredPosition = new Vector2(localPoint.x, localPoint.y);
                }
            }
        }

 

오른쪽(1) 클릭 시

1. 클릭 위치를 미니맵 로컬 좌표로 변환

2. 좌표 정규화 > 월드 좌표로 변환

3. NavMeshAgent를 사용해 플레이어 이동

4. pointIcon을 클릭 위치에 표시


        UpdateMinions();

 

미니언 및 카메라 아이콘 갱신 함수 호출

 


 

    void UpdateMinions()
    {
        UpdateCamImage();
        List<GameObject> minions = EnemyManager.Instance.minions;

 

EnemyManager.Instance.minions는 현재 활성 미니언 리스트


 

    void UpdateMinions()
    {
        UpdateCamImage();
        List<GameObject> minions = EnemyManager.Instance.minions;

        foreach (GameObject minion in minions)
        {
            if (minion == null) continue;

            if (!minionIconDict.ContainsKey(minion))
            {
                GameObject prefab = null;
                if (minion.CompareTag("EnemyMinion")) prefab = minionIcon;
                else if (minion.CompareTag("Enumy")) prefab = enemyMinionIcon;

                if (prefab != null)
                {
                    GameObject icon = Instantiate(prefab, minimapRawImage);
                    RectTransform rt = icon.GetComponent<RectTransform>();
                    minionIconDict[minion] = rt;
                }
            }

            if (minionIconDict.TryGetValue(minion, out RectTransform iconRect))
            {
                float xNorm = Mathf.InverseLerp(worldMinX, worldMaxX, minion.transform.position.x);
                float zNorm = Mathf.InverseLerp(worldMinZ, worldMaxZ, minion.transform.position.z);

                float uiX = (xNorm - 0.5f) * minimapRawImage.rect.width;
                float uiZ = (zNorm - 0.5f) * minimapRawImage.rect.height;

                iconRect.anchoredPosition = new Vector2(uiX, uiZ);
            }
        }

 

 

UpdateCamImage() 호출

미니언 위치 갱신 전에 카메라 아이콘을 먼저 갱신합니다.

순서가 중요한 이유: 미니언과 카메라 위치가 동시에 최신화되어야 UI가 일관됨.

 

EnemyManager.Instance.minions

씬에서 활성화된 미니언들의 리스트를 가져옵니다.

유연성: 나중에 미니언이 추가/삭제되더라도 동적으로 반영 가능

 

if (minion == null) continue

이미 삭제된 미니언이 리스트에 있을 수 있으므로 안전하게 무시

 

아이콘 생성

처음 등장한 미니언에는 아이콘을 Instantiate로 생성

생성 위치는 미니맵 UI(minimapRawImage)의 자식으로 설정

**딕셔너리 minionIconDict**에 미니언-아이콘 매핑을 저장 → 다음 업데이트에서 위치 갱신 가능

 

위치 갱신

월드 좌표 → 정규화(0~1) → UI 좌표

(xNorm - 0.5f) * width는 미니맵 중심을 (0,0)으로 맞추기 위한 변환

매 프레임마다 아이콘 위치를 갱신하여 실시간 미니맵 반영

 


 

        List<GameObject> toRemove = new List<GameObject>();
        foreach (var kvp in minionIconDict)
        {
            if (!minions.Contains(kvp.Key) || kvp.Key == null)
            {
                Destroy(kvp.Value.gameObject);
                toRemove.Add(kvp.Key);
            }
        }
        foreach (var m in toRemove) minionIconDict.Remove(m);

 

삭제된 미니언 아이콘 제거

씬에서 사라진 미니언 아이콘은 Destroy()

딕셔너리에서도 제거 → 메모리 누수 방지

 

 


 

    private void UpdateCamImage()
    {
        if (cameraIcon != null && mainCamera != null)
        {
            float camXNorm = Mathf.InverseLerp(worldMinX, worldMaxX, mainCamera.transform.position.x);
            float camZNorm = Mathf.InverseLerp(worldMinZ, worldMaxZ, mainCamera.transform.position.z);

            float camUIX = (camXNorm - 0.5f) * minimapRawImage.rect.width;
            float camUIZ = (camZNorm - 0.5f) * minimapRawImage.rect.height;

            cameraIcon.anchoredPosition = new Vector2(camUIX, camUIZ);
        }
    }

 

 

null 체크

cameraIcon 또는 mainCamera가 없으면 갱신하지 않음 → NullReferenceError 방지

 

월드 좌표 → 정규화

Mathf.InverseLerp(worldMinX, worldMaxX, mainCamera.transform.position.x)
→ 월드 X 좌표를 0~1 사이 값으로 변환

Z 좌표도 동일

 

UI 좌표 변환

(camXNorm - 0.5f) * minimapRawImage.rect.width
→ 미니맵 중심 (0,0) 기준 좌표로 변환

X, Z 각각 변환 후 cameraIcon.anchoredPosition에 적용

 

결과

미니맵 상에서 카메라가 항상 플레이어 주변 위치를 실시간으로 표시

미니언과 동일한 좌표 변환 방식 사용 → UI 일관성 확보

 

고정적인 오브젝트는 이미지에 아이콘 배치

 

 

딕셔너리를 사용하여 매 프레임마다 미니언 아이콘을 생성하지 않고

기존 아이콘과 미니언이 매핑되어 쉽게 제어 (Follow & Remove)가 가능했습니다.


 

다음은 미니맵을 클릭 시 카메라의 이동을 제어하기 위해

 MinimapDragCamera 스크립트를 생성했습니다.

 

using UnityEngine;
using UnityEngine.EventSystems;
using Cinemachine;

public class MiniMapDragCamera : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
    [SerializeField] private RectTransform minimapRect;
    [SerializeField] private CinemachineVirtualCamera vcam;
    [SerializeField] private Transform playerTransform;
    [SerializeField] private float dragSpeed = 0.05f;
    [SerializeField] private Transform ground;
    private Vector2 lastPointerPos;
    private bool dragging = false;
    private Transform originalFollow;
    private CinemachineTransposer transposer;

    void Start()
    {
        if (vcam == null)
        {
            Debug.LogError("카메라 없음");
            return;
        }

        originalFollow = vcam.Follow;
        transposer = vcam.GetCinemachineComponent<CinemachineTransposer>();
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        if (RectTransformUtility.RectangleContainsScreenPoint(minimapRect, eventData.position, eventData.pressEventCamera))
        {
            RectTransformUtility.ScreenPointToLocalPointInRectangle(minimapRect, eventData.position, eventData.pressEventCamera, out lastPointerPos);
            dragging = true;

            if (vcam != null)
            {
                vcam.Follow = null;
                vcam.gameObject.SetActive(true);
            }

            MoveCameraToClickPosition(eventData);
        }
    }
    private void MoveCameraToClickPosition(PointerEventData eventData)
    {
        Vector2 localPoint;
        Camera cam = eventData.pressEventCamera;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(minimapRect, eventData.position, cam, out localPoint);

        float clickXNorm = (localPoint.x + minimapRect.rect.width / 2f) / minimapRect.rect.width;
        float clickZNorm = (localPoint.y + minimapRect.rect.height / 2f) / minimapRect.rect.height;

        float groundWidth = 10f * ground.localScale.x;
        float groundHeight = 10f * ground.localScale.z;

        float worldMinX = ground.position.x - groundWidth / 2f;
        float worldMaxX = ground.position.x + groundWidth / 2f;
        float worldMinZ = ground.position.z - groundHeight / 2f;
        float worldMaxZ = ground.position.z + groundHeight / 2f;

        float targetX = Mathf.Lerp(worldMinX, worldMaxX, clickXNorm);
        float targetZ = Mathf.Lerp(worldMinZ, worldMaxZ, clickZNorm);

        if (transposer != null)
        {
            transposer.m_FollowOffset = new Vector3(targetX, transposer.m_FollowOffset.y, targetZ);
        }
        else
        {
            vcam.transform.position = new Vector3(targetX, vcam.transform.position.y, targetZ);
        }
    }
    public void OnDrag(PointerEventData eventData)
    {
        if (!dragging || vcam == null) return;

        // 마우스 위치에 따라 카메라 절대 이동
        MoveCameraToClickPosition(eventData);
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        dragging = false;

        if (vcam != null && originalFollow != null)
        {
            vcam.Follow = originalFollow;
            vcam.gameObject.SetActive(false);
        }
    }
}

 


 

 

1. IPointerDownHandler, IDragHandler, IPointerUpHandler : UI 이벤트(마우스 클릭, 드래그, 클릭 해제) 처리

public class MiniMapDragCamera : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler

 


 

2.  변수 선언부

    [SerializeField] private RectTransform minimapRect;
    [SerializeField] private CinemachineVirtualCamera vcam;
    [SerializeField] private Transform playerTransform;
    [SerializeField] private float dragSpeed = 0.05f;
    [SerializeField] private Transform ground;
    private Vector2 lastPointerPos;
    private bool dragging = false;
    private Transform originalFollow;
    private CinemachineTransposer transposer;

 

minimapRect

미니맵 UI

 

vcam

드래그 이동할 카메라

(현재 카메라는 Cinemachine의 Follow를 Player로 지정해서 사용하고 있으며

또한 Space Key를 이용해 활성 / 비활성화를 하고 있는데 이를 제어하기 위함)

 

playerTransform

Player의 위치

 

dragSpeed
미니맵 드래그 속도

 

ground

지형

 

lastPointerPos

드래그 이전 마우스 위치

 

dragging 

드래그 상태 여부

 

originalFollow

vcam의 CinemachineTransposer 컴포넌트

(FollowOffset을 직접 바꿔서 카메라 위치 조정을 하기 위함)

 


 

3. Start() 함수

    void Start()
    {
        if (vcam == null)
        {
            Debug.LogError("카메라 없음");
            return;
        }

        originalFollow = vcam.Follow;
        transposer = vcam.GetCinemachineComponent<CinemachineTransposer>();
    }

 

vcam 할당 체크.

원래 Follow 대상 저장.

CinemachineTransposer 가져오기. (카메라 위치 조정에 사용)

 


 

4. OnPointerDown() : 클릭 시작

    public void OnPointerDown(PointerEventData eventData)
    {
        if (RectTransformUtility.RectangleContainsScreenPoint(minimapRect, eventData.position, eventData.pressEventCamera))
        {
            RectTransformUtility.ScreenPointToLocalPointInRectangle(minimapRect, eventData.position, eventData.pressEventCamera, out lastPointerPos);
            dragging = true;

            if (vcam != null)
            {
                vcam.Follow = null;
                vcam.gameObject.SetActive(true);
            }

            MoveCameraToClickPosition(eventData);
        }
    }

 

 

클릭 위치가 미니맵 안인지 확인.

lastPointerPos 초기화 → 드래그 시작 기준.

드래그 상태 dragging = true.

Follow 해제 → 드래그 및 클릭 이동 가능.

클릭하면 해당 위치로 카메라 이동 (MoveCameraToClickPosition() 호출).

 

 


 

5. MoveCameraToClickPosition()

    private void MoveCameraToClickPosition(PointerEventData eventData)
    {
        Vector2 localPoint;
        Camera cam = eventData.pressEventCamera;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(minimapRect, eventData.position, cam, out localPoint);

        float clickXNorm = (localPoint.x + minimapRect.rect.width / 2f) / minimapRect.rect.width;
        float clickZNorm = (localPoint.y + minimapRect.rect.height / 2f) / minimapRect.rect.height;

        float groundWidth = 10f * ground.localScale.x;
        float groundHeight = 10f * ground.localScale.z;

        float worldMinX = ground.position.x - groundWidth / 2f;
        float worldMaxX = ground.position.x + groundWidth / 2f;
        float worldMinZ = ground.position.z - groundHeight / 2f;
        float worldMaxZ = ground.position.z + groundHeight / 2f;

        float targetX = Mathf.Lerp(worldMinX, worldMaxX, clickXNorm);
        float targetZ = Mathf.Lerp(worldMinZ, worldMaxZ, clickZNorm);

        if (transposer != null)
        {
            transposer.m_FollowOffset = new Vector3(targetX, transposer.m_FollowOffset.y, targetZ);
        }
        else
        {
            vcam.transform.position = new Vector3(targetX, vcam.transform.position.y, targetZ);
        }
    }

 

 

클릭 좌표를 미니맵 로컬 좌표로 변환.

좌표를 0~1 범위로 정규화.

바닥(ground) 범위를 참조하여 월드 좌표로 변환.

Transposer가 있으면 FollowOffset 수정, 없으면 Transform 직접 이동.

 

 


 

6. OnDrag() : 드래그 처리

    public void OnDrag(PointerEventData eventData)
    {
        if (!dragging || vcam == null) return;

        // 마우스 위치에 따라 카메라 절대 이동
        MoveCameraToClickPosition(eventData);
    }

 

 

OnPointerDown() 때 실행되는

MoveCameraToClickPosition() 실행

(클릭 시와 동일한 로직 적용 : 카메라 Transform 따라다니기)


 

 

7. OnPointerUp() : 드래그 종료

    public void OnPointerUp(PointerEventData eventData)
    {
        dragging = false;

        if (vcam != null && originalFollow != null)
        {
            vcam.Follow = originalFollow;
            vcam.gameObject.SetActive(false);
        }
    }

 

 

드래그 상태 종료.

원래 플레이어 Follow로 복원 → 다시 플레이어 추적 시작.

 


 

 

와.. 정말 시간이 언제 이렇게 흘렀는지 모르겠습니다. 😂

 

 

이번 작업을 하면서 약간 시간이 지체됐던 부분이

 

1. 포탑이나 넥서스 등 이미지도 플레이어 아이콘처럼 지속적으로 매 프레임마다 Update()에서 진행하려 하다

도중에 필요가 없을것 같아서 제거한 점.

 

2. Enemy와 Enemy Icon을 서로 다른 List로 관리를 하려 했다가

Dictionary를 사용해서 작업하면 훨씬 간결해질 것 같아서 변경한 점.

 

3. Dictionary를 foreach로 순회 중에 같은 컬렉션을 수정하면

InvalidOperationException: Collection was modified 예외가 발생하여 

이상한 시간지연을 줘서 해결하려고 했다가, 새로운 리스트 선언으로 Remove 한 점.

(foreach는 컬렉션 구조를 고정하고 순회하기 때문에, 삭제/추가가 들어오면 내부 인덱스가 깨진다고 합니다.
지식 +1 Up)

 

4. 맵이 너무 큰 것 같아서 맵만 줄이고 Bake를 한 뒤 오브젝트 배치를 다시 하면 되는데..

오브젝트들 자체의 크기를 키우려고 했던 점..

(다 바보 같긴 하지만 이 부분은 제일 바보 같은 것 같습니다..😓)

 

그래도 조금씩 발전해 나가는 모습을 보니 기분은 좋은 것 같습니다 ㅎㅎ

 


 

감사합니다.🙂

'프로젝트 > 프로젝트 A' 카테고리의 다른 글

# 025 Victory (with. DOTween)  (4) 2025.08.30
# 024 GameManager (with.production)  (2) 2025.08.30
# 022 Map Design  (4) 2025.08.28
# 021 Golem (2) (with. PlayerHpBar)  (7) 2025.08.26
# 020 Golem (1) (with. State)  (2) 2025.08.25