Making an item lookup system

Okay, so I’ve been toying with a loot system in my game. I want to be able to punch in numbers for:

  • Rarity (Trash, common, uncommon, rare)
  • Level of item (might ditch this and go with pure rarity?)
  • Type (Weapon, potion)
  • Subtype (Axe, dagger)

And get a list of (say) all axes of common rarity at level 5. Currently this works like this: rarity[1].level[5].type[1].subtype[2].heresTheItem. However I don’t get the feeling this is super efficient - even if it is very intuitive for me. (I could use a multi-dimensional array, but I hear this is less efficient.)

So I asked about, and have another solution: give every item a code at the beginning of its name. Say: 1512 for the axe above. Then, on Awake(), these codes all get added to one Dictionary that then uses a lookup system.

Personally, for my sanity, I would much rather use either the first system or some modified version of it where the rarity etc is coded into a single class. However, I’m aware that that might not be what’s best for my players. Though it would be better for modders.

What are your thoughts? Make it populate a huge and possibly confusing Dictionary in Awake()? Have a ludicrous series of nested arrays? Or some third option I’ve not thought of?

Why not just use scriptable objects to define loot tables?

Otherwise, you would use both concepts. Have a database scriptable object that holds onto a reference of everything, or use addressables of Resources to load them as desired into a collection. Then just write API methods that let you retrieve all items that match a set of conditions, ergo, a predicate.

It’s not particularly complicated either:

public class ItemDatabase : ScriptableObject
{
    [SerializeField]
    private List<Item> _allItems = new();
   
    public List<Item> GetItems(Predicate<Item> filter)
    {
        var results = new List<Item>();
       
        int count = _allItems.Count;
        for (int i = 0; i < count; i++)
        {
            var item = _allItems[i];
            bool result = filter.Invoke(item);
           
            if (result == true)
            {
                results.Add(item);
            }
        }
       
        return results;
    }
}

// usage
List<Item> items = itemDatabase.GetItems(x => x.Rarity == Rarity.SuperDuperRare);

Huh. So are you thinking I should have something like an array of scriptable objects, each holding a portion of the loot database organised (say) by type? Then I would attach this to the thing dropping the loot itself, rather than having anything central?

Check out this thread , literally from yesterday. I’ve unloaded an example in post #3 which might give you an idea for a “database”. Then you can easily apply spiney’s idea of predicates to look up for any item you’d like.

He didn’t suggest to portion your database. You want a central repository which you can easily query about.

Then I’m a little confused because of the scriptable object part. Why would I want to use that when I can just have the list in my core game file if it’s just going to be the one list in a class? Is there a benefit to doing so? I saw the other thread, but it didn’t really help me deicide what to do.

Creating and modifying items through the editor.

Oh, thanks. I already have objects for the items themselves. I’m just not sure why I would put the list in an object attached to a file instead of on a regular file. It seems like an extra step I don’t need.

Cause your items have to be referenced somewhere, they just happen to give an example doing it in an SO because that’s a common way people do it.

Like @lordofduct mentioned SOs have the advantage that I can reference them in the Inspector. You can’t do that with normal objects without writing custom editor code. Your approach will work but based off of what you’ve told us you’re reinventing the wheel. At this point anything you want to do is going to be extra work.

As for your OP…

What am I looking at here? Do you have various lists of objects that have lists of object that have lists of objects that have lists of objects?

I mean… I can think of a way to do something like this to result in the ability to write some code that looks like what you say. But I have no idea if that’s what you actually did since we literally have no idea what these objects with indexer accessors actually are.

If you could show us the actual data objects in play here maybe I can wrap my head around what it is you’re actually attempting there and make a judgement call of its efficacy.

…

Aside from that, yeah, unless your game has thousands upon thousands of objects… I’d just have a flat list and query it. It could really just be done with a List and Linq:

//* is just a stand-in for whatever type they are... I don't really care what type they are this is pseudo-code.
public class ItemData
{
    public * Rarity;
    public * Level;
    public * ItemType;
    public * SubType;
}

//elsewhere
var items= new List<ItemData>() { ... }; //list should contain all items

var matchingItems = items.Where(o => o.Rarity == 1 && o.Level == 5 && o.ItemType == 1 && o.SubType == 2);
var specificItem = items.FirstOrDefault(o => o.Rarity == 1 && o.Level == 5 && o.ItemType == 1 && o.SubType == 2);

Sure a little garbage will be generated every lookup, and it’s an O(n) operation, but… again… how big is this collection really?

Again… I don’t necessarily know what your rarity/level/type/subtype thing above is, but it likely has a much larger memory footprint since you’re creating some collection for each field you can search by. Furthermore… if those are lists, you kind of just limited yourself to properties that are integer. What if you wanted to search by name or some other non-numeric datatype? Use a dictionary? This is a lot of collections.

It’s like you setup a table on a database and then told the database to index EVERY column of the table. Why? You just inflated the size of your db… for what? Usually you index a column that is common to lookup by (like a users profile will often be looked up by userid so you’d index the userid column).

Its a set of nested 1D arrays that organise the objects first by rarity, then by level, then type, and so on. The idea was I would just punch in an ID number to get a specific list. It seemed like overkill to me, and I thought I would ask about it. If I needed a specific item it would be by number or iterating through the list.

Thanks, I didn’t think about making it a List because the items already have a scriptable object that stores such things, but Linq seems like a good idea. Would the garbage be a problem?

I haven’t had any major issues with garbage in years, and I use linq regularly.
I likely wouldn’t try using it every single frame 100s of times… but a one off query here or there isn’t an issue.

Fair enough!

Seriously, there is really no reason to write code like this. You’re only hurting yourself and your project.

Make an API for these things and make query functions to get particular ones.

Make it robust so that it can serve your entire app:

  • give me all rarity 1 items

  • give me rarity 1 items that are level 5

  • give me rarity 1 items that are level 5 or lower

  • give me rarity 1 items that are level 5 or higher

  • give me all 2-handed weapons

  • give me all 1-handed weapons with upgrade sockets

etc.

In the future if you completely replace the way data is stored in your game, nothing else in your game changes except that one class responsible for querying and returning stuff.

Writing any kind of API also helps you break the areas of responsibility up more clearly, improving the reliability and ease of working in your project.

Here are some examples of how this might look in code

List<Item> items;
items = MyItems.SelectWhere( item => item.Rarity == 1 && item.Level == 5 );
items = MyItems.SelectWhere( item => item.Rarity == 1 && item.Level <= 5 );
items = MyItems.SelectWhere( item => item.Rarity == 1 && item.Level >= 5 );
items = MyItems.SelectWhere( item => item is IWeaponItem && item.HasTag(TagEnum.TwoHanded) );
items = MyItems.SelectWhere( item => item is IWeaponItem && item.HasTag(TagEnum.OneHanded)
                                                         && item.HasTag(TagEnum.WithSockets) );

Naturally, if this is your end goal, you try to build a system that can get you to this level of flexibility.
In this particular case I’m assuming a SelectWhere method which can be made like this

public List<Item> SelectWhere(Predicate<Item> match) {
  var results = new List<Item>();
  foreach(var item in _internalCollectionOfItems) {
    if(match(item)) results.Add(item);
  }
  return results;
}

MyItems itself can be a singleton pattern, or a static library full of project-wide accessors.

It also assumes that Item is an object with the following interface

public class Item {

  string _name;
  public string Name => _name;

  int _rarity;
  public int Rarity => _rarity;

  int _level;
  public int Level => _level;

  HashSet<TagEnum> _tags;

  public bool HasTag(TagEnum tag)
    => _tags.Contains(tag);

}

Of course, I’m omitting many other methods with which you would do everything else not shown here.
But by systematically introducing queryable items, lists and sets, you open up a world of possibilities.

Edit: Forgot about the weapon