Skip to main content

8.5 - LINQ

As a game developer, you'll frequently work with collections of data: lists of enemies, dictionaries of items, arrays of waypoints, and more. Traditionally, manipulating these collections required writing loops and conditional statements, which can be verbose and error-prone. Language Integrated Query (LINQ) provides a more elegant solution.

What Is LINQ?

LINQ (pronounced "link") is a set of features in C# that adds powerful query capabilities to the language syntax. It allows you to write declarative queries to filter, sort, group, join, and transform data from various sources, including:

  • In-memory collections (arrays, lists, dictionaries)
  • Databases (through LINQ to SQL or Entity Framework)
  • XML documents
  • Web services
  • And more

LINQ brings the power of database-like queries directly into your C# code, making data manipulation more concise, readable, and maintainable.

LINQ Syntax Options

LINQ offers two syntax styles: query syntax and method syntax.

Query Syntax

Query syntax resembles SQL and uses keywords like from, where, select, and orderby:

// Find all active enemies with health below 50
var weakEnemies = from enemy in enemies
where enemy.IsActive && enemy.Health < 50
orderby enemy.Health ascending
select enemy;

Method Syntax

Method syntax uses extension methods and lambda expressions:

// The same query using method syntax
var weakEnemies = enemies
.Where(e => e.IsActive && e.Health < 50)
.OrderBy(e => e.Health);

Both syntaxes are functionally equivalent, and you can choose whichever you find more readable for a given scenario. Method syntax is generally more flexible and offers some operations that don't have query syntax equivalents.

Basic LINQ Operations

Let's explore the most common LINQ operations using examples relevant to game development.

Filtering with Where

The Where operation filters a collection based on a predicate:

// Find all enemies within attack range
float attackRange = 10f;
var enemiesInRange = enemies.Where(e => Vector3.Distance(player.Position, e.Position) <= attackRange);

// Find all pickable items
var pickableItems = gameObjects.Where(obj => obj.IsPickable && !obj.IsCollected);

Sorting with OrderBy/OrderByDescending

The OrderBy and OrderByDescending operations sort a collection:

// Sort enemies by distance to player (closest first)
var sortedEnemies = enemies
.OrderBy(e => Vector3.Distance(player.Position, e.Position));

// Sort items by value (highest first), then by weight (lowest first)
var sortedItems = inventory
.OrderByDescending(item => item.Value)
.ThenBy(item => item.Weight);

Projection with Select

The Select operation transforms each element in a collection:

// Extract just the names of all enemies
var enemyNames = enemies.Select(e => e.Name);

// Create a new anonymous type with selected properties
var enemyData = enemies.Select(e => new {
e.Name,
e.Health,
Threat = e.Damage * e.AttackSpeed
});

Aggregation Operations

LINQ provides several methods for aggregating data:

// Count enemies with specific properties
int bossCount = enemies.Count(e => e.IsBoss);

// Sum the total value of all inventory items
float totalValue = inventory.Sum(item => item.Value);

// Find the enemy with the highest health
Enemy toughestEnemy = enemies.MaxBy(e => e.Health);

// Calculate the average damage of all weapons
float averageDamage = weapons.Average(w => w.Damage);

Element Operations

These operations retrieve specific elements from a collection:

// Get the first enemy that's a boss, or null if none exists
Enemy firstBoss = enemies.FirstOrDefault(e => e.IsBoss);

// Get a single item with a specific ID (throws if not found or if multiple matches exist)
Item uniqueItem = items.Single(i => i.ID == "artifact_123");

// Get the enemy closest to the player
Enemy closestEnemy = enemies
.OrderBy(e => Vector3.Distance(player.Position, e.Position))
.First();

Quantifiers

Quantifiers check if elements in a collection satisfy a condition:

// Check if any enemy is a boss
bool hasBoss = enemies.Any(e => e.IsBoss);

// Check if all players are ready
bool allReady = players.All(p => p.IsReady);

// Check if the collection contains a specific item
bool hasHealthPotion = inventory.Contains(healthPotion);

Practical Example: Enemy Targeting System

Let's create a more complex example that uses LINQ to implement an enemy targeting system:

public class TargetingSystem
{
private Player _player;
private List<Enemy> _enemies;

public TargetingSystem(Player player, List<Enemy> enemies)
{
_player = player;
_enemies = enemies;
}

public Enemy GetBestTarget()
{
// Get all enemies that are:
// 1. Alive
// 2. Within line of sight
// 3. Within maximum targeting range
var validTargets = _enemies.Where(e =>
e.IsAlive &&
IsInLineOfSight(_player, e) &&
Vector3.Distance(_player.Position, e.Position) <= _player.MaxTargetingRange
);

// If the player has a preferred target type, prioritize those enemies
if (_player.PreferredTargetType != EnemyType.None)
{
var preferredTargets = validTargets.Where(e => e.Type == _player.PreferredTargetType);

// If we found preferred targets, use only those
if (preferredTargets.Any())
validTargets = preferredTargets;
}

// From the valid targets, select the best one based on a scoring system
return validTargets
.OrderByDescending(e => CalculateTargetScore(e))
.FirstOrDefault();
}

private float CalculateTargetScore(Enemy enemy)
{
float distanceScore = 1.0f / (Vector3.Distance(_player.Position, enemy.Position) + 1);
float healthScore = 1.0f - (enemy.Health / enemy.MaxHealth);
float threatScore = enemy.Damage * enemy.AttackSpeed * 0.1f;

// Bosses get a priority boost
float bossMultiplier = enemy.IsBoss ? 2.0f : 1.0f;

return (distanceScore + healthScore + threatScore) * bossMultiplier;
}

private bool IsInLineOfSight(Player player, Enemy enemy)
{
// Implementation would use raycasting to check for obstacles
// Simplified for this example
return true;
}
}

This example demonstrates how LINQ can make complex data operations more readable and maintainable.

Deferred Execution

An important concept in LINQ is deferred execution. Most LINQ queries don't execute immediately when they're defined; instead, they execute when the results are actually enumerated:

// This query is defined but not executed yet
var activeEnemies = enemies.Where(e => e.IsActive);

// Later, add more enemies to the original collection
enemies.Add(new Enemy { Name = "Goblin", IsActive = true });

// Now the query executes, including the newly added enemy
foreach (var enemy in activeEnemies)
{
Console.WriteLine(enemy.Name);
}

This behavior can be both powerful and potentially confusing. If you need to "lock in" the results at a specific point, you can force immediate execution with methods like ToList(), ToArray(), or ToDictionary():

// Force immediate execution and store the results
var activeEnemiesList = enemies.Where(e => e.IsActive).ToList();

// Adding more enemies now won't affect activeEnemiesList
enemies.Add(new Enemy { Name = "Goblin", IsActive = true });

LINQ and Performance

While LINQ makes your code more readable, it's important to consider performance implications, especially in performance-critical game code:

  1. Multiple iterations: Chaining multiple LINQ operations can result in multiple iterations over the same data.

  2. Memory allocations: Operations that create new collections (like ToList()) allocate memory, which can trigger garbage collection.

  3. Deferred execution overhead: The infrastructure for deferred execution adds some overhead compared to direct loops.

For most game scenarios, these concerns are minor, and the readability benefits outweigh the performance costs. However, in performance-critical code (like code that runs every frame for hundreds of entities), you might want to use traditional loops instead.

LINQ in Unity

Unity fully supports LINQ, but there are some considerations:

  1. Include the namespace: Add using System.Linq; at the top of your scripts.

  2. Performance in hot paths: Avoid complex LINQ queries in performance-critical code like Update() methods.

  3. IL2CPP compatibility: Some advanced LINQ features might not work correctly when building with IL2CPP for certain platforms.

Unity Relevance

Unity's collections like List<T> and arrays work seamlessly with LINQ. LINQ is particularly useful for filtering and transforming collections of game objects, components, or custom game data outside of performance-critical code paths.

Practical Game Development Examples

Inventory System

public class InventorySystem
{
private List<Item> _items = new List<Item>();

// Get all items of a specific type
public IEnumerable<Item> GetItemsByType(ItemType type)
{
return _items.Where(item => item.Type == type);
}

// Get the total value of the inventory
public float GetTotalValue()
{
return _items.Sum(item => item.Value);
}

// Find items that match a search term
public IEnumerable<Item> SearchItems(string searchTerm)
{
return _items.Where(item =>
item.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ||
item.Description.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)
);
}

// Group items by type and count them
public Dictionary<ItemType, int> GetItemCounts()
{
return _items
.GroupBy(item => item.Type)
.ToDictionary(group => group.Key, group => group.Count());
}
}

Quest System

public class QuestSystem
{
private List<Quest> _quests = new List<Quest>();

// Get all active quests
public IEnumerable<Quest> GetActiveQuests()
{
return _quests.Where(q => q.Status == QuestStatus.Active);
}

// Get quests that can be completed (all objectives are done)
public IEnumerable<Quest> GetCompletableQuests()
{
return _quests.Where(q =>
q.Status == QuestStatus.Active &&
q.Objectives.All(o => o.IsCompleted)
);
}

// Get the next recommended quest based on player level
public Quest GetRecommendedQuest(int playerLevel)
{
return _quests
.Where(q => q.Status == QuestStatus.Available && q.RequiredLevel <= playerLevel)
.OrderBy(q => Math.Abs(q.RecommendedLevel - playerLevel))
.ThenByDescending(q => q.Priority)
.FirstOrDefault();
}
}

Conclusion

LINQ is a powerful feature that can significantly improve the readability and maintainability of your code when working with collections. It allows you to express complex data operations in a concise, declarative way, focusing on what you want to achieve rather than how to achieve it.

While LINQ might have a slight learning curve, mastering it will make you a more effective C# programmer and game developer. As you progress, you'll find yourself reaching for LINQ operations instinctively whenever you need to work with collections of data.

In the next section, we'll take a deeper dive into generics, exploring how they enable you to create flexible, type-safe code that works with any data type.