[Unity] Zerry Library (11) - MiniMap with UI Tool Kit

suhan0304·2024년 7월 16일

유니티 - Zerry Library

목록 보기
10/11
post-thumbnail

저번에 나침반을 구현했던 것처럼 미니맵도 한 번 구현해보자.


Design the full map

먼저 UI Document를 하나 생성해주자.

Match Game View로 해놓고 실제 화면 해상도에서 작업할 수 있다.


Create the styles

그런 다음 Map이라는 이름으로 새 스타일 시트를 추가해준다.

그런 다음 .root-container-full이라는 이름으로 USS 셀렉터, 스타일 클래스를 만들어준다.

아래와 같이 속성을 설정해준다.

  • Flex > Grow: 1
  • Align > Align Items: Center
  • Align > Justify Content: Center
  • Margin & Padding > Margin: 5px

이 외에도 두 가지 스타일을 더 만들어준다.

Selector: .map-container

  • Size > Width: 50%
  • Size > Height: 90%
  • Display > Overflow: Hidden
  • Align > Align Items: Center
  • Align > Justify Content: Center

Selector: .map-img

  • Position > Position: Absolute
  • Align > Align Items: Center
  • Align > Justify Content: Center
  • Size > Width: 115%
  • Size > Height: 100%
  • Background > Image > Sprite: game_map
  • Background > Scale Mode: scale-and-crop

Add the VisualElements

  • 이제 Container 라는 VisualElement를 하나 추가해주고 .root-container-full 스타일을 적용해준다.
  • Container 자식으로 Map, 스타일은 .map-container를 적용해준다.
  • Map의 자식으로 Image, 스타일은 .map-img를 적용해준다.

UI 빌더가 아래와 같이 나온다.


이제 Map 오브젝트를 하나 만들어서 UI Document 컴포넌트를 추가해준다. Source Asset은 Map.uxml로 해주고, 패널 세팅을 하나 새로 만들어서 적용해준다.


Minimap Design

지금 게임을 시작하면 맵이 계속해서 노출되기 때문에 플레이어가 보이질 않는다. 새롭게 스타일을 하나 추가해서 맵을 숨겨보자. 아래 코드를 Map.uss에 추가해준다.

Map.uss

.root-container-mini {
    flex-grow: 1;
    align-items: flex-end;
    margin-left: 5px;
    margin-right: 5px;
    margin-top: 5px;
    margin-bottom: 5px;
    justify-content: flex-start;
}

.root-container-mini #Map {
    width: 150px;
    height: 150px;
    border-left-width: 2px;
    border-right-width: 2px;
    border-top-width: 2px;
    border-bottom-width: 2px;
    border-left-color: rgba(255, 255, 255, 255);
    border-right-color: rgba(255, 255, 255, 255);
    border-top-color: rgba(255, 255, 255, 255);
    border-bottom-color: rgba(255, 255, 255, 255);
}

.root-container-mini #Image {
    position: absolute;
    -unity-background-scale-mode: scale-and-crop;
    width: 512px;
    height: 512px;
}

스타일의 이름 뒤에 #이 붙으면 그 뒤의 이름과 일치하는 모든 요소에 이것을 첨부하도록 작동된다.

  • 먼저 .root-container-mini 스타일 클래스에 있는 모든 요소를 가져온다.
  • 그런 다음 #이름과 일치하는 요소의 모든 자식을 가져온다. (Map 또는 Image)
  • 그런 다음 해당 자식에만 스타일을 적용한다.

UI 빌더로 가서 .root-container-mini로 바꾸면 아래와 같이 바뀐다.


Code the map modes

"M" 버튼으로 전체 크기 맵과 미니 맵 사이를 전환할 수 있도록 아래와 같이 코드를 작성하고 Map 오브젝트의 컴포넌트로 추가해준다.

MapControler.cs

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

public class MapController : MonoBehaviour
{
    private VisualElement _root;
    private bool IsMapOpen => _root.ClassListContains("root-container-full");

    void Start() {
        _root = GetComponent<UIDocument>().rootVisualElement.Q<VisualElement>("Container");
    }

    void Update() {
        if (Input.GetKeyDown(KeyCode.M)) {
            ToggleMap(!IsMapOpen);
        }
    }

    private void ToogleMap(bool on) {
        _root.EnableInClassList("root-container-mini", !on);
        _root.EnableInClassList("root-container-full", on);
    }
}
  • IsMapOpenroot-container-full 스타일 클래스가 적용되어 있는지를 확인한다.
  • ToggleMap은 bool 매개변수를 활용하여 full과 mini 스타일 클래스를 키고 끄고를 할 수 있도록 해준다.

M을 눌러서 미니맵을 전체 맵으로 켰다, 껐다 할 수 있다.


Design player icon

플레이어가 어디있는지를 나타내기 위해 플레이어 아이콘을 작성해보자.

.player-container {
    width: 10px;
    height: 10px;
}

.player-cone {
    width: 80px;
    height: 60px;
    -unity-background-scale-mode: scale-to-fit;
    position: absolute;
    top: -54px;
    left: -35px;
    -unity-background-image-tint-color: rgba(255, 255, 255, 0.29);
    align-items: center;
    justify-content: flex-end;
    background-image: url("project://database/Assets/Sprites/playerCone.png?fileID=2800000&guid=0474f680ae98d884a8bf7ed7a1836229&type=3#playerCone");
}

.player-arrow {
    width: 10px;
    height: 10px;
    -unity-background-image-tint-color: rgb(255, 50, 73);
    top: 4px;
    background-image: url("project://database/Assets/Sprites/player_arrow.png?fileID=2800000&guid=3e620c7728093c44e811a73f5fb61640&type=3#player_arrow");
}

세 가지 스타일을 위와 같이 추가해준다. 그런 다음

  • Image의 자식으로 Player, 스타일은 .player-container로 설정
  • Player의 자식으로 Cone, 스타일은 .player-cone로 설정
  • Cone의 자식으로 Arrow, 스타일은 .player-arrow로 설정

UI가 아래처럼 바뀐다.


Code the player icon

이제 제일 까다로운 부분인 플레이어의 월드 포지션을 UI 포지션을 맞추도록 해보자.

public GameObject Player;
[Range(1, 15)]
public float miniMultiplyer = 5.3f;
[Range(1, 15)]
public float fullMultiplyer = 7f;
private VisualElement _playerRepresentation;

void Start() {
    _root = GetComponent<UIDocument>().rootVisualElement.Q<VisualElement>("Container");
    _playerRepresentation = _root.Q<VisualElement>("Player");
}

private void LateUpdate()
{
    var multiplyer = IsMapOpen ? fullMultiplyer : miniMultiplyer;
    _playerRepresentation.style.translate = 
        new Translate(Player.transform.position.x * multiplyer, Player.transform.position.z * -multiplyer, 0);
    _playerRepresentation.style.rotate = 
        new Rotate(new Angle(Player.transform.rotation.eulerAngles.y));
}

Translate는 플레리어 게임 오브젝트의 포지션에 따라 Visual Element의 포지션을 이동하는데 사용한다.

해상도나 설정에 따라 다른 승수가 필요할 수 있는데, 지도가 열린 상태에서 캐릭터 위치에 맞게 승수 슬라이더를 조절해서 위치를 맞추도록 한다.


Code the mini-map movement

잘 작동하지만 플레이어를 항상 중앙에 고정하고 미니맵을 이동시켜야 정상적으로 작동한다. 아래와 같이 작성해준다.

private VisualElement _mapContainer;
private VisualElement _mapImage;

void Start() {
    _root = GetComponent<UIDocument>().rootVisualElement.Q<VisualElement>("Container");
    _playerRepresentation = _root.Q<VisualElement>("Player");

    _mapImage = _root.Q<VisualElement>("Image");
    _mapContainer = _root.Q<VisualElement>("Map");
}

private void LateUpdate()
{
    var multiplyer = IsMapOpen ? fullMultiplyer : miniMultiplyer;
    _playerRepresentation.style.translate = 
        new Translate(Player.transform.position.x * multiplyer, Player.transform.position.z * -multiplyer, 0);
    _playerRepresentation.style.rotate = 
        new Rotate(new Angle(Player.transform.rotation.eulerAngles.y));


    if (!IsMapOpen)
    {
        var clampWidth = _mapImage.worldBound.width / 2 - 
            _mapContainer.worldBound.width / 2;
        var clampHeight = _mapImage.worldBound.height / 2 - 
            _mapContainer.worldBound.height / 2;

        var xPos = Mathf.Clamp(Player.transform.position.x * -multiplyer, 
            -clampWidth, clampWidth);
        var yPos = Mathf.Clamp(Player.transform.position.z * multiplyer, 
            -clampHeight, clampHeight);

        _mapImage.style.translate = new Translate(xPos, yPos, 0);
    }
    else
    {
        _mapImage.style.translate = new Translate(0, 0, 0);
    }
}
  • clampWidth, clampHeight : 지도 이미지와 지도 컨테이너 요소 너비를 기반으로 클램프 너비를 계산한다. 지도 이미지가 중앙에 배치되기 때문에 전체 너비의 절반을 사용한다.
  • Mathf.Clamp : 플레이어의 위치를 사용하여 X, Y를 계산한다. Clamp를 사용해서 맵 이미지가 경계를 벗어나지 않도록 한다.

플레이어가 맵의 경계에 가까워질 때까지 지도가 움직여야하도 경계에 도달하면 스크롤이 중지된다. (미니맵 크기를 좀 키워줬다. 당연히 승수도 조절해줬다.)


Dim while the player moves

맵이 전체화면 모드일 때 플레이어가 움직일 수 있도록 움직이는 동안 전체화면 맵이 50% 투명도가 적용되도록 해보자.

private bool _mapFaded;
public bool MapFaded
{
    get => _mapFaded;
    set 
    {
        if (_mapFaded == value) {
            return;
        }

        Color end = !_mapFaded ? Color.white.WithAlpha(.5f) : Color.white;

        _mapImage.experimental.animation.Start(
            _mapImage.style.unityBackgroundImageTintColor.value, end, 500, 
            (elm, val) => { elm.style.unityBackgroundImageTintColor = val; }                
        );

        _mapFaded = value;
    }
}

void LateUpdate()
{
	// 기존 코드 생략
    
    MapFaded = IsMapOpen && PlayerController.Instance.IsMoving;
}

이렇게 하면 맵이 열려있고, 플레이어가 움직일 때 MapFaded가 True로 설정되면서 투명도가 변경된다.


profile
Be Honest, Be Harder, Be Stronger

0개의 댓글