Hi, I have been working on a turn-based strategy game that has over 100 unique units with two variations each. Based off of this fact, I decided to use a small amount of generic base unit GameObjects that I would attach models to when instantiated while getting the unit data from a data file, instead of creating a GameObject for each unit variation. I’m not sure if this is the proper way to do this in Unity, so some feedback for this concept would be appreciated.
Regardless, my question is how to manage the hundreds of units/models needed during the course of the game. Dragging hundreds of objects into an array does not seem like the most effective use of development time and loading from a resources file is frowned upon, but aside from those two methods, I didn’t find any other way of getting the resources. Thanks.
I’ve tried both your methods and prefer dragging 100 objects into an array.
For 2d sprite textures I’ve also done loading by name from a SpriteAtlas pointing to a parent folder I can drop PNGs into at will. The atlas is so clean and convenient, it’s tempting to write a version of this for models, prefabs, materials, etc that automatically sucks them from a folder into the project at compile time, then simply allows accessing them by filename.
This may be overkill for your needs, but I just ran into yet another use case for a dynamic atlas so I wrote one. I’ll leave this here in case it’s useful. It’s a ScriptableObject which updates a list of assets whenever Ctrl+R finds a new one.
using System;
using System.Collections.Generic;
using UnityEngine;
public enum NWAtlasAssetType {
Custom = 0,
Material = 1,
Texture2D = 2,
Mesh = 3,
PrefabGameObject = 4,
}
/// <summary>
/// Sucks assets of a type out of the filesystem at compile time and makes them available at runtime.
/// </summary>
[CreateAssetMenu]
public class NWAtlas : ScriptableObject {
[Header("Include at least one asset to determine Custom type")]
public NWAtlasAssetType assetType = NWAtlasAssetType.Custom;
// show actual asset type for inspector debugging
[ShowInInspector("customAssetType")]
public string _customAssetType = "dummy field for inspector display and label";
public string customAssetType => GetCustomAssetType() == null ? "NONE" : GetCustomAssetType().ToString();
/// <summary>
/// Add individual assets or directories.
/// </summary>
public UnityEngine.Object[] assetsOrDirectories = new UnityEngine.Object[]{};
/// <summary>
/// Includes all individual assets plus all assets of the correct type from assetsOrDirectories.
/// Updated automatically by NWAtlasEditor.UpdateAssetsList.
/// </summary>
[ReadOnly]
public List<UnityEngine.Object> packedAssets = new List<UnityEngine.Object>();
/// <summary>
/// Determine what type of asset should be pulled from any directories in assetsOrDirectories.
/// If assetType is set to Custom, returns the type of the first non-directory in assetsOrDirectories.
/// </summary>
public System.Type GetCustomAssetType() {
switch (assetType) {
case NWAtlasAssetType.Custom:
break;
case NWAtlasAssetType.PrefabGameObject:
return typeof(GameObject);
case NWAtlasAssetType.Material:
return typeof(Material);
case NWAtlasAssetType.Texture2D:
return typeof(Texture2D);
case NWAtlasAssetType.Mesh:
return typeof(Mesh);
default:
throw new ArgumentOutOfRangeException();
}
// use the type of the first non-directory item in assetsOrDictionaries
foreach (UnityEngine.Object obj in assetsOrDirectories) {
if (obj == null || obj is UnityEditor.DefaultAsset) continue;
return obj.GetType();
}
return null;
}
/// <summary>
/// Pull out all/any objects of the given type from the list of packed assets.
/// </summary>
public List<T> GetAssets<T>() where T : UnityEngine.Object {
List<T> results = new List<T>();
foreach (UnityEngine.Object obj in packedAssets) {
if (obj is T objCast) results.Add(objCast);
}
return results;
}
/// <summary>
/// Pull out an asset with the given name from the list of packed assets.
/// </summary>
public T GetAssetByName<T>(string name) where T : UnityEngine.Object {
List<T> assets = GetAssets<T>();
foreach (T asset in assets) {
if (asset.name == name) return asset;
}
return null;
}
}
And its Editor / manager, it goes in Assets/Scripts/Editor:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;
using Object = UnityEngine.Object;
[CustomEditor(typeof(NWAtlas))]
public class NWAtlasEditor : Editor {
public void Awake() {
UpdateAssetsList((NWAtlas)target);
}
public override void OnInspectorGUI() {
EditorGUI.BeginChangeCheck();
base.OnInspectorGUI();
if (EditorGUI.EndChangeCheck() || GUILayout.Button("Force Update Packed Assets")) {
UpdateAssetsList((NWAtlas)target);
}
}
public static void UpdateAssetsList(NWAtlas atlas) {
if (atlas == null) return;
atlas.packedAssets.Clear();
System.Type assetType = atlas.GetCustomAssetType();
if (assetType == null) return;
foreach (UnityEngine.Object obj in atlas.assetsOrDirectories) {
if (obj == null) continue;
if (obj is UnityEditor.DefaultAsset) {
string path = AssetDatabase.GetAssetPath(obj);
if (IsDirectory(path)) {
// pull assets out of a directory
foreach (UnityEngine.Object dirObj in GetAssetsInDirectory(path, assetType)) {
if (!atlas.packedAssets.Contains(dirObj)) {
atlas.packedAssets.Add(dirObj);
}
}
continue;
}
}
// add individual asset to the list
if (!atlas.packedAssets.Contains(obj)) {
atlas.packedAssets.Add(obj);
}
}
}
private static bool IsDirectory(string path) {
return (File.GetAttributes(path) & FileAttributes.Directory) == FileAttributes.Directory;
}
private static List<Object> GetAssetsInDirectory(string path, System.Type type) {
List<Object> results = new List<Object>();
string absolutePath = Application.dataPath + "/"
+ (path.StartsWith("Assets/") ? path.Substring("Assets/".Length) : path);
if (!Directory.Exists(absolutePath)) {
Debug.LogError("NWAtlasEditor.GetAssetsInDirectory invalid path " + path);
return results;
}
foreach (string fileName in Directory.GetFiles(absolutePath)) {
if (fileName.EndsWith(".meta")) continue;
string localPath = "Assets" + fileName.Substring(Application.dataPath.Length);
// only returns the FIRST asset so for FBX with multiple parts will only return one part
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(localPath, type);
if (asset != null) {
results.Add(asset);
}
}
// recurse through child directories
foreach (string directoryName in Directory.GetDirectories(absolutePath)) {
string localPath = "Assets" + directoryName.Substring(Application.dataPath.Length);
results.AddRange(GetAssetsInDirectory(localPath, type));
}
return results;
}
/// <summary>
/// Keep all the NWAtlases up to date when something changes.
/// Called by OnPostprocessAllAssets to update when CTRL+R refresh finds a new asset.
/// </summary>
//[UnityEditor.Callbacks.DidReloadScripts] uncomment to fire when play is hit or scripts are compiled.
public static void OnAssetsChanged() {
string[] guids = AssetDatabase.FindAssets("t:NWAtlas");
foreach (string guid in guids) {
string path = AssetDatabase.GUIDToAssetPath(guid);
NWAtlas atlas = AssetDatabase.LoadAssetAtPath<NWAtlas>(path);
UpdateAssetsList(atlas);
}
}
}
And this triggers all NWAtlases to reload when an asset is added, it goes in Assets/Scripts/Editor:
using UnityEditor;
using UnityEngine;
public class GeneralPostprocessor : AssetPostprocessor {
/// <summary>
/// Fires when Ctrl+R finds any change in the Assets folder.
/// </summary>
static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
NWAtlasEditor.OnAssetsChanged();
}
}
To use, right-click the Project panel, Create > NW Atlas. Choose an asset type, then drag a directory into Assets Or Directories. It will grab all assets of the chosen type from it. Then reference it where needed, and call NWAtlas.GetAssets() for materials, NWAtlas.GetAssets() for textures, NWAtlas.GetAssetByName() for a mesh, etc.
Oh, I just made a simple dictionary ScriptableObject that allowed me to type a name and get a unit that I threw into an array, then dragged the created asset into whatever class that needed it. Odds are that I’m not even going to use it past checking out how a few units look and interact and will probably create a system of ScriptableObjects that I can plug in to make units, as that would be easier than manually coding a solution.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Content/Unit Reference Table")]
public class UnitReferenceTable : ScriptableObject
{
public CombatUnit[] combatUnitReferences;
public Dictionary<string, CombatUnit> unitTable = new Dictionary<string, CombatUnit>();
public void OnEnable()
{
foreach (CombatUnit unit in combatUnitReferences)
{
unitTable.Add(unit.unitStats.combatUnitName, unit);
}
}
public CombatUnit GetCombatUnit(string combatUnitName)
{
return unitTable[combatUnitName];
}
}