Search Unity

Discussion Building a shop inventory that allows for placement of items in Unity and C#

Discussion in 'Scripting' started by winstoniti, Nov 10, 2022.

  1. winstoniti

    winstoniti

    Joined:
    Dec 26, 2020
    Posts:
    5
    I'm working on a project with a shop-like inventory, similar to what is seen in the Theme Hospital games. I decided to post this here instead of on Unity Answers because I thought the problem was specific enough.
    Heres a description of what I'm trying to do. This might get a bit lengthy so I'll try my best to describe the problem and keep things tidy.



    A list of items is displayed in a menu, and to buy these items the quantity must be changed to 1 or higher for these items to be purchased. Quantities at 0 will not be purchased.

    What happens when an item is purchased? If you purchase 2 Desks for example, the game will spawn a desk, which can be moved with the pointer, and clicked so it can be placed. Once the first desk is placed, the second desk will spawn. The same thing will happen, move and place.

    Now what happens if more than one item is purchased? 3 Desks, 1 Table, 2 Chairs for example. The 2 desks will spawn as described above, but as soon as the desks are placed, a chair will spawn so it can be positioned and placed, and once is placed the second chair will spawn so it can be moved and placed etc. Lastly, once chairs are placed, the 1 Table will be spawned to be positioned and placed.

    So far I have everything else working, to the best of my ability. I have the menu UI working, along with corresponding - and + buttons. I have a List of data that is built from a struct, the list I called PurchaseList, that holds a name and quantity. Once I purchase the items I want, the list is sent through an event which is recieved in a seperate Monobehaviour script called InstantiateItems.

    The PurchaseList is then sorted using a sorting method, and reversed, so I can get the item with the largest quantity first and the item with the smallest quantity last. This is where I've gotten to at this point. I have a list of items that have a quantity higher than zero, those items with a quantity of 0 are discounted.

    So if I purchase 1 Table, 3 Chairs, 2 Desks I get a list back saying:

    name: Chair, quantity: 3
    name: Desk, quantity: 2
    name: Table, quantity: 1

    The list has its own private variables like name and quantity from the struct.

    The difficult part I'm having is being able to spawn an object one at a time, and once those objects of that item have been placed, the next item in the List is then spawned. I followed a bunch of tutorials online to get me to this point.

    The shop manager code is quite large so I've only got the relevant parts to show:

    This code is in a monobehaviour script called ShopManager.

    Code (CSharp):
    1. public void PurchaseItems()
    2.     {
    3.         for (int i = 0; i < furnitureShopItemsSO.Length; i++)
    4.         {
    5.             if (furnitureShopItemsSO[i].quantityToBuy > 0)
    6.             {
    7.                 ItemsData item = new ItemsData();
    8.                 item.name = furnitureShopItemsSO[i].name;
    9.                 item.quantity = furnitureShopItemsSO[i].quantityToBuy;
    10.                 purchaseItemlist.Add(item);
    11.             }
    12.         }
    13.         SendItemsListEvent?.Invoke(this, new PurchaseItemsEventArgs { itemData = purchaseItemlist });
    14.     }
    The purchased list is then given to the monobehaviour script called InstantiateItems.

    Code (CSharp):
    1. public void InstantiateObjects(object sender, ShopManager.PurchaseItemsEventArgs e)
    2.     {
    3.         DeskBehaviour.HasBeenPlacedEvent += ResetAllAfterPlacement;
    4.  
    5.         foreach (var item in e.itemData)
    6.         {
    7.             ItemsData newItem = new ItemsData();
    8.             newItem.name = item.name;
    9.             newItem.quantity = item.quantity;
    10.             purchaseItemList.Add(newItem);
    11.         }
    12.         purchaseItemList.Sort(SortFunc);
    13.         purchaseItemList.Reverse();
    14.  
    15.         // check to test ordering
    16.         foreach (var item in purchaseItemList)
    17.         {
    18.             Debug.Log(item.name + " " + item.quantity);
    19.         }
    20.         for (int i = 0; i < purchaseItemList.ToArray().Length; i++)
    21.         {
    22.             if (i == 0)
    23.             {
    24.                 if (purchaseItemList.ToArray()[i].name == "Desk")
    25.                 {
    26.                     if (purchaseItemList.ToArray()[i].quantity > 0)
    27.                     {
    28.                         InstantiateDesk();
    29.                     }
    30.                 }
    31.             }
    32.  
    33.         }
    34.     }
    Now this code in the InstantiateObjects does not do what I want it to do. It will look at the first in the list, and if it is "Desk", then it knows to spawn a desk but only once, the next part is harder for me to wrap my head around.

    I also have a List in two both scripts. I'm assuming there is a better way to handle this with one list only.

    I feel like I need an event somewhere that knows when to respawn the next object, but I'm not sure. I also could be doing things completely wrong. I am glad I have a list that is working at least. With what I've got so far, some things could be vastly improved but at the moment I'm focusing on a solution to this list problem.

    So I should mention that I am relatively new to C# and Unity and I am still getting used to it all. Its a complicated problem and I may have bitten more than I can chew, but I feel I would learn a lot from this. I originally did a shop inventory similar to this in Javascript, although it never had the list problem, but I could spawn multiples of the same item one at a time.

    If anyone has any suggestions to make, or advice to go in the right direction, I would very much appreciate it. I'd be willing to upload the code to github in the future in case people want to check it out. Thanks for reading. :)
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,697
    Just to put this in perspective, you've come a really long way with what is essentially an extremely-advanced topic (see below).

    The general issue you face sounds like having one giant blob of code that knows how to associate "desk" with the prefab for a desk, "chair" with the prefab for a chair, etc.

    Anytime you have a need for a central thing like that, that's called a "code smell" and means you probably need to refactor your data so each object knows its unique ID and enough stuff about it to exhaustively describe it. ScriptableObjects are very handy for organizing stuff like that.

    Then if your code says "I need a
    desk
    ", it will iterate all known objects asking each one "hey, are you a desk? If so, I need you"

    And as far as this:

    This is an excellent attitude. Here is why:

    These things (inventories, shop systems, character customization, etc) are fairly tricky hairy beasts, definitely deep in advanced coding territory.

    They contain elements of:

    - a database of items that you may possibly possess / equip
    - a database of the items that you actually possess / equip currently
    - perhaps another database of your "storage" area at home base?
    - persistence of this information to storage between game runs
    - presentation of the inventory to the user (may have to scale and grow, overlay parts, clothing, etc)
    - interaction with items in the inventory or on the character or in the home base storage area
    - interaction with the world to get items in and out
    - dependence on asset definition (images, etc.) for presentation

    Just the design choices of an inventory system can have a lot of complicating confounding issues, such as:

    - can you have multiple items? Is there a limit?
    - if there is an item limit, what is it? Total count? Weight? Size? Something else?
    - are those items shown individually or do they stack?
    - are coins / gems stacked but other stuff isn't stacked?
    - do items have detailed data shown (durability, rarity, damage, etc.)?
    - can users combine items to make new items? How? Limits? Results? Messages of success/failure?
    - can users substantially modify items with other things like spells, gems, sockets, etc.?
    - does a worn-out item (shovel) become something else (like a stick) when the item wears out fully?
    - etc.

    Your best bet is probably to write down exactly what you want feature-wise. It may be useful to get very familiar with an existing game so you have an actual example of each feature in action.

    Once you have decided a baseline design, fully work through two or three different inventory tutorials on Youtube, perhaps even for the game example you have chosen above.

    Breaking down a large problem such as inventory:

    https://forum.unity.com/threads/weapon-inventory-and-how-to-script-weapons.1046236/#post-6769558

    If you want to see most of the steps involved, make a "micro inventory" in your game, something whereby the player can have (or not have) a single item, and display that item in the UI, and let the user select that item and do things with it (take, drop, use, wear, eat, sell, buy, etc.).

    Everything you learn doing that "micro inventory" of one item will apply when you have any larger more complex inventory, and it will give you a feel for what you are dealing with.
     
    winstoniti likes this.
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    This is one of those things that sounds easy but does need a reasonable knowledge of C# to do.

    So firstly, the system in charge of placing items should be separate to that of buying items. When you buy x number of items, the shop system should do not much more than tell the placement system what the player gets to put down. I would imagine once you've bought your items, the shop goes away (either closes entirely or hides itself) to be replaced by some form of item placement overlay.

    Having these separate will make it a lot easier to design each of them without affecting one another.

    Once you have everything that's been bought and into a collection, you just take the first (or last) one in the list and let the player place it. Once done, remove it from the collection and rinse and repeat.

    How to do this code wise? Well I would be better off making a video tutorial rather than writing it, but this is how I'd approach the problem:
    • Get the purchasing of one item working
    • Make sure this one item is being sent off to the other system successfully (Debug.Log!)
    • Get the placement of one item working
    • Allow the player to buy multiple items and verify they are being sent off to the placement system
    • Get the placement of multiple items working
    Idea being, break down the problem into smaller more approachable steps that you can build upon. It's a good approach when you feel a little overwhelmed by the tasks you've set yourself.

    And if need be, break these systems down into smaller piece too.
     
    winstoniti and Kurt-Dekker like this.
  4. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,913
    You can do the placements using recursive functions calls. Or you can do it with coroutines using a while loop, or async tasks.

    For example, this will wait until left-click is pressed before continuing:
    Code (csharp):
    1.    void Start()
    2.     {
    3.         StartCoroutine(InstantiateObjectsSequence());
    4.     }
    5.  
    6.     IEnumerator InstantiateObjectsSequence()
    7.     {
    8.         var purchaseItemsList = new List<(string Name, int Quantity)>()
    9.         {
    10.             ("chair", 5),
    11.             ("table", 2),
    12.             ("desk", 1)
    13.         };
    14.  
    15.         Debug.Log($"Started sequence.");
    16.         foreach (var item in purchaseItemsList)
    17.         {
    18.             yield return StartCoroutine(PlaceObject(item)); // Wait for placing items to complete
    19.             yield return null; // Wait a frame so input doesn't trigger twice
    20.         }
    21.         Debug.Log($"Finished sequence.");
    22.     }
    23.  
    24.     IEnumerator PlaceObject((string Name, int Quantity) purchaseData)
    25.     {
    26.         Debug.Log($"Started placing {purchaseData.Name}s");
    27.  
    28.         var counter = 0;
    29.  
    30.         // Repeats if more to be placed
    31.         while (counter < purchaseData.Quantity)
    32.         {
    33.             if (Input.GetMouseButtonDown(0))
    34.             {
    35.                 counter++;
    36.                 Debug.Log($"Placed {counter}/{purchaseData.Quantity}");
    37.             }
    38.             yield return null;
    39.         }
    40.  
    41.         Debug.Log($"Finished placing {purchaseData.Name}");
    42.     }
     
    winstoniti and spiney199 like this.
  5. winstoniti

    winstoniti

    Joined:
    Dec 26, 2020
    Posts:
    5
    A code smell is a concept I've encountered before but never really thought about it. I'm going to take everything you mentioned into consideration.

    Thanks for helping me think about it. I'll take your advice into consideration.

    I gave this a go, and it worked!. But I realise I should work on doing it with recursive functions, which is a concept I'm familiar with as having done it in other programming languages, so I'm going to work on that. Thanks for the advice, it is appreciated.