Skip to main content

Loading and Saving Collectables

Written by: Akram Taghavi-Burris | © Copyright 2024
Status

While we've implmented colletables into our game, what if we wanted to quickly create multiple variations of collectables. Using a CSV file to store their datawould allow us to efficiently manage collectable items in our game, allowing for easy expansion and modification without changing the underlying code. In order to read this data we will implement a CollectableLoader class that reads this CSV file and converts the data into usable Scriptable Objects within Unity.

Creating the CSV File

To start, we need to create a CSV file that contains all the information about our collectables. A simple way to create this file is by using a spreadsheet application such as Microsoft Excel or Google Sheets. Follow these steps:

  1. Open Excel or Google Sheets: Start a new spreadsheet.

  2. Define the Columns: Create the following headers in the first row:

    • CollectableName: The name of the collectable item.
    • PointValue: The points awarded for collecting the item.
    • CollectedSound: The path to the audio clip that will be played when the item is collected.
    • IsDataSaved: A boolean (true/false) value to indicate if the data is saved in the CSV file.
  3. Add Collectable Data: Below the headers, input data for each collectable. For example:

    CollectableNamePointValueCollectedSoundIsDataSaved
    Organge10Audio/OrgangeCollecttrue
    Peach15Audio/PeachCollecttrue
    Pizza100Audio/PizzaCollecttrue
  4. Save as CSV: Once you've entered the data, save the file as collectableData.csv. If using Excel, select "CSV (Comma delimited) (*.csv)" as the file format. Ensure that the file is placed in the Assets/StreamingAssets directory of your Unity project, as Unity will need to access it at runtime.

StreamingAssets Folders

Unity has a series of special folders designed for different use cases. When it comes to accessing external content, such as CSV or JSON files, we can utilize either the StreamingAssets or Resources folder. However, for importing CSV files, StreamingAssets is often the better choice. This folder allows for more straightforward file manipulation and access. Since CSV files can be large and are not directly tied to Unity's asset system, using StreamingAssets provides greater flexibility, enabling you to read and write these files at runtime without the overhead of Unity's resource management.

Updating CollectableData

The IsDataSaved boolean property serves an important role in our collectable data management system. It indicates whether a specific collectable's data has been saved to the CSV file. This property will initially be set to true for all entries loaded from the CSV, signifying that they are up-to-date and have been saved successfully. Conversely, we will need to updated the CollectableData scriptable object to also keep track of this property. The highlighted lines below demonstrate how we can implment this:

CollectableData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Creation menu for ScriptableObject
[CreateAssetMenu(fileName = "New Collectable", menuName = "Collectable Data")]
public class CollectableData : ScriptableObject
{
public string CollectableName;
public int PointValue;
public AudioClip CollectedSound;
public bool IsDataSaved = false;

//Force the asset and CollectableName to be the same, using OnValidate()
//OnValidate runs whenever there is a change in the Editor
private void OnValidate()
{
if (string.IsNullOrEmpty(CollectableName))
{//set collectable name to the name of the asset
CollectableName = this.name;
}
else
{//set the aset name ot the colllectable name
this.name = CollectableName;
}//end if
}//end OnValidate()
}

By tracking whether modifications to collectable data have been saved back to the CSV, we can efficiently manage updates and ensure that the data remains consistent across different parts of the application. This will ultimately enhance our data handling capabilities and contribute to a more robust and user-friendly experience.

Collectable Loader

Now that we have our CSV file ready, we need to implement a CollectableLoader class that will perform the following tasks:

  • Load the CSV Data: Read the CSV file from the specified path and extract the information for each collectable.

  • Create Collectable Instances: For each row in the CSV file, create a new instance of CollectableData, populating its properties with the values from the file.

  • Return a List: Compile all instances into a list and return this list for further use within the game.

Here's the implementation of the CollectableLoader class:

CollectableLoader.cs
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class CollectableLoader : MonoBehaviour
{
[Tooltip("File name and extension")]
[SerializeField]
private string _fileName; //Name of file to load
private string _filePath; //Path to the csv file

// Gets the path to the CSV file in the StreamingAssets directory
private string GetFilePath()
{
// Combine the StreamingAssets path with the CSV file name
return Path.Combine(Application.streamingAssetsPath, _fileName);
}//end GetFilePath()

// Loads collectable data from a CSV file, creates ScriptableObject assets, and returns a list of CollectableData
public List<CollectableData> LoadCollectables()
{
// Initialize a list to hold the loaded collectable data
List<CollectableData> collectableDataList = new List<CollectableData>();

// Get the full path to the CSV file
_filePath = GetFilePath();

// Check if the CSV file exists; if not, log an error and return an empty list
if (!File.Exists(_filePath))
{
Debug.LogError("CSV file not found at " + _filePath);
return collectableDataList; // Return empty list if CSV not found
}

// Read all lines from the CSV file into an array
string[] csvLines = File.ReadAllLines(_filePath);

// Process each line in the CSV to create CollectableData assets
collectableDataList = ProcessCSVLines(csvLines);

// Save any changes to the assets and refresh the AssetDatabase
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();

Debug.Log("Collectable data loaded and assets created.");

return collectableDataList; // Return the list of created CollectableData
}//end LoadCollectables()

// Processes each line of the CSV, creating CollectableData assets and returning a list of them
private List<CollectableData> ProcessCSVLines(string[] csvLines)
{
// Initialize a list to hold the created CollectableData instances
List<CollectableData> collectableDataList = new List<CollectableData>();

// Loop through each line of the CSV, starting from the second line (index 1) to skip the header
for (int i = 1; i < csvLines.Length; i++)
{
// Split the line into individual data fields using commas
string[] data = csvLines[i].Split(',');

// Check if the collectableData was created successfully; if so, add it to the list
if (collectableData != null)
{
collectableDataList.Add(collectableData);
}//end if(collectableData != null)

}//end for

// Return the list of created CollectableData instances
return collectableDataList;
}//end ProcessCSVLines()
}

Load Collectable Data in Editor

While our previous code demonstrates how to load CollectableData from a CSV file, we haven't yet specified when and how this loading code should run. For now, let's assume that we want to load this data directly into the Unity Editor, enabling our level designer to easily access and apply it to prefabs or objects within the editor environment. To achieve this, we need to modify the CollectableLoader to create CollectableData assets within Unity's asset folder structure.

In order to achieve this we will enhance our CollectableLoader by adding the following:

  • Context Menu: This attribute enables LoadCollectables to be invoked directly from the Inspector, making it easy to load collectable data and create assets on demand.
GameObject in Scene

Because CollectableLoader inherits from MonoBehaviour the class will need to be attached to a game object in the scene before the context menu is available.

  • Load Collectables in Editor Method: The LoadCollectablesInEditor calls LoadCollectables to handle the main logic for loading the data, creating CollectableData assets, and returning a list of them.

  • Processing CSV Lines: Already processes each line of the csv file. We need to update this so that after processing all lines, it passes that data to the CreateCollectableData method for creating the asset.

  • Create Collectable Data Asset: The CreateCollectableData method is responsible for taking data and turning it into a CollectableData asset, which is saved to a specified folder in the Asset folder. In this instance we will define a Collectables folder in the root of the Asset folder to be used.

Asset Folder

Note that the Collectables folder in the Assets folder will need to be created before running the Load Collectables menu or an an error will return citing that the destination folder does not exists.

Implementation

The highlighted code shows the revisions to the CollectableLoader in order to add these assets to the editor.

CollectableLoader.cs
public class CollectableLoader : MonoBehaviour
{
[Tooltip("File name and extension")]
[SerializeField]
private string _fileName; //Name of file to load
private string _filePath; //Path to the csv file

// Gets the path to the CSV file in the StreamingAssets directory
private string GetFilePath()
{
// Combine the StreamingAssets path with the CSV file name
return Path.Combine(Application.streamingAssetsPath, _fileName);
}//end GetFilePath()

// Loads collectable data from a CSV file, creates ScriptableObject assets, and returns a list of CollectableData
public List<CollectableData> LoadCollectables()
{
// Initialize a list to hold the loaded collectable data
List<CollectableData> collectableDataList = new List<CollectableData>();

// Get the full path to the CSV file
_filePath = GetFilePath();

// Check if the CSV file exists; if not, log an error and return an empty list
if (!File.Exists(_filePath))
{
Debug.LogError("CSV file not found at " + _filePath);
return collectableDataList; // Return empty list if CSV not found
}

// Read all lines from the CSV file into an array
string[] csvLines = File.ReadAllLines(_filePath);

// Process each line in the CSV to create CollectableData assets
collectableDataList = ProcessCSVLines(csvLines);

// Save any changes to the assets and refresh the AssetDatabase
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();

Debug.Log("Collectable data loaded and assets created.");

return collectableDataList; // Return the list of created CollectableData
}//end LoadCollectables()

// Processes each line of the CSV, creating CollectableData assets and returning a list of them
private List<CollectableData> ProcessCSVLines(string[] csvLines)
{
// Initialize a list to hold the created CollectableData instances
List<CollectableData> collectableDataList = new List<CollectableData>();

// Loop through each line of the CSV, starting from the second line (index 1) to skip the header
for (int i = 1; i < csvLines.Length; i++)
{
// Split the line into individual data fields using commas
string[] data = csvLines[i].Split(',');

// Create a CollectableData asset from the parsed data
CollectableData collectableData = CreateCollectableData(data);

// Check if the collectableData was created successfully; if so, add it to the list
if (collectableData != null)
{
collectableDataList.Add(collectableData);
}//end if(collectableData != null)

}//end for

// Return the list of created CollectableData instances
return collectableDataList;
}//end ProcessCSVLines()

//Context menu for loading collectables, calls the LoadCollectablesInEditor()
[ContextMenu("Load Collectables")]
public void LoadCollectablesInEditor()
{
LoadCollectables();
}

// Creates a CollectableData asset from an array of string data fields
private CollectableData CreateCollectableData(string[] data)
{
// Create a new instance of CollectableData as a ScriptableObject
CollectableData collectableData = ScriptableObject.CreateInstance<CollectableData>();

// Assign the name of the collectable from the first column of the CSV
collectableData.CollectableName = data[0];

// Attempt to parse PointValue, defaulting to 0 if parsing fails
collectableData.PointValue = int.TryParse(data[1], out var pointValue) ? pointValue : 0;

// Load the associated sound effect from the third column; if not found, will be null
collectableData.CollectedSound = Resources.Load<AudioClip>(data[2]);

// Parse the boolean value from the fourth column; assign false if parsing fails
collectableData.IsDataSaved = bool.TryParse(data[3], out bool isDataSaved) ? isDataSaved : false;

// Define the asset path where the CollectableData asset will be saved
string assetPath = Path.Combine("Assets/Collectables", $"{collectableData.CollectableName}.asset");

// Create the asset in the specified path
AssetDatabase.CreateAsset(collectableData, assetPath);

// Return the created CollectableData asset
return collectableData;
}//end CreateCollectableData()
}
Adding a Context Menu

The [ContextMenu("Menu name")] attribute in Unity is a simple way to add a custom menu item to the Inspector for a specific component. When you use this attribute above a method in a script, Unity will add a menu option with the specified "Menu name" to the right-click context menu of the component in the Inspector.

Load Collectables

With our updates to the CollectableLoader script complete, the next step is to set it up in the Unity Editor to load our collectable data. Follow these steps:

  1. Create a GameObject: In any scene (the specific scene doesn't matter for this setup), create an empty GameObject and name it "CollectableLoader" or something similar.

  2. Attach the Script: Attach the CollectableLoader component to the GameObject. You can do this by dragging the script onto the GameObject or by selecting Add Component in the Inspector and finding CollectableLoader.

  3. Ensure Collectables Folder Exists: Make sure there's a Collectables folder in the Assets directory of the Project window. This folder is where our generated assets will be stored.

  4. Load Collectables: With the CollectableLoader GameObject selected, go to the Inspector, right-click on the CollectableLoader script, and choose Load Collectables from the context menu. The script will now run, reading the data from the CSV file and creating CollectableData assets.

  5. Check Assets: Return to the Project window, and you'll see the Collectables folder populated with new assets, each representing an entry from the CSV file.

Update Collectable Data Assets

If you update the CSV file, you can easily bring those changes into Unity by re-running the Load Collectables context menu option. Simply make your desired changes to the CSV file, select the CollectableLoader GameObject in the scene, and right-click the CollectableLoader script in the Inspector to choose Load Collectables again. Even if CollectableData assets already exist, the script will update these assets with the latest data from the CSV, ensuring the collectable information in Unity matches the current CSV file contents.

Save Collectable Data to CSV File

Just as we want to load our external data into the editor, there may be instances where we need to save a new CollectableData back to the CSV file to ensure consistency. This is particularly useful when creating new collectable items that should be reflected in our external data source. To facilitate this, we will create a CollectableSaver class, which will be called by the CollectableData scriptable object whenever a new item is created.

CollectableSaver Class

The CollectableSaver class manages the logic for saving CollectableData to a CSV file. The implementation includes the following components:

  • Save Collectable Data Method: The SaveCollectableData method accepts a CollectableData object as an argument. It constructs the file path for the CSV file located in the StreamingAssets directory of the application. This method handles both saving new data and updating existing data.

  • Prepare New Line: The PrepareNewLine method extracts properties from the CollectableData object (e.g., collectable name, point value, and collect sound) and formats the values accordingly. It converts numerical values to strings and appropriately handles any null values. This method compiles the CollectableData properties into a formatted string for saving to the CSV file.

  • Updating Data: The logic to check if a collectable already exists in the CSV file has been streamlined into the UpdateCollectableData method. If the collectable is found, this method updates the existing line; if not, the new line is added to the list for saving.

  • Create a New CSV File: The CreateNewFile method is responsible for creating a new CSV file if it does not already exist. It initializes the file with a header line to define the structure of the data.

Implementing CollectableSaver

Now that we have updated our CSV file format, let's implement the CollectableSaver class. The CollectableSaver will manage the saving and updating of CollectableData objects, ensuring that all relevant fields, including IsDataSaved, are properly handled.

CollectableSaver.cs
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class CollectableSaver : MonoBehaviour
{
private static string _fileName = "collectableData.csv";
private static string _filePath; //Path to the csv file

// Gets the path to the CSV file in the StreamingAssets directory
private static string GetFilePath()
{
return Path.Combine(Application.streamingAssetsPath, _fileName);
}//end GetFilePath()

// Saves the collectable data to a CSV file.
public static void SaveCollectableData(CollectableData collectableData)
{
// Ensure IsDataSaved is set to true before saving
collectableData.IsDataSaved = true;

// Get the file path where the data will be saved
_filePath = GetFilePath();

// Check if the file does not exists
if (!File.Exists(_filePath))
{
CreateNewFile(_filePath); //create it and write the header

}//end if (!File.Exists(filePath))

// Read all lines from the existing file
List<string> lines = new List<string>(File.ReadAllLines(_filePath));

// Prepare the new line for the CSV file based on the collectable data
string newLine = PrepareNewLine(collectableData);

// Update the collectable data if it exists
if (UpdateCollectableData(lines, collectableData, newLine))
{
// Log the update if successful
Debug.Log($"Collectable data updated: {newLine}");
}
else
{
// If the collectable was not found, add it as a new line
lines.Add(newLine);

Debug.Log($"Collectable data saved: {newLine}");
}//end if (UpdateCollectableData())

// Write the content back to the file
File.WriteAllLines(_filePath, lines.ToArray());

// Set IsSaved to true after saving
collectableData.IsDataSaved = true;

}//end SaveCollectableData()

// Checks if the collectable already exists in the provided lines and updates if found.
private static bool UpdateCollectableData(List<string> lines, CollectableData collectableData, string newLine)
{
// Start from 1 to skip the header
for (int i = 1; i < lines.Count; i++)
{
// Split the existing line by commas
string[] parts = lines[i].Split(',');

// Check if the collectable name matches (assuming it�s the first column)
if (parts[0] == collectableData.CollectableName)
{
// Update the existing line with the new data
lines[i] = newLine;
return true; // Indicate that the item was updated
}
}
return false; // Indicate that the item was not found
} // end UpdateCollectableData()

// Prepares a formatted string for the CSV line based on the collectable data.
private static string PrepareNewLine(CollectableData collectableData)
{
// Retrieve each property, handle null or empty cases appropriately
string collectableName = string.IsNullOrEmpty(collectableData.CollectableName)
? string.Empty
: collectableData.CollectableName;

// Convert to string
string collectablePointValue = collectableData.PointValue.ToString();

// Use the sound name or an empty string if null
string collectableCollectedSound = collectableData.CollectedSound != null
? collectableData.CollectedSound.name
: string.Empty;

// Always set IsDataSaved to "true" in the CSV
string isSaved = "true";

// Format and return the new CSV line
return $"{collectableName},{collectablePointValue},{collectableCollectedSound},{isSaved}";

}//end PrepareNewLine

// Creates a new CSV file and writes the header if it does not exist.
private static void CreateNewFile(string filePath)
{
// Write the header for the CSV file
File.WriteAllText(filePath, "CollectableName,PointValue,CollectedSound\n");

}//end CreateNewFile()
}
CSV File Names in CollectableSaver

The CollectableSaver class has a hardcoded CSV file name, unlike CollectableLoader, where the name is set in the inspector. Because CollectableSaver is not attached to a GameObject, its fields cannot be modified in the editor. This class is intended to function as an editor script, running within Unity's editor environment. To support dynamic input for the file name, a custom editor class would need to be created to control how CollectableSaver appears in the editor. However, creating this custom editor interface is beyond the scope of this lesson.

Updating the CollectableData

Next, we need to update the CollectableData scriptable object to include both the IsDataSaved field and a context menu option that allows us to save the collectable data directly from the Unity Inspector. Here's the updated CollectableData script:

CollectableData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Creation menu for ScriptableObject
[CreateAssetMenu(fileName = "New Collectable", menuName = "Collectable Data")]
public class CollectableData : ScriptableObject
{
public string CollectableName;
public int PointValue;
public AudioClip CollectedSound;
public bool IsDataSaved = false;

//Force the asset and CollectableName to be the same, using OnValidate()
//OnValidate runs whenever there is a change in the Editor
private void OnValidate()
{
//If there is a null or no value to CollectableName
if (string.IsNullOrEmpty(CollectableName))
{
// Set CollectableName to the name of the asset
CollectableName = this.name;
}
else
{
// Set the asset name to the CollectableName
this.name = CollectableName;
}//end if
}//end OnValidate()

//Context menu for loading collectables, calls the LoadCollectablesInEditor()
[ContextMenu("Save Collectables")]
public void SaveCollectable()
{
// Use the CollectableSaver to save the collectable data
CollectableSaver.SaveCollectableData(this);
}//end SaveCollectable()

}

Saving Collectable Data

With the implementation of the CollectableSaver class and the updated CollectableData scriptable object, we now have a robust system for saving collectable data back to a CSV file. This system allows you to maintain consistency between your in-game assets and external data. To implement this functionality, follow these simple steps:

  1. Create a New Collectable: Right-click in the Project window and navigate to Create > Collectable Data.

    • Name the new CollectableData asset (e.g., "Coin", "Gem").
  2. Configure the Collectable: With the newly created CollectableData asset selected, in the inspector window set the values for the fields:

    • Collectable Name: Enter the desired name (e.g., "Gold Coin").
    • Point Value: Set a point value (e.g., 10).
    • Collect Sound: Assign an audio clip for the collectable's sound effect.
  3. Save the Collectable Data: Wth the CollectableData asset selected in the Inspector window, right-click on the and choose Save Collectables from the context menu. This will save the asset data back to the csv file.

By following these steps, you can effectively create and manage your collectable data within Unity, ensuring that your project remains organized and up-to-date.

Summary

Having established a solid system for loading collectable data from a CSV file and saving updates back to it within the Unity Editor, we have laid the groundwork for effective data management during game development. Next, we will expand our focus to runtime data handling by introducing a spawner that will allow us to load and save collectable data dynamically during gameplay. This transition will enhance our game's flexibility and interactivity, ensuring that collectables can be managed efficiently while the game is running.