Prototyping with modular systems

These last couple weeks I’ve been working on a simple arena shooter, but this time focusing on Unity’s component-based architecture. What this means is that rather than making huge classes for some entities (like PCs and NPCs), I’ve been working on several small modules that all work independently of one another.

One of the important thing I’ve done has been splitting each object’s model and controller in two separate classes which means that, for example, both PCs and NPCs use a single model for movement, but each of them has a separate controller: the PC’s takes joystick input, while the NPC’s uses a rudimentary AI. This means that it’s a lot easier to keep track of the main movement model class and then make tweaks to its controllers depending on which entity will be using it.

Another element I’ve worked on is that each model inherits behaviors from a base Modifiable class, which works in tandem with Modifier objects to slightly alter an entity’s base properties. I’ve implemented two possible modifiers behaviors that work together:

  • Additive value changes, such as +5 movement speed or -12.5 attack damage
  • Multiplier value changes, such as 150% attack speed or 75% armor

Each Modifiable object runs a coroutine on a set timer that executes every 1/60th of a second for now, and polls each active modifier before applying the result to the base value. For example, the simple way the Mobile2D object moves an object along the X and Z axes is:

movementVector = (Vector3.right * movement.x + Vector3.forward * movement.y) * ((baseSpeed + modifier) * multiplier * Time.deltaTime);

Then, Modifiables use the following code for recalculating the additive and multiplier values:

    public delegate void ModifierEvent(ref float modifierValue);

    protected event ModifierEvent additiveModifiers;
    protected event ModifierEvent multiplierModifiers;

    private IEnumerator RecalculateModifiers()
    {
        while(true)
        {
            modifier = 0;
            if (additiveModifierCount > 0) additiveModifiers(ref modifier);

            multiplier = 1;
            if (multiplierModifierCount > 0) multiplierModifiers(ref multiplier);

            yield return new WaitForSeconds(updateDelay);
        }
    }

A variation of the constant value modifier is the TimedModifier class, which overrides its base class’ UpdateModifier method the following way:

    override protected void UpdateModifier(ref float modifierValue)
    {
        float resultingValue;
        if (decayOverTime) resultingValue = Mathf.Lerp(value, 0, (Time.time - awakeTime) / duration);
        else               resultingValue = value;

        if (type == Type.Additive) modifierValue += resultingValue;
        else                       modifierValue *= resultingValue;
    }

TimedModifiers add two functionalities to the base Modifier class: removing themselves after a set period of time, and also presenting the option of doing a linear interpolation of their values.

Rather than adding up a counter using Time.deltaTime on Unity’s Update event, I save Time.time on awakeTime and use that to calculate the lerp’s range value. Finally, I remove the modifier by invoking a coroutine that merely waits for duration seconds rather than constantly checking whether it should be gone or not.

Another interesting thing of using modules that interact with each other is that, when properly encapsulated, you can easily replace a sub-module with another and have the entity continue behaving as expected. For example, my current object pool for projectiles is a placeholder that instantiates objects in runtime rather than relying on proper pooling (because I haven’t gotten around to working on that sub-system at all yet), but all I’ll have to do is change how the pools’ GetProjectile method works internally since outside objects only care about getting Projectile instances from it.

Something I’ve also worked on is a middleman to Unity’s input system, since the inability to change input buttons during runtime is a pretty big issue. Things are mostly in place by now, so I guess I might write about it for my next post.

Leave a Reply