GDKB

Functions

Beginner18 Minutes readUpdated July 21, 20262 related concepts

A function is a named, reusable block of code that takes zero or more inputs (parameters), does work, and optionally produces an output (a return value). Functions are the primary tool for breaking a program into understandable pieces: instead of one long block of code, a program becomes a set of named operations that call each other — `TakeDamage`, `FindClosestEnemy`, `IsGrounded` — each readable on its own.

On this page

Functions

Summary

A function is a named, reusable block of code that takes zero or more inputs (parameters), does work, and optionally produces an output (a return value). Functions are the primary tool for breaking a program into understandable pieces: instead of one long block of code, a program becomes a set of named operations that call each other — TakeDamage, FindClosestEnemy, IsGrounded — each readable on its own.

Mental Model

A function is a small machine with a name, a slot for each input, and (at most) one output slot. You don't need to know how the machine works internally to use it correctly — only its name, what it needs, and what it gives back. That boundary (the signature: name, parameter types, return type) is a contract; the body behind it can change entirely without affecting anyone who only relies on the signature.

Calling a function is handing values to the machine and waiting for its answer; the machine runs its own internal steps (which may themselves call other machines) and hands back a result, then execution resumes exactly where it left off, at the call site.

Core Explanation

  • Parameters vs. arguments. A parameter is the named placeholder in a function's declaration (void TakeDamage(int amount) — amount is the parameter). An argument is the actual value supplied at the call site (TakeDamage(10) — 10 is the argument). The distinction matters when reading error messages and documentation precisely.
  • Return values. A function that produces a result declares a return type and uses return value; to hand it back — the call site can then use that value as an expression (see Expressions). A function with no meaningful result to hand back is declared void (C#/C++) and simply performs its work; return; with no value can still exit it early.
  • Pass by value vs. pass by reference. For value-type parameters (int, struct), the function receives its own copy — modifying the parameter inside the function does not change the caller's variable. For reference-type parameters (class instances), the function receives the same reference the caller holds — modifying the object through it is visible to the caller, though reassigning the parameter itself to a different object is not (see Data Types for the value/ reference distinction this rests on). C# and C++ both offer explicit by-reference parameters (ref/out in C#, & in C++) when a value type genuinely needs to be modified in place by the callee.
  • Overloading lets multiple functions share a name as long as their parameter lists differ (in count or type) — the compiler picks the matching one based on the arguments supplied at each call site. Attack() and Attack(int bonusDamage) can coexist as two distinct functions with the same name.
  • Default parameters let a parameter be omitted at the call site, substituting a declared default value — reducing the need for several overloads that only differ by a trailing, usually-unchanged argument.
  • Recursion — a function calling itself, with each call working on a smaller version of the problem until a base case stops the recursion. Powerful for naturally self-similar problems (traversing a tree of parent/child transforms) but requires a guaranteed-reachable base case to avoid recursing until the call stack overflows.
  • Pure functions vs. functions with side effects. A pure function's output depends only on its inputs, and it changes nothing outside itself (Add(a, b)); a function with side effects also modifies state beyond its return value (TakeDamage mutates health). Neither is wrong, but knowing which kind a function is changes what a caller can safely assume about calling it more than once, or in a different order.

Syntax / Examples

C# (Unity)

text
// Signature: name, parameters, return type.
int Clamp(int value, int min, int max)
{
    if (value < min) return min;
    if (value > max) return max;
    return value;
}

void TakeDamage(int amount) // void: no return value
{
    health = Clamp(health - amount, 0, maxHealth);
}

// Overloading
void Attack() => Attack(baseDamage);
void Attack(int bonusDamage) => target.TakeDamage(baseDamage + bonusDamage);

// Default parameter
void Log(string message, bool isWarning = false) { /* ... */ }

C++ (Unreal)

cpp
int32 Clamp(int32 Value, int32 Min, int32 Max)
{
    if (Value < Min) return Min;
    if (Value > Max) return Max;
    return Value;
}

void ATakeDamage(int32 Amount)
{
    Health = Clamp(Health - Amount, 0, MaxHealth);
}

// Pass by reference — modifies the caller's variable directly
void Normalize(FVector& Vector)
{
    Vector = Vector.GetSafeNormal();
}

Unreal Blueprint

A Function (created in the My Blueprint panel) has an entry node with output pins for each parameter and, if it returns a value, a Return Node with an input pin for the result — the same name/parameters/return-type signature, expressed as pins instead of a text declaration. Calling it is a single node with input pins matching the parameters and (if any) an output pin matching the return value.

Practical Example

Extracting repeated "closest target" logic (seen in Loops) into a function makes it reusable and gives it a name that documents intent:

csharp
Enemy FindClosest(Vector3 origin, List<Enemy> candidates)
{
    Enemy closest = null;
    float closestDistance = float.MaxValue;

    foreach (Enemy enemy in candidates)
    {
        float distance = Vector3.Distance(origin, enemy.position);
        if (distance < closestDistance)
        {
            closest = enemy;
            closestDistance = distance;
        }
    }
    return closest;
}

// Call sites — the loop itself no longer needs to be re-written each time.
Enemy nearestToPlayer = FindClosest(player.position, enemies);
Enemy nearestToTurret = FindClosest(turret.position, enemies);

The function's signature (origin, candidates, returns an Enemy) is everything a caller needs to know; neither call site needs to know the loop exists inside it.

Common Mistakes

  • Functions that do too much. A function handling input, game logic, and rendering all at once is hard to test, hard to reuse, and hard to name honestly. A function whose name needs "and" to describe it (UpdateAndRenderAndSave) is a sign it should be split.
  • Assuming a class-type parameter can't be affected by the callee — it can: mutating the object's fields inside the function is visible to the caller, because both hold a reference to the same object, even though the parameter variable itself is a local copy of the reference.
  • Recursion without a reachable base case — a stack overflow crash, not a graceful failure; every recursive function needs a condition that is guaranteed to eventually be true.
  • Overloading in a way that makes call sites ambiguous or surprising — overloads should be genuinely equivalent operations at different levels of detail, not unrelated behaviors sharing a name.
  • Ignoring a return value that reports failure — a function that returns bool or an error code to signal success/failure is silently useless if every caller discards the result without checking it.

Best Practices

  • Name a function after what it does, specifically enough that the name alone tells a caller when to use it.
  • Keep a function focused on one responsibility; extract a helper function when a block starts handling more than one concern.
  • Prefer returning a value over mutating an out-parameter when either would work — it makes the function easier to use as an expression.
  • Document (via the signature and, where needed, a short comment) whether a function has side effects a caller must be aware of.
  • Give every recursive function an explicit, obviously-reachable base case, written and tested before the recursive case.

Exercises

  1. Extract a block of repeated code (write a small example with the same five lines appearing twice) into a function, and verify both call sites still behave identically.
  2. Write two overloads of a Heal function — one taking no arguments (heals a fixed default amount) and one taking an explicit amount — and explain how the compiler chooses between them.
  3. Write a recursive function that sums the values in a singly linked list, identifying its base case explicitly.
  4. Take a function that mutates a class-typed parameter's field and explain, using the value/reference distinction, why the caller sees the change.
  • Loops — a loop body that grows complex is a strong candidate for extraction into a named function.
  • Classes — a class's methods are functions that operate on a particular object's state.

References

Related Concepts