유니티

[유니티] 스킬 트리 만들기

k_min0203 2025. 4. 10. 23:45

2학년 1분기 동아리 산출물로 만들 게임에서 스킬트리를 만드는 역할을 맡게 되었다.

 

나는 자동으로 선을 연결해주는 시스템을 만들기를 원했다.

 

그래서 자동으로 선을 연결해주는 시스템부터 만들기로 했다.

 

자동으로 선 연결하는 기능 만들기

같이 공동 작업을 하는 친구가 SO데이터의 이름을 Fruits로 지어 스킬 버튼의 이름을 Fruits로 통일하기로 했다.

 

    [SerializeField] private FruitsSO fruitsSO; //스킬의 SO 데이터
    [SerializeField] private List<Fruits> _connectedFruits; //연결할 스킬
    [SerializeField] private bool isRootFruits; //시작하는 스킬인가
    [SerializeField] private float width = 10; //연결하는 선의 두께
    
    [HideInInspector]
    [field:SerializeField] public List<Image> ConnectedNode { get; private set; } //연결된 선을 담을 List
    public Button FruitsButton { get; private set; } = null; //스킬의 버튼
    public bool IsActive { get; set; } //활성화가 됐는가
    public bool CanPurchase { get; private set; } = false; //구매할 수 있는가

기본적으로 필요한 변수들을 만들어주고

 

    public void Initialize()
    {
        FruitsButton = GetComponentInChildren<Button>();
        FruitsButton.onClick.AddListener(SelectFruits);
        fruitsSO.Fruits = this;

        if(isRootFruits) connectedFruits.ForEach(f => f.CanPurchase = true);
    }

Initialize에서 몇몇 변수들을 할당해주고 만약 시작하는 스킬이라면 해당 스킬의 부모들을 구매할 수 있게 해준다.

 

   [ContextMenu("ConnectLine")]
   private void ConnectLine()
    {
        foreach (Fruits f in connectedFruits)
        {
            for (int i = 0; i < f.ConnectedNode.Count; i++)
            {
                if (f.ConnectedNode[i] == null)
                    f.ConnectedNode.RemoveAt(i);
            }

            if (f.ConnectedNode.Count > 0)
            {
                foreach (var node in f.ConnectedNode)
                {
                    if (node != null)
                        DestroyImmediate(node.gameObject);
                }

                f.ConnectedNode.Clear();
            }
        }
    }

contextMenu로 에디터에서 함수를 실행할 수 있게 해주고

 

연결된 스킬에 foreach를 돌렸다.

 

만약 연결하려는 스킬에서 ConnectedNode의 원소가 이미 있거나 공간만 할당되어 있다면 전부 삭제해주고 리스트를 비워준다.

 

    private void ConnectNode(Vector3 pos1, Vector3 pos2, Image node, bool isVert)
    {
        Vector3 centerPos = (pos1 + pos2) / 2f;
        float distance = Vector3.Distance(pos1, pos2);

        node.rectTransform.position = centerPos;

        if (isVert)
            node.rectTransform.sizeDelta = new Vector2(width, distance + width);
        else
            node.rectTransform.sizeDelta = new Vector2(distance + width, width);
    }

ConnectNode 함수를 만들어 파라미터로 두 위치를 받고 연결하는데 사용될 이미지,

그리고 가로인지 세로인지 판별할 isVert를 받는다.

 

두 위치를 더하고 2로 나누어 center를 구하고

 

Vector3.Distance로 두 위치 사이의 거리를 구해준다.

 

연결할 이미지의 위치를 center로 맞춰주고 isVert가 true라면

 

rectTransform.sizeDelta를 사용해 알맞게 크기를 맞춰주게 만들었다.

 

선 연결은 3개의 Image를 생성해 위 그림과 같이 연결 해줄것이다.

Transform root = f.transform.Find("Nodes");                                  
GameObject[] obj = new GameObject[3];                                        
Image[] nodes = new Image[3];                                                
                                                                             
for (int i = 0; i < 3; i++) {                                                
    obj[i] = new GameObject($"Node{i}");                                     
    nodes[i] = obj[i].AddComponent<Image>();                                 
    nodes[i].transform.SetParent(root, false);                               
                                                                             
    f.ConnectedNode.Add(nodes[i]);                                           
}                                                                            
                                                                             
var rect = transform as RectTransform;                                       
Vector2 node1Pos = Vector2.zero;                                             
Vector2 selfPos = rect.position;                                             
Vector2 fruitsPos = f.GetComponentInChildren<Image>().rectTransform.position;
                                                                             
for (int i = 0; i < 2; i++)                                                  
{                                                                            
    if (node1Pos == Vector2.zero) {                                          
        node1Pos = new Vector2(selfPos.x, (fruitsPos.y + selfPos.y) / 2);    
        ConnectNode(selfPos, node1Pos, nodes[0], true);                      
    }                                                                        
                                                                             
    Vector3 node2Pos = new Vector2(fruitsPos.x, node1Pos.y);                 
    ConnectNode(node1Pos, node2Pos, nodes[1], false);                        
    ConnectNode(node2Pos, fruitsPos, nodes[2], true);                        
}

앞서 만든 ConnectNode 함수를 이용해 3개의 노드를 연결 해줄것이다.

 

첫번째의 노드 연결은 자기위치의 x와 타겟스킬의 y를 2로 나눈 위치를 중심으로 잡아준 값과 자신의 위치를 연결해주었다.

두번째 노드는 연결할 스킬 f의 x값과 이전에 사용한 node1pos의 y값을 가지고 있는 위치 벡터와  node1pos을 연결해주었다.

 

마지막으로 세번째 노드는 이전에 사용된 node2pos와 연결할 스킬의 위치를 연결해주었다.

 

이렇게 연결하는 기능은 구현이 되었다.

    [ContextMenu("ClearAllNode")]
    private void ClearAllNode()
    {
        foreach(var fruits in connectedFruits)
        {
            fruits.ConnectedNode.ForEach(n => DestroyImmediate(n.gameObject));
            fruits.ConnectedNode.Clear();
        }
    }

그 외에 편의기능으로 연결된 Node를 모두 삭제하는 함수도 만들어주었다.

 

노드 연결하기

노드 삭제하기