오늘 학습 키워드
오브젝트 풀링, LINQ
오늘 학습 한 내용을 나만의 언어로 정리하기
BulletShooter
스킬 이펙트를 입혀봐요
- 아주 평범하게 Resources/SkillEffects 안에 있는 스킬 prefab을 Instantiate로 부르려고 함
- 생성되고 2초 정도 지나면 Destroy 되도록 함
근데 이거 너무 자주 생성되지 않나
- 그래서 오브젝트 풀링을 쓰려고 함.
using System.Collections.Generic;
using UnityEngine;
public class EffectPoolManager : MonoBehaviour
{
public static EffectPoolManager Instance;
private Dictionary<string, Queue<GameObject>> poolDict = new();
private void Awake()
{
if (Instance == null)
Instance = this;
else
Destroy(gameObject);
}
/// <summary>
/// Get Effect From Pool Dict
/// </summary>
/// <param name="effectName"></param>
/// <param name="position"></param>
/// <param name="rotation"></param>
/// <returns></returns>
public GameObject GetEffect(string effectName, Vector3 position, Quaternion rotation, float size)
{
GameObject effect;
if (poolDict.ContainsKey(effectName) && poolDict[effectName].Count > 0)
{
effect = poolDict[effectName].Dequeue();
effect.SetActive(true);
}
else
{
GameObject prefab = Resources.Load<GameObject>($"SkillEffects/{effectName}");
if (prefab == null)
{
Debug.LogWarning($"Effect '{effectName}' not found in Resources/Effects/");
return null;
}
effect = Instantiate(prefab);
}
effect.transform.position = position;
effect.transform.rotation = rotation;
effect.transform.localScale = Vector3.one * size;
return effect;
}
/// <summary>
/// Return to Pool Dict
/// </summary>
/// <param name="effectName"></param>
/// <param name="effect"></param>
public void ReturnEffect(string effectName, GameObject effect)
{
effect.SetActive(false);
if (!poolDict.ContainsKey(effectName))
poolDict[effectName] = new Queue<GameObject>();
poolDict[effectName].Enqueue(effect);
}
}
- 풀링 : 매번 생성/삭제 안하고 재사용하는거
- 이펙트가 아예 없을 때에만 생성해 주고, 나머지는 재사용임
- 저렇게 풀 매니저 만든 다음에, 모든 스킬에다가 이렇게 만들어주면 됨
public class ExplosionSkillLogic : ISkillLogic
{
public void Activate(GameObject caster, GameObject target, SkillData data)
{
Vector3 spawnPos = caster.transform.position + Vector3.up;
GameObject fx = EffectPoolManager.Instance.GetEffect("Explosion", spawnPos, Quaternion.identity);
// 일정 시간 후 다시 반환
caster.GetComponent<MonoBehaviour>().StartCoroutine(ReturnAfterDelay("Explosion", fx, 2f));
}
private IEnumerator ReturnAfterDelay(string name, GameObject fx, float delay)
{
yield return new WaitForSeconds(delay);
EffectPoolManager.Instance.ReturnEffect(name, fx);
}
}
매번 스킬 함수 내에다가 써주는게 귀찮아요
- 그래서 오브젝트가 알아서 풀에 반환되게 함
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AutoPoolReturner : MonoBehaviour
{
public string effectName;
public float returnDelay = 2f;
private void OnEnable()
{
StartCoroutine(ReturnCoroutine());
}
private IEnumerator ReturnCoroutine()
{
yield return new WaitForSeconds(returnDelay);
EffectPoolManager.Instance.ReturnEffect(effectName, gameObject);
}
}
- 이 스크립트를 애니메이션 프리팹에다가 붙여주면 끝
- 그러면 스킬 Activate 부분이 엄청 간소화 됨
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ExplosionSkillLogic : ISkillLogic
{
public string AnimName { get; private set; } = "Explosion_01";
public void Activate(GameObject caster, GameObject target, SkillData skillData)
{
Vector3 spawnPos = caster.transform.position - new Vector3 (0, 3.5f, 0);
GameObject fx = EffectPoolManager.Instance.GetEffect(AnimName, spawnPos, Quaternion.identity, 5f);
}
}
스킬이 안따라와요
/// <summary>
/// Get Effect From Pool Dict
/// </summary>
/// <param name="effectName"></param>
/// <param name="position"></param>
/// <param name="rotation"></param>
/// <returns></returns>
public GameObject GetEffect(string effectName, float size)
{
GameObject effect;
if (poolDict.ContainsKey(effectName) && poolDict[effectName].Count > 0)
{
effect = poolDict[effectName].Dequeue();
effect.SetActive(true);
}
else
{
GameObject prefab = Resources.Load<GameObject>($"SkillEffects/{effectName}");
if (prefab == null)
{
Debug.LogWarning($"Effect '{effectName}' not found in Resources/Effects/");
return null;
}
// 부모를 똑바로 설정하자
effect = Instantiate(prefab, FindAnyObjectByType<PlayerCtrl>().transform);
}
effect.transform.localScale = Vector3.one * size;
return effect;
}
- instantiate에서 부모 설정 안해줘서 그럼
TextRPG
오늘 한 일
-
전투할 때 플레이어 턴 수정 : 기존에는 제대로 선택 안했을 때도 턴이 넘어가버렸음
-
전투할 때 스킬 / 아이템 사용 가능하게 수정
-
직업 변경 창 생성
- 특정 아이템을 가지고 있으면 전직할 수 있도록 했음
// Player.cs
// TODO : 직업마다 필요한 아이템 반환
public static string? JobItemName(Jobs job)
{
return job switch
{
_ => null
};
}
// JeonJikScene.cs
private bool CanChangeClass(Player.Jobs job)
{
string? needItemName = Player.JobItemName(job);
if (needItemName != null)
{
bool hasItem = GameManager.player.Inventory.Any(x => x.Name == needItemName);
return hasItem;
}
else
{
return true;
}
}
이거때매 LINQ 공부해봄..
LINQ 기본 문법
var result = someList
.Where(x => 조건) // 필터링
.Select(x => 변환) // 변환 (projection)
.OrderBy(x => 키) // 오름차순 정렬
.OrderByDescending(x => 키) // 내림차순 정렬
.ThenBy(x => 키) // 다중 정렬
.ToList(); // List로 변환
Where
- 조건 걸기
// 공격력이 10 이상인 아이템만 List로 가져오기
var strongItems = items.Where(i => i.Attack > 10).ToList();
Select
- 특정 요소만 가져오기
// 이름 부분만 가져옴
var names = items.Select(i => i.Name).ToList();
OrderBy, OrderByDescending, ThenBy
- 정렬
var sorted = items
.OrderBy(i => i.Type) // 타입 오름차순
.ThenByDescending(i => i.Price) // 같은 타입이면 가격 높은 순
.ToList();
- OrderBy 다음에는 ThenBy 사용됨
Any, All
- 조건 검사
- Any : 하나라도 있으면
- All : 모두 통과하면
bool hasPotion = items.Any(i => i.Name.Contains("포션"));
bool allCheap = items.All(i => i.Price < 1000);
First, FirstOrDefault, SingleOrDefault
- First : 가장 먼저 조건 만족하는 것
- FirstOrDefault : First랑 같은데, 조건 만족하는게 없으면 null 반환
- SingleOrDefault : 딱 하나만 있으면 true, 없으면 null, 여러개 있으면 Exception.
var firstWeapon = items.First(i => i.Type == ItemType.Weapon);
var maybeItem = items.FirstOrDefault(i => i.Name == "전설의 검"); // 없으면 null 반환
var item = items.SingleOrDefault(i => i.Name == "전설의 검");
if (item != null)
Console.WriteLine($"아이템 찾음: {item.Name}");
else
Console.WriteLine("아이템 없음 or 여러 개 존재함");
GroupBy
- 그룹화
var grouped = items.GroupBy(i => i.Type); // 타입별로 묶기
foreach (var group in grouped)
{
Console.WriteLine($"== {group.Key} ==");
foreach (var item in group)
Console.WriteLine(item.Name);
}
Count, Sum, Max, Min, Average
- 이름에서 유추 가능한대로임
int count = items.Count(i => i.Type == ItemType.Weapon);
int totalCost = items.Sum(i => i.Price);
int maxAtk = items.Max(i => i.Attack);
Distinct, DistinctBy
- 중복 제거용
- Distinct : 전체 객체가 동일한지 비교함. (Equals 일일히 따져봐야됨. 특히 클래스는 참조비교라 어려울것)
- DistinctBy : 특정 속성값이 동일한지 비교함. (이게 더 쓸만할거임)
var items = new List<Item>
{
new Item("포션", "HP 회복", 10, ItemType.Usable),
new Item("포션", "HP 회복", 10, ItemType.Usable),
new Item("전설의 검", "강력한 무기", 50, ItemType.Weapon),
new Item("화염검", "불 속성 무기", 40, ItemType.Weapon)
};
var distinctItems = items.Distinct().ToList(); // 아마 작동 잘 안될거임
var distinctByName = items.DistinctBy(i => i.Name).ToList(); // 이러면 포션 하나 남음
캐릭터 저장 슬롯 4개까지 만들기
public void LoadPlayerData(int slot)
{
List<string> strings = new();
string path = GetSlotPath(slot);
if (File.Exists(path))
{
string json = File.ReadAllText(path);
player = JsonSerializer.Deserialize<Player>(json);
player.LoadSkillsFromJson();
strings.Add($"슬롯 {slot}에서 불러오기 완료.");
UI.DrawBox(strings);
}
else
{
strings.Add($"슬롯 {slot}에 저장된 데이터가 없습니다. 새로 생성합니다.");
UI.DrawBox(strings);
Console.ReadKey();
NewPlayerData(slot);
}
selectedSlot = slot;
Console.ReadKey();
}
public void ShowSaveSlots()
{
Console.Clear();
List<string> strings = new();
for (int i = 1; i <= 4; i++)
{
string path = GetSlotPath(i);
if (File.Exists(path))
{
string json = File.ReadAllText(path);
Player? p = JsonSerializer.Deserialize<Player>(json);
strings.Add($"{i}. {p?.Name} (Lv.{p?.Level}) - {Player.JobsKorean((Player.Jobs)p?.Job)}");
}
else
{
strings.Add($"{i}. [빈 슬롯]");
}
}
UI.DrawTitledBox("저장 슬롯", null);
UI.DrawLeftAlignedBox(strings);
}
public void SavePlayerData(int slot)
{
List<string> strings = new();
string json = JsonSerializer.Serialize(player, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(GetSlotPath(slot), json);
strings.Add($"슬롯 {slot}에 저장 완료!");
UI.DrawBox(strings);
Console.ReadKey();
}
public void DeleteSlot(int slot)
{
List<string> strings = new();
string path = GetSlotPath(slot);
if (File.Exists(path))
{
File.Delete(path);
strings.Add($"슬롯 {slot} 삭제 완료.");
}
else
{
strings.Add($"슬롯 {slot}에 저장된 데이터가 없습니다.");
}
UI.DrawBox(strings);
Console.ReadKey();
}
학습하며 겪었던 문제점 & 에러
문제 1
- 문제&에러에 대한 정의
상점에서 아이템 구매가 안됨
- 내가 한 시도
디버깅을 돌려봤는데, 아이템을 구매하려면 아이템 이름을 적어어야 했음 문제는 아이템 이름을 똑바로 못 받아오고 있었음.
- 해결 방법
상점 부분 개발하신 분한테 알려드렸음
- 새롭게 알게 된 점
사용자의 입력을 늘 그대로 받아올 수 있을거라고 생각하지 말자..
- 이 문제&에러를 다시 만나게 되었다면?
디버깅의 생활화.. 중단점 걸고 찾아봐야지..
문제 2
- 문제&에러에 대한 정의
게임 꺼질 때 저장하게 하고싶었음
- 내가 한 시도
GameManager의 소멸자에다가 넣어놨음. 근데 안됨.
- 해결 방법
프로그램이 종료될 때 실행되는 이벤트에다가 추가하자.
public GameManager()
{
SkillManager.Initialize();
if (instance == null)
{
instance = this;
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
}
else
{
throw new Exception("GameManager Instance는 하나만 존재할 수 있습니다.");
}
Directory.CreateDirectory(saveDir);
}
private void OnProcessExit(object? sender, EventArgs e)
{
SavePlayerData(selectedSlot);
}
내일 학습 할 것은 무엇인지
- 캐릭터 슬롯 추가 좀 더 테스트해보기