오늘 학습 키워드

오브젝트 풀링, 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);
}

내일 학습 할 것은 무엇인지

  • 캐릭터 슬롯 추가 좀 더 테스트해보기