Daily free asset available! Did you claim yours today?
The cover for How to Shuffle Multiple Lists in Unity for Procedural Generation

How to Shuffle Multiple Lists in Unity for Procedural Generation

March 14, 2025

Understanding the Problem: Synchronized Shuffling

Ever watched your meticulously crafted procedurally generated forest turn into a chaotic mess because the trees were floating in the sky? That’s the problem synchronized shuffling solves. This article explores synchronized shuffling techniques in Unity, offering practical examples and performance insights.

A photograph of a winding river cutting through a vibrant green valley under a clear blue sky

If you are creating a game and need assets, consider checking out Strafekit, a marketplace with unlimited game assets to use in your projects.

Synchronized shuffling means keeping related elements at the same index across different lists together during the shuffle.

Use cases include terrain data, object placement, and enemy spawning. Imagine shuffling terrain height data, object types, and enemy types. You’d want the enemy type to stay connected to its designated terrain height.

Collections.Shuffle shuffles a single list, breaking relationships between lists.

Method 1: Index-Based Shuffling

This method shuffles the lists indirectly by shuffling a list of indices. This method is straightforward and efficient for smaller lists.

First, generate a list of integers representing the original indices of the lists (0, 1, 2, …).

Then, use a standard shuffling algorithm like Fisher-Yates on the index list.

Finally, iterate through the shuffled index list and reorder elements in all lists based on the new index order.

public static void ShuffleMultipleLists<TItem>(List<TItem> list1, List<TItem> list2)
{
    if (list1.Count != list2.Count) throw new ArgumentException("Lists must be the same size");

    List<int> indices = Enumerable.Range(0, list1.Count).ToList();
    System.Random rng = new System.Random();
    int n = indices.Count;
    while (n > 1)
    {
        n--;
        int k = rng.Next(n + 1);
        int value = indices[k];
        indices[k] = indices[n];
        indices[n] = value;
    }

    List<TItem> temp1 = new List<TItem>(list1);
    List<TItem> temp2 = new List<TItem>(list2);

    for (int i = 0; i < indices.Count; i++)
    {
        list1[i] = temp1[indices[i]];
        list2[i] = temp2[indices[i]];
    }
}

Method 2: Zip and Sort (LINQ)

This method uses LINQ to combine, sort, and separate the lists. This method offers concise syntax but may have performance overhead due to the creation of intermediate collections.

First, use Zip to combine corresponding elements from all lists into tuples.

Then, create a random key for each combined element.

Next, sort the combined collection based on the random key.

using System.Linq;

public static void ShuffleMultipleListsLinq<T1, T2>(List<T1> list1, List<T2> list2)
{
    if (list1.Count != list2.Count) throw new ArgumentException("Lists must be the same size");

    var zipped = list1.Zip(list2, (l1, l2) => new { L1 = l1, L2 = l2, Key = Guid.NewGuid() })
                      .OrderBy(x => x.Key)
                      .ToList();

    for (int i = 0; i < zipped.Count; i++)
    {
        list1[i] = zipped[i].L1;
        list2[i] = zipped[i].L2;
    }
}

Finally, separate the sorted collection back into individual lists.

A photograph of a minimalist painting featuring overlapping geometric shapes in muted earth tones

Method 3: Custom Data Structure

This approach encapsulates related elements into a single data structure. This method improves code clarity and organization.

First, define a class or struct to hold corresponding elements from all lists.

public class DataPair<T1, T2>
{
    public T1 Item1 { get; set; }
    public T2 Item2 { get; set; }
}

Then, create a list of these custom data structures.

Next, shuffle the list of custom data structures.

public static void ShuffleMultipleListsCustom<T1, T2>(List<T1> list1, List<T2> list2)
{
    if (list1.Count != list2.Count) throw new ArgumentException("Lists must be the same size");

    List<DataPair<T1, T2>> combined = new List<DataPair<T1, T2>>();
    for (int i = 0; i < list1.Count; i++)
    {
        combined.Add(new DataPair<T1, T2> { Item1 = list1[i], Item2 = list2[i] });
    }

    System.Random rng = new System.Random();
    int n = combined.Count;
    while (n > 1)
    {
        n--;
        int k = rng.Next(n + 1);
        DataPair<T1, T2> value = combined[k];
        combined[k] = combined[n];
        combined[n] = value;
    }

    for (int i = 0; i < combined.Count; i++)
    {
        list1[i] = combined[i].Item1;
        list2[i] = combined[i].Item2;
    }
}

Finally, extract the shuffled elements back into separate lists.

Performance Considerations and Optimization

A photograph of a geometric abstract painting with sharp lines and contrasting colors.

Profiling is crucial. Use Unity’s Profiler to measure the performance of each method with your specific data to identify bottlenecks.

Sorting-based approaches (LINQ) typically have O(n log n) complexity. Index-based shuffling can be O(n).

Minimize memory allocations and avoid unnecessary iterations. For large lists, prefer arrays over lists for performance due to their contiguous memory allocation.

Consider the impact on frame rate if shuffling during gameplay. Pre-calculate shuffles when possible.

Best Practices and Error Handling

Debug.Assert(list1.Count == list2.Count, "Lists must be the same length!");

This will halt execution in the Unity editor if the condition is false, helping you catch errors early.

When working with structs, be mindful of value vs. reference types. Shuffling lists of structs involves copying the entire struct.

This can be less efficient than shuffling lists of class instances (references).

Handle empty lists gracefully to avoid unexpected behavior.

Write unit tests to verify the correctness of the shuffling. These tests should cover different scenarios, including lists of different sizes and data types.

Prioritize readability and maintainability by using clear variable names, comments, and consistent code formatting.

Advanced Techniques and Extensions

A photograph of a close-up of sand dunes with undulating patterns created by the wind

Use a fixed seed for reproducible shuffling, useful for debugging and level design. Replace System.Random with new System.Random(seed) to achieve this.

Shuffle only a portion of the lists based on specific criteria. Create a predicate function to identify the elements to shuffle and apply the shuffling algorithm only to those elements.

Encapsulate the shuffling methods into a reusable utility class or extension method. This promotes code reuse and simplifies the shuffling process.

Combined shuffling with other procedural generation techniques to create more complex and varied content. For example, you could combine this with techniques discussed in A Beginner’s Guide to 3D Game Development to create more complex and varied content.

Conclusion

Choosing the right shuffling method depends on your specific needs. If you are working with very large datasets and performance is critical, the index-based method may be the most performant option due to its O(n) complexity. However, if code readability is paramount and performance is less critical, the custom data structure approach might be more suitable, as it encapsulates the data more clearly. The LINQ method offers conciseness but might introduce performance overhead due to intermediate collection creation. Consider the trade-offs between performance, code clarity, and ease of implementation when making your decision.