Написал систему бонусов для ранера в Unity. Насколько правильно я это сделал?
Я новичок в unity и мне нужен совет правильно ли я делаю, и если нет то интересно что не так и как будет лучше с точки зрения архитектуры кода. Код работает.
Абстрактный класс самого бонуса
public abstract class BonusPlayer : MonoBehaviour
{
[SerializeField] private float _duration;
[SerializeField] private Sprite _icon;
[SerializeField] private AudioClip _soundEffect;
[SerializeField] private ParticleSystem _specialEffect;
protected Player Player;
private AudioSource _audioSource;
protected float Duration { get => _duration; set => _duration = value; }
protected Sprite Icon { get => _icon; set => _icon = value; }
private void Start()
{
_audioSource = GetComponent<AudioSource>();
}
private void OnTriggerEnter(Collider other)
{
if (other.TryGetComponent(out Player player))
{
Player = player;
StartBonus();
}
}
public virtual void StartBonus()
{
if (_specialEffect != null)
{
Instantiate(_specialEffect, transform.position, transform.rotation, transform);
}
if (_soundEffect != null)
{
_audioSource.clip = _soundEffect;
_audioSource.Play();
}
StartCoroutine(TimeBonusAction());
}
public IEnumerator TimeBonusAction()
{
yield return new WaitForSeconds(_duration);
StopBonus();
}
public virtual void StopBonus()
{
StopCoroutine(SoundTimer());
gameObject.SetActive(false);
}
Код уже конкретного бонуса для увеличения количества очков
public class BonusScore : BonusPlayer
{
[SerializeField] private int _bonusScore;
public override void StartBonus()
{
base.StartBonus();
Player.ShowBonusUi(Icon, Duration);
Player.BonusScore(_bonusScore);
}
public override void StopBonus()
{
base.StopBonus();
Player.BonusScoreEnd();
}
}
И код отображения иконки подобранного бонуса на экране
public class BonusUI : MonoBehaviour
{
[SerializeField] private CanvasGroup _canvasGroup;
[SerializeField] private Text _textDuration;
[SerializeField] private Image _iconBonus;
[SerializeField] private Player Player;
private float _duration;
private void Update()
{
_textDuration.text = _duration.ToString("0");
_duration -= Time.deltaTime;
if (_duration < 0)
{
_canvasGroup.alpha = 0;
}
}
private void OnEnable()
{
Player.ShowBonusIcon += ShowBonus;
}
private void OnDisable()
{
Player.ShowBonusIcon -= ShowBonus;
}
public void ShowBonus(Sprite icon, float duration)
{
_canvasGroup.alpha = 1;
_duration = duration;
_iconBonus.sprite = icon;
} }
Скрипт отображения иконки подобранного бонуса на экран, вызывается через событие в скрипте Player больше всего интересует правильно ли у меня это реализовано. Или лучше делать вывод на экран не касаясь скрипта игрока?
код класса Player
public void ShowBonusUi(Sprite icon, float duration)
{
ShowBonusIcon?.Invoke(icon, duration);
}
public void BonusScore(int bonus)
{
_scoreMultiplae = bonus;
}
public void BonusScoreEnd()
{
_scoreMultiplae = 3;
}
Ответы (1 шт):
Для начинающего не плохо. Но ответственность каждого класса размыта.
BonusPlayer, имеет данные о длительности, визуализации, сам себя собирает, активирует и деактивирует, пиццу заказывает, детей из детского сада забирает... многовато!BonusUIпо идее занимается отображением, но сам вычисляет время и работает без конечно...Player... почему он вообще занимается UI?
Нужен объект трека который можно собирать:
// collectable item model
public abstract class CollectableItem : MonoBehaviour
{
public UnityEvent OnCollect;
public void Collect ()
{
OnCollect?.Invoke();
}
}
_soundEffect и _specialEffect не нужны, потому что их можно запустить через событие, и не только их, а все что угодно, хоть настройки материала поменять, хоть вообще без ничего.
Подоранный предмет может быть чем угодно, 100gold, хоть 10xp, _duration и _icon тут тоже лишние.
То что отличает именно бафы.
public interface IBuff
{
BuffData Data { get; }
float elapsed { get; }
}
// collectable buff model
public abstract class CollectableBuff : CollectableItem
{
[SerializeField] private BuffData _data;
public BuffData Data => _data;
public IBuff Buff { get; }
}
// набор данных связан, образуя вместе единицу и передается пакетом,
// как например в UI визуализацию, поэтому нет смысла их разделять
[Serializable]
public struct BuffData
{
public string title;
public Sprite icon;
public float duration;
}
Наследники могут быть бонусами абсолютно любого типа.
// multiply all score income
public abstract class ScoreMultiplayerBuff : CollectableBuff
{
[SerializeField] private float _multilayer;
public float Multiplayer => _multilayer;
public IBuff Buff => new MultiplayerModifier(Data, _multilayer);
}
// add score to each monster kill
public abstract class KillScoreBonusBuff : CollectableBuff
{
[SerializeField] private float _amount;
public float Amount => _amount;
public IBuff Buff => new KillModifier(Data, _amount);
}
Сбор предметов это отдельная ответственность, ни как не связанная с Player чья задача бежать, прыгать и т.д.
// collect collectable items
public class Collector : MonoBehaviour
{
[SerializeField] private BuffHandler _buff;
[SerializeField] private Wallet _wallet;
[SerializeField] private Inventory _inventory;
private void OnTriggerEnter (Collider other)
{
if (other.TryGetComponent(out CollectableItem item))
{
if (item is CollectableBuff buff)
OnCollect(buff);
else if (item is CollectableCurrency currency)
OnCollect(currency);
else if (item is CollectableResources resources)
OnCollect(resources);
OnCollect(item);
}
}
private void OnCollect (CollectableBuff buff)
{
_buff.Add(buff.Buff);
}
private void OnCollect (CollectableCurrency currency)
{
_wallet.Add(currency.type, currency.amount);
}
private void OnCollect (CollectableResources resources)
{
_inventory.Add(resources.content);
}
}
У тебя бафф подбирал и обрабатывал сам себя, а BuffUI не имел с ним никакой связи. Должен быть обработчик бафов, занимающийся их жизненным циклом, а визуализация в интерфейсе и обработка эффекта уже головняк других классов, о которых обработчик и знать не знает.
// handles the life cycle of buffs
public class BuffHandler : MonoBehaviour
{
// BuffView подписывается на эти события и занимается только отображением
// Еще нужен обработчик который проверяет бафы на типы и отправляет кому нужно,
// например, если IBuff это ScoreModifier (MultiplayerModifier, KillModifier),
// то он передается ScoreHandler обрабатывающйи получение очков,
// подобно Collector который разделяет суп и мух на более верхнем уровне CollectableItem
public Action<IBuff> OnAdded;
public Action<IBuff> OnExpired;
private List<IBuff> _buffs;
public void Start ()
{
_buffs = new List<IBuff>();
}
public void Add (IBuff buff)
{
_buffs.Add(buff);
OnAdded?.Invoke(buff);
}
public void Expired (IBuff buff)
{
_buffs.Remove(buff);
OnExpired?.Invoke(buff);
}
private void Update ()
{
// for с конца поскольку истекшие бафы удаляются
for (int i = _buffs.Count-1; i > -1; i--)
{
IBuff buff = _buffs[i];
buff.elapsed += Time.deltaTime;
if (buff.elapsed >= buff.Data.duration)
Expired(buff);
}
}
}
п.с. классы типа BuffHandler, ScoreHandler, Wallet, Inventory не обязательно должны быть MonoBehaviour.
Закрытости и наследование хромает
- делать
protected F get/setсвойство дляprivate _fэто тоже самое, что поставить самому полю_fмодификатор доступаprotected, атрибут[SerializeField]на него так-же распространяется. - наследники
BonusPlayer, могут делать с полемprotected Playerчто вздумается, хотя по задумке не должны, свойство, позволяющее толькоprotected getотсутствует и поле должно бытьprivate, либо без поля и только свойствоprotected Player { get; private set; }. virtual M()метод плохая практика, из-за того, что в наследнике может не бытьbase.M(). Если базовое выполнение обязательно, то пускайM()в конце себя вызывает пустой методvirtual/abstract OnM()и у наследника не будет такого выбора, который ему не нужен.
