Сергей Морозов

Программист на Unity из Нижнего Новгорода.

Делаю мобильные игры

У меня задача стояла такая - я хотел иметь рандомные характеристики айтемов. Например, меч с уроном 1-2, где 1-2 это не минимальная и максимальная атаки, а рендж для генерации урона.

То есть когда игрок получал лут с этим айтемом - ему добавлялся либо меч либо с уроном 1, либо меч с уроном 2.

Прототип (меч 1-2) определялся такой моделькой:

public interface IItemDefinition  
{  
    string ID { get; }  
    LocalizedString Title { get; }  
    LocalizedString Description { get; }  
    AssetReferenceSprite Icon { get; }  
    ItemCategory ItemCategory { get; }  
    ItemAttributeCollection ItemAttributeCollection { get; }  
}

public interface IItemAttributeCollection  
{  
    IReadOnlyList<ItemAttribute> ItemAttributes { get; }  
}

ItemCategory - пустой абстрактный класс, аналог enum. Не уверен в его необходимости, но пока не удалил.

Сам айтем (конкретный меч с уроном 1, например) определялся так:

public interface IItemInstance  
{  
    LocalizedString Title { get; }  
    LocalizedString Description { get; }  
    AssetReferenceSprite Icon { get; }  
    IItemDefinition ItemDefinition { get; }  
    ItemAttributeCollection ItemAttributeCollection { get; }  
}

ItemAttributeCollection в IItemInstance представляли собой только реальные статы, а в IItemDefinition - статы генерации + реальные статы.

Например, реальный стат:

public class HealthCombatStatAttribute : CombatStatAttribute<HealthCharacterCombatDataStat>  
{  
    public override CharacterCombatDataStatType CharacterCombatDataStatType => CharacterCombatDataStatType.Health;  
}

public abstract class CombatStatAttribute<T> : ItemAttribute, ICombatStatAttribute where T : CharacterCombatDataStat, new()  
{  
    public abstract CharacterCombatDataStatType CharacterCombatDataStatType { get; }  
  
    public T Value = null!;  
  
    public CharacterCombatDataStat CharacterCombatDataStat => Value;  
}  

Причем валюты тоже определялись в рамках системы как айтемы с особым атрибутом:

public class CurrencyItemAttribute : ItemAttribute
{
    public CurrencyDefinition CurrencyDefinition = null!;
}

А вот статы генерации:

public class HealthCombatStatRangeAttribute : CombatStatRangeAttribute<HealthCharacterCombatDataStat>  
{  
    public override CharacterCombatDataStatType CharacterCombatDataStatType => CharacterCombatDataStatType.Health;  
  
    public override ItemAttribute GetFinalVersion() => new HealthCombatStatAttribute {Value = new HealthCharacterCombatDataStat {Value = Random.Range(MinValue.Value, MaxValue.Value)}};  
}

public abstract class CombatStatRangeAttribute<T> : ItemAttribute where T : CharacterCombatDataStat  
{  
    public abstract CharacterCombatDataStatType CharacterCombatDataStatType { get; }  
  
    [JsonProperty]  
    public T MinValue = null!;  
  
    [JsonProperty]  
    public T MaxValue = null!;  
}  

Конвертация статов генерации ItemDefinition в реальные статы ItemInstance происходила через метод GetFinalVersion

public abstract class ItemAttribute  
{  
    public virtual ItemAttribute GetFinalVersion() => this.CloneJson();  
}

Иерархия данных у статов была такой:

public class HealthCharacterCombatDataStat : IntCharacterCombatDataStat  
{  
    public override CharacterCombatDataStatType CharacterCombatDataStatType => CharacterCombatDataStatType.Health;  
    public override CharacterCombatDataStat WithBonus(CharacterCombatDataStat characterCombatDataStat)  
    {        return new HealthCharacterCombatDataStat  
        {  
            Value = Value + ((HealthCharacterCombatDataStat) characterCombatDataStat)!.Value  
        };  
    }
}


public abstract class IntCharacterCombatDataStat : CharacterCombatDataStat<int>  
{  
}

public abstract class CharacterCombatDataStat<T> : CharacterCombatDataStat where T : struct  
{  
    public T Value;  
}
public abstract class CharacterCombatDataStat  
{  
    public abstract CharacterCombatDataStatType CharacterCombatDataStatType { get; }  
    public abstract CharacterCombatDataStat WithBonus(CharacterCombatDataStat value);  
}

С виду всё это кажется довольно сложным, но в этом есть логика. Какие-то статы типа Health задаются интом, какие-то типа CriticalMultiplier - флоатом, и могут быть замороченные типа AOEBonusDamage с интом MaxTargets и вторым интом BonusDamage.

Вся эта система мне и сейчас кажется логичной и гибкой, не нравится мне в ней только енум CharacterCombatDataStatType и абстрактные классы вместо интерфейсов. Посмотрим как это всё изменится после рефакторинга.