스킬 선택창 만들기
우선 스킬 창에서 값들을 가져온다. 그 방법으로 UI_StageResult 인스펙터창에서 skill 컴포넌트(스크립트)를 가진 오브젝트를
할당해줬다.
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class UI_StageResult : UI_Popup
{
[SerializeField] private SkillManager skill;
[Header("Buttons&Labels")]
public Button option1Button;
public Button option2Button;
public Button option3Button;
public void Init()
{
skill = GameObject.Find("Weapon_Bow").GetComponent<SkillManager>();
var candidates = new Dictionary<string, System.Action>();
if (skill.shootSpeed < 50f)
candidates[$"shootSpeed +1 ({skill.shootSpeed} -> {skill.shootSpeed + 1})"] =
() => skill.shootSpeed += 1f;
if (skill.arrowCount < 5)
candidates[$"arrowCount +1 ({skill.arrowCount} -> {skill.arrowCount + 1})"] =
() => skill.arrowCount += 1;
if (!skill.addGhost)
candidates["addGhost"] = () => skill.addGhost = true;
if (skill.addBomb < 3) // ?덉떆 ?곹븳 3
candidates[$"addBomb +1 ({skill.addBomb} -> {skill.addBomb + 1})"] =
() => skill.addBomb += 1;
if (!skill.addPenetrates)
candidates["addPenetrates"] = () => skill.addPenetrates = true;
if (skill.addSpread < 5)
candidates[$"addSpread +1 ({skill.addSpread} -> {skill.addSpread + 1})"] =
() => skill.addSpread += 1;
if (!skill.addChase)
candidates["addChase"] = () => skill.addChase = true;
var keys = new List<string>(candidates.Keys);
var picked = new List<string>();
var rnd = new System.Random();
int pickCount = Math.Min(3, keys.Count);
for (int i = 0; i < pickCount; i++)
{
int idx = rnd.Next(keys.Count);
picked.Add(keys[idx]);
keys.RemoveAt(idx);
}
var buttons = new[] { option1Button, option2Button, option3Button };
for (int i = 0; i < buttons.Length; i++)
{
var btn = buttons[i];
btn.onClick.RemoveAllListeners();
if (i < picked.Count)
{
string label = picked[i];
Action apply = candidates[label];
btn.gameObject.SetActive(true);
btn.GetComponentInChildren<TextMeshProUGUI>().text = label;
GameObject popupRoot = this.gameObject;
btn.onClick.AddListener(() =>
{
apply();
GameManager.instance.IsStageClear = true;
Destroy(popupRoot);
});
}
else
{
btn.gameObject.SetActive(false);
}
}
}
}
몇가지만 모르고있어 공부했다.
우선 keys.RemoveAt(idx)
key가 List<string> 일때, RemoveAt<int index) 는 리스트에서 인덱스 index에 해당하는 요소를 제거하는것이다.
예를들어
var fruits = new List<string> { "Apple", "Banana", "Cherry"}
fruits.RemoveAt(1);
이라면
Banana가 인덱스1이니까 제거되고
fruits = {"Apple", Cherry"}
가 된다.
Remove(item)은 값(item)을 찾아서 제거한 코드였고
RemoveRange(startIndex, count) : 연속된 여러요소 제거하는...
Clear() : 리스트 모두 비움
Insert(index, item) : 특정위치에 새 요소 삽입
같은 실행코드들도 존재한다.
그렇게 본다면..
int pickCount = Math.Min(3, keys.Count); 해서
루프 횟수(i < pickCount)로 고정한다음에, (최대라해바야 3)
루프 3번돌면서 예를들어
keys.Count가 7이라면 rnd.Next(7) → 0~6 중 하나
예를 들어 idx == 3 → picked에 keys[3]을 추가
keys.RemoveAt(3)로 리스트에서 인덱스 3 요소(원래 4번째)를 삭제
이제 keys.Count는 6이 된다.
두 번째 반복
이제 rnd.Next(6) → 0~5 중 하나
또 하나 뽑고, 그 인덱스의 요소를 제거 → keys.Count는 5로 줄어듦
세 번째 반복
rnd.Next(5) → 0~4 중 하나
다시 뽑고 제거 → keys.Count는 4가 됨
var keys = new List<string>(candidates.Keys);
이 한 줄은 루프 밖에서 한 번만 실행되어
원본 candidates.Keys(딕셔너리의 키들)를 복사한 새로운 List<string>를 만든다.
이후 RemoveAt은 이 복사본(list)만 수정할 뿐,
원본 candidates 딕셔너리에는 영향을 주지는 않는다.
여기서 한번더 복습하자면 new List는 얕은 복사의 방법이다.
List<T> 자체는 참조타입이라서
var a = somLists; 하면 a와 someLists는 같은 객체를 가리킨다
하지만
var keys = new List<string>(candidates.Keys);
이 구문은 새로운 List<string> 객체를 생성하는 코드이고
new List<T>(IEnumerable<T> collection) 생성자는
내부에서 새로운 배열(또는 버퍼)를 만들고
collection을 한 원소씩 꺼내서(foreach)
그 원소들을 새로운 배열에 복사해 넣는다.
따라서 keys 변수는 원본 cadidates.keys가 아니라
원본에 있는 키들을 한번 읽어서 담아놓은 완전히 다른리스트 객체가된다.
new List<string>(...)는 얕은 복사에 가깝다.
리스트의 구조(어떤 원소가 몇 개 들어있는지)만 복사
원소(string)들은 같은 참조(문자열 리터럴이나 기존 키 문자열)를 가리킨다.
하지만 리스트 그 자체(메모리 버퍼)는 별도이므로
keys.RemoveAt(idx);
해도 원본딕셔너리의 키 목록은 전혀 건드리지않는다.
깊은복사라고 말그대로 원본객체 전체를 통째로 복제해서 객체의 원본이 어떤 참조도 공유하지않도록
하는 방식도 있다.
물론 얕은복사라지만, List안에 있는 값들을 더하거나 빼거나, 값자체를 변경한다고 ( 예: keys[0] = "새문자열";)
candidates.keys 의 값이 바뀌지는 않는다.
그이유는 c#의 string은 불변(immutable) 이어서 문자열 내부가 절대 바뀌지않는다.
딕셔너리 키 자체도 외부에서 그 키를 직접 수정 또는 삭제할 수 없다.
읽기전용으로 제공하고만있다. 딕셔너리는 RemoveAt(int index) 같은 인덱서기반 삭제 메서드가없다.
Dictionary(TKey, TValue> 는 내부적으로 해시 테이블을 이용해 키를 해시값으로 바로 찾아가는 구조다.
따라서 0번째 요소 같은 개념이없고 키를 알아야 Remove(key)로 삭제할수있다.
var dict = new Dictionary<string, int> {
["A"] = 1,
["B"] = 2,
["C"] = 3
};
// ❌ dict.RemoveAt(1); // 컴파일 오류
dict.Remove("B"); // ✅ 키 "B"에 해당하는 항목 삭제
해당 Remove를 실행하면
딕셔너리에서 키- 값 쌍으로 제거된다.
그래서 이번 예제 코드에서도 키목록을 리스트로 복사후 위치지정삭제하는 방식으로 응용한것이다.
참고로 가변객체라면 얕은복사의 경우 값이 참조타입이면서 동시에 수정한 부분이 오리지널에도 적용되게 되는데
그예시로
public class Person
{
public string Name;
public int Age; // Age 필드를 언제든 바꿀 수 있다
}
var p1 = new Person { Name = "Alice", Age = 20 };
var p2 = p1; // p2도 동일한 인스턴스를 가리킴
p2.Age = 30; // p1.Age도 30으로 바뀜!
Console.WriteLine(p1.Age); // 30
새로 new 해서 객체를 만들었음에도, 변경가능한 상태다.
비슷한 상황을 예시로들어보자면
// 가변 객체 예시
var person = new Person { Name = "Alice" };
originalList.Add(person);
var copyList = new List<Person>(originalList);
copyList[0].Name = "Bob";
// originalList[0].Name 도 "Bob" 으로 바뀜
//불변객체 예시
var original = new List<string> { "Hello" };
var copy = new List<string>(original); // 얕은 복사
// 문자열 내부 문자를 직접 바꾸는 건 애초에 불가
// copy[0][0] = 'h'; // 컴파일 오류!
// 대신 리스트의 요소를 교체하는 건 가능하지만
copy[0] = "World";
Console.WriteLine(original[0]); // 여전히 "Hello"
Dictionary<TKey, TValue> 기본문법을 한번더 짚고넘어가보자
var candidates = new Dictionary<string, Action>(); 이라면
값저장은
candidates["speedUp"] = () => speed += 1;
// key: "speedUp", value: 람다(() => speed += 1)
candidates[key] = value;
대괄호 인덱서 문법을 통해 key에 해당하는 값을 value로 할당(추가 또는 교체)
값 읽어오기는
Action upgrade = candidates["speedUp"]
var upgrade = candidates[key];
key가 딕셔너리에 있어야만함, 있다면, 그에 대응하는 value를 반환한다.
안전하게 읽으려면,
ContainsKey 사용해서 키 존재 여부 검사
if(candidates.ContainsKey("speedUp"))
candidates["speedUp"]();
TryGetValue 사용해서 존재 시 값 반환, 없으면 false
if(candidates.TryGetValue("speedUp", out Action action))
action(); // 바로 실행
말고도
Add(key, value) 존재하지 않는 키만 추가 (중복 시 예외)
Clear() 모든 항목 제거
도 존재한다.
new List<string>(candidates.Keys) 는 무슨 문법일까? 소괄호에 매개변수로 들어가는것같다.
List<T> 생성자 중 하나는 IEnumerable<T>(열거 가능한 컬렉션)을 받는다는 뜻으로
candidates.Keys는 Dictionary<string, Action>에 있는 모든 키(ICollection<string>)를 열거할 수 있는 컬렉션.
일반 List<string>과 다른 점?
var empty = new List<string>();
아무것도 넣지않은 빈 리스트
생성자에 컬렉션을 넘기면, 넘긴 컬렉션의 모든 요소를 복사해서 리스트 초기값으로 사용
var fruits = new List<string>( new[] { "A", "B", "C" } );
// fruits에는 "A","B","C"가 들어있는 상태로 시작
여기서
candidates.keys 문법은,,
속성(프로퍼티)를 사용한것이다.
Dictionary<TKey,TValue>.Keys
현재 딕셔너리에 저장된 모든 키만 모아서 돌려줌 (ICollection<TKey>)
비슷한 속성으로
dictionary.Values : **값(들)**만 모아서 돌려주는 컬렉션
dictionary.Count : 항목 개수
dictionary.Item[key] : 대괄호 인덱서, 앞서 본 dictionary[key]
RemoveAllListeners() 는 UnityEvent 중 Button.onClick 의 API로
지금까지 onClick.AddListener(...)로 등록했던 모든 콜백(리스너)을 한 번에 제거한다.
AddListener(UnityAction call) 새 클릭 콜백 등록
RemoveListener(UnityAction call) 특정 콜백만 제거
RemoveAllListeners() 모든 콜백 제거
Invoke() 직접 이벤트 호출 (보통 내부에서 실행)
이런식으로있다.
여기서
if (i < picked.Count)
{
string label = picked[i];
Action apply = candidates[label];
btn.gameObject.SetActive(true);
btn.GetComponentInChildren<TextMeshProUGUI>().text = label;
GameObject popupRoot = this.gameObject;
btn.onClick.AddListener(() =>
{
apply();
GameManager.instance.IsStageClear = true;
Destroy(popupRoot);
});
여기서쓰인 apply(); 함수가 뭘까?
apply는 Action 델리게이트 변수였다.
Action apply = candidates[label]; 이라고 변수에 할당했다.
apply(); 는 해당 액션 델리게이트를 호출하는것으로
내부적으로는
apply.Invoke(); 와 동일하다.
// 예시
void Foo() { Console.WriteLine("Foo!"); }
Action a = Foo;
a(); // "Foo!" 출력
--------------------------
스킬 아이콘 오른쪽에 정렬하기 공부.
using System.Collections;
using System.Collections.Generic;
using Systehttp://m.Reflection.Emit;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class UI_GameScene : MonoBehaviour
{
public TextMeshProUGUI stageText;
public Button optionButton;
//public Button statusButton;
public TextMeshProUGUI goldText;
[Header("Skill Icons")]
public RectTransform skillsContainer;
public GameObject skillIconPrefab;
private Dictionary<string, TextMeshProUGUI> skillIconMap = new Dictionary<string, TextMeshProUGUI>();
private GridLayoutGroup grid;
private SkillManager skillManager;
[Header("UI Prefabs (Resources/UI)")]
public string questListName = "UI_QuestList";
public string healthBarName = "UI_HealthBar";
private UI_QuestList questList;
public Slider healthSlider;
public TextMeshProUGUI healthText;
private PlayerManager player;
private void Awake()
{
grid = skillsContainer.GetComponent<GridLayoutGroup>();
skillManager = FindObjectOfType<SkillManager>();
GameManager.instance.OnSkillUpgraded += OnSkillUpgraded;
PlayerController.Instance.OnGoldChanged += UpdateGoldUI;
UpdateGoldUI(PlayerController.Instance.Gold);
}
private void Start()
{
player = FindObjectOfType<PlayerManager>();
if (player == null)
{
Debug.LogError("PlayerManager not found in the scene.");
return;
}
player.OnHealthChanged += SetHealth;
player.OnPlayerDie += ShowFailPopup;
SetHealth(player.CurrentHealth, player.MaxHealth);
SetInfo();
}
public void Init()
{
GameManager.instance.OnStageUpdated -= OnStageUpdated;
GameManager.instance.OnStageUpdated += OnStageUpdated;
optionButton.onClick.AddListener(OnClickOptionButton);
//statusButton.onClick.AddListener(OnClickStatusButton);
//questList = UIManager.Instance.ShowPopup<UI_QuestList>(questListName);
//questList.Init();
//var stat = PlayerStat.Instance;
//healthBar.SetHealth(stat.CurrentHealth, statMaxHealth: stat.MaxHealth);
//stat.OnHealthChanged += (cur, max) =>
//{
// healthBar.SetHealth(cur, max);
//};
}
public void SetInfo()
{
Refresh();
}
void Refresh()
{
OnStageUpdated();
// TODO: 다른 UI 요소 리프레시 호출
}
void OnStageUpdated()
{
stageText.text = "스테이지 " + GameManager.instance.Stage.ToString();
}
void OnClickOptionButton()
{
UI_OptionPopup optionPopup = UIManager.Instance.ShowPopup<UI_OptionPopup>("UI_OptionPopup");
optionPopup.Init();
}
//void OnClickStatusButton()
//{
// UI_StatsPopup statusPopup = UIManager.Instance.ShowPopup<UI_StatsPopup>("UI_StatsPopup");
// statusPopup.Init();
//}
public void SetHealth(int current, int max)
{
healthSlider.maxValue = max;
healthSlider.value = current;
healthText.text = $"{current}/{max}";
}
private void ShowFailPopup()
{
var popup = UIManager.Instance.ShowPopup<UI_StageFailPopup>("UI_StageFailPopup");
popup.Init();
}
private void OnDestroy()
{
GameManager.instance.OnSkillUpgraded -= OnSkillUpgraded;
}
private void OnSkillUpgraded(string label)
{
AddSkillIcon(label);
UpdateGridConstraint();
}
private void AddSkillIcon(string label)
{
string key = label.Split(' ')[0];
string value = GetSkillValue(key);
if(skillIconMap.TryGetValue(key, out var existingText))
{
existingText.text = $"{key}: \n{value}";
}
else
{
var icon = Instantiate(skillIconPrefab, skillsContainer, false);
var rt = icon.GetComponent<RectTransform>();
rt.localScale = Vector3.one;
var iconText = icon.GetComponentInChildren<TextMeshProUGUI>();
iconText.text = $"{key}: \n{value}";
skillIconMap[key] = iconText;
}
}
private string GetSkillValue(string key)
{
switch (key)
{
case "shootSpeed":
return skillManager.shootSpeed.ToString();
case "arrowCount":
return skillManager.arrowCount.ToString();
case "addGhost":
return skillManager.addGhost ? "True" : "False";
case "addBomb":
return skillManager.addBomb.ToString();
case "addPenetrates":
return skillManager.addPenetrates ? "True" : "False";
case "addSpread":
return skillManager.addSpread.ToString();
case "addChase":
return skillManager.addChase ? "True" : "False";
default:
return "";
}
}
private void UpdateGridConstraint()
{
float width = skillsContainer.rect.width;
float cellAndSpacing = grid.cellSize.x + grid.spacing.x;
int maxColumns = Mathf.Max(1, Mathf.FloorToInt((width + grid.spacing.x) / cellAndSpacing));
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
grid.constraintCount = maxColumns;
}
private void UpdateGoldUI(int gold)
{
goldText.text = $"Gold: {gold}";
}
}
'Unity 개발 공부' 카테고리의 다른 글
| [내배캠] 본캠 41일차. 물건집기, 로테이션 (0) | 2025.06.04 |
|---|---|
| [내배캠] 본캠 34일차. 3d 작업시 트러블슈팅 (0) | 2025.05.23 |
| [내배캠] 본캠 23일차. 싱글턴, 패턴매칭 (0) | 2025.05.09 |
| [내배캠] 본캠 22일차. 2d 메타버스 만들기 트러블 슈팅 (0) | 2025.05.07 |
| [내배캠] 본캠 18일차. 그래프 알고리즘, 다익스트라 (1) | 2025.04.30 |