The Stitch framework for Unity


Intro

Throughout the development of Death's Doorstep, I've created a lot of common C# classes that I can re-use for future projects. A lot of this work is done to save me time in the future, but at the same time I've used it as a way to keep my code clean and structured.

I call it a framework because in reality it's just a light wrapper on top of what Unity already provides. The classes I've made are just MonoBehaviours, but they piece together in such a way that I can isolate logic chunks. This keeps my classes from ballooning in size, which makes it harder to edit or build onto code that I've already written.

In this article I'm going to try to cover how the Stitch framework is structured, as well as why I chose to make certain decisions. By reading this, it should make it easier for you to follow along with my Youtube vlog. If you still have any questions, then feel free to ask them in the comments section :)

Stitch.MonoExtend

The only time I use MonoBehaviour directly in the Stitch framework, is through the MonoExtend class. Like the name suggests, MonoExtend is a simple wrapper around the MonoBehaviour class. The only reason this class exists is because I didn't like to have to create a full Vector3, just to change the position of a single axis. As you'll see below, MonoExtend defines some helpers that make it easy to change the position or scale of an object, even if it's only for a single axis.

namespace Stitch {
  public class MonoExtend : MonoBehaviour {
    /***********************************
     * GLOBAL POSITION
     **********************************/
    public float x {
      get { return transform.position.x; }
      set { transform.position = new Vector3(value, y, z); }
    }
    public float y {
      get { return transform.position.y; }
      set { transform.position = new Vector3(x, value, z); }
    }
    public float z {
      get { return transform.position.z; }
      set { transform.position = new Vector3(x, y, value); }
    }
    public Vector2 xy {
      get { return (Vector2)transform.position; }
      set { transform.position = new Vector3(value.x, value.y, z);  }
    }
    public Vector3 xyz {
      get { return transform.position; }
      set { transform.position = value; }
    }
    /***********************************
     * LOCAL POSITION
     **********************************/
    public float localX {
      get { return transform.localPosition.x; }
      set { transform.localPosition = new Vector3(value, localY, localZ); }
    }
    public float localY {
      get { return transform.localPosition.y; }
      set { transform.localPosition = new Vector3(localX, value, localZ); }
    }
    public float localZ {
      get { return transform.localPosition.z; }
      set { transform.localPosition = new Vector3(localX, localY, value); }
    }
    public Vector2 localXy {
      get { return (Vector2)transform.localPosition; }
      set { transform.localPosition = new Vector3(value.x, value.y, localZ);  }
    }
    public Vector3 localXyz {
      get { return transform.localPosition; }
      set { transform.localPosition = value; }
    }
    /***********************************
     * LOCAL SCALE
     **********************************/
    public Vector2 hv {
      get { return (Vector2)transform.localScale; }
      set { transform.localScale = new Vector3(value.x, value.y, transform.localScale.z); }
    }
    public float h {
      get { return hv.x; }
      set { transform.localScale = new Vector3(value, transform.localScale.y, transform.localScale.z); }
    }
    public float v {
      get { return hv.y; }
      set { transform.localScale = new Vector3(transform.localScale.x, value, transform.localScale.z); }
    }
  }
}

Stitch.Root

This is the most important class to know about. I try to avoid defining any logic in this class directly, but that's because it exists to serve a different purpose. It is the central point of all of the scripts attached to a single object. The GameObject for my game's hero is already split between 25+ different classes, and I'm still fairly early on in development. This is how it currently looks:

namespace Mummy {
  public class Root : Stitch.Root {
    protected override void Services() {
      Extend(new Movement(this));
      Extend(new Squish(this));
      Extend(new Pivot(this));
      Extend(new AdjustLimit(this));
      Extend(new BodyCollisions(this));
      Extend(new WhipCollisions(this));
      Extend(new InputBuffer(this));
      Extend(new CoyoteTime(this));
      Extend(new Combo(this));
    }
    protected override void States() {
      Initial<Drone>();
      Define<God>();
      Define<Standing>();
      Define<Walking>();
      Define<Running>();
      Define<Ducking>();
      Define<Jumping>();
      Define<Sliding>();
      Define<Walljumping>();
      Define<Gliding>();
      Define<Blasting>();
      Define<Flowering>();
      Define<Whip1>();
      Define<Whip2>();
      Define<WhipAir>();
      Define<WhipRun>();
      Define<WhipUp>();
    }
    protected override void Load() {
      // Adds a global reference to this object that other
      // objects can use (ex. enemies, camera, etc)
      game.refs.Add("Mummy", this);
    }
  }
}

The Root object itself reads more like a table of contents than a complex object. At a glance you can see all of the Services and States that are assigned to the object, each with their own unique logic.

I do want to point out the Load() method here. You will also see this used later on as well, it's just a wrapper for Unity's Awake().

Stitch.Service

Services are the only classes that do not directly inherit from MonoExtend. Their purpose is to share common logic across multiple classes on a single Root object. Originally I put a lot of this logic in the Root class itself, but that quickly caused it to balloon in size with logic that wasn't necessarily related. By allowing a Root to have X amount of services, I can isolate all that logic, keeping everything clean and concise. On top of that, I can share a common service across multiple objects:

namespace Stitch {
  public class Health : Service {
    // We need to define the constructor so that
    // it can reference "root"
    public Health(Root root) : base(root) {}
    public int hp;
    public void Damage(int amount) {
      hp = Math.Max(hp - amount, 0);
      // Fire events based on the amount of health remaining.
      // Other States or Services can hook into these events if
      // they need to update anything based on the change
      if (hp == 0)
        root.events.Fire("Health:Depleted");
      else
        root.events.Fire("Health:Decrease");
    }
  }
}

Above you can see an example of the Health service that I use for multiple objects in Death's Doorstep. This class will likely grow over time, but all of my enemies share this same service, so any changes I make benefit all my enemy types at once. Since services are defined directly on the Root, any States can call the Damage method as well as respond to the Decrease/Depleted events that it emits.

Stitch.State

If you've watched any of my vlogs, I am a strong advocate of using a State machine for game development. If you're not using one, you should be. If you're skeptical, give it a try. It may be a different approach then you're used to, but in the end it's 100% worth it. I decided to create my own state machine, but there are existing frameworks out there that are likely better than I've built.

A Root object can have any number of States defined, but only one can be active at a point in time. Like the Root object, a State is nothing more than a MonoExtend. When the Scene begins, the Root will add the components declared in the States() method. All of these components will be disabled by default though, leaving only the current state enabled.

Below is an example of the state that my Jellyfish enemy uses to chase the hero:

namespace Jellyfish {
  public class Creeping : State {
    const float CREEP_ACCEL = 0.025f;
    // These accessors are just convenient shorthand for loading
    // other Chunks & Services. "root.Get" is to access a Chunk,
    // "root.Use" is to access a Service. 
    private Body body => root.Get<Body>();
    private Sketch sketch => root.Get<Sketch>();
    private Rotation rotation => root.Use<Rotation>();
    private ChangeEyes changeEyes => root.Use<ChangeEyes>();
    private Collisions collisions => root.Use<Collisions>();
    // This is called when the entering/exiting the Creeping state.
    // This allows me to toggle event listeners on/off based on whether this
    // is the current State.
    protected override void Subscribe() {
      collisions.Listen(this);
    }
    // Called when this state becomes the current state
    protected override void Enabled() {
      root.Disable<Friction>();
      changeEyes.Angry();
      sketch.Speed = 1.5f;
    }
    // Called when this is no longer the current state
    protected override void Disabled() {
      root.Enable<Friction>();
    }
    // A wrapper for MonoBehaviour.Update()
    // It is called everytime the screen refreshes, but only
    // if this is the current state
    protected override void Draw() {
      rotation.RotateBySpeed();
    }
    // A wrapper for MonoBehaviour.FixedUpdate()
    // It is called with every Unity fixed update, but only
    // if this is the current state
    protected override void Physics() {
      if (Vector2.Distance(xy, root.refs.Get("Mummy").xy) > Jellyfish.Root.CREEP_DISTANCE) {
        // If the Mummy is out of range of this object,
        // then exit this state and enter the Floating state where the Jellyfish
        // will wait for the Mummy to come in range again
        root.Enter("Floating");
      } else {
        // If the Mummy is in range, then chase >:)
        body.AccelerateToPoint(root.refs.Get("Mummy").xy, CREEP_ACCEL);
      }
    }
  }
}


Stitch.Chunk

The last class type that I'm going to cover is the concept of a Chunk. These are essentially MonoBehaviours that assign themselves to the nearest Root for easy access. You'll notice that they don't show up in the Root definition above, that's because unlike Services and States, they are added directly in the Unity editor.

I often use Chunks to wrap common Unity components due to a couple of benefits. First of all, it's easier for me to interface with a Chunk while using the Stitch framework. I can access their data from any State/Service on any GameObject with O(n) efficiency. On top of that, I can add any helpers that I like to the standard Unity components.

namespace Stitch {
  [RequireComponent(typeof(Body))]
  [AddComponentMenu("_Stitch_/Gravity")]
  public class Gravity : Chunk {
    [Tooltip("The amount of downward momentum to apply each step.")]
    public float power;
    // Gravity relies on the Body Chunk to actually move the object,
    // it just applies it's own force on each Physics update.
    private Body body => root.Get<Body>();
    protected override void Physics() {
      body.Accelerate(-Vector2.up * power);
    }
  }
}


Summary

That should cover the basics of how I use Stitch framework alongside Unity. Everything I'm adding to Stitch is still very much a WIP, so there's no public place to access the code. I'm not against releasing a public version in the future if there's interest, but I already have my hands full with my own game atm

Again, feel free to ask any questions in the comments :)

Leave a comment

Log in with itch.io to leave a comment.