Search Unity

Discussion Using a ScriptableObject Based Inventory System and a FSM to Trigger Interactions

Discussion in 'Scripting' started by BenevolenceGames, Feb 8, 2023.

  1. BenevolenceGames

    BenevolenceGames

    Joined:
    Feb 17, 2021
    Posts:
    128
    The Explanation
    I have written an Inventory System that collects data stored as ScriptableObjects in a WorldItem mono, then uses a class called Slot to manage them and their stack size. The inventory displays these horizontally like so --

    InventoryUI.png
    and when the user scrolls the mouse the inventory selection changes. When the user presses space, the FSM on the PlayerController enters the Interaction State, and my problem begins.

    The Problem
    When the player selects a Tool, I need several things to happen in several places. I need the proper IK rig to turn on, I need the GridManager to compare the tool to the gridobject for interactions there, etc. Normally, I would have an abstract class and have each tool type derive from that and implement it's own version of Use(), but with the Scriptable Object set up I don't think I can do that. I feel like this is getting messy, quickly, and if growth occurs it would become a monolith before I could blink.

    What I'm Asking
    Could someone observe this code, and try to guide me toward a workflow that would at least try to incorporate my current inventory system so I don't have to completely rewrite this.

    Who knew storing and pulling items in and out of thin air would be such a big task...



    The Interact State


    Code (CSharp):
    1. public class InteractState : State
    2. {
    3.     public PlayerController playerController;
    4.     GameObject toolBeingUsed;
    5.     Rig rigBeingUsed;
    6.     int actionHash = Animator.StringToHash("Action");
    7.     int actionIDHash = Animator.StringToHash("ActionID");
    8.     public bool finished = true;
    9.     int actionID = 0;
    10.  
    11.     public InteractState (MonoBehaviour context) : base(context){
    12.         if (context is PlayerController player){
    13.             playerController = player;
    14.         }
    15.     }
    16.  
    17.     // Get the currently selected item
    18.     // If it's a consumable use it
    19.     // if it's a tool call that particular tools use function
    20.     // need to set the IKRig somehow
    21.     // seems spaghetti to me
    22.    
    23.  
    24.     public override void Enter()
    25.     {
    26.         base.Enter();
    27.  
    28.         // Sets a bool to false to prevent exiting the state before animation is done
    29.         finished = false;
    30.         // Subscribes the method to set the bool back to true to the Interaction events triggered by the animator state behaviour
    31.         InteractionEvents.OnInteractionExit += ExitInteractionState;
    32.         // Gets the currently selected item
    33.         ItemData currentlySelectedItemData = playerController.inventory.slots[playerController.inventorySelectionIndex].isOccupied ? playerController.inventory.slots[playerController.inventorySelectionIndex].item : null;
    34.  
    35.         // Analyzes the type of item and calls appropriate methods
    36.  
    37.  
    38.         switch (currentlySelectedItemData){
    39.             case ToolData :
    40.                 UseTool(currentlySelectedItemData as ToolData);
    41.                 break;
    42.             case SeedData :
    43.                 // Insert code to plant seeds
    44.                 break;
    45.             case ItemData:
    46.                 Debug.Log("Whoa!");
    47.                 playerController.animator.SetFloat(actionIDHash, (float)currentlySelectedItemData.actionType);
    48.                 playerController.animator.SetTrigger(actionHash);
    49.                 playerController.inventory.RemoveItemFromInventory(playerController.inventorySelectionIndex, 1);
    50.                 break;
    51.             case null:
    52.                 ExitInteractionState();
    53.                 break;
    54.         }
    55.     }
    56.  
    57.     public void UseTool(ToolData toolToUse){
    58.         Debug.Log("Using Tool!!!");
    59.         switch(toolToUse.toolType){
    60.             case ToolType.Hoe :
    61.                 toolBeingUsed = playerController.hoeModel;
    62.                 rigBeingUsed = playerController.TwoHandRig;
    63.                 UseHoe();
    64.                 break;
    65.         }
    66.         // Find a way to have a tool have another script.....?
    67.  
    68.         // This is not very modular or expansive. Need to find a way to make this more
    69.         // data driven and less performance intensive
    70.     }
    71.  
    72.     void UseHoe(){
    73.         playerController.animator.SetFloat(actionIDHash, (float)ActionAnimatorID.Hoe);
    74.         rigBeingUsed.weight = 1.0f;
    75.         toolBeingUsed.SetActive(true);
    76.         playerController.animator.SetTrigger(actionHash);
    77.  
    78.  
    79.         // We're getting the grid object just in front of the player and
    80.         // calling the set ground prefab passing in the mound prefab we have
    81.         // stored on this class. Should we do this differently?
    82.         // Is there a better way? A better place for this function?
    83.  
    84.         GridObject gridObj = GridManager.instance.GameGrid.GetGridObject(playerController.transform.position + playerController.transform.forward * GridManager.instance.GameGrid.GetCellSize());
    85.         Debug.Log(gridObj.x + " | " + gridObj.z);
    86.         if (gridObj?.groundType == GroundType.Hard){
    87.             gridObj.SetGroundPrefab(GameManager.instance.tilledGroundPrefab);
    88.             gridObj.groundType = GroundType.Tilled;
    89.         }          
    90.     }
    91.  
    92.     public override void Update()
    93.     {
    94.         base.Update();
    95.     }
    96.  
    97.     public override void Exit()
    98.     {
    99.         base.Exit();
    100.         // Unsubscribes from the animation behaviour
    101.         InteractionEvents.OnInteractionExit -= ExitInteractionState;
    102.  
    103.         // For some reason placing the rig weight function in the ExitInteractionState method
    104.         // caused an issue where the weight was never returned to zero.
    105.         // maybe worht looking into?
    106.         if (rigBeingUsed != null){
    107.             rigBeingUsed.weight = 0.0f;
    108.             rigBeingUsed = null;
    109.         }
    110.     }
    111.  
    112.     void ExitInteractionState(){
    113.         // Set the tool model back to invisible and
    114.         // release the reference for the next assignment
    115.         toolBeingUsed?.SetActive(false);
    116.         toolBeingUsed = null;
    117.         finished = true;
    118.     }
    119. }
    The Complete Code (I'm Sorry)

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine.InputSystem;
    4. using System;
    5. using UnityEngine;
    6. using UnityEngine.Animations.Rigging;
    7.  
    8.  
    9. [RequireComponent(typeof(CharacterController))]
    10. public class PlayerController : MonoBehaviour
    11. {
    12.  
    13.     // State Machines
    14.     // All behaviour is handled in it's corresponding state.
    15.     // The states then report to the playerController any
    16.     // changes to the core values here (movement, rotation, etc)
    17.     // The idea is to have each state effect the values housed here in PlayerController,
    18.     // which houses the actual methods and scripts running the player
    19.  
    20.     StateMachine MovementStateMachine;
    21.     StateMachine HealthStateMachine;
    22.     StateMachine ActionStateMachine;
    23.     StateMachine StaminaStateMachine;
    24.  
    25.     Coroutine MovementRoutine;
    26.     Coroutine ActionRoutine;
    27.  
    28.     // Movement States
    29.     StandState standState;
    30.     WalkState walkState;
    31.     SprintState sprintState;
    32.  
    33.     // Health States
    34.     State healthyState;
    35.     State woundedState;
    36.     State mortallyWoundedState;
    37.     State deadState;
    38.  
    39.     // Action States
    40.     IdleState idleState;
    41.     InteractState interactingState;
    42.  
    43.     // Stamina States
    44.     State refreshedState;
    45.     State normalState;
    46.     State lowState;
    47.     State exhaustedState;
    48.     State passedOutState;
    49.  
    50.  
    51.     // Movement Variables
    52.     [Header("Movement")]
    53.  
    54.     public float WalkSpeed = 2.0f;
    55.     public float RunSpeed = 5.6f;
    56.     public float acceleration = 10.0f;
    57.     public float rotationSpeed = 10.0f;
    58.     public float verticalVelocityChangeRate = 100f;
    59.  
    60.     [HideInInspector] public bool movementAllowed = true;
    61.     [HideInInspector] public float desiredForwardVelocity;
    62.     [HideInInspector] public float desiredLateralVelocity;
    63.     [HideInInspector] public float desiredVerticalVelocity;
    64.     Vector2 forwardAndLateralVelocity;
    65.     float verticalVelocity;
    66.     GridObject occupiedCell;
    67.    
    68.  
    69.     [Header("Gravity")]
    70.     // Gravity Variables
    71.     [SerializeField] float gravityTimeoutDelta = 0.0f;
    72.     [SerializeField] float gravityTimeout = .25f;
    73.     float gravity;
    74.  
    75.  
    76.     public GameObject gridCellSelectionOutline;
    77.  
    78.     [Header("Health")]
    79.     // Health Variables
    80.     //
    81.     //
    82.     //
    83.  
    84.  
    85.  
    86.  
    87.     // Stamina Variables
    88.     //
    89.     //
    90.     //
    91.  
    92.     [Header("Animation")]
    93.     // Animation Variables
    94.     public Animator animator;
    95.     public Rig TwoHandRig;
    96.     int speedHash = Animator.StringToHash("Speed");
    97.     int actionIDHash = Animator.StringToHash("ActionID");
    98.     int actionHash = Animator.StringToHash("Action");
    99.     [HideInInspector] public int actionID;
    100.  
    101.     [Header("Input")]
    102.     // Input
    103.     PlayerInput playerInput;
    104.     public PlayerInputActions inputActions;
    105.     public CharacterController characterController;
    106.  
    107.  
    108.  
    109.    
    110.     [Header("Inventory")]
    111.  
    112.     public Inventory inventory;
    113.     public int inventorySelectionIndex;
    114.  
    115.  
    116.  
    117.     [Header("Tool Model References")]
    118.     public GameObject hoeModel;
    119.  
    120.  
    121.  
    122.     [Header("Debug")]
    123.     [SerializeField] MovementState debugCurrentMovementState;
    124.     [SerializeField] ActionState debugCurrentActionState;
    125.     public string debugString;
    126.  
    127.  
    128.  
    129.      ////////////////Begin Script\\\\\\\\\\\\\\\\\
    130.     ///////////////////////\\\\\\\\\\\\\\\\\\\\\\\\
    131.     void Awake(){
    132.         characterController = GetComponent<CharacterController>();
    133.  
    134.     }
    135.  
    136.     // Transition predicates for Movement States
    137.     Func<bool> walking => () => inputActions.Player.Move.ReadValue<Vector2>() != Vector2.zero && !sprinting() && interactingState.finished;
    138.     Func<bool> standing => () => !walking() && !sprinting();
    139.     Func<bool> sprinting => () => inputActions.Player.Move.ReadValue<Vector2>() != Vector2.zero && inputActions.Player.Sprint.IsPressed() && interactingState.finished;
    140.  
    141.     // Transition Predicates for Action States
    142.     Func<bool> idle => () => !interacting() && interactingState.finished;
    143.     Func<bool> interacting => () => inputActions.Player.Interact.IsPressed() && interactingState.finished;
    144.  
    145.     void OnEnable(){
    146.         inputActions = new PlayerInputActions();
    147.         inputActions.Enable();
    148.         inputActions.Player.ChangeInventorySelection.performed += OnScrollMouse;
    149.     }
    150.  
    151.     void Start(){
    152.  
    153.         standState = new StandState(this);
    154.         walkState = new WalkState(this);
    155.         sprintState = new SprintState(this);
    156.        
    157.         idleState = new IdleState(this);
    158.         interactingState = new InteractState(this);
    159.  
    160.         MovementStateMachine = new StateMachine(standState);
    161.         ActionStateMachine = new StateMachine(idleState);
    162.  
    163.         standState.AddTransition(new Transition(walking, walkState, standState));
    164.         standState.AddTransition(new Transition(sprinting, sprintState, standState));
    165.         walkState.AddTransition(new Transition(sprinting, sprintState, walkState));
    166.         walkState.AddTransition(new Transition(standing, standState, walkState));
    167.         sprintState.AddTransition(new Transition(standing, standState, sprintState));
    168.         sprintState.AddTransition(new Transition(walking, walkState, sprintState));
    169.  
    170.         idleState.AddTransition(new Transition(interacting, interactingState, idleState));
    171.         interactingState.AddTransition(new Transition(idle, idleState, interactingState));
    172.  
    173.  
    174.         MovementRoutine = StartCoroutine(RunStateMachine(MovementStateMachine));
    175.         ActionRoutine = StartCoroutine(RunStateMachine(ActionStateMachine));
    176.  
    177.     }
    178.  
    179.     void Update(){
    180.         HandleGravity();
    181.         HandleMovement();
    182.         HandleRotation();
    183.  
    184.         switch (MovementStateMachine.currentState){
    185.             case StandState :
    186.             debugCurrentMovementState = MovementState.Standing;
    187.             break;
    188.  
    189.             case WalkState :
    190.             debugCurrentMovementState = MovementState.Walking;
    191.             break;
    192.  
    193.             case SprintState :
    194.             debugCurrentMovementState = MovementState.Sprinting;
    195.             break;
    196.         }
    197.  
    198.         switch (ActionStateMachine.currentState){
    199.             case IdleState :
    200.             debugCurrentActionState = ActionState.Idle;
    201.             break;
    202.  
    203.             case InteractState :
    204.             debugCurrentActionState = ActionState.Interacting;
    205.             break;
    206.         }
    207.     }
    208.  
    209.     IEnumerator RunStateMachine(StateMachine machine){
    210.         while(machine.currentState != null){
    211.             machine.Update();
    212.             yield return new WaitForEndOfFrame();
    213.         }
    214.         yield return null;
    215.     }
    216.  
    217.     // Pulls the character downwards at a rate depenedent on it's characterControllers grounded state
    218.     // Also uses a timer function to smoothe the transition between grounded and not, for climbing rough terrain
    219.     void HandleGravity(){
    220.         if (characterController.isGrounded){
    221.             // grounded
    222.             gravityTimeoutDelta = 0.0f;
    223.             gravity = -.25f;
    224.         }
    225.         else if (!characterController.isGrounded && gravityTimeoutDelta < gravityTimeout){
    226.             // not grounded and timeout has time remaining
    227.             gravityTimeoutDelta += Time.deltaTime;
    228.             gravity = -.25f;
    229.         }
    230.         else if (!characterController.isGrounded && gravityTimeoutDelta >= gravityTimeout){
    231.             // not grounded and timeout has expired
    232.             gravity = -9.8f;
    233.         }
    234.         desiredVerticalVelocity = gravity;
    235.         verticalVelocity = Mathf.Lerp(verticalVelocity, desiredForwardVelocity, verticalVelocityChangeRate * Time.deltaTime);
    236.     }
    237.  
    238.     // Moves the character via it's characterController
    239.     void HandleMovement(){
    240.         Vector2 desiredVelocity = new Vector2(desiredLateralVelocity, desiredForwardVelocity);
    241.         if (forwardAndLateralVelocity != desiredVelocity){
    242.             forwardAndLateralVelocity = Vector2.Lerp(forwardAndLateralVelocity, desiredVelocity, acceleration * Time.deltaTime);
    243.         }
    244.         characterController.Move(new Vector3(forwardAndLateralVelocity.x, desiredVerticalVelocity, forwardAndLateralVelocity.y) * Time.deltaTime);
    245.        
    246.         // Animation
    247.         float speed = desiredVelocity.magnitude;
    248.         animator.SetFloat(speedHash, speed);
    249.     }
    250.  
    251.     // Rotates the character to look in the direction of the input
    252.     void HandleRotation(){
    253.         Vector3 desiredRotationDirection = new Vector3(desiredLateralVelocity, 0, desiredForwardVelocity);
    254.         if (desiredRotationDirection.magnitude > 0){
    255.             Quaternion desiredRotation = Quaternion.LookRotation(desiredRotationDirection);
    256.             transform.rotation = Quaternion.RotateTowards(transform.rotation, desiredRotation, rotationSpeed * Time.deltaTime);
    257.         }
    258.     }
    259.  
    260.     // Inventory Stuff -- Do I need to move this somewhere else?
    261.  
    262.     public delegate void ScrollMouse(int index);
    263.     public event ScrollMouse OnMouseScroll;
    264.  
    265.     void OnScrollMouse(InputAction.CallbackContext context){
    266.         inventorySelectionIndex += (int)(context.ReadValue<float>() / 120f);
    267.  
    268.         if (inventorySelectionIndex > inventory.maxSize - 1){
    269.             inventorySelectionIndex = 0;
    270.         }
    271.  
    272.         if (inventorySelectionIndex < 0){
    273.             inventorySelectionIndex = inventory.maxSize - 1;
    274.         }
    275.  
    276.         if (OnMouseScroll != null){
    277.             OnMouseScroll(inventorySelectionIndex);
    278.         }
    279.     }
    280.  
    281.  
    282.     void OnTriggerEnter(Collider collider){
    283.         if (collider.TryGetComponent<WorldItem>(out WorldItem item)){
    284.             if (item.isLoose){
    285.                 inventory.AddItemToInventory(item, this.transform.position);
    286.             }
    287.         }
    288.  
    289.         // else if enemy
    290.         // else if npc
    291.         // else if blah blah blah ... gotta be a better way
    292.     }
    293. }
    294.  
    ______________________________________________________________________________________

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.InputSystem;
    6. using UnityEngine.EventSystems;
    7.  
    8. public class Inventory
    9. {
    10.     public List<Slot> slots;
    11.     public int maxSize;
    12.  
    13.     public delegate void InventoryContentsChanged(List<Slot> slots);
    14.     public event InventoryContentsChanged OnInventoryContentsChanged;
    15.  
    16.     public Inventory(int maxSize){
    17.         this.maxSize = maxSize;
    18.         slots = new List<Slot>(maxSize);
    19.  
    20.         for (int i = 0; i < slots.Capacity; i++)
    21.         {
    22.             Slot slot = new Slot(null, 0);
    23.             slots.Add(slot);
    24.         }
    25.     }
    26.  
    27.     public void AddItemToInventory(ItemData itemToAdd, int quantity){
    28.         // cycle through slots
    29.         foreach(Slot slot in slots){
    30.             // if the slot is occupied by the item being added
    31.             if (slot.isOccupied && slot.item == itemToAdd){
    32.                 slot.quantity += quantity;
    33.                 // Trigger inventory event
    34.                 TriggerInventoryChange(slots);
    35.                 return;
    36.             }
    37.         }
    38.        
    39.         foreach (Slot slot in slots)
    40.         {
    41.             if (!slot.isOccupied){
    42.                 slot.item = itemToAdd;
    43.                 slot.quantity = quantity;
    44.                 // Trigger Inventory event
    45.                 TriggerInventoryChange(slots);
    46.                 return;
    47.             }
    48.         }
    49.  
    50.         Debug.Log("Inventory Full!");
    51.     }
    52.  
    53.     public void AddItemToInventory(ItemData itemToAdd, int quantity, int index){
    54.         if (!slots[index].isOccupied){
    55.             slots[index].item = itemToAdd;
    56.             slots[index].quantity = quantity;
    57.         }
    58.         else {
    59.             AddItemToInventory(itemToAdd, quantity);
    60.         }
    61.     }
    62.  
    63.     public void AddItemToInventory(WorldItem worldItem, Vector3 position){
    64.         AddItemToInventory(worldItem.itemData, worldItem.quantity);
    65.         if (slots.Contains(slots.Find(slot => slot.item == worldItem.itemData))){
    66.             worldItem.AcquireWorldItem(position);
    67.         }
    68.         else{
    69.             worldItem.Shake();
    70.         }
    71.     }
    72.  
    73.     public void RemoveItemFromInventory(ItemData itemToRemove, int quantity){
    74.         foreach (Slot slot in slots){
    75.             if (slot.isOccupied && slot.item == itemToRemove){
    76.                 slot.quantity -= quantity;
    77.                 if (slot.quantity <= 0){
    78.                     slot.quantity = 0;
    79.                     slot.item = null;
    80.                 }
    81.                 TriggerInventoryChange(slots);
    82.                 return;
    83.             }
    84.         }
    85.     }
    86.  
    87.     public void RemoveItemFromInventory(int index, int quantity){
    88.         if (slots[index].isOccupied){
    89.             RemoveItemFromInventory(slots[index].item, quantity);
    90.         }
    91.     }
    92.  
    93.     void TriggerInventoryChange(List<Slot> slots){
    94.         if (OnInventoryContentsChanged != null){
    95.             OnInventoryContentsChanged(slots);
    96.         }
    97.     }
    98. }
    ______________________________________________________________________________________

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [System.Serializable]
    6. public class Slot
    7. {
    8.     public ItemData item = null;
    9.     public bool isOccupied {get {return item != null;} private set{}}
    10.     public int quantity = 0;
    11.  
    12.     public Slot(ItemData item, int quantity){
    13.         this.item = item;
    14.         this.quantity = quantity;
    15.     }
    16. }
    17.  
    ______________________________________________________________________________________

    Code (CSharp):
    1. public class GridObject {
    2.     public GroundType groundType;
    3.     public GameObject groundPrefab;
    4.     public Plant plant;
    5.     public bool wet = false;
    6.     public bool isPlanted { get {return plant != null;} private set{}}
    7.     public int x;
    8.     public int z;
    9.     Grid<GridObject> grid;
    10.  
    11.  
    12.    
    13.     public GridObject(Grid<GridObject> grid, int x, int z){
    14.         this.grid = grid;
    15.         this.x = x;
    16.         this.z = z;
    17.         groundType = GroundType.Hard;
    18.     }
    19.  
    20.     public Vector3 CellCenter(){
    21.         // returns the center of the tile regardless of tile size
    22.         return grid.GetWorldPosition(x, z) + new Vector3(grid.GetCellSize() * .5f, 0, grid.GetCellSize() * .5f);
    23.     }
    24.  
    25.     public void SetGroundPrefab(GameObject objectToSet){
    26.         if (groundPrefab != null){
    27.             GameObject.Destroy(groundPrefab);
    28.         }
    29.         groundPrefab = GameObject.Instantiate(objectToSet, CellCenter(), Quaternion.identity);
    30.     }
    31.  
    32.     public void WaterGround(){
    33.         if (!wet){
    34.             wet = true;
    35.         }
    36.     }
    37.  
    38.     public void Plant(){
    39.        
    40.     }
    41.  
    42. }
    43.  
    44. public enum GroundType{
    45.     Hard,
    46.     Tilled,
    47.     Planted
    48. }
    ______________________________________________________________________________________


    I feel like if you've made it this far and you want more, you'll ask. (Super huge thanks to you, person that read ALL of that)
     

    Attached Files:

  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,736
    These things (inventory, shop systems, character customization, crafting, 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.

    Breaking down large problems in general:

    https://forum.unity.com/threads/opt...n-an-asteroid-belt-game.1395319/#post-8781697

    As for FSMs, well, I have a position on those too...

    I suggest never using the term "state machine." Instead, just think:

    - I have to keep track of some THING(s)
    - That THING might change due to reasons
    - Depending on that THING, my code might act differently

    That's it. That's all it is. Really!! The classic example is a door:

    - track if it is open or closed
    - if it is open, you could close it
    - if it is closed, you could open it
    - if it is open you could walk through it
    - if it is closed you could bump into it

    Wanna make it more complex? Put a latch on one side of the door.

    This is my position on finite state machines (FSMs) and coding with them:

    https://forum.unity.com/threads/state-machine-help.1080983/#post-6970016

    I'm kind of more of a "get it working first" guy.

    Ask yourself, "WHY would I use FSM solution XYZ when I just need a variable and a switch statement?"

    All generic FSM solutions I have seen do not actually improve the problem space.

    Your mileage may vary.

    "I strongly suggest to make it as simple as possible. No classes, no interfaces, no needless OOP." - Zajoman on the Unity3D forums.
     
    Unifikation likes this.
  3. BenevolenceGames

    BenevolenceGames

    Joined:
    Feb 17, 2021
    Posts:
    128
    I have a firm grasp on what I’d like it to do, several flow charts mapping the UI and a guide document for what it is supposed to do. It is at present an attempt at a one for one of Stardew Valleys primary inventory (the bar across the top, not the openable part…yet…)

    As for the rest, I will read the links and do more self education then reply again. I just wanted to say thanks for the quick reply and offer that small piece of info in case it helps…

    —Edit—

    I should probably mention that the inventory system works. It correctly picks up the world item, grabs its item data , and stores it within the inventory slot, also tracks stack size correctly and can even drop an item back into the world, consume it, or hoe the ground. I just wanted more expandability because at present it seems like it’s going to be a nightmare to expand upon. Just the tool part though (as far as I know now…haha)
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,925
    Items/Inventory/Equipment systems are monoliths. No real way around that. In a survival craft project I have easily over 100 script assets in the assembly for items/inventory stuff alone.

    Dive right in and plough ahead; don't be afraid because there's going to be a lot of code and most importantly don't let yourself be stuck with analysis paralysis.

    Some things to keep in mind:
    • Design your systems like an API
    • Keep the scope of each object small (Single Responsibility Principle!)
    • Separate data from visuals
    Items are just data objects. Inventories are data objects with an API to mutate them. The inventory UI should just read this data and generate an appropriate representation. The 'use item' state should just read the actively equipped item and gleam the information it cares about to situate itself correctly. So on and so forth.
     
    Unifikation likes this.
  5. BenevolenceGames

    BenevolenceGames

    Joined:
    Feb 17, 2021
    Posts:
    128
    A
    I should probably also mention that my knowledge comes solely from these forums, YouTube videos, and hundreds of hours of coding. I have no formal background in programming, and so things like “Design your systems like APIs” escape me. The point being I don’t have the formal education of programming as a whole, and because of that the underlying logics, universal jargon, and core principles aren’t something I have in depth knowledge on.

    I will Google how APIs are designed, best practices and such. I will also try to make myself more aware of core workflows and things like “Single Responsibility Principle”, then take another crack at it today.

    @Kurt-Dekker I think FSMs are more about modularity than problem solving. But you are correct that they definitely seem to murk the waters. It was SUPPOSED to break the logic into more tolerable chunks for MY readability, but it seems like, as you said, the FSM merely introduces more complex topics while not offering much in optimization or actually solving issues. I find myself now expanding what was a simple FSM into a HSM with several layers.

    At any rate, that’s an issue for refactoring next I suppose. At present moment I am still reading materials on solutions for inventory systems.

    The big issue is the use of tools. I just want to be able to call a single UseTool(toolToUse) method from the Interact state and it call that method on the tool class. Problem is the stored reference is to the scriptable object and the Monobehaviour carrying gameObject is destroyed when the inventory grabs the ref for the item data. I suppose I should just store the gameObject itself and set its active state to false? I just don’t know. I stared at the monitor for several hours stuck in what was so wonderfully called Analysis Paralysis trying to determine the best approach. I have rebuilt this system 4 times, and it’s holding up all other progress.

    Will update with progress.
     
    Unifikation likes this.
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,736
    The cleanest approach for this stuff will be something like this:

    - a ScriptableObject that captures everything you need about each inventory item

    It could implement interfaces to express what it can do.

    Or it could have other objects slotted into it.

    In any case, as @spiney199 so wisely points out,

    This is critical. You should (ideally) be able to do EVERYTHING with that item from that one ScriptableObject, such as:

    - spawn inventory representation (or at least give me a reference to it)
    ---> give it to the inventory grid manager
    --------> that manager is responsible for telling the UI to display it
    - spawn into world lying on ground
    ---> knows what prefab
    ---> knows how to safely get it on the ground without falling through
    - spawn into world in player's hand
    ---> which hand, how does it anchor, etc
    - activate it from players hand (use it)
    ---> conditions saying when (eg, shovel can only be used when on dirt)
    ---> conditions saying what happens

    etc

    The thinking here is that with interfaces and a central API that every item has, you can begin to organize stuff.

    But yes, it is an INSANE amount of work to get right.
     
  7. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,913
    I think you are realising that each item is different and requires different functions. There won't be any shortcuts. Sometimes it's easier to write a dedicated component for each weapon that works standalone from the rest of your code, then merge/abstract later.

    Functions can be stored in dictionaries, or you can use abstract/inheritance. This can replace the switch statements. This will let you call a general Use function without knowing which function will actually be called.

    It's sometimes easier to think about the input first, and visualise the controls on the screen, like Witcher 3 in the bottom-right corner. When you equip something you need to switch your input context (active control scheme) to match that item. If you have no control over input and are using Input.GetButton everywhere, it will be difficult. Sometimes you can simplify it down to PrimaryFire/SecondaryFire so long as all items are covered by that. Right now you only have the idea of "Using" an item.
     
    BenevolenceGames and Kurt-Dekker like this.
  8. BenevolenceGames

    BenevolenceGames

    Joined:
    Feb 17, 2021
    Posts:
    128

    I was not aware you could store functions in dictionaries. I am going to see how that’s accomplished immediately.

    As for an update I’m currently rewriting some things to follow that SRP (Single Responsibility Principle) but it’s difficult to break up logic without creating a crap load of dependencies. I’m mustering through though.

    As for the inventory and using system, I think you’re right. I think the tools are going to need to have their own script attached or an interface or something. That’s where I’m stuck, but you guys are offering me some solid advice here to lead me to the correct thinking.

    I don’t think I’ll need to change control schemes as each item has a single simple animation and every item is used by the same button. Want to talk to a villager? Space bar while in front of a villager. Want to chop down a tree? Space, in front of tree, with axe highlighted in the inventory bar. Etc…. However if I did it should be fairly easy as I’m using the New Input System and subscribe to events as needed.

    —Side Note—

    Why does it seem that the more “optimized” a code is the less readable it becomes? Just something I’ve noticed while googling things like SerializedDictionaries and such for the future saving/loading of all this. Crazy how different we’ve programmed computers to think.
     
    Last edited: Feb 10, 2023