4.2 - Method Parameters
Method parameters allow you to pass data into methods, making them more flexible and reusable. In this section, we'll explore the different types of parameters in C# and how to use them effectively in your Unity projects.
Basic Parameter Passing
In its simplest form, a parameter is a variable that receives a value when a method is called:
public void DisplayMessage(string message)
{
Console.WriteLine(message);
}
// Calling the method
DisplayMessage("Hello, World!"); // Outputs: Hello, World!
You can define multiple parameters by separating them with commas:
public void DisplayPlayerInfo(string name, int level, string characterClass)
{
Console.WriteLine($"Player: {name} | Level: {level} | Class: {characterClass}");
}
// Calling the method
DisplayPlayerInfo("Gandalf", 50, "Wizard");
// Outputs: Player: Gandalf | Level: 50 | Class: Wizard
Value Parameters vs. Reference Parameters
Understanding how parameters are passed is crucial for writing effective C# code. There are two main ways parameters can be passed:
Value Parameters (Pass by Value)
By default, C# uses pass-by-value for parameters. This means a copy of the value is passed to the method:
public void IncrementNumber(int number)
{
number += 1;
Console.WriteLine($"Inside method: number = {number}");
}
// Calling the method
int x = 5;
IncrementNumber(x);
Console.WriteLine($"After method call: x = {x}");
// Output:
// Inside method: number = 6
// After method call: x = 5
Notice that the original variable x
remains unchanged because the method received and modified a copy of its value.
Reference Parameters (Pass by Reference)
To allow a method to modify the original variable, you can use the ref
keyword:
public void IncrementNumber(ref int number)
{
number += 1;
Console.WriteLine($"Inside method: number = {number}");
}
// Calling the method
int x = 5;
IncrementNumber(ref x);
Console.WriteLine($"After method call: x = {x}");
// Output:
// Inside method: number = 6
// After method call: x = 6
Now the original variable x
is modified because the method received a reference to it, not a copy.
When using ref
parameters:
- The parameter must be initialized before being passed to the method
- The
ref
keyword must be used both in the method declaration and at the call site
Value Types vs. Reference Types
It's important to understand that C# has two categories of types:
Value Types
Value types include:
- Numeric types (
int
,float
,double
, etc.) bool
char
struct
enum
When you pass a value type to a method, a copy of the value is passed (unless you use ref
).
Reference Types
Reference types include:
- Classes
- Interfaces
- Delegates
- Arrays
string
(although it behaves like a value type due to immutability)
When you pass a reference type to a method, a copy of the reference is passed, not a copy of the object itself. This means the method can modify the object's properties:
public class Player
{
public string Name;
public int Health;
}
public void DamagePlayer(Player player, int damageAmount)
{
player.Health -= damageAmount;
Console.WriteLine($"Inside method: player.Health = {player.Health}");
}
// Calling the method
Player hero = new Player { Name = "Hero", Health = 100 };
DamagePlayer(hero, 20);
Console.WriteLine($"After method call: hero.Health = {hero.Health}");
// Output:
// Inside method: player.Health = 80
// After method call: hero.Health = 80
Even though we didn't use ref
, the Player
object's properties were modified because we passed a reference to the object.
However, the method cannot replace the entire object:
public void ReplacePlayer(Player player)
{
player = new Player { Name = "Replacement", Health = 50 };
Console.WriteLine($"Inside method: player.Name = {player.Name}");
}
// Calling the method
Player hero = new Player { Name = "Hero", Health = 100 };
ReplacePlayer(hero);
Console.WriteLine($"After method call: hero.Name = {hero.Name}");
// Output:
// Inside method: player.Name = Replacement
// After method call: hero.Name = Hero
To allow the method to replace the entire object, you would need to use ref
:
public void ReplacePlayer(ref Player player)
{
player = new Player { Name = "Replacement", Health = 50 };
Console.WriteLine($"Inside method: player.Name = {player.Name}");
}
// Calling the method
Player hero = new Player { Name = "Hero", Health = 100 };
ReplacePlayer(ref hero);
Console.WriteLine($"After method call: hero.Name = {hero.Name}");
// Output:
// Inside method: player.Name = Replacement
// After method call: hero.Name = Replacement
Output Parameters (out
)
The out
parameter is similar to ref
, but with one key difference: the method must assign a value to the parameter before it returns. This is useful when you want a method to return multiple values:
public bool TryParse(string input, out int result)
{
if (int.TryParse(input, out result))
{
return true;
}
else
{
result = 0;
return false;
}
}
// Calling the method
string userInput = "123";
if (TryParse(userInput, out int parsedValue))
{
Console.WriteLine($"Successfully parsed: {parsedValue}");
}
else
{
Console.WriteLine("Failed to parse input");
}
When using out
parameters:
- The parameter doesn't need to be initialized before being passed to the method
- The method must assign a value to the parameter before it returns
- The
out
keyword must be used both in the method declaration and at the call site
In C# 7.0 and later, you can declare the out
variable inline at the call site:
if (TryParse("123", out int parsedValue))
{
// Use parsedValue
}
Optional Parameters
Optional parameters allow you to specify default values for parameters, making them optional when calling the method:
public void CreateEnemy(string name, int health = 100, bool isElite = false)
{
string type = isElite ? "Elite" : "Normal";
Console.WriteLine($"Created {type} enemy: {name} with {health} health");
}
// Calling the method with different numbers of arguments
CreateEnemy("Goblin");
// Output: Created Normal enemy: Goblin with 100 health
CreateEnemy("Orc", 150);
// Output: Created Normal enemy: Orc with 150 health
CreateEnemy("Dragon", 500, true);
// Output: Created Elite enemy: Dragon with 500 health
Rules for optional parameters:
- Optional parameters must appear after all required parameters
- The default value must be a constant expression (like a literal or a constant field)
- You can't have optional
ref
orout
parameters
Named Arguments
Named arguments allow you to specify an argument by matching it with its parameter name, regardless of position:
public void CreateCharacter(string name, string className, int level, bool isHero)
{
Console.WriteLine($"Created {(isHero ? "hero" : "NPC")} character: {name}, a level {level} {className}");
}
// Calling with positional arguments
CreateCharacter("Gandalf", "Wizard", 50, true);
// Calling with named arguments
CreateCharacter(
name: "Gandalf",
className: "Wizard",
level: 50,
isHero: true
);
// Named arguments can be in any order
CreateCharacter(
isHero: true,
level: 50,
className: "Wizard",
name: "Gandalf"
);
// You can mix positional and named arguments
// (positional arguments must come before named ones)
CreateCharacter("Gandalf", "Wizard", isHero: true, level: 50);
Named arguments are particularly useful when:
- A method has many parameters
- A method has multiple optional parameters and you want to specify only some of them
- The purpose of an argument isn't clear from its value alone
Parameter Arrays (params
)
The params
keyword allows a method to accept a variable number of arguments of the same type:
public void DisplayItems(params string[] items)
{
Console.WriteLine($"You have {items.Length} items:");
for (int i = 0; i < items.Length; i++)
{
Console.WriteLine($"{i+1}. {items[i]}");
}
}
// Calling with different numbers of arguments
DisplayItems("Sword");
DisplayItems("Sword", "Shield", "Potion");
DisplayItems(); // Valid - creates an empty array
// You can also pass an array directly
string[] inventory = { "Sword", "Shield", "Potion", "Map" };
DisplayItems(inventory);
Rules for params
parameters:
- A method can have only one
params
parameter - The
params
parameter must be the last parameter in the method declaration - The
params
parameter must be a single-dimensional array
Practical Examples in Game Development
Let's look at some practical examples of using different parameter types in Unity game development:
Example 1: Character Stats System
using UnityEngine;
public class CharacterStats : MonoBehaviour
{
[SerializeField] private int baseHealth = 100;
[SerializeField] private int baseStrength = 10;
[SerializeField] private int baseDefense = 5;
private int currentHealth;
void Start()
{
InitializeStats();
}
// Basic parameter passing
public void InitializeStats()
{
currentHealth = CalculateMaxHealth(baseHealth, GetLevelModifier());
Debug.Log($"Character initialized with {currentHealth} health");
}
// Multiple parameters
public int CalculateMaxHealth(int baseValue, float modifier)
{
return Mathf.RoundToInt(baseValue * modifier);
}
// Optional parameter
public float GetLevelModifier(int level = 1)
{
return 1f + (level * 0.1f);
}
// Reference parameter
public void ModifyHealth(ref int health, int amount)
{
health += amount;
Debug.Log($"Health modified by {amount}, new value: {health}");
}
// Output parameter
public bool TryGetSurvivalChance(int damage, out float survivalChance)
{
if (currentHealth <= 0)
{
survivalChance = 0f;
return false;
}
survivalChance = Mathf.Clamp01((float)currentHealth / (damage * 2));
return true;
}
// Params parameter
public void ApplyStatusEffects(params StatusEffect[] effects)
{
Debug.Log($"Applying {effects.Length} status effects");
foreach (StatusEffect effect in effects)
{
effect.Apply(this);
}
}
// Method with many parameters using named arguments
public void UpdateAllStats(
int health = -1,
int strength = -1,
int defense = -1,
bool resetHealth = false,
bool applyBuffs = true)
{
if (health >= 0) baseHealth = health;
if (strength >= 0) baseStrength = strength;
if (defense >= 0) baseDefense = defense;
if (resetHealth)
{
currentHealth = CalculateMaxHealth(baseHealth, GetLevelModifier());
}
if (applyBuffs)
{
ApplyStatBuffs();
}
Debug.Log("Stats updated");
}
private void ApplyStatBuffs()
{
// Apply buffs logic
}
}
public class StatusEffect
{
public string Name { get; set; }
public float Duration { get; set; }
public void Apply(CharacterStats character)
{
Debug.Log($"Applied {Name} effect for {Duration} seconds");
}
}
Example 2: Inventory System
using UnityEngine;
using System.Collections.Generic;
public class InventorySystem : MonoBehaviour
{
[SerializeField] private int maxSlots = 20;
private List<Item> items = new List<Item>();
// Basic parameter
public bool HasSpace()
{
return items.Count < maxSlots;
}
// Multiple parameters
public bool AddItem(Item item, int quantity = 1)
{
if (!HasSpace())
{
Debug.Log("Inventory is full");
return false;
}
// Check if item is stackable and already exists
if (item.IsStackable)
{
for (int i = 0; i < items.Count; i++)
{
if (items[i].ID == item.ID)
{
// Stack with existing item
items[i].Quantity += quantity;
Debug.Log($"Added {quantity} {item.Name} to existing stack");
return true;
}
}
}
// Add as new item
item.Quantity = quantity;
items.Add(item);
Debug.Log($"Added {item.Name} to inventory");
return true;
}
// Reference parameter
public bool TransferItem(ref Item item, InventorySystem targetInventory)
{
if (targetInventory.HasSpace())
{
bool success = targetInventory.AddItem(item);
if (success)
{
RemoveItem(item);
item = null; // Clear the reference
return true;
}
}
return false;
}
// Output parameter
public bool TryGetItem(string itemName, out Item foundItem)
{
foreach (Item item in items)
{
if (item.Name == itemName)
{
foundItem = item;
return true;
}
}
foundItem = null;
return false;
}
// Params parameter
public void RemoveItems(params Item[] itemsToRemove)
{
foreach (Item item in itemsToRemove)
{
RemoveItem(item);
}
}
private void RemoveItem(Item item)
{
items.Remove(item);
Debug.Log($"Removed {item.Name} from inventory");
}
// Method with optional parameters
public Item[] FindItems(
string nameFilter = "",
ItemType typeFilter = ItemType.Any,
int minValue = 0,
int maxValue = int.MaxValue,
bool includeEquipped = true)
{
List<Item> results = new List<Item>();
foreach (Item item in items)
{
// Skip if not matching name filter
if (!string.IsNullOrEmpty(nameFilter) &&
!item.Name.Contains(nameFilter))
{
continue;
}
// Skip if not matching type filter
if (typeFilter != ItemType.Any &&
item.Type != typeFilter)
{
continue;
}
// Skip if value is outside range
if (item.Value < minValue || item.Value > maxValue)
{
continue;
}
// Skip equipped items if not included
if (!includeEquipped && item.IsEquipped)
{
continue;
}
results.Add(item);
}
return results.ToArray();
}
}
public enum ItemType
{
Any,
Weapon,
Armor,
Consumable,
Quest,
Material
}
public class Item
{
public int ID { get; set; }
public string Name { get; set; }
public ItemType Type { get; set; }
public int Value { get; set; }
public int Quantity { get; set; }
public bool IsStackable { get; set; }
public bool IsEquipped { get; set; }
}
Example 3: Combat System
using UnityEngine;
using System.Collections.Generic;
public class CombatSystem : MonoBehaviour
{
// Basic parameters
public void Attack(Character attacker, Character target)
{
int damage = CalculateDamage(attacker.Strength, target.Defense);
ApplyDamage(target, damage);
}
// Multiple parameters with default values
public int CalculateDamage(int strength, int defense, float critMultiplier = 1.5f)
{
int baseDamage = Mathf.Max(1, strength - defense / 2);
// 10% chance of critical hit
bool isCritical = Random.value < 0.1f;
if (isCritical)
{
return Mathf.RoundToInt(baseDamage * critMultiplier);
}
return baseDamage;
}
// Reference parameter
public void ApplyDamage(Character target, int damage, ref bool wasLethal)
{
target.Health -= damage;
Debug.Log($"{target.Name} took {damage} damage");
if (target.Health <= 0)
{
target.Health = 0;
wasLethal = true;
Debug.Log($"{target.Name} was defeated");
}
else
{
wasLethal = false;
}
}
// Overload without the ref parameter
public void ApplyDamage(Character target, int damage)
{
bool wasLethal = false;
ApplyDamage(target, damage, ref wasLethal);
}
// Output parameters
public bool TryGetTargetInRange(
Vector3 position,
float range,
LayerMask targetLayer,
out Character nearestTarget)
{
Collider[] colliders = Physics.OverlapSphere(position, range, targetLayer);
float closestDistance = float.MaxValue;
nearestTarget = null;
foreach (Collider collider in colliders)
{
Character character = collider.GetComponent<Character>();
if (character != null && !character.IsDead)
{
float distance = Vector3.Distance(position, collider.transform.position);
if (distance < closestDistance)
{
closestDistance = distance;
nearestTarget = character;
}
}
}
return nearestTarget != null;
}
// Params parameter
public void ApplyAreaEffect(Vector3 center, float radius, DamageType damageType, params Character[] immuneCharacters)
{
Collider[] colliders = Physics.OverlapSphere(center, radius);
foreach (Collider collider in colliders)
{
Character character = collider.GetComponent<Character>();
if (character != null && !character.IsDead)
{
// Check if character is immune
bool isImmune = false;
foreach (Character immuneCharacter in immuneCharacters)
{
if (character == immuneCharacter)
{
isImmune = true;
break;
}
}
if (!isImmune)
{
// Calculate damage based on distance from center
float distance = Vector3.Distance(center, character.transform.position);
float damagePercent = 1f - (distance / radius);
int damage = Mathf.RoundToInt(30 * damagePercent);
ApplyDamage(character, damage);
ApplyStatusEffect(character, damageType);
}
}
}
}
// Named parameters example
public void ApplyStatusEffect(
Character target,
DamageType damageType,
float duration = 5f,
float tickInterval = 1f,
int damagePerTick = 5)
{
switch (damageType)
{
case DamageType.Fire:
target.ApplyEffect(new BurningEffect(duration, tickInterval, damagePerTick));
break;
case DamageType.Ice:
target.ApplyEffect(new FrozenEffect(
duration: duration,
slowAmount: 0.5f,
damagePerTick: damagePerTick / 2,
tickInterval: tickInterval * 2
));
break;
case DamageType.Poison:
target.ApplyEffect(new PoisonEffect(
duration: duration * 2,
tickInterval: tickInterval,
damagePerTick: damagePerTick / 2,
spreadRadius: 0f
));
break;
default:
Debug.Log($"No status effect for damage type: {damageType}");
break;
}
}
}
public enum DamageType
{
Physical,
Fire,
Ice,
Lightning,
Poison
}
public class Character
{
public string Name { get; set; }
public int Health { get; set; }
public int Strength { get; set; }
public int Defense { get; set; }
public bool IsDead => Health <= 0;
public void ApplyEffect(StatusEffect effect)
{
Debug.Log($"{Name} affected by {effect.GetType().Name} for {effect.Duration} seconds");
}
}
public abstract class StatusEffect
{
public float Duration { get; protected set; }
public StatusEffect(float duration)
{
Duration = duration;
}
}
public class BurningEffect : StatusEffect
{
public float TickInterval { get; private set; }
public int DamagePerTick { get; private set; }
public BurningEffect(float duration, float tickInterval, int damagePerTick)
: base(duration)
{
TickInterval = tickInterval;
DamagePerTick = damagePerTick;
}
}
public class FrozenEffect : StatusEffect
{
public float SlowAmount { get; private set; }
public float TickInterval { get; private set; }
public int DamagePerTick { get; private set; }
public FrozenEffect(float duration, float slowAmount, float tickInterval, int damagePerTick)
: base(duration)
{
SlowAmount = slowAmount;
TickInterval = tickInterval;
DamagePerTick = damagePerTick;
}
}
public class PoisonEffect : StatusEffect
{
public float TickInterval { get; private set; }
public int DamagePerTick { get; private set; }
public float SpreadRadius { get; private set; }
public PoisonEffect(float duration, float tickInterval, int damagePerTick, float spreadRadius)
: base(duration)
{
TickInterval = tickInterval;
DamagePerTick = damagePerTick;
SpreadRadius = spreadRadius;
}
}
Best Practices for Method Parameters
1. Keep Parameter Count Low
Methods with too many parameters are hard to use and maintain. Aim for 4 or fewer parameters. If you need more, consider:
- Creating a parameter object (class or struct) to group related parameters
- Breaking the method into smaller methods
- Using optional parameters for less common options
2. Order Parameters Logically
Place the most important parameters first, followed by less important ones. Group related parameters together.
// Poor parameter order
public void CreateEnemy(bool isElite, int health, string name, Vector3 position)
// Better parameter order
public void CreateEnemy(string name, Vector3 position, int health, bool isElite)
3. Use Descriptive Parameter Names
Parameter names should clearly indicate what the parameter is for:
// Poor parameter names
public void Move(float x, float y, float z, float s)
// Better parameter names
public void Move(float xPosition, float yPosition, float zPosition, float speed)
4. Use ref
and out
Sparingly
While ref
and out
parameters are useful, they can make code harder to understand and maintain. Use them only when necessary:
- Use
ref
when you need to modify a variable passed to the method - Use
out
when a method needs to return multiple values - Consider returning a tuple or custom type instead of using multiple
out
parameters
5. Validate Parameters
Check parameter values at the beginning of the method to ensure they're valid:
public void TakeDamage(int amount, Character source)
{
// Validate parameters
if (amount < 0)
{
throw new ArgumentException("Damage amount cannot be negative", nameof(amount));
}
if (source == null)
{
throw new ArgumentNullException(nameof(source), "Damage source cannot be null");
}
// Method implementation
}
6. Use Named Arguments for Clarity
When calling methods with many parameters or boolean flags, use named arguments to make the code more readable:
// Without named arguments - what do these booleans mean?
CreateCharacter("Gandalf", "Wizard", 50, true, false, true);
// With named arguments - much clearer
CreateCharacter(
name: "Gandalf",
className: "Wizard",
level: 50,
isHero: true,
isImmortal: false,
canUseSpells: true
);
7. Document Parameters with XML Comments
Use XML comments to document what each parameter is for:
/// <summary>
/// Creates a new enemy at the specified position.
/// </summary>
/// <param name="enemyType">The type of enemy to create</param>
/// <param name="position">The position to spawn the enemy</param>
/// <param name="health">Initial health of the enemy (default: 100)</param>
/// <param name="isElite">Whether the enemy is an elite version (default: false)</param>
/// <returns>The created enemy GameObject</returns>
public GameObject SpawnEnemy(
EnemyType enemyType,
Vector3 position,
int health = 100,
bool isElite = false)
{
// Method implementation
}
Conclusion
Method parameters are a powerful feature that make your methods more flexible and reusable. By understanding the different types of parameters and when to use each one, you can write more effective and maintainable code.
In this section, we've covered:
- Basic parameter passing
- Value vs. reference parameters
- Value types vs. reference types
- Output parameters
- Optional parameters
- Named arguments
- Parameter arrays
- Practical examples in game development
- Best practices for method parameters
In the next section, we'll explore return values, which allow methods to send data back to their callers.
In Unity development, effective use of method parameters is essential for:
- Creating flexible, reusable components
- Implementing configurable game systems
- Building editor tools with customizable options
- Designing clean APIs for your game systems
- Optimizing performance by avoiding unnecessary object creation
Understanding parameter passing is particularly important when working with Unity's component-based architecture, where you often need to pass data between different components and systems.