2024.10.18(금)
팀 프로젝트 주차가 시작되었는데 여러 고전 게임들 3개 중 하나를 선택해서 재해석하고 구현하는 것이다. 우리팀은 그 중에 Dodge게임을 선택했다. 프로젝트 기간은 1주일이다.
현재 팀 프로젝트에서 만들고 있는 것은 Dodge와 Shooting게임 그리고 뱀서라이크를 결합한 게임이다.
사방에서 총을 쏘는 적이 나오고, 플레이어는 적과 총알을 피하면서 적을 파괴하는 게임이다.
그리고 플레이어는 아이템을 통해 폭탄을 얻거나, 체력을 회복하거나, 총알 개수를 늘리거나, 총알 데미지를 올리거나, 스피드나 체력같은 스탯을 올릴 수 있다.
나는 프로젝트 내에서 아이템 부분을 맡았다.
아이템을 관리하기 위해 ScriptableObject
를 만들어서 Item들의 정보들을 관리하기로 결정했다.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public enum ItemType
{
StatModifier,
UsableItem
}
[CreateAssetMenu(fileName ="ItemSO", menuName = "DodgeController/Items/ItemSO")]
public class ItemSO : ScriptableObject
{
[Header("Base Info")]
public Sprite itemSprite;
public ItemType itemType;
public string itemName;
public Color itemBackColor;
public Color fontColor;
public string itemInfoText;
public string statInfoText;
public float stat;
private void OnValidate()
{
itemName = this.name;
}
}
여기서 만든 ItemType이라는 enum
타입으로 StatModifier와 UsableItem을 나눠서 스탯을 변경해주는 아이템과 사용아이템으로 분류했다.
각 타입은 다음과 같은 기능을 할 것이다.
itemName은 OnValidate
를 이용해 자동으로 생성된 아이템의 이름을 가지고 오게 만들었다.
그리고 나머지 필드들은 SelectPanel의 Button들에 있는 이미지와 색들에 적절히 넣어줄 것이다.
이렇게 만든 ItemSO를 이용해 아이템 인스턴스들을 만들어 준다.
그리고 이렇게 만든 아이템들은 각 Button들에 부착된 ItemButtonHandler를 통해 Button에 아이템의 정보가 들어가게 된다.
using System;
using UnityEngine;
using UnityEngine.UI;
public class ItemButtonHandler : MonoBehaviour
{
[SerializeField] private Image buttonImage;
[SerializeField] private Image itemImage;
[SerializeField] private Text optionText;
[SerializeField] private Text statText;
public void SetUpButton(ItemSO itemSO)
{
itemImage.sprite = itemSO.itemSprite;
buttonImage.color = itemSO.itemBackColor;
optionText.color = itemSO.fontColor;
statText.color = itemSO.fontColor;
optionText.text = itemSO.itemInfoText;
statText.text = itemSO.statInfoText;
}
}
다음으로 뱀서라이크의 특징인 레벨업할 시 선택지 3개를 랜덤으로 제공하여 보여주는 기능을 추가할 것이다.
ItemSelectionManager를 통해 각 버튼들에 아이템을 랜덤으로 보여준다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditorInternal.Profiling.Memory.Experimental;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;
public class ItemSelectionManager : MonoBehaviour
{
public static ItemSelectionManager instance;
[SerializeField] private GameObject selectPanel;
[SerializeField] private List<ItemSO> allItems;
[SerializeField] private List<ItemButtonHandler> optionButtons;
private int selectionOptionCount = 3;
[SerializeField] private StatHandler statHandler;
private void Awake()
{
if (instance == null)
{
instance = this;
}
else
{
Destroy(gameObject);
}
}
public void DisplaySelectionOptions()
{
//selectPanel을 활성화하고 버튼에 아이템을 랜덤으로 넣는다.
selectPanel.SetActive(true);
List<ItemSO> selectedItems = GetRandomItems(selectionOptionCount);
for (int i = 0; i < optionButtons.Count; i++)
{
optionButtons[i].SetUpButton(selectedItems[i]);
int index = i;
optionButtons[i].GetComponent<Button>().onClick.RemoveAllListeners();
optionButtons[i].GetComponent<Button>().onClick.AddListener(() => OnItemSelected(selectedItems[index]));
}
}
private List<ItemSO> GetRandomItems(int count)
{
List<ItemSO> selectedItems = new List<ItemSO>();
List<int> selectedIndexes = new List<int>();
while (selectedItems.Count < count)
{
int randomIndex = Random.Range(0, allItems.Count);
if (selectedIndexes.Contains(randomIndex) == false)
{
selectedItems.Add(allItems[randomIndex]);
selectedIndexes.Add(randomIndex);
}
}
return selectedItems;
}
private void OnItemSelected(ItemSO selectedItemSO)
{
//TODO : 아이템 선택 처리 로직
if (selectedItemSO.itemType == ItemType.StatModifier)
{
statHandler.ModifiyCharacterStat(selectedItemSO);
}
else if (selectedItemSO.itemType == ItemType.UsableItem)
{
}
Debug.Log($"Button {selectedItemSO.itemName} onClick assigned");
selectPanel.SetActive(false);
}
}
이제 플레이어의 레벨업이나 아이템을 먹을 시 DisplaySelectionOptions를 호출해주면 아이템 선택창이 다음과 같이 나오게 된다.
그러면 이제 각 아이템들의 동작을 관리해 줘야 하는데, 이것은 Dictionary
를 이용해 관리했다.
StatModifer와 UsableItem으로 크게 동작을 나누고 각각의 아이템 이름에 따라 세부적으로 기능을 나눠주었다.
각 이름별로 기능을 나눠주는 동작은 StatModifer와 UsableItem 각각 ItemToStatAssigner와 GetUsableItemAssigner에서 만들어 주었다. 이렇게 해준다면 아이템을 추가할 때, 이곳에서 기능을 작성해주면 되므로 유지보수/확장 측면에서 좋다고 할 수 있다.
using System;
using System.Collections.Generic;
using UnityEngine;
public class ItemToStatAssigner : MonoBehaviour
{
private Dictionary<string, Action<HealthStatSO, float>> statModifiers;
private void Awake()
{
statModifiers = new Dictionary<string, Action<HealthStatSO, float>>
{
{"IncreaseBulletNum", (stat, value) => stat.bulletNum += (int)value },
{"IncreaseBulletDamage", (stat, value) => stat.ATK += value },
{"IncreaseSpeed", (stat, value) => stat.speed += value },
{"IncreaseHealth", (stat, value) => stat.maxHP += value }
//StatModifier추가시 여기에 기능 추가
};
}
public void ModifyStatBasedOnItem(ItemSO itemSO, HealthStatSO currentStat)
{
if (itemSO.itemType == ItemType.StatModifier)
{
if (statModifiers.TryGetValue(itemSO.itemName, out Action<HealthStatSO, float> modifyAction))
{
modifyAction(currentStat, itemSO.stat);
Debug.Log("스탯변경 완료.");
}
else
{
Debug.Log($"{itemSO.itemName}에 대한 스탯 변경 규칙이 정의되지 않았습니다.");
}
}
}
}
using System;
using System.Collections.Generic;
using UnityEngine;
public class GetUsableItemAssigner : MonoBehaviour
{
[SerializeField] private HealthSystem playerHealthSystem;
private Dictionary<string, Action<float, HealthStatSO>> getUsableItemActions;
private void Awake()
{
getUsableItemActions = new Dictionary<string, Action<float, HealthStatSO>>()
{
{"RecoveryPotion", GetRecoveryPotion },
{"Bomb", GetBomb}
};
}
public void GetRecoveryPotion(float itemStat, HealthStatSO currentStat)
{
playerHealthSystem.ChangeHealth(itemStat);
Debug.Log("Use RecoveryPotion");
}
public void GetBomb(float itemStat, HealthStatSO currentStat)
{
// TODO : itemStat으로 폭탄의 데미지를 조정한다.
// 플레이어의 Bomb의 개수를 관리하는 곳에 Bomb +1
Debug.Log("Get Bomb");
}
}
다음은 캐릭터의 stat을 변경해주는 StatHandler이다. 다른 팀원분이 작성해주신 틀에 스탯을 변경하는 부분을 추가하였다.
using System;
using System.Collections.Generic;
using UnityEngine;
public class StatHandler : MonoBehaviour
{
[SerializeField] private HealthStatSO baseStat;
public HealthStatSO CurrentStat { get; private set; }
public ItemToStatAssigner itemToStatAssigner;
private void Awake()
{
UpdateCharacterStat();
}
private void UpdateCharacterStat()
{
CurrentStat = (HealthStatSO)baseStat.Clone();
}
//스탯 변경 추가
public void ModifiyCharacterStat(ItemSO itemSO)
{
itemToStatAssigner.ModifyStatBasedOnItem(itemSO, CurrentStat);
}
}
이렇게 어느정도 Item들을 추가하고 사용하는 방법과 아이템 구조를 확장성과 유지보수성을 고려하며 작성해 보았다.
이제 추가적으로 작업해야 하는 부분들에 대해 이야기해 보자면, 아직 아이템이 맵에 랜덤으로 생성되서 플레이어가 먹는 로직이나, 플레이어가 레벨업하는 로직이 없기 때문에 선택지가 활성화되는 순간이 없는데, 이것을 만들어 줘야 할 것이다.
그리고 아무래도 선택 시 바로 사용이 되는 아이템이 있고, Bomb처럼 유저가 이후에 자유롭게 타이밍 맞춰서 사용하는 아이템이 있으니 UseItemAssigner도 필요하다고 생각된다.
이상 2024.10.18 TIL마침.