Skip to main content

4.1 - Introduction to Methods

Methods (also called functions in many programming languages) are one of the most fundamental building blocks in programming. They allow you to organize your code into logical, reusable units that perform specific tasks. Understanding methods is crucial for writing clean, maintainable, and efficient code in any programming language, including C#.

What Are Methods?

A method is a block of code that performs a specific task. It has a name, can take inputs (parameters), and can return a value. Methods help you:

  1. Organize code into logical, manageable pieces
  2. Avoid repetition by making code reusable
  3. Simplify complex operations by breaking them down into smaller steps
  4. Improve readability by giving meaningful names to blocks of code
  5. Isolate functionality, making debugging and testing easier

Think of methods as verbs in your code—they do things. Good method names often start with verbs like Calculate, Get, Set, Update, Create, or Delete.

Method Anatomy

Here's the basic syntax for defining a method in C#:

[access_modifier] [return_type] [method_name]([parameters])
{
// Method body - code that executes when the method is called

[return statement]; // Optional, required if return_type is not void
}

Let's break down each part:

  • Access Modifier: Determines the visibility of the method (e.g., public, private, protected)
  • Return Type: Specifies what type of value the method returns (or void if it doesn't return anything)
  • Method Name: The identifier used to call the method
  • Parameters: Input values the method needs to perform its task (optional)
  • Method Body: The code that executes when the method is called
  • Return Statement: Sends a value back to the caller (required if the return type is not void)

A Simple Method Example

Let's start with a basic example:

public void SayHello()
{
Console.WriteLine("Hello, World!");
}

This method:

  • Is public (accessible from outside the class)
  • Returns void (doesn't return any value)
  • Is named SayHello
  • Takes no parameters
  • Prints "Hello, World!" to the console when called

To call (invoke) this method, you would write:

SayHello(); // Outputs: Hello, World!

Methods with Parameters

Parameters allow you to pass data into a method. They make methods more flexible and reusable.

public void Greet(string name)
{
Console.WriteLine($"Hello, {name}!");
}

To call this method:

Greet("Alice"); // Outputs: Hello, Alice!
Greet("Bob"); // Outputs: Hello, Bob!

You can have multiple parameters, separated by commas:

public void DisplayPlayerInfo(string name, int level, string characterClass)
{
Console.WriteLine($"Player: {name} | Level: {level} | Class: {characterClass}");
}

To call this method:

DisplayPlayerInfo("Gandalf", 50, "Wizard");
// Outputs: Player: Gandalf | Level: 50 | Class: Wizard

Methods with Return Values

Methods can return values using the return statement. The return type in the method declaration specifies what type of value will be returned.

public int Add(int a, int b)
{
return a + b;
}

To use this method:

int sum = Add(5, 3); // sum will be 8
Console.WriteLine($"The sum is: {sum}");

// Or directly:
Console.WriteLine($"The sum is: {Add(5, 3)}");

The return type can be any valid C# type, including:

  • Primitive types (int, float, bool, etc.)
  • Reference types (string, object, custom classes)
  • Collection types (array, List<T>, etc.)
  • void (indicating no return value)

Why Use Methods?

Let's look at a practical example to understand why methods are valuable. Consider this code without methods:

// Calculate and display the area of a rectangle
double length1 = 5.0;
double width1 = 3.0;
double area1 = length1 * width1;
Console.WriteLine($"Rectangle 1 area: {area1}");

// Calculate and display the area of another rectangle
double length2 = 7.0;
double width2 = 2.0;
double area2 = length2 * width2;
Console.WriteLine($"Rectangle 2 area: {area2}");

// Calculate and display the area of a third rectangle
double length3 = 4.0;
double width3 = 4.0;
double area3 = length3 * width3;
Console.WriteLine($"Rectangle 3 area: {area3}");

Now, let's refactor this using methods:

public double CalculateRectangleArea(double length, double width)
{
return length * width;
}

public void DisplayRectangleArea(double length, double width, int rectangleNumber)
{
double area = CalculateRectangleArea(length, width);
Console.WriteLine($"Rectangle {rectangleNumber} area: {area}");
}

// Usage:
DisplayRectangleArea(5.0, 3.0, 1);
DisplayRectangleArea(7.0, 2.0, 2);
DisplayRectangleArea(4.0, 4.0, 3);

Benefits of the method-based approach:

  1. Less code repetition: The area calculation logic is written once
  2. Easier maintenance: If you need to change how area is calculated or displayed, you only change it in one place
  3. Better readability: The method names clearly describe what the code does
  4. Easier testing: You can test the CalculateRectangleArea method independently

Methods in Unity

In Unity, methods are particularly important because they form the backbone of MonoBehaviour scripts. Unity provides several special methods (often called "event methods" or "message methods") that get called automatically at specific times:

using UnityEngine;

public class PlayerController : MonoBehaviour
{
// Called when the script instance is being loaded
void Awake()
{
Debug.Log("Awake is called");
}

// Called before the first frame update
void Start()
{
Debug.Log("Start is called");
}

// Called once per frame
void Update()
{
Debug.Log("Update is called");
}

// Called when the GameObject is destroyed
void OnDestroy()
{
Debug.Log("OnDestroy is called");
}

// Your custom methods
public void Jump()
{
Debug.Log("Player jumps!");
// Jump implementation
}

public void TakeDamage(int amount)
{
Debug.Log($"Player takes {amount} damage!");
// Damage implementation
}
}

In Unity, you'll typically:

  1. Use Unity's built-in methods like Start() and Update() for lifecycle events
  2. Create your own methods for game-specific functionality
  3. Call methods in response to user input, collisions, triggers, and other game events

Method Organization

As your code grows, organizing methods becomes important. Here are some guidelines:

Keep related methods close to each other in your code:

// Input-related methods
public void ProcessInput()
{
// ...
}

public void HandleJumpInput()
{
// ...
}

// Movement-related methods
public void Move(Vector3 direction)
{
// ...
}

public void Jump()
{
// ...
}

// Combat-related methods
public void Attack()
{
// ...
}

public void TakeDamage(int amount)
{
// ...
}

2. Method Order

A common convention is to order methods by:

  1. Unity lifecycle methods (Awake, Start, Update, etc.)
  2. Public methods
  3. Private methods
  4. Utility methods

3. Method Length

Keep methods focused on a single task. If a method grows too long (more than 20-30 lines), consider breaking it into smaller methods:

// Instead of one large Update method:
void Update()
{
ProcessInput();
UpdateMovement();
CheckEnvironment();
UpdateAnimation();
}

// Separate methods for each responsibility
void ProcessInput()
{
// Handle player input
}

void UpdateMovement()
{
// Update player position
}

void CheckEnvironment()
{
// Check for collisions, triggers, etc.
}

void UpdateAnimation()
{
// Update player animations
}

Practical Examples in Game Development

Let's look at some practical examples of methods in game development:

Example 1: Player Movement

using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float jumpForce = 5f;

private Rigidbody rb;
private bool isGrounded;

void Start()
{
rb = GetComponent<Rigidbody>();
}

void Update()
{
CheckGrounded();
ProcessMovementInput();
ProcessJumpInput();
}

void CheckGrounded()
{
// Cast a ray downward to check if the player is on the ground
isGrounded = Physics.Raycast(transform.position, Vector3.down, 1.1f);
}

void ProcessMovementInput()
{
// Get horizontal and vertical input
float horizontalInput = Input.GetAxis("Horizontal");
float verticalInput = Input.GetAxis("Vertical");

// Create movement vector
Vector3 movement = new Vector3(horizontalInput, 0f, verticalInput);

// Apply movement
MovePlayer(movement);
}

void ProcessJumpInput()
{
if (Input.GetButtonDown("Jump") && isGrounded)
{
Jump();
}
}

void MovePlayer(Vector3 direction)
{
// Normalize direction to prevent faster diagonal movement
if (direction.magnitude > 1f)
{
direction.Normalize();
}

// Apply movement
transform.Translate(direction * moveSpeed * Time.deltaTime);
}

void Jump()
{
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
}

Example 2: Health System

using UnityEngine;
using UnityEngine.UI;

public class HealthSystem : MonoBehaviour
{
[SerializeField] private int maxHealth = 100;
[SerializeField] private Image healthBar;
[SerializeField] private GameObject deathEffect;

private int currentHealth;
private bool isInvulnerable;

void Start()
{
InitializeHealth();
}

void InitializeHealth()
{
currentHealth = maxHealth;
UpdateHealthBar();
}

public void TakeDamage(int damageAmount)
{
if (isInvulnerable)
return;

currentHealth -= damageAmount;
currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);

UpdateHealthBar();
PlayDamageEffect();

if (currentHealth <= 0)
{
Die();
}
else
{
StartInvulnerabilityPeriod();
}
}

public void Heal(int healAmount)
{
currentHealth += healAmount;
currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);

UpdateHealthBar();
PlayHealEffect();
}

void UpdateHealthBar()
{
if (healthBar != null)
{
healthBar.fillAmount = (float)currentHealth / maxHealth;
}
}

void PlayDamageEffect()
{
// Play damage animation, sound, or particle effect
Debug.Log("Playing damage effect");
}

void PlayHealEffect()
{
// Play healing animation, sound, or particle effect
Debug.Log("Playing heal effect");
}

void StartInvulnerabilityPeriod()
{
isInvulnerable = true;
Invoke("EndInvulnerabilityPeriod", 1f);
}

void EndInvulnerabilityPeriod()
{
isInvulnerable = false;
}

void Die()
{
Debug.Log("Player died");

// Instantiate death effect
if (deathEffect != null)
{
Instantiate(deathEffect, transform.position, Quaternion.identity);
}

// Disable player or destroy game object
gameObject.SetActive(false);
}

public int GetCurrentHealth()
{
return currentHealth;
}

public float GetHealthPercentage()
{
return (float)currentHealth / maxHealth;
}
}

Example 3: Weapon System

using UnityEngine;
using System.Collections;

public class WeaponSystem : MonoBehaviour
{
[System.Serializable]
public class WeaponData
{
public string name;
public GameObject prefab;
public int damage;
public float fireRate;
public int ammoCapacity;
public AudioClip fireSound;
public ParticleSystem muzzleFlash;
}

[SerializeField] private WeaponData[] availableWeapons;
[SerializeField] private Transform weaponSocket;

private WeaponData currentWeapon;
private GameObject weaponInstance;
private int currentAmmo;
private bool isReloading;
private float nextFireTime;

void Start()
{
if (availableWeapons.Length > 0)
{
EquipWeapon(0); // Equip first weapon by default
}
}

void Update()
{
if (Input.GetButton("Fire1") && CanFire())
{
FireWeapon();
}

if (Input.GetKeyDown(KeyCode.R) && !isReloading && currentAmmo < currentWeapon.ammoCapacity)
{
StartCoroutine(ReloadWeapon());
}

// Weapon switching with number keys
for (int i = 0; i < availableWeapons.Length && i < 9; i++)
{
if (Input.GetKeyDown((i + 1).ToString()))
{
EquipWeapon(i);
break;
}
}
}

bool CanFire()
{
return currentWeapon != null &&
currentAmmo > 0 &&
!isReloading &&
Time.time >= nextFireTime;
}

void FireWeapon()
{
// Update firing cooldown
nextFireTime = Time.time + (1f / currentWeapon.fireRate);

// Reduce ammo
currentAmmo--;

// Play effects
PlayMuzzleFlash();
PlayFireSound();

// Perform raycast for hit detection
PerformRaycast();
}

void PerformRaycast()
{
Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
RaycastHit hit;

if (Physics.Raycast(ray, out hit, 100f))
{
// Check if we hit something damageable
IDamageable damageable = hit.collider.GetComponent<IDamageable>();
if (damageable != null)
{
damageable.TakeDamage(currentWeapon.damage);
}

// Create impact effect
CreateImpactEffect(hit);
}
}

void PlayMuzzleFlash()
{
if (currentWeapon.muzzleFlash != null)
{
currentWeapon.muzzleFlash.Play();
}
}

void PlayFireSound()
{
if (currentWeapon.fireSound != null)
{
AudioSource.PlayClipAtPoint(currentWeapon.fireSound, transform.position);
}
}

void CreateImpactEffect(RaycastHit hit)
{
// Create impact effect based on surface type
Debug.Log($"Hit {hit.collider.name} at {hit.point}");
}

IEnumerator ReloadWeapon()
{
isReloading = true;
Debug.Log($"Reloading {currentWeapon.name}...");

// Play reload animation/sound

// Wait for reload time
yield return new WaitForSeconds(1.5f);

// Refill ammo
currentAmmo = currentWeapon.ammoCapacity;

isReloading = false;
Debug.Log("Reload complete");
}

void EquipWeapon(int weaponIndex)
{
if (weaponIndex < 0 || weaponIndex >= availableWeapons.Length)
return;

// Destroy current weapon instance if it exists
if (weaponInstance != null)
{
Destroy(weaponInstance);
}

// Set current weapon data
currentWeapon = availableWeapons[weaponIndex];

// Instantiate new weapon
if (currentWeapon.prefab != null && weaponSocket != null)
{
weaponInstance = Instantiate(currentWeapon.prefab, weaponSocket);
weaponInstance.transform.localPosition = Vector3.zero;
weaponInstance.transform.localRotation = Quaternion.identity;

// Set up references to particle systems
currentWeapon.muzzleFlash = weaponInstance.GetComponentInChildren<ParticleSystem>();
}

// Reset ammo to full
currentAmmo = currentWeapon.ammoCapacity;

Debug.Log($"Equipped {currentWeapon.name}");
}

public string GetCurrentWeaponName()
{
return currentWeapon != null ? currentWeapon.name : "None";
}

public int GetCurrentAmmo()
{
return currentAmmo;
}

public int GetMaxAmmo()
{
return currentWeapon != null ? currentWeapon.ammoCapacity : 0;
}
}

Best Practices for Methods

1. Use Descriptive Names

Method names should clearly describe what the method does:

// Poor naming
public void DoStuff()

// Better naming
public void CalculateDamage()
public void MovePlayerToPosition()
public void SpawnEnemyAtLocation()

2. Follow the Single Responsibility Principle

Each method should do one thing and do it well:

// Too many responsibilities in one method
public void UpdatePlayer()
{
// Process input
// Update position
// Check collisions
// Update animations
// Check health
// Update UI
}

// Better: Split into focused methods
public void UpdatePlayer()
{
ProcessInput();
UpdatePosition();
CheckCollisions();
UpdateAnimations();
CheckHealth();
UpdateUI();
}

3. Keep Methods Short

Aim for methods that are 20-30 lines or less. Shorter methods are easier to understand, test, and maintain.

4. Limit the Number of Parameters

Try to keep the number of parameters to 4 or fewer. If you need more, consider using a parameter object:

// Too many parameters
public void CreateEnemy(string name, int health, float speed, int damage,
Vector3 position, Quaternion rotation, bool isElite,
string[] abilities, float spawnDelay)

// Better: Use a parameter object
public class EnemyConfig
{
public string Name;
public int Health;
public float Speed;
public int Damage;
public Vector3 Position;
public Quaternion Rotation;
public bool IsElite;
public string[] Abilities;
public float SpawnDelay;
}

public void CreateEnemy(EnemyConfig config)

5. Use XML Documentation Comments

Document your methods with XML comments to provide information about what they do, their parameters, and return values:

/// <summary>
/// Deals damage to the target and applies any special effects.
/// </summary>
/// <param name="target">The character to damage</param>
/// <param name="amount">Amount of damage to deal</param>
/// <param name="damageType">Type of damage (fire, ice, etc.)</param>
/// <returns>True if the target was damaged, false if they were immune</returns>
public bool DealDamage(Character target, int amount, DamageType damageType)
{
// Method implementation
}

6. Avoid Side Effects

Methods should avoid changing state that isn't obvious from the method name:

// Unexpected side effect
public int CalculateScore()
{
int score = player.Kills * 100 + player.Coins * 10;
player.Health = 100; // Unexpected side effect!
return score;
}

// Better: Side effect is clear from the name
public int CalculateScoreAndHealPlayer()
{
int score = player.Kills * 100 + player.Coins * 10;
player.Health = 100;
return score;
}

Conclusion

Methods are the building blocks of your code, allowing you to create modular, reusable, and maintainable programs. By organizing your code into well-named, focused methods, you make it easier to understand, debug, and extend.

In this section, we've covered:

  • What methods are and why they're important
  • How to define and call methods
  • Methods with parameters and return values
  • Method organization and best practices
  • Practical examples in game development

In the next sections, we'll dive deeper into method parameters, return values, method overloading, and more advanced method concepts.

Unity Relevance

In Unity, well-structured methods are essential for:

  • Organizing MonoBehaviour scripts
  • Responding to Unity's lifecycle events (Awake, Start, Update, etc.)
  • Handling user input
  • Implementing game mechanics
  • Creating reusable components
  • Building modular systems (inventory, combat, movement, etc.)

As your Unity projects grow in complexity, good method design becomes increasingly important for maintaining code quality and developer sanity.