Taming the Code Jungle: FC/IS in Game Development

The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.

Joe Armstrong

A couple years ago, I learned from some smart friends of mine about FC/IS, or Functional Core/Imperative Shell. Probably the most important video on the topic is Boundaries by Destroy All Software.

Boundaries is a very crucial piece of thinking on the topic. But in this article I want to explain what FC/IS is, and then focus in on its defining criteria: creating boundaries with your data.

Within an FC/IS world, code can be separated into two distinct camps:

  • Functional Core – The code that does the work, ie, it manipulates data and modifies the state of the application.
  • Imperative Shell – The code that executes the core and maintains the state.
  • The boundary between these two camps is defined by the data shared between them.

An Example

Let’s start with the following class:

public class PlayerMovement : MonoBehaviour
{

    public Rigidbody2D rBod;
    public float force = 10.0f;
    
    private bool _onGround;
    
    void Update()
    {
        if (Input.GetKey(KeyCode.Space) 
            && _onGround)
        {
            rBod.AddForce(new Vector2(0.0f, force));
            _onGround = false;
        }
    }
    // ... etc.

}

The above code example is object-oriented imperative programming, which I’m going to call OOI moving forward. It is stream of consciousness programming. The data and procedures are entangled in one class. While this is a smaller example, think about OOI in larger context: ‘god’ objects, singletons, and all the things that use them. These tiny classes likely have dependencies on big data.

Its All About The Data

By analyzing the data used by OOI code, it is possible to define boundaries for where to separate your code’s core from its shell. This data helps reduce and isolate the dependencies of usage into smaller, more manageable units. Each function in the core works only with the data it needs, nothing more.

While the shell contains the variables used, and calls into the core:

So if we put these together, we can rewrite our class like so:

public class MovementCore
{
    public static void ApplyForceToRigidBody(
        Rigidbody2D rBod, 
        Vector2 force)
    {
        if(force != Vector2.zero)
        {
            rBod.AddForce(force);
        }
    }

    public static void Jump(
        bool shouldJump, 
        float force, 
        ref Vector2 outForce, 
        ref bool onGround)
    {
      if(shouldApplyForce)
      {
        outForce.y = force;
        onGround = false;
      }
      onGround = onGround;
    }
}
public class PlayerMovementShell : MonoBehaviour
{

    public Rigidbody2D rBod;
    public float force = 10.0f;
    
    private bool _onGround;
    private const int GroundLayer = 10;
    
    void Update()
    {
        // state variable for our rigid body.
        Vector2 forceVector; 

        // Update our state variables based on the inputs.
        PlayerMovementCore.Jump(
            Input.GetKey(jumpKey) && _onGround, 
            force, 
            ref forceVector, 
            ref _onGround);

        // Apply our updated state to our variables.
        PlayerMovementCore.ApplyForceToRigidBody(
            rBod, 
            forceVector);

    }
    // ... etc.

}

The PlayerMovement code is now split into a functional core and imperative shell. It is no longer tied to an OOI class like it was before. It is transformed into an imperative shell that calls into functional core code, using it to modify its state and update its variables. The core can be more easily re-used, and also importantly, tested (more on this in future articles).

Avoiding Bad Data Contracts

A problem of OOI code is the tendency over time for the codebase to be one giant interdependent ball of code. Sometimes I want to just eat the banana, but you can’t. How do we fix this? How can we isolate our bananas from our gorillas?

Frequently this issue is related to what I call bad data contracts: Passing way too much data, or passing a whole object instead of just the necessary parameters:

fxManager.SpawnFx(attackingUnit, attackFxType);

The code above passes the ‘attackingUnit’ to the FXManager. Does the FXManager really need the whole ‘attackingUnit’ in order to spawn the vfx?

public class FXManager
{
	//...
	public void SpawnFx(Unit source, FXType attackFxType)
	{
	    FXUnit prefabToSpawn = prefabs[attackFxType];
	    FXUnit fxUnit = Instantiate(prefabToSpawn)
	    fxUnit.transform.position = source.position;
	}
	//...
}

The answer? Not really. We just need the Vector3 of the unit. Maybe what we want is something like:

public void SpawnFx(Vector3 spawnLocation, FXType attackFxType)
{
    FXUnit prefabToSpawn = prefabs[attackFxType];
    FXUnit fxUnit = Instantiate(prefabToSpawn)
    fxUnit.transform.position = spawnLocation;
    // ..
}

This is slightly better. We have improved our contract, but our FXManager is still mixing its state and its execution. Plus, it could be kind of useful to actually pass in the Unit object to simplify things.

But there still is a bad contract: our unit relies on an instance of FxManager. FxManager itself is a dependency, possibly a HUGE one, and this breaks our pattern. I want FxManager to be lightweight, providing an interface to the internal functionality. While we can’t necessarily remove this dependency, maybe we can shift things around so that what we care the most about, the execution no longer has deep dependencies.

What if we could have the functionality of FxManager…without having an FxManager?

public class FxManager
{
    Dictionary<FxType, FxUnit> _prefabs;

    public void SpawnFx(
        Unit source, 
        FxType attackFxType)
    {
        SpawnFx(
            source.transform.position, 
            attackFxType);
    }

    public void SpawnFx(
        Vector3 spawnLocation, 
        FxType attackFxType)
    {
        var prefab = FxCore.GetPrefabForFxType(
            attackFxType, _prefabs);
        FxCore.SpawnUnit(prefab, position);
    }
}
public static class FxCore
{
    public static FxUnit GetPrefabForFxType(
        FxType attackFxType, 
        Dictionary<FxType, FxUnit> prefabs)
    {
        return prefabs[attackFxType];
    }

    public static FxUnit SpawnUnit(
        FxUnit unitPrefab, 
        Vector3 position)
    {
        FxUnit fxUnit = Instantiate(prefabToSpawn)
        fxUnit.transform.position = spawnLocation;      
    }
}

We now have functional code outside of the FxManager in FxCore. Unit and FxManager are still object-oriented inter-dependent, and contractually obligated objects. But now, their functionality is not.

Now our ability to spawn Fx isn’t actually tied to FxManager:

public class MyButton
{
	public FxUnit fxPrefab;

	public void OnClick()
	{
		FXCore.SpawnUnit(fxPrefab, transform.position);
	}
}

And while there may still be a jungle connected to a banana over in Imperative-Land, Functional-Land allows me to just eat a banana. By moving the functional core to rely on more basic data types, the interdependencies are reduced (We can continue to reduce interdependencies further, but this is a topic for another article) .

Code Readability

I highly value readability of my code and I go over the importance of it in my Code Readability article. One of the things I like about this pattern is how it really improves the readability of the code. Sometimes putting functional code into the core isn’t to make it more re-usable, its simply to make it more readable! Even if multiple areas of the application aren’t using the core, it is still quite possible to reap the benefits.

A ‘Structural Pattern’

Unlike MVC or ECS, or many other types of patterns, FC/IS is not an architectural pattern. It actually works alongside these patterns. Its what I would call a ‘structural’ pattern. It is a ‘girder’ within the architecture. If you are familiar with data-oriented ECS architectures, you may see a lot of similarities between it and FC/IS.

In Summary

Object oriented software design provides a lot of benefits, but there are many facets of development that, in practice, cause problems. Frequently composition or interfaces are patterns that developers rely on. But FC/IS within object-oriented code is under-utilized and provides many benefits. Frequently it is desired to break something down into smaller chunks, but a ‘chunk’ may still be partitioned in a way that isn’t very re-usable, and still has dependencies. Sometimes the answer isn’t more objects, but dividing objects into functional and imperative behaviors.

Imperative code is messy but frequently necessary. It is not *bad*. But it can be a grab bag of data pulled from multiple sources and then reorganized in order to achieve the required effect. FC/IS is a great way to write (and refactor!) code to make it clearer, more testable, and with less dependencies.

I have a lot more to say on this, this is just the tip of the iceberg. If you liked this, let me know on twitter!