OK so as not to blow Brian’s mind away, I’ll only ask one more question for today. After our death, how do I get my player to drop everything in his inventory, except an ‘x’ number of items (which are the ‘x’ most valuable items in his inventory)? Basically I come here today seeking a system that saves ‘x’ amount of items from falling from our inventory if we die, but I want everything else to fall for ‘x’ amount of seconds before permanently disappearing from the game world
The x amount of seconds before dissappearing, I believe should be well handled in another of your topics.
You’re going to need a method in Inventory that returns all available inventory.
public IEnumerable<InventorySlot> GetAllInventoryItems() =>
slots.Where(s => s.item != null && s.number > 0).OrderByDescending(s => s.item.GetPrice());
This creates an IEnumerable of all active InventorySlots, sorted from most expensive to least expensive based on the item’s price. If you wanted to value the entire slot (say 10 potiions @ 10 each might be worth more than one dagger @ 50), then you could use
public IEnumerable<InventorySlot> GetAllInventoryItems() =>
slots.Where(s => s.item != null && s.number > 0).OrderByDescending(s => s.item.GetPrice() * s.number); //would value the entire slot as one larger price.
Then I would put this method in RandomDropper:
public void DropInventoryItems()
{
Inventory inventory = Inventory.GetPlayerInventory();
int keep = 0;
foreach (Inventory.InventorySlot inventorySlot in inventory.GetAllInventoryItems().ToArray())
{
if (keep < numberToKeep) continue;
keep++;
inventory.RemoveItem(inventorySlot.item, inventorySlot.number);
DropItem(inventorySlot.item, inventorySlot.number);
}
}
What this does is first converts the IEnumerable to an Array. The reason for this is that as soon as you apply .ToArray() or .ToList() to a collection, it creates a copy of the collection in the heap. Were you to simply iterate over the collection without converting it to an array, then you would get a runtime error when you removed items from the Inventory because you would be messing with the collection while it was being iterated over.
It then has a counter that you’ll compare to the number you want to keep. Best to make that a SerializeField in RandomDropper. Once it’s exceeded the number to keep, the remainder of the items are removed from the Inventory and dropped (creating a pickup, which from a previous topic, we know should destroy itself after a designated amount of time).
Fair warning: Some players will not appreciate this mechanic. That being said, some people will enjoy the challenge, as long as it’s achieveable to get back to the items.
OK I’m not getting any compiler errors, but it’s not working (Player is not dropping anything at death)… My guess is, this method is not being called yet, right? (P.S: I integrated the serialized variable of ‘numberToKeep’, but it’s not dropping anything to begin with)
And yes, I’m aware some players might hate that, but that’s the nature of the game. If it’s not challenging, where’s the fun in playing it?
Edit 1: I tried applying the method to the ‘onDie’ Unity Event, and… nope, still not working
Edit 2: Can we integrate the Player money pouch and Equipment Slots in the system too?
What are you linking in the OnDie Unity Event? Should be RandomDropper | DropInventoryItems…
Let’s not complicate things just yet…
Yup, that’s what I’m doing. I went to the Players’ OnDie Unity Event, linked the Player himself (since he has a RandomDropper.cs file on him anyway), and picked ‘RandomDropper.DropInventoryItems()’ in there, and it didn’t work for some reason (I tried that in both the Scene and Asset version of the player, but it’s not working)…
OK Here’s a little Update:
I did a little bit of Debugging, and the function works, but I’m not exactly sure of the logic of the ‘if’ statement (I did comment it out to get the function to work). This one to be specific:
For some reason, it’s stopping the function from working, and I can’t get my player to leave any expensive loot left over in his bag either for some reason… (but the onDie referring to it works perfectly fine)
My solution was this line:
if (keep > numberToKeep) break;
But for some reason, I can’t keep 3 items on death (it did work for a second, then it didn’t work, and I have no idea why)
The list is sorted according to value, with the most valuable items coming first.
I do realize what I did wrong, though… I incremented keep AFTER checking it, and with continue, the rest of the block doesn’t run.
Move the keep++ to the line before if keep
keep++;
if(keep <= numberToKeep) continue; // <= because at this point, on the first item, keep will now be 1
Yup, that fixed it (for a moment I thought it made the death scenario terrible, mainly because of the messed up AggroGroup I currently have). Thanks again Brian
(Can we try integrating the equipment and pouch now? If we are diving into the equipment though, we might want to consider integrating them into measuring the total value of everything we have in both the equipment and inventory slots, so we know what remains in the end (if some equipment remain as they are valuable, we want to send them to the inventory instead of keeping them on the player [post-death]))
The dropping inventory after death was relatively easy… Mixing the Equipment and the Inventory requires a different mechanism that will know WHERE the things I sorted came from. The solution above was about 20 minutes from rough draft to finished methods… This new ask will likely take a few hours of time, and that’s before posting and inevitable redrafts for corner cases I might have missed (or typos). For now, with so many unresolved issues in your other threads, I’m loath to dive into this one.
in the meanwhile, is there any chance we can give this a go anytime soon? Please? The game is more stable than it was 4 days ago now
I just want the death system to count the equipment to drop them as well, don’t bother with the pouch (if that helps in anyway)
It doesn’t.
We’ll need a structure that includes the SOURCE of the item in addition to the item itself, and each container that can drop items will have to provide an enumeration of it’s items within these structs, then the structs will need to be sorted, and as we drop each thing, we’ll need to go to the source from the struct and remove it.
I’ll work on it, then we’ll spend two days implementing it, but when I can.
oh wow that sounds like quite a challenge… Sure, I’ll be waiting for that amazing day
Well, considering I’m done with the new JSON system, and I fixed a few major bugs, can we PLEASE try this out now?
Please be patient and remember my priorities. Without revealing too much personal info, this weekend will be a challenge just to keep up with the directly course related issues due to family emergency.
I’ll take that time as an opportunity to start developing a small part of my game world then, at least the tutorial island perhaps. If I ask more questions now, I risk forgetting these functions
Hope your family emergency ends well by the way
hi @Brian_Trotter can we kindly bump this thread? I still have my eyes on it, but my hands are on the crafting system for the moment (or if you’re ready to start it, by all means we can as well)
You actually did bump it by replying to it.
I’ll see what I can do this weekend.
Wonderful - I’ll be waiting
hi again Brian, just a heads-up before we start working on this. Do you recall the bank transfer issue, where a huge inventory massively slowed down my bank transfer operations? Yup, that issue exists with my inventory item drop on death as well (because it’s an inventory transfer to the ground as well, now that I think of it. Although it’s not as bad as the bank transfer, but it’s still significant), and will probably exist with the equipment transfer on death, be it to the ground or the inventory
Just letting you know this exists (and I can use some help with the inventory drop UI refresh issue, as it’s currently a bit too slow. I tried following up with the inventory.cs code, but I really tend to lose focus quickly ). For the moment, I’m trying to speed up my Inventory Drop algorithm by placing this line in our little algorithm:
if (inventorySlot.item == null) continue;
so now it looks like this (P.S: it’s in ‘RandomDropper.cs’):
public void DropInventoryItems() {
Inventory inventory = Inventory.GetPlayerInventory();
int keep = 0;
foreach (Inventory.InventorySlot inventorySlot in inventory.GetAllInventoryItems().ToArray()) {
keep++;
if (inventorySlot.item == null) continue;
if (keep <= numberToKeep) continue;
inventory.RemoveItem(inventorySlot.item, inventorySlot.number);
DropItem(inventorySlot.item, inventorySlot.number);
}
}
But it still lags quite a bit. Can you please help me fix it on the fly as well?
Ok, so this is going to start with a bit of rework… as what we want is a way to decide what items to keep based on a collection of items from both the Inventory and the EquipableItems, and we want it to be source-neutral, based on the value of the items.
So here’s the information I think is relevant…
- The Item
- The number, in the event it’s a stack.
- The relative value of the item. For now, we’re simply going to use price, but this could be affected by things like the item’s level, stats, etc.
- A callback to remove the item.
using System.Collections.Generic;
namespace GameDevTV.Inventories
{
public struct DroppableItem
{
public InventoryItem item;
public int number;
public float relativeValue;
public System.Action dropItemCallback;
}
public interface IDroppableItemHolder
{
IEnumerable<DroppableItem> GetDroppableItems();
void TriggerUpdated();
}
}
So here we have a struct with the things I’ve mentioned. Note that the dropItemCallback is simply a System.Action. We’re going to use a special Lambda expression also known as a closure to satisfy this, as we want the selection process to be source neutral.
The interface will go on the Equipment and Inventory methods, which will require these classes to implement the method GetDroppableItems().
So let’s look at Inventory’s GetDroppableItems()
public IEnumerable<DroppableItem> GetDroppableItems()
{
for(int i=0;i<inventorySize;i++)
{
if (slots[i].item == null || slots[i].number <= 0) continue;
int slotNum = i;
DroppableItem droppableItem = new DroppableItem()
{
item = slots[i].item,
number = slots[i].number,
relativeValue = slots[i].item.GetPrice(),
dropItemCallback = () =>
{
slots[slotNum].item = null;
slots[slotNum].number = 0;
}
};
yield return droppableItem;
}
}
So what we’re doing is going through each slot one by one and determining if there is anything in it. If so, a DropableItem is created with the appropriate fields set.
Now for the magic part, the closure…
The beauty of Lambda expressions is that you can capture temporary information now within the scope and call the method later. There is a restriction to this, however. For example, you cannot use
slots[i].item=null;
slots[i].number=0;
Because i is an iterater outside of the scope that changes. The solution to this is to copy i into it’s own variable inside the scope with
slotnum=i;
Then we can use
slots[slotNum] = null;
slots[slotNum] = 0;
It’s an easy restriction to miss, and not all code editors will catch the issue.
We also need to implement TriggerUpdated(), which will be called by our RandomDropper after we drop the items. We need to do this, because the way we’re removing the Inventory Items, we’re NOT triggering inventoryUpdated. This is to prevent lots of redraw calls in the UI.
public void TriggerUpdated()
{
inventoryUpdated?.Invoke;
}
We’ll take care of Equipment’s interface functions after we’ve tested the system a bit… (actually, that will be your challenge, but we’ll get to that at the end of this).
For now, just put these methods in Equipment (along with adding IDroppableItemHolder to the interfaces:
IEnumerable<DroppableItem> GetDroppableItems()
{
yield break;
}
public void TriggerUpdated()
{
equipmentUpdated?.Invoke();
}
Now we need to head to RandomDropper.cs
What we need in RandomDropper is to
- Gather all IDroppableItemHolders
- Collect the IDroppableItemsHolders DroppableItems
- Sort the items from highest value to the lowest value
- Keep the desired number of items to keep
- Drop the rest and remove them from their respective holders with dropItemCallback?.Invoke()
- Trigger the updates after all items have dropped.
So let’s take these one maybe two at a time…
- Gather all the IDroppableItemHolders
- Collect the IDroppableItemsHolders DroppableItems
We’re actually going to use an IEnumerable for this that our main function will call.
IEnumerable<DroppableItem> GetAllDroppableItems()
{
foreach (IDroppableItemHolder itemHolder in GetComponents<IDroppableItemHolder>())
{
foreach (DroppableItem droppableItem in itemHolder.GetDroppableItems())
{
yield return droppableItem;
}
}
}
This method makes it so that no matter how many IDroppableItemHolder components you have, they are all gathered into one IEnumerable, and because we’ll be using the special callback closures, RandomDropper does not know (or care) where they come from.
Next up:
- Sort the items from highest value to the lowest value
For this, it’s time to get to our public method that we’ll be linking to OnDeath:
public void DropIDroppableItems()
{
foreach (DroppableItem droppableItem in GetAllDroppableItems().OrderByDescending(d=>d.number*d.relativeValue))
{
}
}
We’re getting all of the items from the GetAllDroppableItems() method and we’re sorting them by their value. That sets us up to drop the items we want, using some of the logic from the code in my original draft.
- Keep the desired number of items to keep
- Drop the rest and remove them from their respective holders with dropItemCallback?.Invoke()
public void DropIDroppableItems()
{
int keep = -1;
foreach (DroppableItem droppableItem in GetAllDroppableItems().OrderByDescending(d=>d.number*d.relativeValue))
{
keep++;
if (keep < numberToKeep) continue;
droppableItem.dropItemCallback?.Invoke(); //Remove the items
DropItem(droppableItem.item, droppableItem.number);
}
}
Notice the droppableItem.dropItemCallback?.Invoke(); line. This call the method that was passed into the DroppableItem struct, and thanks to the magic of closures, the calling method doesn’t need to know anything about where the item came from or how to remove it. The closure handles this for us.
Now there’s just one last thing to do, to invoke the Updates… for that, we’re going to write another helper method in DroppableItem
- Trigger the updates after all items have dropped.
void TriggerDroppableUpdates()
{
foreach (IDroppableItemHolder holder in GetComponents<IDroppableItemHolder>())
{
holder.TriggerUpdated();
}
}
This simply gathers all of the IDroppableItemHolders and calls their TriggerUpdated() methods, which, of course, calls the respective updated events. By doing this instead of making remove calls that invoke the event, we’ll save a great deal of create and destroy cycles.
Here are the final methods for RandomDropper
public void DropIDroppableItems()
{
int keep = -1;
foreach (DroppableItem droppableItem in GetAllDroppableItems().OrderByDescending(d=>d.number*d.relativeValue))
{
keep++;
if (keep < numberToKeep) continue;
droppableItem.dropItemCallback?.Invoke(); //Remove the items
DropItem(droppableItem.item, droppableItem.number);
}
TriggerDroppableUpdates();
}
void TriggerDroppableUpdates()
{
foreach (IDroppableItemHolder holder in GetComponents<IDroppableItemHolder>())
{
holder.TriggerUpdated();
}
}
IEnumerable<DroppableItem> GetAllDroppableItems()
{
foreach (IDroppableItemHolder itemHolder in GetComponents<IDroppableItemHolder>())
{
foreach (DroppableItem droppableItem in itemHolder.GetDroppableItems())
{
yield return droppableItem;
}
}
}
Now for your challenge: Further up this post, I left a stub for Equipment.GetDroppableItems()
public IEnumerable<DroppableItem> GetDroppableItems()
{
yield break;
}
Your challenge is to flesh out Equipment’s GetDroppableItems…
Here are a few hints:
- Instead of a for(int i= style loop, you’ll need to do a foreach loop over the equippedItems. A further hint on this hint, if you do a foreach on equippeditems, the type you’ll be iterating over is a
KeyValuePair<EquipSlot, EquipableItem>
- You’ll need to construct a DroppableItem for each item, the item will be the Value of the pair you’re iterating over. The number will be 1, The value will be the pair’s Value.GetPrice()
- The tricky part is the dropItemCallback. You can’t use the pair.Key because that’s the iterator, even though the Key is the EquipSlot, so like the Inventory, you’ll need to create a copy of the key to use in the dropItemCallback. You also don’t want to use Equipment.Remove() because that will likely trigger an update. Instead, you’ll want to use the equippedItems.Remove() passing in the equipslot you copied from the Key.