Inventory UI acting strange in updating amounts - c#

So I have an item class, an inventory class and the UIController scripts. When the player goes over the item on the field the player should have the item added to the inventory, if he/she has the item it should increase the amount by one.
The issue I'm having is the numbers on the UI are random and sometimes items add amounts other times they just use a new slot. I have added the three scripts I mentioned above.
public Item()
{
amount = 0;
}
public int GetItemAmount()
{
return amount;
}
public void IncreaseAmount(int amt)
{
amount += amt;
}
public void SpawnItem()
{
Instantiate(prefab);
}
public Inventory()
{
inventory = new List<Item>();
}
// Add item to inventory. if item is already in inventory, increase amount.
public void AddItem(Item item)
{
if (inventory.Count == 0)
{
inventory.Add(item);
}
else
{
for (int i = 0; i < GetInventory().Count; i++)
{
if (this.inventory[i] == item)
{
inventory[i].IncreaseAmount(1);
break;
}
else
{
inventory.Add(item);
}
break;
}
}
}
void Start()
{
player = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerController>();
inventoryItems = CreateInventoryItemUI(itemSlotImage);
}
private void Update()
{
var playerItems = player.inventory.GetInventory();
for (int i = 0; i <= playerItems.Count ; i++)
{
//Debug.Log("The value of i is : " + i);
if (playerItems[i] != null)
{
inventoryItems[i].GetComponentInChildren<Image>().sprite = playerItems[i].imageIcon;
var amountUI = inventoryItems[i].transform.Find("ItemSlotAmount").GetComponent<Text>();
amountUI.gameObject.SetActive(true);
amountUI.text = playerItems[i].GetItemAmount().ToString();
Debug.Log("The items being added will be: " + amountUI.text);
inventoryItems[i].gameObject.SetActive(true);
}
}
}
I think I just another set of eyes. I debugged and checked reference of the increase amount method and it's only used in the inventory script.
Any ideas would be very helpful.

You are checking if the added Item is exactly the same object as one in the list, as in the same reference. I'm assuming this is probably not what you want, please post the part of the code calling AddItem otherwise.
Two suggestions:
First, add an Id property to Item and compare if the item being added has the same Id, instead of comparing the references.
Second, this would probably be cleaner and more efficient if you used a Dictionary instead of a List for storing the items. Give it a try!

Related

Need to change values in classes without changing its reference

I am currently making an application which tracks information on players and monsters for a tabletop game.
Currently I've got classes for "Monsters". This class contains information such as its name, maxHP, speed, attacks etc. I've managed to make a Database which contains the default values for each type of monster. What I currently need to do is make it possible to change things such as name (Monster > Monster 1, Monster 2 etc), change its HP, and some other things.
I understand that I need to make a copy of such, but I am uncertain on how to do this.
What I currently tried is the following:
public class DatabaseService
{
public List<Player> Players { get; set; }
public List<Monster> AllMonsters { get; set; }
public List<Monster> ActiveMonsters = new List<Monster>();
public bool RollForHP = false;
//Main Database Service
public DatabaseService()
{
Players = GetPlayers();
AllMonsters = GetAllMonsters();
}
public void DoStuff()
{
AddMonsterByName("Goblin", 2);
AddMonsterByName("Adult White Dragon", 1);
AddMonsterByName("Duergar", 4);
foreach (Monster monster in ActiveMonsters) { Console.WriteLine(monster.Name); }
}
//Converts the .json list with all players to Classes, which are then stored in the list "Players" if the "IsInParty" is true
private List<Player> GetPlayers()
{
var path = #"C:\Users\MyName\source\repos\DndAdvancedInitiativeTracker\Logic\Database\Players.json";
var json = File.ReadAllText(path);
var players = JsonConvert.DeserializeObject<List<Player>>(json);
List<Player> inPartyPlayers = new List<Player>();
foreach (var player in players)
{
if (player.IsInParty == true) { inPartyPlayers.Add(player); }
}
return inPartyPlayers;
}
//Converts the .json list with all monsters to Classes, which are then stored in the list "AllMonsters"
private List<Monster> GetAllMonsters()
{
var path = #"C:\Users\MyName\source\repos\DndAdvancedInitiativeTracker\Logic\Database\Monsters.json";
var json = File.ReadAllText(path);
var monsters = JsonConvert.DeserializeObject<List<Monster>>(json);
return monsters;
}
//Adds a given monster to the "ActiveMonsters" list
public void AddMonsterByName(string monsterName, int amountOfMonsters)
{
for (int i = 0; i < amountOfMonsters; i++)
{
List<Monster> DatabaseCopy = AllMonsters.Clone();
DatabaseCopy = AllMonsters;
Monster monster = DatabaseCopy.Find(x => x.Name == monsterName);
Console.WriteLine(monster.Name);
var number = CheckIfNameExistsInList(monsterName);
monster.Name = monsterName + " " + (number + i).ToString();
ActiveMonsters.Add(monster);
}
}
private int CheckIfNameExistsInList(string monsterName)
{
var counter = 1;
foreach (var monster in ActiveMonsters)
{
if (monster.Name.Contains(monsterName))
{
counter += 1;
}
}
return counter;
}
}
In the "DoStuff" Method, I try to add 2 goblins, then a dragon, then a goblin again. The first goblin is named to "Goblin 1" correctly, but the second loop fails, because the AllMonsters' name for goblins is now "Goblin 1" because of the reference type, therefore, the second "Goblin" search in AllMonsters is never found, and returns null.
I'm not sure why you're copying your database (and doing so for every iteration of a for loop which is quite wasteful), but your current check code CheckIfNameExistsInList will always return 1 even if there are no matches in the list.
You can simplify your AddMonstersByName (and use a simple check for previous monster entries) as follows:
public void AddMonstersByName(string name, uint number = 1)
{
var count = AllMonsters.Count(x => x.Name.Contains(name));
for (var i = 1; i <= number; i++)
{
var num = count + i;
AllMonsters.Add(new Monster(){Name= name+num.ToString()});
}
}
This was tested in a simple Console App:
var dataService = new DataService();
dataService.AddMonstersByName("Goblin", 2);
dataService.AddMonstersByName("Dragon", 2);
dataService.AddMonstersByName("Goblin", 2);
foreach (var monster in dataService.AllMonsters)
{
Console.WriteLine($"{monster.Name}");
}
where
public class DataService
{
public List<Monster> AllMonsters = new List<Monster>();
public void AddMonstersByName(string name, uint number = 1)
{
var count = AllMonsters.Count(x => x.Name.Contains(name));
for (var i = 1; i <= number; i++)
{
var num = count + i;
AllMonsters.Add(new Monster(){Name= name+num.ToString()});
}
}
}
public class Monster
{
public string Name { get; set; }
}

Creating stackables in my inventory system

While attempting to build an inventory system including stackables. The inventory system works great, except for the stackables part.
Right now, I've hit a full stop due to an infinite loop.
Here's the code, with comments, so hopefully you'll be able to understand what I was trying to accomplish.
void AddItem(int id, int count)
{
while(count > 0) // Continue running as long as there is an item to be added
{
for(int i = 0; i < inventory.Count; i++) // Search through the inventory
{
if(inventory[i].itemID == id) // We've found an item with the appropriate ID
{
int maxAdd = inventory[i].itemMaxStack - inventory[i].itemCurrStack; // Figure out how much we can add to this stack
if(count > maxAdd) // There's not enough room to fit the entire stack, so add what we can and continue the -for- loop
{
inventory[i].itemCurrStack = inventory[i].itemMaxStack;
count -= maxAdd;
continue;
}
if(count <= maxAdd) // There's enough room to fit the entire stack, so add it in.
{
inventory[i].itemCurrStack += count;
count = 0;
}
if(inventory[i].itemCurrStack == inventory[i].itemMaxStack) // We found a stack, but it's already full, so continue the -for- loop
{
continue;
}
} else if(inventory[i].itemName == null) // There were no items with the specified ID, so let's create one.
{
for(int j = 0; j < database.items.Count; j++)
{
if(database.items[j].itemID == id)
{
inventory[i] = database.items[j];
break; // Break out of the -for- loop, since we've found what we're looking for.
}
}
}
}
}
}
You don't really need to do this with a loop. You can just stack the difference between the source and the target's max, and if there's any left, just dump it in the inventory as a new stack.
Edit: Added a little more plumbing to clarify, based on your comment. This is significantly different than your original question, but it demonstrates my point.
public abstract class Loot
{
public int Count { get; set; }
public virtual int MaxCount { get; set; }
}
public class Inventory : ICollection<Loot>
{
public void Stack(Loot source, Loot target)
{
var availableOnTarget = target.MaxCount - target.Count;
var amountToStack = Math.Min(availableOnTarget, source.Count);
target.Count += amountToStack;
source.Count -= amountToStack;
if (target.Count == target.MaxCount && source.Count > 0)
{
this.Add(source);
}
}
// ICollection implementation...
}
// This could be in Inventory, or the Player, or a gameplay manager...
// Personally I'd implement it in the Inventory class, if there was only
// one player with only one inventory. I'm sticking to the semantics of
// my first version, though.
public class Caller
{
public void TryAddItemToInventory<TLoot>(Inventory inventory, TLoot itemToAdd) where TLoot:Loot
{
var sourceType = itemToAdd.GetType();
var stackTarget = inventory.OfType<TLoot>().First(i => i.Count < i.MaxCount);
if (stackTarget != null)
{
inventory.Stack(itemToAdd, stackTarget);
}
else
{
inventory.Add(itemToAdd);
}
// You need to check if the inventory exists, if it has enough room to accommodate
// the item, what happens to overflow, etc. Left all that out for brevity.
}
}

How to use except and any with strings instead of classes

Like said above how would I, I have some code using linq and except and any(). Currently I use items(a class), to be identified and such, but this is causing problems, so I would like them to be identified by strings instead, but im not sure how. The two functions with the code are: ContainsAllItems, and the other one is in the craft function.
Thanks!
EDIT; To make more clear just in case, right now it checks the items in two lists, instead of that what im asking is how to make it that it checks the itemname of the items in the lists instead, but still make it have the same effect, so it would destroy items and check items and make sure when destroying that not more than needed are destroyed.
If you would not want to look through the code here are the two lines:
public static bool ContainsAllItems(List<Item> a, List<Item> b)
{
return !b.Except(a).Any();
}
and
List<Item> list = new List<Item>();
list = InventoryItems.Except(Recipe.InputItem).ToList();
InventoryItems = list;
Code:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class PlayerInventory : MonoBehaviour {
public int MaxInventory = 26;
public List<Item> InventoryItems = new List<Item>();
void OnGUI(){
if (GUILayout.Button ("+5 Wood")) {
AddItem(ItemConstructors.Wood());
AddItem(ItemConstructors.Wood());
AddItem(ItemConstructors.Wood());
AddItem(ItemConstructors.Wood());
AddItem(ItemConstructors.Wood());
}
if (GUILayout.Button ("Craft Wall")) {
Craft(CraftingRecipes.WoodenWall());
}
}
public void AddItem(Item _item){
if (InventoryItems.Count < MaxInventory) {
InventoryItems.Add(_item);
//SaveItems();
}
}
public void RemoveItem(Item _item){
InventoryItems.Remove (_item);
}
public void UseItem(Item _item){
if (_item.ItemType == "Food") {
ConsumeItem(_item);
}
SaveItems();
}
public void ConsumeItem(Item _item){
if (_item.ItemDur > 1) {
_item.ItemDur--;
}
else if(_item.ItemDur <= 1){
RemoveItem(_item);
}
}
public void SaveItems(){
for (int i = 0; i<MaxInventory; i++) {
PlayerPrefs.SetString("Itemn"+i,InventoryItems[i].ItemName);
PlayerPrefs.SetString("Itemt"+i,InventoryItems[i].ItemType);
PlayerPrefs.SetInt("Itemv"+i,InventoryItems[i].ItemValue);
PlayerPrefs.SetInt("Itemd"+i,InventoryItems[i].ItemDur);
}
//LoadItems ();
}
public void LoadItems(){
InventoryItems.Clear();
for (int i = 0; i<MaxInventory; i++) {
Item it = new Item();
it.ItemName = PlayerPrefs.GetString("Itemn"+i);
it.ItemType = PlayerPrefs.GetString("Itemt"+i);
it.ItemValue = PlayerPrefs.GetInt("Itemv"+i);
it.ItemDur = PlayerPrefs.GetInt("Itemd"+i);
if(it.ItemName.Length <= 2 && it.ItemType.Length <= 2){
}
else{
InventoryItems.Add(it);
}
}
}
public void Craft(CraftingItem Recipe){
bool canCraft = ContainsAllItems (InventoryItems,Recipe.InputItem);
if(canCraft){
if(InventoryItems.Count < MaxInventory){
List<Item> list = new List<Item>();
list = InventoryItems.Except(Recipe.InputItem).ToList();
InventoryItems = list;
AddItem(Recipe.OutputItem);
}
else{
Debug.Log("Not enough space!");
}
}
else{
Debug.Log("Invalid Items.");
}
}
public static bool ContainsAllItems(List<Item> a, List<Item> b)
{
return !b.Except(a).Any();
}
}

Fastest and easiest way to get max, min, mean from multiple objects

I need to find the max, min and mean (stats) value of a list of objects that have its own stats (ResultGroup class stats, based on all Result stats).
When I add objects the values are easily updated, but if I change or remove one of them, then I need to find the stats again. Usually there will be more than 40.000 items, and I need it to be a fast operation.
Is there any better way than looping through all items?
public class ResultGroup
{
private Stats resultStats;
//I need an updated stats
public Stats ResultStats
{
get { return resultStats; }
}
private readonly ObservableCollection<Result> results = new ObservableCollection<Result>();
public ObservableCollection<Result> Results
{
get
{
return results;
}
}
public ResultGroup()
{
this.resultStats = new Stats();
this.results.CollectionChanged += new NotifyCollectionChangedEventHandler(CollectionChanged);
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
//It works ok on add.
Stats lastResultStat = this.results[this.results.Count - 1].Stat;
if (resultStats.Max < lastResultStat.Max)
resultStats.Max = lastResultStat.Max;
if (resultStats.Min > lastResultStat.Min)
resultStats.Min = lastResultStat.Min;
resultStats.Mean = (resultStats.Mean * (this.results.Count - 1) + lastResultStat.Mean) / this.results.Count;
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
this.resultStats = StatsFactory();
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
//Need to find the stats here
}
else if (e.Action == NotifyCollectionChangedAction.Replace)
{
//Need to find the stats here
}
}
private Stats StatsFactory()
{
Stats dataStats = new Stats();
dataStats.Max = float.MinValue;
dataStats.Min = float.MaxValue;
dataStats.Mean = 0;
return dataStats;
}
}
public class Result
{
private float[] data;
//Another class will fill data and set the Stats (max, min, mean)
public float[] Data
{
get { return data; }
}
public Result(int lenght)
{
this.data = new float[lenght];
}
private Stats stat;
public Stats Stat
{
get { return stat; }
set { stat = value; }
}
}
public class Stats
{
public float Max { get; set; }
public float Min { get; set; }
public float Mean { get; set; }
}
When removing an item, you need only loop through all items, when the removed item equals the current min/max.
When replacing an item, you need only loop through all items, when the removed item equals the current min/max and the new item is greater/smaller.
Have you tried to use LINQ operators like Min, Max and Average inside CollectionChanged?
Have you tried to use DB for this ?
as DB has indexing which can help. Also have a look at KDB or SAP's HANA which has Vertical/Column based DB which makes seeping through millions of rows in milliseconds.
Maybe simple file based DB like SqlLite will help. (that should help with reducing memory usage as well if you are dealing with large amounts of data)
I think you can cache max, min values when you first initialize a collection, then you can compare new values with cached value.
I can suggest the next algorithm: if i had a huge list of values I would split it on the ranges and make a collection for each range. For each collection I would have a cached mean value that would be recalculated when a collection would be changed. When I add new value(or change) I would see the stats of the element and find the collection with needed range. In this situation we get a stats as additional index and we must find max and min values only in the certain collections(first, last). The mean value we could get from mean values of all collections. Max, Min values we can cache too in the first and last collections.
//Better have a custom collection with the required properties inside the collection and then have the linq on top of the collection to store the aggregate values...
public class ObserCol: ObservableCollection<int>
{
private int _maxValue = 0;
public ObserCol() {
base.CollectionChanged +=new NotifyCollectionChangedEventHandler(CollectionChanged);
}
public int MaxValue{
get {
return _maxValue;
}
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
//Can use Linq to get the Max or Other Aggregate values..
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
}
else if (e.Action == NotifyCollectionChangedAction.Replace)
{
}
}
}

Limit on number of items in list box in WinForms

I have a WinForms application which uses a list box to display list of items. My application hangs whent the number of items in the listbox exceeds some 150 items. Is this the property of ListBox control that it can hold only so many items? If so, I would request you to provide a solution to this problem.
Thanks,
Rakesh.
It all depends on what you are binding, if you are binding simple key value pairs you can instantly bind 10k easy. You might want to try adding the items in a loop instead of binding to see if there is a certain item it hangs on.
for (int i = 0; i < 10000; i++)
{
listBox1.Items.Add("item:" + i.ToString());
}
You can back your listbox by a larger dataset and use a paging mechanism or you can add an event listener for SizeChanged and disable adding when it reaches your max.
First tip, always..
SuspendLayout();
// fill your lists
ResumeLayout();
Second tip, use AddRange when possible.
Third, and it may be overkill, create your own ListBox...
public class LimitedListBox : ListBox
{
private int _maxItems = 100;
public LimitedListBox()
{
SetItems(new LimitedObjectCollection(this, _maxItems));
}
public int MaxItems
{
get { return _maxItems; }
set { _maxItems = value; }
}
/// <summary>
/// This is the only 'bug' - no design time support for Items unless
/// you create an editor.
/// </summary>
public new LimitedObjectCollection Items
{
get
{
if (base.Items == null)
{
SetItems(new LimitedObjectCollection(this, _maxItems));
}
return (LimitedObjectCollection) base.Items;
}
}
private void SetItems(ObjectCollection items)
{
FieldInfo info = typeof (ListBox).GetField("itemsCollection",
BindingFlags.NonPublic | BindingFlags.Instance |
BindingFlags.GetField);
info.SetValue(this, items);
}
#region Nested type: LimitedObjectCollection
public class LimitedObjectCollection : ObjectCollection
{
private int _maxItems;
public LimitedObjectCollection(ListBox owner, int maxItems)
: base(owner)
{
_maxItems = maxItems;
}
public LimitedObjectCollection(ListBox owner, ObjectCollection value, int maxItems)
: base(owner)
{
_maxItems = maxItems;
AddRange(value);
}
public LimitedObjectCollection(ListBox owner, object[] value, int maxItems)
: base(owner)
{
_maxItems = maxItems;
AddRange(value);
}
public int MaxItems
{
get { return _maxItems; }
set { _maxItems = value; }
}
public new int Add(object item)
{
if (base.Count >= _maxItems)
{
return -1;
}
return base.Add(item);
}
public new void AddRange(object[] items)
{
int allowed = _maxItems - Count;
if (allowed < 1)
{
return;
}
int length = allowed <= items.Length ? allowed : items.Length;
var toAdd = new object[length];
Array.Copy(items, 0, toAdd, 0, length);
base.AddRange(toAdd);
}
public new void AddRange(ObjectCollection value)
{
var items = new object[value.Count];
value.CopyTo(items, 0);
base.AddRange(items);
}
}
#endregion
}
Why don't you just parse your database something like this.
int Total = yourarray.GetLength(0);
Then all you have to do is this in your new array.
double [] new = double[Total];
array.copy(Total,new);
Now you have an array thats dynamic. Anytime your database grows it automatically populates
the new array.
Or if you can do a select count statement on your database you can get the total number or rows and then pipe that to a string. Then you use the string to make control the array. Hope this helps
I just got an "out of memory" error message when filling a list box. The problem was not anything to do with too many items. There was a bug in my code and the items in the list box were returning null in the ToString() method. So the Dot Net error message was wrong and confusing.

Categories