Creating an Underwater Survival Game in Unity - Part 3

Creating an Underwater Survival Game (like Subnautica) Part 3 — Inventory

This article is a part of the series:
Creating an Underwater Survival Game (like Subnautica) in Unity

Ever wanted to create a game like Subnautica in Unity? Check out Part 3 of our guide here, where we go through how to set up the inventory system for the game.

A link to a package containing the project files of this tutorial series can also be found at the end of this article.

Video authored, edited and subtitled by Sarah Kagda

1. Inventory

Welcome back! In this portion of our tutorial series, we will be focusing on how to create an inventory system for the game.

In order for our player to pick up and keep items, we need an inventory!

Since we’re modelling this game after Subnautica, we will have to first analyse and understand how the inventory system works. In Subnautica, the inventory system is grid-based, where you can pick up items of varying sizes to store them inside the player inventory. This means that an item has to fit in the slots allocated by the inventory – if an item does not fit, it will not be picked up by the player. On top of that, the inventory should be able to sort itself.

Now that we know this, we know that we will need three things for this inventory system:

  1. 3D models of objects that will spawn in the world that players can pick up
  2. Grid System
  3. Sprites with pictures of the 3D model items for display purposes

Let’s get started, shall we?

2. Install Assets

In this project, we use many free external assets. There are a few reasons for this. In general, coding is a lot of work, and to try to code every single thing in the game is a lot more work than necessary. We don’t want to reinvent the wheel. You’ll see that this is a common practice among programmers. We utilise code from other applications or games with similar functionalities and adjust them according to what we need, and that saves us a lot of time because others have run through similar problems and come up with relevant solutions. Open-source code is quite reliable as it has been tested by many people and most likely have been revised many times. This way, we can focus on using our time on creating new systems for the game.

The external assets we’ll be using in the tutorial are as follows:

You can go ahead and import the first two assets by going to Window > Package Manager. The third asset created by CodeMonkey is an excellent grid system that is simple to work with and modify to suit your own needs. They have even made a bunch of different things using that grid system. If that interests you, you can click here for the video in which that grid system was created.

5 July 2023 Update: Please copy CodeMonkey’s Grid class from this forum topic instead. They have recently updated their code, and the new version of their code is not compatible with this tutorial.

Create a new folder in your Scripts folder and name it Inventory. Import the CodeMonkey script for grid into the folder. This grid takes in a grid object, sets up a grid of a specified width, height and cell size, then each cell in the grid will create a grid object to place within it. You may take a look at the comments in the script to see what each function will do.

We’ll be creating our own grid objects to be put within the grid later on in the tutorial. But first, let us set up our prefabs and scriptables.

3. Item Scriptables and Prefabs

The first thing we’ll need to do is create a scriptable object to store the data of each item. For instance, if we wanted to have an item called ‘small fish’, we would need something to store the name, ID number, size, sprite, and prefabs of the object, and so on.

Prefabs are a special type of component that allows fully configured GameObjects to be saved in the Project for reuse. These assets can then be shared between scenes, or even other projects without having to be configured again.

UnityLearn

a. Create New Scriptable Object – ItemScriptableObject

Since it is a scriptable object, we will need to create an asset menu. This allows us to create an instance of this script as an ItemScriptableObject in the menu. We also do not need the Start() or Update() functions, so you can go ahead and remove that. We will also be changing MonoBehaviour into ScriptableObject.

Let’s first declare the variables we need as mentioned earlier – itemName, ID, size, sprite and prefab. The sprite will be what picture will pop up whenever this item is displayed in the inventory. When we remove an item from the inventory, we need to remove it from the inventory menu and also spawn it into the world. We’ll name this variable worldPrefab.

ItemScriptableObject.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "item", menuName = "ScriptableObjects/itemScriptableObject", order = 1)]
public class ItemScriptableObject : MonoBehaviour ScriptableObject
{
    public int id;
    public string itemName;
    public Vector2 size; //how many across x and how many across y
    public Sprite sprite;
    public GameObject WorldPrefab;
}

b. Create folder in Assets called “ScriptableObjects”

This is where we can store all our item data. If you right-click inside the folder, you’ll see that there’s an extra menu item now called ScriptableObject. Go ahead and create three of these Scriptable Objects and rename them ‘Small Fish’, ‘Medium Fish’ and ‘Large Fish’. Though the item name can differ from the Scriptable Object name in the inspector menu, we will name them the same way for simplicity this time. Now that these Scriptable Objects have been created, we can now set up the prefabs we need.

c. Set up prefabs

Create a folder in Assets called Prefabs. Go into the Fish prefabs that you will find in the Fish – PolyPack folder you’ve imported, and drag them into the scene.

Drag and drop prefabs into the scene
Drag and drop prefabs into the scene

Select all three of the prefabs and unpack them.

Unpack prefabs
Unpack prefabs

Article continues after the advertisement:


Add a Rigidbody and Box Collider as components using the Inspector menu. Simply click ‘Add Component’ and you can search for these two components. They will automatically set it to the correct size on the Scriptable Objects. Now you can save them as prefabs!

remove the mesh collider that the prefabs are using
Note: Because we added a box collider to each of the prefabs, we will need to go into each of the prefabs to remove the mesh collider that they’re using.

c. Set up prefabs for UI

We will need to set up a prefab for the UI so that when we display the inventory system in the UI, we will have one image for each spot.

Go into PlayerCanvas, create an empty GameObject and name it InventoryItem. There will be one of these for each item in the inventory. This GameObject should be 100 by 100 (which is its default size), and should also have an anchor at the bottom left. This is because each anchor grid anchor point is on the bottom left. Ensure that you press Shift + Alt when you set your anchor. Shift sets the pivot of the image to that point, and Alt will set the item to that specific corner.

Inventory Object Alignment
Inventory Object Alignment. For a clearer run through of this process, skip to this part of the video.

Now, create a child GameObject of InventoryItem and name it Image. This object will hold the image. Even if the pivot is meant to be at the bottom left, the images should start from the top left. So go ahead and set the pivot for this object at the top left and also hold Shift + Alt once again. Set the size to 100 by 100 and add an image component in the inspector section. You can use a generic fish for the image for now. You can also drag it anywhere on the canvas as this is simply going to be a prefab and so it does not really matter where you place this. If you are still unclear about how to do so for this next part, you can refer to this part of the video.

And, voila! We now have a prefab to display each item in the inventory. You can go ahead and drag it into your prefabs folder and remove it from the scene.

e. Set Up World Object Prefabs

World object prefabs are items that you can pick up from the physical world. Go into the Small Fish prefab, and you’ll see in the inspector column that there is a ‘World Prefab’. Click into this, and you’ll see something like this:

Now, we need a way to store the ItemScriptableObject data on this GameObject. If we spawn this object, we will need to know which ItemScriptableObject it is.

f. Create Class InteractableObject to store Item Scriptable Object data

We’ll need a class to store the ItemScriptableObject data. Go into Scripts > Inventory and create a new script. We will name this new script InteractableObject. This will be a class that can be inherited from, so that you can always get the Item Scriptable Object data from the object. This keeps the item data of the particular world object or inventory image. When coding InteractableObject, you can remove the start and update functions. Simply add a reference to ItemScriptableObject, and you’re done!

InteractableObject.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InteractableObject : MonoBehaviour
{
    public ItemScriptableObject item;
}

Go ahead and assign this script to all the prefabs and assign the items accordingly. To do so, just select an ItemScriptableObject, like ‘Small Fish’, and set it for all the prefabs, including the inventory item prefab.

For the inventory item, add the Interactable Object Script but assign them nothing. Then, whenever it is instantiated, it can be assigned then.

Assign nothing to the Item portion of the Interactable Object (Script)
Assign nothing to the Item portion of the Interactable Object (Script)

That’s all for the setting up of the creation of an inventory system!

4. Set up Grid and InventorySystem script

Next, we will work on the Grid and InventorySystem scripts. The purpose of the InventorySystem script is to help manage all the functions needed for the inventory. This includes:

  • Sorting the items in the grid
  • Picking up items
  • Removing items in the inventory

The first thing we need to do is create the inventory system and set up the ability to open up or close an inventory by pressing on Tab. So let’s create a new script.

a. Set up InventorySystem script

Create a new C# script inside the Inventory folder and name it InventorySystem. We will be using UnityEngine.UI so remember to add that in!

This would be a Monobehaviour script, so you will not need to delete it for this script. Use a get and private set for the InventorySystem instance to make sure that other scripts cannot modify this instance. In this script, we’ll be needing a grid and you need to type in what kind of grid you will need. We can leave this blank for now, since later on we will be creating a grid object that we will fill in there.

We also need to put in the grid width. In Subnautica, the width is 6, and its height is 8, so we will use these measurements. We’ll also set the cell size to 100f. You can amend this according to what you prefer, based on the resolution of the game. To do this, you would take the length of the screen and divide it in to 6 and 8 spots to see if you can achieve the right cell size.

We’ll need to keep a list of all the items in the inventory, so that we can sort it in the grid. Create a list called inventoryList. Then, we’ll create a reference to the menu to the menu of the inventory that we will be opening and closing, which we will name inventoryTab.

We’ll also need a reference to the prefab that we created earlier on to display on the grid. Then, create a rule that will help keep track of whether the inventory system is open or closed by using a Boolean operation called inventoryOpen.

Instead of Start(), we will change it to Awake(), and set the Instance to be this. Now, in Update(), in order to set up the opening and closing the inventory, we will use some if-else statements to help us do so. So, if Tab is pressed, and the inventory is not open, the system should open the inventory. Set the cursor lock state to none, and then open the inventoryTab. If the inventory is already open, we will need to close the inventoryTab. Lastly, we will need to change inventoryOpen to be the opposite of itself.

Note: Remember that we still have grid object empty – and will fill this out later once we have completed the grid object.

Now we can assign our InventorySystem script to Game Manager and keep the inventory list empty for the time being, and we will also fill this up later. For the UI prefab for GameManager, go into the Prefab folder and drag the InventoryItem prefab into it. It should look like this after you have done so:

Drag InventoryItem Prefab into Inventory System (Script)
Drag InventoryItem Prefab into Inventory System (Script)

And once you’re done, you should have the following script:

InventorySystem.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class InventorySystem : MonoBehaviour
{
    public static InventorySystem Instance { get; private set; }

    int gridWidth = 6;
    int gridHeight = 8;
    float cellSize = 100f;

    //list of all items in inventory
    public List<ItemScriptableObject> inventoryList = new List<ItemScriptableObject>();

    public GameObject inventoryTab;
    public GameObject uiPrefab;
    public bool inventoryOpen;

    private void Awake()
    {
        Instance = this;
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            if (!inventoryOpen)
            {
                Cursor.lockState = CursorLockMode.None;
                inventoryTab.SetActive(true);
            }
            else
            {
                Cursor.lockState = CursorLockMode.Locked;
                inventoryTab.SetActive(false);
            }
            inventoryOpen = !inventoryOpen;
        }
    }
}

Article continues after the advertisement:


Go to Player Canvas and create an empty object and name it ‘Inventory’. This is what will store our inventory system. Anchor it to the left and set the pivot to be at the bottom. Set the size to 600 by 800 and add an image component to it. You can set the image color to any color, we use blue in the tutorial just to act as the background of the inventory system for now. This means that when the inventory is open, the blue will appear, and when it closes, it disappears! You can now drag the Inventory object into the Inventory Tab, and it should look like this:

Inventory Object in Inventory Tab
Inventory Object in Inventory Tab

Disable it by unchecking the box next to ‘Inventory’ in the inspector so that when it starts, the inventory is correctly disabled.

Disable Inventory
Disable Inventory

b. Set Up PlayerController Script

We’ll also need to make sure that our player is not able to move or look around while the inventory is open. To do so, go into the Player object and freeze all the position and rotations for the player under Rigidbody, like so:

Freeze positions in Rigidbody
Freeze Position and Rotation in Rigidbody

So now, if you go to Scripts > Player > Open PlayerController, we will begin on working on cutting off the movement for the player for when the inventory system is open.

Cache the InventorySystem instance and call it iSystem. In the Start() function, get the instance of the InventorySystem. Remember to do this within the Start() rather than directly on the variable itself as the instance is only set in Awake() of InventorySystem. Go into FixedUpdate() and use an if statement. If the inventory is not open, then we can use our movement code. Otherwise, the moment code should not be run. This same logic will be applied to the Lookaround() Code.

PlayerController.cs

InventorySystem iSystem;

void Start()
    {
        rb = GetComponent<Rigidbody>();
        t = this.transform;

        Cursor.lockState = CursorLockMode.Locked;

        inWater = false;

        iSystem = InventorySystem.Instance;
    }

private void FixedUpdate()
    {
        if (!iSystem.inventoryOpen)
        {
            SwimmingOrFloating();
            Move();
        }
    }

void Update()
    {
        if (!iSystem.inventoryOpen)
        {
            LookAround();
        }

        //debug function to unlock cursor
        if (Input.GetKey(KeyCode.Escape))
        {
            Cursor.lockState = CursorLockMode.None;
        }
    }

c. Set up GridObject class

Each section of the grid will store one instance of a GridObject. This helps keeps track of what is stored in each slot in the inventory. Go ahead and create a default grid object class like so:

GridObject.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GridObject : MonoBehaviour
{
    private Grid<GridObject> grid;
    public int x;
    public int y;

    public ItemScriptableObject item;

    //class constructor
    public GridObject(Grid<GridObject> grid, int x, int y)
    {
        this.grid = grid;
        this.x = x;
        this.y = y;
        item = null;
    }

    //overrides tostring method so it shows both x, y and name value
    public override string ToString()
    {
        return x + ", " + y + "\n" + item.name;
    }

    //changes what object placed in this grid object
    public void SetItem(ItemScriptableObject item)
    {
        this.item = item;
        //trigger event handler
        grid.TriggerGridObjectChanged(x, y);
    }

    //remove object from a grid position
    public void ClearItem()
    {
        item = null;
        //call event handler
        grid.TriggerGridObjectChanged(x, y);
    }

    //get the object that is placed in the grid
    public ItemScriptableObject GetItem()
    {
        return item;
    }
}

To summarise, here are the things we will need to add to the GridObject class next:

  • itemImage : Each grid object will have an instance of the inventory image prefab we made earlier to display the contents of the GridObject
  • tempItem: A temporary slot needs to be allocated so that when we sort the inventory and it doesn’t work, we will still maintain the previous sorting. For instance, when you pick up an item, the system will not be able to know if it can fit until it performs the sorting algorithm. In essence, if the algorithm fails, we can just reject the new item and the inventory remains in the state it was previously sorted in.
  • tempItem Function: To get and set the tempItem
  • SetItem Function Edits: This is so that when you set the item, it updates the itemImage.

GridObject.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GridObject
{
    public static GameObject inventoryTab;
    public static GameObject uiPrefab;
    private Grid<GridObject> grid;
    public int x;
    public int y;
    private GameObject itemImage;

    public ItemScriptableObject item;
    public ItemScriptableObject tempItem;


    //class constructor
    public GridObject(Grid<GridObject> grid, int x, int y)
    {
        this.grid = grid;
        this.x = x;
        this.y = y;
        item = null;
    }

    //overrides tostring method so it shows both x, y and name value
    public override string ToString()
    {
        return x + ", " + y + "\n" + item.name;
    }

    //changes what object placed in this grid object
    public void SetItem(ItemScriptableObject item)
    {
        this.item = item;
        if(itemImage == null)
        {
            itemImage = GameObject.Instantiate(uiPrefab, new Vector3(0, 0, 0) * grid.GetCellSize(), Quaternion.identity, inventoryTab.transform);
        }
        itemImage.GetComponentInChildren<Image>().sprite = item.sprite;
        itemImage.GetComponentsInChildren<RectTransform>()[1].sizeDelta = grid.GetCellSize() * item.size;
        itemImage.GetComponent<RectTransform>().anchoredPosition = new Vector3(x, y, 0) * grid.GetCellSize();
        itemImage.GetComponentInChildren<InteractableObject>().item = item;
        itemImage.SetActive(true);
        //trigger event handler
        grid.TriggerGridObjectChanged(x, y);
    }

    //remove object from a grid position
    public void ClearItem()
    {
        item = null;
        if (itemImage != null)
        {
            itemImage.SetActive(false);
        }
        //call event handler
        grid.TriggerGridObjectChanged(x, y);
    }

    //get the object that is placed in the grid
    public ItemScriptableObject GetItem()
    {
        return item;
    }

    public void SetTemp(ItemScriptableObject item)
    {
        tempItem = item;
    }

    public bool EmptyTemp()
    {
        return tempItem == null;
    }

    public void ClearTemp()
    {
        tempItem = null;
    }

    public ItemScriptableObject GetTemp()
    {
        return tempItem;
    }

    public void SetTempAsReal()
    {
        ClearItem();
        if (!EmptyTemp())
        {
            SetItem(tempItem);
        }
        ClearTemp();
    }
}

And now, we can go ahead and create our grid like so:

InventorySystem.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class InventorySystem : MonoBehaviour
{
    public static InventorySystem Instance { get; private set; }

    private Grid<GridObject> grid;

    int gridWidth = 6;
    int gridHeight = 8;
    float cellSize = 100f;

    //list of all items in inventory
    public List<ItemScriptableObject> inventoryList = new List<ItemScriptableObject>();

    public GameObject inventoryTab;
    public GameObject uiPrefab;
    public bool inventoryOpen;

    private void Awake()
    {
        Instance = this;

        GridObject.inventoryTab = inventoryTab;
        GridObject.uiPrefab = uiPrefab;

        //create the grid
        grid = new Grid<GridObject>(gridWidth, gridHeight, cellSize, new Vector3(0, 0, 0), (Grid<GridObject> g, int x, int y) => new GridObject(g, x, y));

    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            if (!inventoryOpen)
            {
                Cursor.lockState = CursorLockMode.None;
                inventoryTab.SetActive(true);
            }
            else
            {
                Cursor.lockState = CursorLockMode.Locked;
                inventoryTab.SetActive(false);
            }
            inventoryOpen = !inventoryOpen;
        }
    }
}

Article continues after the advertisement:


5. Inventory sorting

First, we will add Linq to the the InventorySystem script – so go ahead and add using System.Linq; to the beginning of your code. When sorting the inventory, we are going to want the inventory to sort all items in the inventory according to size from largest to smallest. The algorithm for it will look like this:

  1. For every item, run a check on every single index.
  2. Each check, starting from the cell index, will check if the object is larger than the other objects, one by one, from that index.
  3. For instance, if you’re at a particular index and the object is two by two, it will check the cell below, to the right and the cell one diagonal from it. If all those cells are empty, it will set the object to be in the first cell and fill the rest of the cells with a filler object that we will need to create.

We will need to create an ItemScriptableObject to take the place of that filler object, so go into your Scriptables folder once again and create a new Scriptable Object and name it ‘Filler Object’. In the inspector menu, we can set the Id of the Item Scriptable Object to -1 and name it “filler”.  It does not need a size and we can simply leave the rest of it blank since its meant to be a filler object.

To summarize, here are some of the pieces of code we need in the InventorySystem script:

  • Add a variable ItemScriptableObject named fillerItem: This is where we’re going to assign our filler items in the future. Now you can create a new region that functions to sort the inventory.
  • Create function  AssignItemToSpot(): This is to assign items to grid objects. This function will be a version that takes in ItemScriptableObject and the coordinates, and it will just assign that object to the coordinates. The second part of this is that if you give it an ItemScriptableObject and then a list of coordinates, then it will assign all of those coordinates to be the ItemScriptableObject and filler objects.
  • AssignItemToSpot(): This function is meant for large objects, where we will be taking this list of coordinates and assign items to them accordingly, whereby if it is the first coordinate, it will be set to the item, and if it is any other coordinate, it will be set to a filler item.
  • Version 2 of AssignItemToSpot(): This is for one by one objects, so we can put in a coordinate as well as the item.
  • Create a function to reset temp values: For this code, we’re basically saying that for every grid object in the grid array, it clears a temp. However, if you want to access grid array, you actually have to go to grid and change grid array to public instead of private. In the original CodeMonkey code, the grid array is set as private, but if you change it to public you can access it to get every GameObject easier.
    • When given an ItemScriptableObject and a coordinate, it will check that coordinate and surrounding coordinates to see if there is enough space to fit the item. For instance, if you put in a two by two sized item and a coordinate, it will check that coordinate as well as the coordinate below, to the right, and diagonally from it.
  • Getting all coordinates based on size of the item: Recall that our grid starts from the bottom left and goes to the right then upwards – but what we want is to sort starting from the top left instead of the bottom left. This is why we’ll be doing the y for this portion in negative rather than positive. If one of these coordinates are out of bounds, we will immediately return false.
  • Check coordinates: After getting all coordinates, we will need to verify if all of them are empty. If all of them are empty, then the code will reach this point where we return true to AssignItemToSpot(). This function will now, when given a single coordinate, check if it can fit a large item!
  • AvailSpot() Function: This function, when given an item, checks whether that item is a one by one item or a large item. Then, it runs through every spot in the grid to verify if it can find a spot to fit it. This will be a Boolean function – if there are no spots available, it will need to return false to indicate to the inventory sorting algorithm that the current sorting will not work. Again, since the grid by default starts from the bottom left, we need to make sure that it starts from the top left. Then, we need to check if the spot is empty. If it is not, then go ahead and skip to the next grid spot until we reach an empty one, and then we can check if the item is size one. If the item is size one and this grid spot is empty, you can then assign it to the spot and return true.
  • CheckIfFits() Function: If the item is not that size, we will need to create a function to verify if it fits.
  • SortItems() Function: This function is boolean – we want the function to sort the inventory list based on the size of each item. We can do this using Linq.

Recall: We had a public list of Item Scriptables which is used to store all the items we have in the inventory. This is where SortItems() is going to sort the items from!

  • Create variable, sortedList: Here, we will use Linq OrderByDescending. Next, we need to go through each item in the list and see if we can find a spot for them. For every item that has been put in, the next item will take that item spot into account. To do so, we will use AvailSpot() with the current item. If the system returns a false, we will create a warning message to notify the player that the current sort does not work. It should reset the temporary values, and then return false. This way, nothing gets set as the item.
  • If AvailSpot() is applied to every item and all of it returns true, we continue to set all the temporary values as the real values. This will be done using a second foreach loop in the initial one we just created above. Just like in the reset temp values, we are going to use a similar loop.
  • However, instead of clearing all the temporary objects, we will just use obj.SetTempAsReal(). This is so that when every object has been successfully put into temporary spots, it will go through this foreach loop and set all the temporary spots as real. It will set the item for every single grid and assign the images, item scriptables, and sizes appropriately. Once this is done, return true to finish it up.

InventorySystem.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using UnityEngine.UI;

public class InventorySystem : MonoBehaviour
{
    public static InventorySystem Instance { get; private set; }

    private Grid<GridObject> grid;

    int gridWidth = 6;
    int gridHeight = 8;
    float cellSize = 100f;

    //list of all items in inventory
    public List<ItemScriptableObject> inventoryList = new List<ItemScriptableObject>();

    public GameObject inventoryTab;
    public GameObject uiPrefab;
    public bool inventoryOpen;

    public ItemScriptableObject fillerItem;

    // Start is called before the first frame update
    void Awake()
    {
        Instance = this;

        GridObject.inventoryTab = inventoryTab;
        GridObject.uiPrefab = uiPrefab;

        //create the grid
        grid = new Grid<GridObject>(gridWidth, gridHeight, cellSize, new Vector3(0, 0, 0), (Grid<GridObject> g, int x, int y) => new GridObject(g, x, y));
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            if (!inventoryOpen)
            {
                Cursor.lockState = CursorLockMode.None;
                inventoryTab.SetActive(true);
            }
            else
            {
                Cursor.lockState = CursorLockMode.Locked;
                inventoryTab.SetActive(false);
            }
            inventoryOpen = !inventoryOpen;
        }
    }

    #region Functions to sort the inventory

    //assign items to gidobjects
    void AssignItemToSpot(ItemScriptableObject item, List<Vector2> coords)
    {
        for (int i = 0; i<coords.Count; i++)
        {
            int x = (int)coords[i].x;
            int y = (int)coords[i].y;
            if (i != 0)
            {
                grid.GetGridObject(x, y).SetTemp(fillerItem);
            }
            else
            {
                grid.GetGridObject(x, y).SetTemp(item);
            }
        }
    }

    void AssignItemToSpot(ItemScriptableObject item, int x, int y)
    {
        grid.GetGridObject(x, y).SetTemp(item);
    }

    void ResetTempValues()
    {
        Debug.Log("reset temp");
        foreach(GridObject obj in grid.gridArray)
        {
            obj.ClearTemp();
        }
    }

    bool CheckIfFits(ItemScriptableObject item, Vector2 gridCoordinate)
    {
        List<Vector2> coordsToCheck = new List<Vector2>();

        //get all the coordinates based on the size of the item
        for (int x = 0; x < item.size.x; x++)
        {
            for (int y = 0; y > -item.size.y; y--)
            {
                //if one of the coords is out of bounds, return false
                if((x + gridCoordinate.x) >= gridWidth || (gridCoordinate.y + y) >= gridHeight)
                {
                    return false;
                }

                coordsToCheck.Add(new Vector2(x + gridCoordinate.x, gridCoordinate.y + y));
            }
        }

        //check all the coordinates
        foreach(Vector2 coord in coordsToCheck)
        {
            if(!grid.GetGridObject((int)coord.x, (int)coord.y).EmptyTemp())
            {
                //if there is something in one of these coordinates, return false
                return false;
            }
        }

        //return true
        AssignItemToSpot(item, coordsToCheck);
        return true;
    }

    //check through every spot to find the next available spot
    bool AvailSpot(ItemScriptableObject item)
    {
        for (int y = gridHeight - 1; y >= 0; y--)
        {
            for(int x = 0; x < gridWidth; x++)
            {
                //check if the spot is empty
                if (grid.GetGridObject(x, y).EmptyTemp())
                {
                    //check if size one
                    if(item.size == Vector2.one)
                    {
                        AssignItemToSpot(item, x, y);
                        return true;
                    }
                    else
                    {
                        if(CheckIfFits(item,new Vector2(x, y)))
                        {
                            return true;
                        }
                    }
                }
                
            }
        }

        //after checking every coordinate, no spots found
        return false;
    }

    //function returns true if all items can be sorted, and sorts them properly
    //returns false if items cannot be sorted and deletes all the temporary values
    bool SortItems()
    {
        Debug.Log("SortItems");

        //sort items by size
        var sortedList = inventoryList.OrderByDescending(s => s.size.x * s.size.y);

        //place items systematically
        foreach (ItemScriptableObject item in sortedList)
        {
            bool hasSpot = AvailSpot(item);
            if (hasSpot == false)
            {
                Debug.Log("doesnt fit!");
                ResetTempValues();
                return false;
            }
        }

        foreach (GridObject obj in grid.gridArray)
        {
            obj.SetTempAsReal();
        }

        return true;

    }
    
    #endregion
}

And that’s all for the coding portion for sorting items! You can go ahead and test this code by going to Awake() and call SortItems(). Save this code and go back to Unity, go into Game Manager, into the Inventory System script in the inspector menu and add some Item Scriptable Objects like so:

Add some Item Scriptable Objects into Inventory List
Add some Item Scriptable Objects into Inventory List

Article continues after the advertisement:


Remember to also set the UI Prefab right below this by dragging the InventoryItem prefab we have from Assets > Prefabs into the UI Prefab. Lastly, set the filler item as Filler Object.

The last thing we need before we can test this out, go into our ClearItem() function and create an if loop. If the itemImage!= null, then we drag itemImage.SetActive(false) into the loop. If there is no item image that has been generated for that, we will have to set it.

Your inventory will be sorted by size, and it should look something like this when you run the code by pressing play:

Sorted Inventory
Sorted Inventory

6. Picking Up Items

The last portion of this tutorial will be how we will implement the functions that will help players pick up and throw items away for and from the inventory.

First, go into the InventorySystem script we have. In order to pick up objects from the world, a Raycast must be sent from the camera of the player. To do so, we’ll start by adding a few variables.

In InventorySystem, we’re going to keep a reference to the camera and also a public variable to keep track of how long a Raycast to implement – we will name this playerReach since, depending on how long the Raycast is, that is how the player is going to be able to select objects. Here is what we need:

  • Create a #region called Interacting with Items: We will create a few functions that will handle pickups and disposing of items.
  • Create HoverObject() Function: This function will perform a Raycast that will find any objects with the InteractableObject script on it, then return the InteractableObject that it has found. If the Raycast hits an objuect, it will return the InteractableObject component of that object is hit. Otherwise, it will return a null.
  • PickUpItem() Function: This function will take in an InteractableObject. This function will be called up when an item is picked up. When an item is picked up, the item is added to the inventoryList and then check if SortItems() returns a true or false.
    • If it returns a false, it has to be removed from the inventoryList and display an error message on the screen notifying the player that the inventory is full, then return.
    • Otherwise, the function will not return and instead, destroy the GameObject from the World because it is now inside the inventory.
  • Go into Update() and insert a variable i to be HoverObject(): If i!=null (a.k.a if a player is hovering over an object), then the player can left click to call the function PickupItem().

InventorySystem.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using UnityEngine.UI;

public class InventorySystem : MonoBehaviour
{
    public static InventorySystem Instance { get; private set; }

    private Grid<GridObject> grid;

    int gridWidth = 6;
    int gridHeight = 8;
    float cellSize = 100f;

    //list of all items in the inventory
    public List<ItemScriptableObject> inventoryList = new List<ItemScriptableObject>();

    public GameObject inventoryTab;
    public GameObject uiPrefab;
    public bool inventoryOpen;

    public GameObject cam;

    public float playerReach;

    public ItemScriptableObject fillerItem;

    private void Awake()
    {
        Instance = this;

        GridObject.inventoryTab = inventoryTab;
        GridObject.uiPrefab = uiPrefab;

        //create the grid
        grid = new Grid(gridWidth, gridHeight, cellSize, new Vector3(0, 0, 0), (Grid g, int x, int y) => new GridObject(g, x, y));

        SortItems();

    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            if (!inventoryOpen)
            {
                Cursor.lockState = CursorLockMode.None;
                inventoryTab.SetActive(true);
            }
            else
            {
                Cursor.lockState = CursorLockMode.Locked;
                inventoryTab.SetActive(false);
            }
            inventoryOpen = !inventoryOpen;
        }
        
        InteractableObject i = HoverObject();

        if(i!= null)
        {
            //check if the player left clicks
            if (Input.GetMouseButtonDown(0))
            {
                //pickup item
                PickUpItem(i);
            }
        }
    }

    #region Interacting with items

    InteractableObject HoverObject()
    {
        RaycastHit hit;
        if(Physics.Raycast(cam.transform.position,cam.transform.forward, out hit, playerReach))
        {
            return hit.collider.gameObject.GetComponent<InteractableObject>();
        }

        return null;
    }

    //called when you pick up something
    void PickUpItem(InteractableObject itemPicked)
    {
        inventoryList.Add(itemPicked.item);

        //sort inventory
        if(SortItems() == false)
        {
            //remove it from the inventory list
            inventoryList.Remove(itemPicked.item);

            //error
            Debug.Log("inventory full!");

            return;
        }

        //if all goes well, destroy the object
        Destroy(itemPicked.gameObject);
    }

    #endregion

    #region functions to sort the inventory


}

Now, your player should be able to pick up objects from the World! To test this out, return to Unity, go into Game Manager and assign the camera. To do so, just drag the Main Camera under Player in the hierarchy into the ‘Cam’ field in the inspector menu! Set the Player Reach to be 5. It should look like this:

Assign Main Camera to Cam
Assign Main Camera to Cam

Now we can hit run to test out our code and try to pick up objects from the World!

6. Discarding Items

Lastly, we want to be able to right click an item to throw it out while we are in the inventory. In order for the system to detect which icon the player is clicking on, we will have to use the EventSystem. In the EventSystem, it has a Standalone Input Module which provides some functionality for detecting if a player is hovering over a UI object or not. We can use this information to get the UI object that the player is hovering over, destroy it, and then spawn a prefab of the ItemScriptableObject into the World. In order to do this, we need to extend the Standalone Input Module class so that we can access this point of data since it is a private variable.

Here is what we will need to do:

  • To extend the Standalone Input Module class, go back into Assets > Scripts > Inventory and create a new script. We will name this ExtendedStandaloneInputModule.
  • Remove all of the default using and add in using UnityEngine.EventSystems;: This is a solution that we found on StackOverflow! Instead of inheriting from Monobehaviour, we can inherit from StandaloneInputModule, and also remove the Start() and Update() functions.
  • Create a private instance of the ExtendedStandaloneInputModule class, and set the instance to be this.
  • Create a PointerEventData called eventData: This public static function that returns the pointer event data of the cursor. From this instance, we will retrieve the pointer data. In essence, what we are doing here is extending the Standalone Input Module to add an extra function that allows us to get the pointer data of the player’s cursor instead of having it be private, so we can now access it from our InventorySystem script!

ExtendedStandaloneInputModule.cs

using UnityEngine.EventSystems;

public class ExtendedStandaloneInputModule : StandaloneInputModule
{
    public static PointerEventData GetPointerEventData(int pointerId = -1)
    {
        PointerEventData eventData;
        _instance.GetPointerData(pointerId, out eventData, true);
        return eventData;
    }

    private static ExtendedStandaloneInputModule _instance;

    protected override void Awake()
    {
        base.Awake();
        _instance = this;
    }
}
  • Go back to the InventorySystem script and add using Unity.EventSystems; in the beginning of the script.
  • Go back to the Interacting with Items region and create function RemoveItem(): This allows us to remove an item from the inventory list, then re-sort the inventory.

Finally, it should spawn the World Prefab of that item that was deleted. Now, because we are removing an item, we do not actually need to check if SortItems() will return a false, because it would not possibly happen since the number of items is reduced. Last but not least, it should spawn the object that was removed at the camera’s position.

  • Go back to the Update() function and add another section to check for when the player right clicks: If the cursor is hovering over one of the UI objects, then we get the PointerEventData, which returns to the system all of the items that the cursor is currently hovering over.

The system should be able to cycle through all of them and if the data retrieved has an InteractableObject component, it means that it is an item which can then be removed and have its prefab spawned back into the world! Once the system identifies the InteractableObject, we can then break out of the loop.

InventorySystem.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class InventorySystem : MonoBehaviour
{
    public static InventorySystem Instance { get; private set; }

    private Grid<GridObject> grid;

    int gridWidth = 6;
    int gridHeight = 8;
    float cellSize = 100f;

    //list of all items in the inventory
    public List<ItemScriptableObject> inventoryList = new List<ItemScriptableObject>();

    public GameObject inventoryTab;
    public GameObject uiPrefab;
    public bool inventoryOpen;

    public GameObject cam;

    public float playerReach;

    public ItemScriptableObject fillerItem;

    private void Awake()
    {
        Instance = this;

        GridObject.inventoryTab = inventoryTab;
        GridObject.uiPrefab = uiPrefab;

        //create the grid
        grid = new Grid(gridWidth, gridHeight, cellSize, new Vector3(0, 0, 0), (Grid g, int x, int y) => new GridObject(g, x, y));

        SortItems();

    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            if (!inventoryOpen)
            {
                Cursor.lockState = CursorLockMode.None;
                inventoryTab.SetActive(true);
            }
            else
            {
                Cursor.lockState = CursorLockMode.Locked;
                inventoryTab.SetActive(false);
            }
            inventoryOpen = !inventoryOpen;
        }
        
        InteractableObject i = HoverObject();

        if(i!= null)
        {
            //check if the player left clicks
            if (Input.GetMouseButtonDown(0))
            {
                //pickup item
                PickUpItem(i);
            }
        }

        if (Input.GetMouseButtonDown(1))
        {
            if (EventSystem.current.IsPointerOverGameObject())
            {
                PointerEventData hoveredObj = ExtendedStandaloneInputModule.GetPointerEventData();
                foreach(GameObject currentObj in hoveredObj.hovered)
                {
                    InteractableObject io = currentObj.GetComponent<InteractableObject>();
                    if (io != null)
                    {
                        Debug.Log("remove " + io.item.name);
                        RemoveItem(io.item);
                        break;
                    }
                }
            }
        }
    }

    #region Interacting with items

    InteractableObject HoverObject()
    {
        RaycastHit hit;
        if(Physics.Raycast(cam.transform.position,cam.transform.forward, out hit, playerReach))
        {
            return hit.collider.gameObject.GetComponent<InteractableObject>();
        }

        return null;
    }

    //called when you pick up something
    void PickUpItem(InteractableObject itemPicked)
    {
        inventoryList.Add(itemPicked.item);

        //sort inventory
        if(SortItems() == false)
        {
            //remove it from the inventory list
            inventoryList.Remove(itemPicked.item);

            //error
            Debug.Log("inventory full!");

            return;
        }

        //if all goes well, destroy the object
        Destroy(itemPicked.gameObject);
    }

    //remove object from inventory and spawn it in the world
    void RemoveItem(ItemScriptableObject item)
    {
        inventoryList.Remove(item);
        SortItems();
        Instantiate(item.WorldPrefab, cam.transform.position, cam.transform.rotation);
    }

    #endregion

    #region functions to sort the inventory


}

Article continues after the advertisement:


Before we test this out, go back to Unity and into EventSystem. Click on Debug and we will be able to change the default script to the one we just created, ExtendedStandaloneInputModule.

Debug to Change Default Script
Debug to Change Default Script
Change Default Script to ExtendedStandaloneInputModule
Change the Default Script to ExtendedStandaloneInputModule

Return it back to normal after you have done this, and you should be able to see that the Script has been changed to ExtendedStandaloneInputModule.

And.. Voila! Now we can test our code!

Conclusion

Thank you all for tuning in to Part 3 of our game tutorial series for Creating an Underwater Survival Game like Subnautica! Here are all the scripts we’ve created for this tutorial for your reference.

If you need to, you can also download the project files.

ItemScriptableObject.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "item", menuName = "ScriptableObjects/itemScriptableObject", order = 1)]
public class ItemScriptableObject : ScriptableObject
{
    public int id;
    public string itemName;
    public Vector2 size; //how amny across x and how many across y
    public Sprite sprite;
    public GameObject WorldPrefab;
}

InteractableObject.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InteractableObject : MonoBehaviour
{
    public ItemScriptableObject item;
}

InventorySystem.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class InventorySystem : MonoBehaviour
{
    public static InventorySystem Instance { get; private set; }

    private Grid<GridObject> grid;

    int gridWidth = 6;
    int gridHeight = 8;
    float cellSize = 100f;

    //list of all items in the inventory
    public List<ItemScriptableObject> inventoryList = new List<ItemScriptableObject>();

    public GameObject inventoryTab;
    public GameObject uiPrefab;
    public bool inventoryOpen;

    public GameObject cam;

    public float playerReach;

    public ItemScriptableObject fillerItem;

    // Start is called before the first frame update
    void Awake()
    {
        Instance = this;

        GridObject.inventoryTab = inventoryTab;
        GridObject.uiPrefab = uiPrefab;

        //create the grid
        grid = new Grid<GridObject>(gridWidth, gridHeight, cellSize, new Vector3(0, 0, 0), (Grid<GridObject> g, int x, int y) => new GridObject(g, x, y));

        SortItems();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            if (!inventoryOpen)
            {
                Cursor.lockState = CursorLockMode.None;
                inventoryTab.SetActive(true);
            }
            else
            {
                Cursor.lockState = CursorLockMode.Locked;
                inventoryTab.SetActive(false);
            }
            inventoryOpen = !inventoryOpen;
        }

        InteractableObject i = HoverObject();

        if(i!= null)
        {
            //check if the player left clicks
            if (Input.GetMouseButtonDown(0))
            {
                //pickup item
                PickUpItem(i);
            }
        }

        if (Input.GetMouseButtonDown(1))
        {
            if (EventSystem.current.IsPointerOverGameObject())
            {
                PointerEventData hoveredObj = ExtendedStandaloneInputModule.GetPointerEventData();
                foreach(GameObject currentObj in hoveredObj.hovered)
                {
                    InteractableObject io = currentObj.GetComponent<InteractableObject>();
                    if(io != null)
                    {
                        Debug.Log("remove " + io.item.name);
                        RemoveItem(io.item);
                        break;
                    }
                }
            }
        }
    }

    #region Interacting with items

    InteractableObject HoverObject()
    {
        RaycastHit hit;
        if(Physics.Raycast(cam.transform.position,cam.transform.forward, out hit, playerReach))
        {
            return hit.collider.gameObject.GetComponent<InteractableObject>();
        }

        return null;
    }

    //called when you pick up something
    void PickUpItem(InteractableObject itemPicked)
    {
        inventoryList.Add(itemPicked.item);

        //sort inventory
        if(SortItems() == false)
        {
            //remove it from the inventory list
            inventoryList.Remove(itemPicked.item);

            //error
            Debug.Log("inventory full!");

            return;
        }

        //if all goes well, destroy the object
        Destroy(itemPicked.gameObject);
    }

    //remove object from inventory and spawn it in the world
    void RemoveItem(ItemScriptableObject item)
    {
        inventoryList.Remove(item);
        SortItems();
        Instantiate(item.worldPrefab, cam.transform.position, Quaternion.identity);
    }

    #endregion

    #region Functions to sort the inventory

    //assign items to gidobjects
    void AssignItemToSpot(ItemScriptableObject item, List<Vector2> coords)
    {
        for (int i = 0; i<coords.Count; i++)
        {
            int x = (int)coords[i].x;
            int y = (int)coords[i].y;
            if (i != 0)
            {
                grid.GetGridObject(x, y).SetTemp(fillerItem);
            }
            else
            {
                grid.GetGridObject(x, y).SetTemp(item);
            }
        }
    }

    void AssignItemToSpot(ItemScriptableObject item, int x, int y)
    {
        grid.GetGridObject(x, y).SetTemp(item);
    }

    void ResetTempValues()
    {
        Debug.Log("reset temp");
        foreach(GridObject obj in grid.gridArray)
        {
            obj.ClearTemp();
        }
    }

    bool CheckIfFits(ItemScriptableObject item, Vector2 gridCoordinate)
    {
        List<Vector2> coordsToCheck = new List<Vector2>();

        //get all the coordinates based on the size of the item
        for (int x = 0; x < item.size.x; x++)
        {
            for (int y = 0; y > -item.size.y; y--)
            {
                //if one of the coords is out of bounds, return false
                if((x + gridCoordinate.x) >= gridWidth || (gridCoordinate.y + y) >= gridHeight)
                {
                    return false;
                }

                coordsToCheck.Add(new Vector2(x + gridCoordinate.x, gridCoordinate.y + y));
            }
        }

        //check all the coordinates
        foreach(Vector2 coord in coordsToCheck)
        {
            if(!grid.GetGridObject((int)coord.x, (int)coord.y).EmptyTemp())
            {
                //if there is something in one of these coordinates, return false
                return false;
            }
        }

        //return true
        AssignItemToSpot(item, coordsToCheck);
        return true;
    }

    //check through every spot to find the next available spot
    bool AvailSpot(ItemScriptableObject item)
    {
        for (int y = gridHeight - 1; y >= 0; y--)
        {
            for(int x = 0; x < gridWidth; x++)
            {
                //check if the spot is empty
                if (grid.GetGridObject(x, y).EmptyTemp())
                {
                    //check if size one
                    if(item.size == Vector2.one)
                    {
                        AssignItemToSpot(item, x, y);
                        return true;
                    }
                    else
                    {
                        if(CheckIfFits(item,new Vector2(x, y)))
                        {
                            return true;
                        }
                    }
                }
                
            }
        }

        //after checking every coordinate, no spots found
        return false;
    }

    //function returns true if all items can be sorted, and sorts them properly
    //returns false if items cannot be sorted and deletes all the temporary values
    bool SortItems()
    {
        Debug.Log("SortItems");

        //sort items by size
        var sortedList = inventoryList.OrderByDescending(s => s.size.x * s.size.y);

        //place items systematically
        foreach (ItemScriptableObject item in sortedList)
        {
            bool hasSpot = AvailSpot(item);
            if (hasSpot == false)
            {
                Debug.Log("doesnt fit!");
                ResetTempValues();
                return false;
            }
        }

        foreach (GridObject obj in grid.gridArray)
        {
            obj.SetTempAsReal();
        }

        return true;

    }
    
    #endregion
}

PlayerController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    //reference the transform
    Transform t;

    InventorySystem iSystem;

    public static bool inWater;
    public static bool isSwimming;
    //if not in water, walk
    //if in water and not swimming, float
    //if in water and swimming, swim

    public LayerMask waterMask;

    [Header("Player Rotation")]
    public float sensitivity = 1;

    //clamp variables
    public float rotationMin;
    public float rotationMax;

    //mouse input variables
    float rotationX;
    float rotationY;

    [Header("Player Movement")]
    public float speed = 1;
    float moveX;
    float moveY;
    float moveZ;

    Rigidbody rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        t = this.transform;

        Cursor.lockState = CursorLockMode.Locked;

        inWater = false;

        iSystem = InventorySystem.Instance;
    }

    private void FixedUpdate()
    {
        if (!iSystem.inventoryOpen)
        {
            SwimmingOrFloating();
            Move();
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        SwitchMovement();
    }

    private void OnTriggerExit(Collider other)
    {
        SwitchMovement();
    }

    void SwitchMovement()
    {
        //toggle inWater
        inWater = !inWater;

        //change the rigidbody accordingly.
        rb.useGravity = !rb.useGravity;
    }

    void SwimmingOrFloating()
    {
        bool swimCheck = false;

        if (inWater)
        {
            RaycastHit hit;
            if(Physics.Raycast(new Vector3(t.position.x,t.position.y + 0.5f,t.position.z),Vector3.down,out hit, Mathf.Infinity, waterMask))
            {
                if(hit.distance < 0.1f)
                {
                    swimCheck = true;
                }
            }
            else
            {
                swimCheck = true;
            }
        }

        isSwimming = swimCheck;
        //Debug.Log("isSwiming = " + isSwimming);
    }

    // Update is called once per frame
    void Update()
    {
        if (!iSystem.inventoryOpen)
        {
            LookAround();
        }
        

        //debug function to unlock cursor
        if (Input.GetKey(KeyCode.Escape))
        {
            Cursor.lockState = CursorLockMode.None;
        }
    }

    void LookAround()
    {
        //get the mous input
        rotationX += Input.GetAxis("Mouse X")*sensitivity;
        rotationY += Input.GetAxis("Mouse Y")*sensitivity;

        //clamp the y rotation
        rotationY = Mathf.Clamp(rotationY, rotationMin, rotationMax);

        //setting the rotation value every update
        t.localRotation = Quaternion.Euler(-rotationY, rotationX, 0);
    }

    void Move()
    {
        //get the movement input
        moveX = Input.GetAxis("Horizontal");
        moveY = Input.GetAxis("Vertical");
        moveZ = Input.GetAxis("Forward");

        //check if the player is in water
        if (inWater)
        {
            rb.velocity = new Vector2(0,0);
        }
        else
        {
            //check if the player is standing still
            if(moveX == 0 && moveZ == 0)
            {
                rb.velocity = new Vector2(0, rb.velocity.y);
            }
        }

        if (!inWater)
        {
            //move the character (land ver)
            t.Translate(new Quaternion(0, t.rotation.y, 0, t.rotation.w) * new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed, Space.World);
        }
        else
        {
            //check if the player is swimming under water or floating along the top
            if (!isSwimming)
            {
                //move the player (floating ver)
                //clamp the moveY value, so they cannot use space or shift to move up
                moveY = Mathf.Min(moveY, 0);

                //conver the local direction vector into a worldspace vector/ 
                Vector3 clampedDirection = t.TransformDirection(new Vector3(moveX, moveY, moveZ));

                //clamp the values of this worldspace vector
                clampedDirection = new Vector3(clampedDirection.x, Mathf.Min(clampedDirection.y, 0), clampedDirection.z);

                t.Translate(clampedDirection * Time.deltaTime * speed, Space.World);
            }
            else
            {
                //move the character (swimming ver)
                t.Translate(new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed);
                t.Translate(new Vector3(0, moveY, 0) * Time.deltaTime * speed, Space.World);
            }
        }

    }
}

Article continues after the advertisement: