4.3 - Return Values
Return values allow methods to send data back to their callers, making them more useful and versatile. In this section, we'll explore how to use return values effectively in your C# code for Unity development.
Why Use Return Values?
Return values serve several important purposes:
- Provide results of calculations or operations
- Indicate success or failure of a method
- Create methods that transform data
- Chain method calls together
- Implement query methods that retrieve information
Basic Return Value Syntax
The return type of a method is specified before the method name in the method declaration. The return
statement is used to send a value back to the caller:
public int Add(int a, int b)
{
int sum = a + b;
return sum;
}
// Calling the method
int result = Add(5, 3); // result will be 8
Console.WriteLine($"The sum is: {result}");
You can also return the value directly without storing it in a variable first:
public int Add(int a, int b)
{
return a + b;
}
Return Types
Methods can return any valid C# type:
Primitive Types
// Returning an integer
public int CalculateDamage(int strength, int weaponPower)
{
return strength * 2 + weaponPower;
}
// Returning a float
public float CalculateDistance(Vector3 a, Vector3 b)
{
return Vector3.Distance(a, b);
}
// Returning a boolean
public bool IsInRange(Vector3 target, float maxDistance)
{
return Vector3.Distance(transform.position, target) <= maxDistance;
}
Reference Types
// Returning a string
public string GetPlayerName()
{
return "Player_" + Random.Range(1000, 9999);
}
// Returning an array
public int[] GenerateLootTable(int itemCount)
{
int[] lootTable = new int[itemCount];
for (int i = 0; i < itemCount; i++)
{
lootTable[i] = Random.Range(1, 101);
}
return lootTable;
}
// Returning a List
public List<Enemy> GetEnemiesInRange(float range)
{
List<Enemy> enemiesInRange = new List<Enemy>();
foreach (Enemy enemy in FindObjectsOfType<Enemy>())
{
if (Vector3.Distance(transform.position, enemy.transform.position) <= range)
{
enemiesInRange.Add(enemy);
}
}
return enemiesInRange;
}
// Returning a custom class
public Player CreatePlayer(string name, PlayerClass playerClass)
{
Player newPlayer = new Player();
newPlayer.Name = name;
newPlayer.Class = playerClass;
// Initialize stats based on class
switch (playerClass)
{
case PlayerClass.Warrior:
newPlayer.Strength = 10;
newPlayer.Intelligence = 5;
newPlayer.Dexterity = 7;
break;
case PlayerClass.Mage:
newPlayer.Strength = 4;
newPlayer.Intelligence = 12;
newPlayer.Dexterity = 6;
break;
case PlayerClass.Rogue:
newPlayer.Strength = 6;
newPlayer.Intelligence = 7;
newPlayer.Dexterity = 12;
break;
}
return newPlayer;
}
Unity-Specific Types
// Returning a GameObject
public GameObject FindClosestEnemy()
{
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
GameObject closest = null;
float closestDistance = Mathf.Infinity;
foreach (GameObject enemy in enemies)
{
float distance = Vector3.Distance(transform.position, enemy.transform.position);
if (distance < closestDistance)
{
closest = enemy;
closestDistance = distance;
}
}
return closest;
}
// Returning a Component
public Weapon GetBestWeapon()
{
Weapon[] weapons = GetComponentsInChildren<Weapon>();
Weapon bestWeapon = null;
int highestDamage = 0;
foreach (Weapon weapon in weapons)
{
if (weapon.Damage > highestDamage)
{
bestWeapon = weapon;
highestDamage = weapon.Damage;
}
}
return bestWeapon;
}
// Returning a Vector3
public Vector3 GetRandomPointInCircle(float radius)
{
Vector2 randomPoint = Random.insideUnitCircle * radius;
return new Vector3(randomPoint.x, 0, randomPoint.y) + transform.position;
}
// Returning a Quaternion
public Quaternion GetLookRotation(Vector3 targetPosition)
{
Vector3 direction = targetPosition - transform.position;
direction.y = 0; // Keep rotation only around Y axis
if (direction != Vector3.zero)
{
return Quaternion.LookRotation(direction);
}
return transform.rotation;
}
The void
Return Type
When a method doesn't need to return a value, you use the void
return type:
public void PlayJumpSound()
{
AudioSource.PlayClipAtPoint(jumpSound, transform.position);
}
Methods with a void
return type can still use the return
statement to exit the method early:
public void TryJump()
{
if (!isGrounded)
{
Debug.Log("Cannot jump while in the air");
return; // Exit the method early
}
// Jump logic
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
PlayJumpSound();
}
Returning Multiple Values
Sometimes you need a method to return more than one value. There are several ways to do this:
1. Using Out Parameters
As we saw in the previous section, you can use out
parameters to return multiple values:
public bool TryGetPlayerPosition(string playerName, out Vector3 position)
{
Player player = FindPlayerByName(playerName);
if (player != null)
{
position = player.transform.position;
return true;
}
position = Vector3.zero;
return false;
}
// Usage
if (TryGetPlayerPosition("Player1", out Vector3 playerPos))
{
// Use playerPos
}
2. Returning a Tuple (C# 7.0+)
Tuples provide a simple way to return multiple values without creating a custom class:
public (int health, int mana, int stamina) GetPlayerStats()
{
return (health: 100, mana: 50, stamina: 75);
}
// Usage
var stats = GetPlayerStats();
Debug.Log($"Health: {stats.health}, Mana: {stats.mana}, Stamina: {stats.stamina}");
// Or deconstruct the tuple
(int health, int mana, int stamina) = GetPlayerStats();
Debug.Log($"Health: {health}, Mana: {mana}, Stamina: {stamina}");
You can also use tuples without naming the elements, but named elements make the code more readable:
// Without names
public (int, int, int) GetPlayerStats()
{
return (100, 50, 75);
}
// Usage
var stats = GetPlayerStats();
Debug.Log($"Health: {stats.Item1}, Mana: {stats.Item2}, Stamina: {stats.Item3}");
3. Returning a Custom Class or Struct
For more complex return values, creating a custom class or struct is often the best approach:
public class RaycastResult
{
public bool HitSomething;
public GameObject HitObject;
public Vector3 HitPoint;
public Vector3 HitNormal;
public float Distance;
}
public RaycastResult PerformRaycast(float maxDistance)
{
RaycastResult result = new RaycastResult();
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, maxDistance))
{
result.HitSomething = true;
result.HitObject = hit.collider.gameObject;
result.HitPoint = hit.point;
result.HitNormal = hit.normal;
result.Distance = hit.distance;
}
else
{
result.HitSomething = false;
}
return result;
}
// Usage
RaycastResult raycastResult = PerformRaycast(100f);
if (raycastResult.HitSomething)
{
Debug.Log($"Hit {raycastResult.HitObject.name} at distance {raycastResult.Distance}");
}
Practical Examples in Game Development
Let's look at some practical examples of using return values in Unity game development:
Example 1: Damage Calculation System
using UnityEngine;
public class DamageCalculator : MonoBehaviour
{
// Return a simple damage value
public int CalculateBaseDamage(int strength, int weaponPower)
{
return Mathf.RoundToInt(strength * 0.8f + weaponPower * 1.2f);
}
// Return a more complex damage result with a custom struct
public struct DamageResult
{
public int TotalDamage;
public int PhysicalDamage;
public int MagicalDamage;
public bool IsCritical;
public float ArmorPenetration;
}
public DamageResult CalculateDamage(
int strength,
int intelligence,
int weaponPower,
int spellPower,
float critChance)
{
DamageResult result = new DamageResult();
// Calculate physical damage
result.PhysicalDamage = Mathf.RoundToInt(strength * 0.8f + weaponPower * 1.2f);
// Calculate magical damage
result.MagicalDamage = Mathf.RoundToInt(intelligence * 1.5f + spellPower);
// Determine if attack is critical
result.IsCritical = Random.value < critChance;
// Apply critical multiplier if applicable
if (result.IsCritical)
{
result.PhysicalDamage = Mathf.RoundToInt(result.PhysicalDamage * 1.5f);
result.MagicalDamage = Mathf.RoundToInt(result.MagicalDamage * 1.5f);
}
// Calculate total damage
result.TotalDamage = result.PhysicalDamage + result.MagicalDamage;
// Calculate armor penetration based on strength
result.ArmorPenetration = strength * 0.05f;
return result;
}
// Return a tuple for a simpler multi-value return
public (int damage, bool isCritical) CalculateQuickDamage(int attackPower)
{
bool isCritical = Random.value < 0.2f;
int damage = attackPower;
if (isCritical)
{
damage = Mathf.RoundToInt(damage * 1.5f);
}
return (damage, isCritical);
}
// Return different damage types using an enum
public enum DamageType
{
Physical,
Fire,
Ice,
Lightning,
Poison
}
public DamageType GetDamageTypeForWeapon(WeaponType weaponType, bool isEnchanted)
{
if (!isEnchanted)
{
return DamageType.Physical;
}
switch (weaponType)
{
case WeaponType.Sword:
return DamageType.Fire;
case WeaponType.Axe:
return DamageType.Lightning;
case WeaponType.Bow:
return DamageType.Poison;
case WeaponType.Staff:
return DamageType.Ice;
default:
return DamageType.Physical;
}
}
public enum WeaponType
{
Sword,
Axe,
Bow,
Staff
}
}
Example 2: Pathfinding Utility
using UnityEngine;
using System.Collections.Generic;
public class PathfindingUtility : MonoBehaviour
{
// Return a simple path as an array
public Vector3[] FindSimplePath(Vector3 start, Vector3 end, float stepDistance = 1f)
{
// Calculate distance and number of steps
float distance = Vector3.Distance(start, end);
int steps = Mathf.CeilToInt(distance / stepDistance);
// Create path array
Vector3[] path = new Vector3[steps + 1];
// Fill path with points
for (int i = 0; i <= steps; i++)
{
float t = i / (float)steps;
path[i] = Vector3.Lerp(start, end, t);
}
return path;
}
// Return a more complex path result with a custom class
public class PathResult
{
public bool PathFound;
public List<Vector3> Waypoints;
public float TotalDistance;
public bool HasObstacles;
}
public PathResult FindPath(Vector3 start, Vector3 end, LayerMask obstacleLayer)
{
PathResult result = new PathResult();
result.Waypoints = new List<Vector3>();
// Simple raycast to check direct path
bool directPathBlocked = Physics.Linecast(start, end, obstacleLayer);
if (!directPathBlocked)
{
// Direct path is clear
result.PathFound = true;
result.Waypoints.Add(start);
result.Waypoints.Add(end);
result.TotalDistance = Vector3.Distance(start, end);
result.HasObstacles = false;
return result;
}
// Direct path is blocked, need to find waypoints
result.HasObstacles = true;
// This is a simplified example - in a real game, you'd use
// a proper pathfinding algorithm like A* here
// For this example, we'll just create a simple detour
Vector3 midpoint = (start + end) / 2f;
midpoint.y += 2f; // Raise the midpoint to go over obstacles
result.Waypoints.Add(start);
result.Waypoints.Add(midpoint);
result.Waypoints.Add(end);
// Calculate total distance
result.TotalDistance = Vector3.Distance(start, midpoint) +
Vector3.Distance(midpoint, end);
result.PathFound = true;
return result;
}
// Return a tuple for a simpler multi-value return
public (bool isPathClear, float distance) CheckDirectPath(Vector3 start, Vector3 end)
{
bool isPathClear = !Physics.Linecast(start, end);
float distance = Vector3.Distance(start, end);
return (isPathClear, distance);
}
// Return the nearest navigable point
public Vector3 FindNearestNavigablePoint(Vector3 position, float maxSearchRadius = 10f)
{
// Check if the position itself is navigable
if (IsPositionNavigable(position))
{
return position;
}
// Search in expanding circles
for (float radius = 1f; radius <= maxSearchRadius; radius += 1f)
{
for (int i = 0; i < 8; i++)
{
float angle = i * Mathf.PI / 4f;
Vector3 checkPos = position + new Vector3(
Mathf.Cos(angle) * radius,
0f,
Mathf.Sin(angle) * radius
);
if (IsPositionNavigable(checkPos))
{
return checkPos;
}
}
}
// No navigable point found within radius
return position;
}
private bool IsPositionNavigable(Vector3 position)
{
// Check if position is on terrain or floor
RaycastHit hit;
if (Physics.Raycast(position + Vector3.up * 10f, Vector3.down, out hit, 20f))
{
// Check if the hit surface is walkable (not an obstacle)
if (!hit.collider.CompareTag("Obstacle"))
{
// Position the point on the surface
Vector3 surfacePoint = hit.point;
// Check for obstacles at character height
bool hasObstacle = Physics.CheckSphere(
surfacePoint + Vector3.up,
0.5f, // Character radius
LayerMask.GetMask("Obstacle")
);
return !hasObstacle;
}
}
return false;
}
}
Example 3: Inventory Item Generator
using UnityEngine;
using System.Collections.Generic;
public class ItemGenerator : MonoBehaviour
{
[System.Serializable]
public class ItemRarity
{
public string Name;
public Color Color;
public float StatMultiplier;
}
[SerializeField] private ItemRarity[] rarities;
// Simple item class
public class Item
{
public string Name;
public ItemType Type;
public int Level;
public ItemRarity Rarity;
public Dictionary<StatType, int> Stats;
public string Description;
public override string ToString()
{
return $"{Rarity.Name} {Name} (Level {Level})";
}
}
public enum ItemType
{
Weapon,
Armor,
Accessory,
Consumable
}
public enum StatType
{
Damage,
Defense,
Health,
Mana,
Strength,
Dexterity,
Intelligence
}
// Return a random item
public Item GenerateRandomItem(int playerLevel)
{
Item item = new Item();
// Randomize item type
item.Type = (ItemType)Random.Range(0, System.Enum.GetValues(typeof(ItemType)).Length);
// Set level based on player level with some variation
item.Level = Mathf.Max(1, playerLevel + Random.Range(-2, 3));
// Select rarity
item.Rarity = GetRandomRarity();
// Generate name based on type and rarity
item.Name = GenerateItemName(item.Type, item.Rarity);
// Generate stats
item.Stats = GenerateItemStats(item.Type, item.Level, item.Rarity);
// Generate description
item.Description = GenerateItemDescription(item);
return item;
}
// Return a specific type of item
public Item GenerateItemOfType(ItemType type, int level, string rarityName = null)
{
Item item = new Item();
item.Type = type;
item.Level = level;
// Find rarity by name or use random
if (!string.IsNullOrEmpty(rarityName))
{
item.Rarity = System.Array.Find(rarities, r => r.Name == rarityName);
// If not found, use common rarity
if (item.Rarity == null)
{
item.Rarity = rarities[0]; // Assuming first rarity is common
}
}
else
{
item.Rarity = GetRandomRarity();
}
item.Name = GenerateItemName(type, item.Rarity);
item.Stats = GenerateItemStats(type, level, item.Rarity);
item.Description = GenerateItemDescription(item);
return item;
}
// Return multiple items as an array
public Item[] GenerateItemSet(int count, int playerLevel)
{
Item[] items = new Item[count];
for (int i = 0; i < count; i++)
{
items[i] = GenerateRandomItem(playerLevel);
}
return items;
}
// Return a tuple with item and its gold value
public (Item item, int goldValue) GenerateItemWithValue(int playerLevel)
{
Item item = GenerateRandomItem(playerLevel);
int goldValue = CalculateItemValue(item);
return (item, goldValue);
}
// Helper methods
private ItemRarity GetRandomRarity()
{
// Simple random selection - in a real game, you'd use weighted probabilities
return rarities[Random.Range(0, rarities.Length)];
}
private string GenerateItemName(ItemType type, ItemRarity rarity)
{
// This would be more sophisticated in a real game
string[] weaponNames = { "Sword", "Axe", "Bow", "Staff", "Dagger" };
string[] armorNames = { "Helmet", "Chestplate", "Gauntlets", "Boots", "Shield" };
string[] accessoryNames = { "Ring", "Amulet", "Bracelet", "Belt", "Cloak" };
string[] consumableNames = { "Potion", "Scroll", "Food", "Elixir", "Bomb" };
string[] prefixes = { "Mighty", "Shining", "Ancient", "Cursed", "Blessed" };
string prefix = Random.value > 0.5f ? prefixes[Random.Range(0, prefixes.Length)] + " " : "";
switch (type)
{
case ItemType.Weapon:
return prefix + weaponNames[Random.Range(0, weaponNames.Length)];
case ItemType.Armor:
return prefix + armorNames[Random.Range(0, armorNames.Length)];
case ItemType.Accessory:
return prefix + accessoryNames[Random.Range(0, accessoryNames.Length)];
case ItemType.Consumable:
return prefix + consumableNames[Random.Range(0, consumableNames.Length)];
default:
return "Unknown Item";
}
}
private Dictionary<StatType, int> GenerateItemStats(ItemType type, int level, ItemRarity rarity)
{
Dictionary<StatType, int> stats = new Dictionary<StatType, int>();
// Base stat value based on level
int baseValue = level * 5;
// Apply rarity multiplier
float multiplier = rarity.StatMultiplier;
// Add stats based on item type
switch (type)
{
case ItemType.Weapon:
stats[StatType.Damage] = Mathf.RoundToInt(baseValue * multiplier);
// 50% chance to add a secondary stat
if (Random.value > 0.5f)
{
StatType secondaryStat = Random.value > 0.5f ?
StatType.Strength : StatType.Dexterity;
stats[secondaryStat] = Mathf.RoundToInt(baseValue * 0.5f * multiplier);
}
break;
case ItemType.Armor:
stats[StatType.Defense] = Mathf.RoundToInt(baseValue * multiplier);
stats[StatType.Health] = Mathf.RoundToInt(baseValue * 2 * multiplier);
break;
case ItemType.Accessory:
// Randomly select 2 stats for accessories
List<StatType> possibleStats = new List<StatType> {
StatType.Health, StatType.Mana, StatType.Strength,
StatType.Dexterity, StatType.Intelligence
};
for (int i = 0; i < 2; i++)
{
if (possibleStats.Count > 0)
{
int index = Random.Range(0, possibleStats.Count);
StatType stat = possibleStats[index];
possibleStats.RemoveAt(index);
stats[stat] = Mathf.RoundToInt(baseValue * 0.8f * multiplier);
}
}
break;
case ItemType.Consumable:
// Consumables typically have effects rather than stats
// But we'll add a health value for this example
stats[StatType.Health] = Mathf.RoundToInt(baseValue * 3 * multiplier);
break;
}
return stats;
}
private string GenerateItemDescription(Item item)
{
string description = $"A level {item.Level} {item.Rarity.Name.ToLower()} {item.Type.ToString().ToLower()}.";
// Add stat descriptions
if (item.Stats.Count > 0)
{
description += " Provides:";
foreach (var stat in item.Stats)
{
description += $"\n+{stat.Value} {stat.Key}";
}
}
return description;
}
private int CalculateItemValue(Item item)
{
// Base value from level
int value = item.Level * 10;
// Multiply by rarity factor
value = Mathf.RoundToInt(value * item.Rarity.StatMultiplier * 2);
// Add value from stats
foreach (var stat in item.Stats)
{
value += stat.Value * 2;
}
return value;
}
}
Best Practices for Return Values
1. Return Early for Guard Clauses
Use early returns to handle edge cases and invalid inputs at the beginning of your methods:
public GameObject FindTargetInRange(float range)
{
// Guard clauses with early returns
if (range <= 0)
{
Debug.LogWarning("Range must be positive");
return null;
}
if (!isActiveAndEnabled)
{
return null;
}
// Main method logic
// ...
}
2. Be Consistent with Return Types
Be consistent with what your methods return, especially for similar methods:
// Inconsistent - similar methods return different types
public GameObject FindEnemy() { /* ... */ }
public Transform FindAlly() { /* ... */ }
// Better - consistent return types
public GameObject FindEnemy() { /* ... */ }
public GameObject FindAlly() { /* ... */ }
3. Use Meaningful Return Values
Return values that are meaningful and useful to the caller:
// Less useful - returns void and modifies a global
public void CalculateScore()
{
globalScore = player.Kills * 100 + player.Coins * 10;
}
// More useful - returns the calculated value
public int CalculateScore()
{
return player.Kills * 100 + player.Coins * 10;
}
4. Consider Nullable Return Types
For methods that might not have a valid result, consider using nullable types or returning null
:
// Using nullable value type (C# 2.0+)
public int? FindHighScore(string playerName)
{
Player player = FindPlayerByName(playerName);
if (player != null)
{
return player.HighScore;
}
return null;
}
// Usage
int? score = FindHighScore("Player1");
if (score.HasValue)
{
Debug.Log($"High score: {score.Value}");
}
else
{
Debug.Log("No high score found");
}
5. Use Tuples for Simple Multiple Returns
For methods that need to return 2-3 related values, tuples are often cleaner than custom classes:
// Using a tuple for a simple result with multiple values
public (bool success, string message) TryPurchaseItem(string itemId, int quantity)
{
Item item = inventory.GetItem(itemId);
if (item == null)
{
return (false, "Item not found");
}
if (item.Price * quantity > playerGold)
{
return (false, "Not enough gold");
}
// Process purchase
playerGold -= item.Price * quantity;
inventory.AddItem(itemId, quantity);
return (true, $"Purchased {quantity} {item.Name}");
}
// Usage
var result = TryPurchaseItem("health_potion", 5);
if (result.success)
{
DisplayMessage(result.message);
}
else
{
DisplayError(result.message);
}
6. Use Custom Classes for Complex Returns
For methods that return complex data structures with more than 3-4 values, create a custom class:
public class CombatResult
{
public bool TargetDefeated;
public int DamageDealt;
public int DamageTaken;
public bool CriticalHit;
public List<StatusEffect> EffectsApplied;
public List<Item> ItemsDropped;
public int ExperienceGained;
}
public CombatResult AttackTarget(Character target)
{
CombatResult result = new CombatResult();
// Fill in the result
return result;
}
7. Document Return Values
Use XML comments to document what your methods return:
/// <summary>
/// Finds all enemies within the specified range.
/// </summary>
/// <param name="range">The search radius in units</param>
/// <returns>
/// A list of Enemy components within range, sorted by distance.
/// Returns an empty list if no enemies are found.
/// </returns>
public List<Enemy> GetEnemiesInRange(float range)
{
// Method implementation
}
Conclusion
Return values are a fundamental part of method design, allowing methods to provide results, indicate success or failure, and send data back to their callers. By understanding the different ways to return values and following best practices, you can create more effective and reusable methods.
In this section, we've covered:
- Basic return value syntax
- Different types of return values
- Returning multiple values with tuples and custom classes
- Practical examples in game development
- Best practices for using return values
In the next section, we'll explore method overloading, which allows you to define multiple methods with the same name but different parameters.
In Unity development, effective use of return values is essential for:
- Creating utility methods that provide calculated values
- Implementing game systems that need to return results (combat, inventory, etc.)
- Building query methods that find game objects or components
- Implementing AI decision-making that returns actions or targets
- Creating editor tools that process and return data
Well-designed return values make your code more modular, testable, and reusable across your Unity projects.