Předměty alias ScriptableObjects. Log #15 (Unity, C#)
28.02.2018

    Na chvíli odbočím od inventáře a zaměřím se na část, která s tím ale hodně souvisí. A to jsou předměty ve hře. Abych mohl dále rozšiřovat inventář, budu už potřebovat nějaké předměty a nějaký systém jak s něma pracovat. V tomhle logu vysvětlím jen způsob, jak předměty budu vytvářet v Unity a v dalších článcích popíšu tvorbu modelu v Blenderu a následně zprovozním interakci s objekty, potom se k inventáři zase vrátím. Hodně dlouho jsem měl systém tvorby předmětů v Unity velice nešťastně řešený, dokud jsem nenarazil na ScriptableObjects.

    Jednoduše řečeno se jedná o kontejner pro data nějakého objektu. Například pokud mám předmět, který má jakékoliv statistiky jako jméno, sílu, obranu a jiné vlastnosti, ScriptableObject mi tyto data bude uchovávat a umožní mi z nich vytvářet instance s těmito daty. Je to pro mě tedy jakási šablona předmětu. Další výhodou je, že ScriptableObjects přímo souvisí s Unity editorem a umožní mi vytvářet předměty přímo pomocí kontextového menu. Využívám k tomu utilitku na kterou jsem narazil na internetu, a která hodně usnadňuje tvorbu předmětů. Klíčem této utilitky je generická funkce, která umožňuje vytvářet předměty předaného typu přímo v editoru v Assets adresáři.

 

using UnityEngine;
using UnityEditor;
using System.IO;

public static class ScriptableObjectUtility
{
    /// <summary>
    //	This makes it easy to create, name and place unique new ScriptableObject asset files.
    /// </summary>
    public static void CreateAsset<T>() where T : ScriptableObject
    {
        T asset = ScriptableObject.CreateInstance<T>();

        string path = AssetDatabase.GetAssetPath(Selection.activeObject);
        if (path == "")
        {
            path = "Assets";
        }
        else if (Path.GetExtension(path) != "")
        {
            path = path.Replace(Path.GetFileName(AssetDatabase.GetAssetPath(Selection.activeObject)), "");
        }

        string assetPathAndName = AssetDatabase.GenerateUniqueAssetPath(path + "/New " + typeof(T).ToString() + ".asset");

        AssetDatabase.CreateAsset(asset, assetPathAndName);

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        EditorUtility.FocusProjectWindow();
        Selection.activeObject = asset;
    }
}

 

    Tuhle třídu je možné zkopírovat jak je a vložit ji do adresáře Assets do složky Editor bez jakýchkoliv úprav. Pro vytvoření předmětu, pak stačí vytvořit script a do něj dát jen krátký kód:

 

using UnityEditor;

public class ItemAsset
{
    [MenuItem("Assets/Create/Item/Item")]
    public static void CreateAsset()
    {
        ScriptableObjectUtility.CreateAsset<Item>();
    }
}

 

    Řádek [MenuItem("Assets/Create/Item/Item")] je cesta, kde najdu svůj předmět když v editoru kliknu pravým a vyberu "Create". Ve funkci CreateAsset() volám generickou funkci CreateAsset<T>(), kterou jsem "vytvořil" výše a jako typ předám třídu, kterou chci vytřvořit (tahle třída pochopitelně musí existovat). Jak tyhle třídy musí vypadat? K tomu se teď dostanu.

    Prakticky může vypadat jakkoliv, je jen jedno pravidlo - a to, že nesmí dědit z MonoBehaviour. Protože se jedná o ScriptableObject, ze kterého tato třída dědí, nemůže pochopitelně dědit z další třídy. Můj systém předmětů bude následující - Item bude třída nadřazená všem předmětům ve hře, ta bude dědit ze ScritpableObject a od ní pak budu odvozovat další třídy. Předpokládám že principy dědičnosti nemusím vysvětlovat, tak budu pokračovat. Příklad uvedu třeba na nástroji krumpáče. Tedy třída Pickaxe je nástroj, proto bude dědit z třídy Tool a protože každý nástroj ve hře, bude zároveň i zbraň, tak z toho vyplývá, že Tool dědí ze třídy Weapon. Zbraně jsou vybavením a budou tedy dědit ze třdy Equipment a nakonec protože každé vybavení je předmět, budu dědit z Item. Každá tato třída nese proměnné, které definují vlastnosti objektu, ty pak přiřazuji ve funkci Instantiate() a hodnoty předávané jako parametry zadávám v inspektoru po vytvoření daného ScriptableObject. Níže je celá uvedená hierarchie.

 

using UnityEngine;

public class Pickaxe : Tool {

    [Header("Pickaxe")]
    public int miningStrength;
    public int miningSpeed;

    public void Instantiate(string itemName, string itemDescription, string itemIconName, string itemPrefabName, int attackStrength, int attackSpeed, int miningStrength, int miningSpeed)
    {
        Instantiate(itemName, itemDescription, itemIconName, itemPrefabName, attackStrength, attackSpeed, true, ToolType.Pickaxe);
        this.miningStrength = miningStrength;
        this.miningSpeed = miningSpeed;
    }
}
using UnityEngine;

public class Tool : Weapon
{
    [Header("Tool")]
    public ToolType toolType;

    public void Instantiate(string itemName, string itemDescription, string itemIconName, string itemPrefabName, int attackStrength, int attackSpeed, bool twoHandedWeapon, ToolType toolType)
    {
        Instantiate(itemName, itemDescription, ItemType.Tools, itemIconName, "Tools/" + itemPrefabName, attackStrength, attackSpeed, false, twoHandedWeapon);
        this.toolType = toolType;
    }
}

public enum ToolType
{
    None,
    Axe,
    Pickaxe
}
using UnityEngine;

public class Weapon : Equipment
{
    [Header("Weapon")]
    public int attackStrength;
    public int attackSpeed;
    public bool longRangedWeapon;
    public bool twoHandedWeapon;

    public void Instantiate(string itemName, string itemDescription, ItemType itemType, string itemIconName, string itemPrefabName, int attackStrength, int attackSpeed, bool longRangedWeapon, bool twoHandedWeapon)
    {
        Instantiate(itemName, itemDescription, itemType, itemIconName, "Weapons/" + itemPrefabName, EquipmentType.RightHand, true);
        this.attackStrength = attackStrength;
        this.attackSpeed = attackSpeed;
        this.longRangedWeapon = longRangedWeapon;
        this.twoHandedWeapon = twoHandedWeapon;
    }

}
using UnityEngine;

public class Equipment : Item
{
    [Header("Equipment")]
    public EquipmentType equipmentType;
    public bool isWeapon;

    public void Instantiate(string itemName, string itemDescription, ItemType itemType, string itemIconName, string itemPrefabName, EquipmentType equipmentType, bool isWeapon)
    {
        Instantiate(itemName, itemDescription, itemType, itemIconName, itemPrefabName, true);
        this.equipmentType = equipmentType;
        this.isWeapon = isWeapon;
    }
}

public enum EquipmentType
{
    None,
    Head,
    Chest,
    Legs,
    Feet,
    RightHand,
    LeftHand,
    Lenght
}
using UnityEngine;

public class Item : ScriptableObject
{
    [Header("Item")]
    public string itemName;
    public string itemDescription;
    public ItemType itemType;
    public Sprite itemIcon;
    public GameObject itemPrefab;
    public bool isEquipment;

    public void Instantiate(string itemName, string itemDescription, ItemType itemType, string itemIconName, string itemPrefabName, bool isEquipment)
    {
        this.itemName = itemName;
        this.itemDescription = itemDescription;
        this.itemType = itemType;
        itemIcon = Resources.Load<Sprite>("UI/Icons/Items/" + itemIconName);
        itemPrefab = Resources.Load<GameObject>("Objects/Items/" + itemPrefabName);
        this.isEquipment = isEquipment;
    }

}

public enum ItemType
{
    Weapons,
    Armor,
    Tools,
    Potions,
    Books,
    Goods
}

 

    Řádek kde je [Header("Item")] v inspektoru vytvoří nadpis nad proměnnou nad kterou je uveden, respektive nad blokem, dokud není uveden další header. Slouží to k rozeznání jaké vlastnosti patří které třídě. Pak public enum ItemType nebo jiný enum v těchto třídách slouží k rozdělení předmětů podle typu, toho využiji později například v inventáři. Některé vlastnosti jsem schopný určit už ve funkci Instantiate(), například pro ItemType itemType vím, že nástroj je ItemType.Tool a nemusím jej vyplňovat v inspektoru. Vytvoření ScriptableObject pak vypadá v inspektoru takhle.

 

Scriptable object

 

    Tímhle jsem si vytvořil pevný základ pro předměty. Stejně vytvořím třídy jako Sword, Armor a podobně a definuji jim vlastnosti. V dalším díle vytvořím model v Blenderu a vložím ho do scény.