What's in your standard C# toolkit?

I’m curious. Do people keep a sort of library/toolkit of generic functions you will need in most games, and if so, what’s in yours?

For example I am just starting out but one of the things I keep at the ready is code for a VR gaze cursor. Another thing I just put in my toolkit is a simple loading screen coroutine.

You don’t have to share code (but you can if you want to) but I’m just curious what are some of the things you keep at hand like this which you find yourself using over and over in projects?

Currently at work we maintain a package server that has several packages for code bases we use. For personal use, not really. If I want to add something to a new project and I know I’ve done the same code before, I just go grab the script from one of my repos and then integrate it.

I don’t necessarily use all of it as the ‘library’ is really a space where I experiment and try out things. But basically my ‘Spacepuppy Framework’ is code I bring into my projects and reuse a lot:

Things I use a lot are:
SPTween - my tween framework

RadicalCoroutine - an extension of Coroutine (though many features I’ve added Unity has been slowly adding to Coroutine over the years)

SPEvent/SPTriggers - an implementation of an event system that is similar to UnityEvent but also predates it and has many features UnityEvent doesn’t have

SPInput - a wrapper around the older unity input system that made it more easily configurable and cross-platform friendly. I hear the new input system Unity has been working on remedy most of the issues I had with the OG one. I haven’t used the new one yet though as my SPInput does everything I need it to.

SPSensors - a ‘sensor’ system that I can use for AI and other things for “finding” objects in a scene. Such as giving my AI entities eyes & ears.

SPWaypoint - waypoint stuff that integrates with SPTween. It’s how we do most our cutscenes and the sort.

SPEditor - an extension system for making it easy for me to extend the unity editor.

SPEntity - an entity model I use for associating a large cluster of GameObjects by its base GameObject. Basically I can use “SPEntity.Pool.GetFromSource(…)” on any child object to grab access of the root and from there access various parts of the “entity”. This is useful since say a ‘Collider’ on the models hand/gun/leg/head… is going to be somewhere in the model’s hierarchy. Yet the Health component is on the root, and the “eyes” sensor is elsewhere, and so on.

SPTime - I give ‘time’ object-identity and with it I can create different scalable time contexts rather than the 2 distinct “scaled/unscaled” time. This is useful for stacking scaled times. Like say I want a “slow-mo” effect on some entities, but still have the game pausible, etc etc.

IRandom - object identity given to Random so that I have a consistent interface for swapping between different random algorithms. So say I have a level generator that is seeded I can instantiate my random algorithm with that seed and feed it into the level generator while still getting to use Random in other places.

Collections - various collections I use a lot. Like BinaryHeap, TempList (a cacheable list to use with nonalloc versions of the unity api), Octree, and some others

MultiTag - ability to add multiple tags to a single GameObject. I don’t really use this much as I avoid tags in code. But my artist/designer likes it for prototyping and cutscenes.

SPAnim - We don’t like mecanim… it may have gotten better since I last used it years ago. But we mainly make old school inspired games with simple janky animations and the legacy unity animation works great for it. But the legacy anim API is hot garbage… so this is just a lib that we use to simplify accessing the legacy anim api.

SPMotor - our standard way of “moving” entities around.

SPPathfinding - implementations of A* and ‘graphs’. I use this for level generators. Actual in game AI pathing generally uses either Unity’s pathing or Aron Granberg’s. SPPathfinding also contains some interface contracts for integrating those pathfinding engines into SPMotor and the sort for easy swapping out which engine we use.

There’s a lot of other ‘crap’ in there as well that we don’t really use. I wrote it for funsies, or I wrote it and later realized it wasn’t really needed. But it’s still there. I’m now working on SP4.0 where I’m trimming out stuff I don’t need, upgrading stuff we use a lot, and other things. Like SPMotor which is getting a huge overhaul right now since we use it a lot and we want to make it better. SPTween is another where we want to have both dynamic ‘string’ accessors but also faster delegate based accessors.

[edit]

I should point out I don’t really suggest people download/use my framework as it stands on github. I don’t really ummm… adequately “support” it in any meaningful manner.

You may wonder why I have it on my github, and that’s just because some times I use it as a reference when sharing concepts/ideas. Also it’s just nice to have things you’ve created in your github when doing job interviews. I really suggest people keep a github for that reason alone… it’s basically a programmer’s “portfolio”.

5 Likes

I’ve got my own custom network API I call JCGNetwork I drop into any network project. Some features it has which I think are obvious, but many other network API’s don’t have are automatic message fragmentation up to around 2GB on all channels, optional encryption per channel, client deviceUniqueIdentifier / IP blacklisting, and a world space distance to player subscription system. The subscription system will skip updates or slow down updates to networked objects distant to the player, but the player should still be able to see.

2 Likes

I only keep a few loose booleans, an integer, and a float, but the float only comes out for special occasions.

Other than that it’s just tons and tons of Debug.Log() statements, and they’ve never let me down.

Seriously, in C# land I don’t really like libraries and assemblies as they seem brittle.

I use source control religiously, every project in its own repo, and I tend to propagate a small core set of routines from game to game, but over time each game customizes it to some extent, and then I only selectively port stuff back to other games.

For my personal code I do share some small blobs of code via git submodules, which we do a LOT of at work at the day job… heavy heavy submodule use. That helps both keep things in sync as well as version-provable and forensically investigable.

In fact, far more important than sharing source is just getting your project under tight source control, with lots and lots of fine-grained commits with good commentary as you progress.

2 Likes

Some utility methods I use a lot:

–remap a range to another (with optional clamping)
–create a List of specified range: simple version counting up from 0, full version with offset and step size
–randomize/shuffle a List

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class utilityScript
{
    // Utility methods

    //-----------------------------------------------------------

    // Map values: Input a range, output another range
    // Simple version, no clamping
    public static float remap(float val, float in1, float in2, float out1, float out2)
    {
        if (in1 == in2) // avoid division by 0
        {
            Debug.Log("Input range must be greater than 0");
            return 0f;
        }
        else
        {
            return out1 + (val - in1) * (out2 - out1) / (in2 - in1);
        }
    }

    // Full version, clamping settable on all 4 range elements (in1, in2, out1, out2)
    public static float remap(float val, float in1, float in2, float out1, float out2,
        bool in1Clamped, bool in2Clamped, bool out1Clamped, bool out2Clamped)
    {
        if (in1 == in2) // avoid division by 0
        {
            Debug.Log("Input range must be greater than 0");
            return 0f;
        }
        else
        {
            if (in1Clamped == true && val < in1) val = in1;
            if (in2Clamped == true && val > in2) val = in2;

            float result = out1 + (val - in1) * (out2 - out1) / (in2 - in1);

            if (out1Clamped == true && result < out1) result = out1;
            if (out2Clamped == true && result > out2) result = out2;

            return result;
        }
    }

    //-----------------------------------------------------------

    // Create list: 2 versions
    // Simple version: Input list length. Starts at 0, step size 1, ascending
    public static List<int> createList(int length)
    {
        if (length <= 0) // list length must be positive
        {
            Debug.Log("List length must be greater than 0");
            return null;
        }
        else
        {
            List<int> tempCreateList = new List<int>(length);
            for (int i = 0; i < length; i++)
            {
                tempCreateList.Add(i);
            }

            //Debug.Log("tempCreateList = " + string.Join(",", tempCreateList));
            return tempCreateList;
        }
    }

    // Full version: Length, starting offset, step size
    // Step size, can be positive (ascending), negative (descending), or 0 (numbers won't change)
    public static List<int> createList(int length, int offset, int stepSize)
    {
        if (length <= 0) // list length must be positive
        {
            Debug.Log("List length must be greater than 0");
            return null;
        }
        else
        {
            List<int> tempCreateList = new List<int>(length);
            for (int i = 0; i < length; i++)
            {
                tempCreateList.Add(offset + (i * stepSize));
            }

            //Debug.Log("tempCreateList = " + string.Join(",", tempCreateList));
            return tempCreateList;
        }
    }

    //-----------------------------------------------------------

    // Randomize/shuffle list
    // Input list, randomize order
    public static List<int> randomizeList(List<int> randList)
    {
        for (int i = 0; i < randList.Count; i++)
        {
            int temp = randList[i];
            int rand = Random.Range(i, randList.Count);
            randList[i] = randList[rand];
            randList[rand] = temp;
        }
        //Debug.Log("randList = " + string.Join(",", randList));
        return randList;
    }

}
2 Likes

I’m not sure how often you would use your createList methods. However if you created them as a general purpose method you should replace

List<int> tempCreateList = new List<int>();

with

List<int> tempCreateList = new List<int>(length);

Otherwise if your “length” is rather large your list would need to unnecessarily re-allocate the internal array several times. By providing an initial capacity the internal array will have the proper size from the beginning.

1 Like

Good to know! I edited it. Thanks!

For my current project, I’ve been splitting my code base into “core” and “universal” scripts for logic related specifically to my application, and for other general-purpose logic that could be used across any genres of games respectively.

Most of the features I have so far are minor quality-of-life things such as extension methods and custom data-types.
Here’s some notable ones:

NumberExt - Extension methods for numeric types.

public static class NumberExt {
  public static float Min(this float value, float min) => value < min ? min : value;

  public static float Max(this float value, float max) => value > max ? max : value;

  public static float Clamp(this float value, float min, float max) => Mathf.Clamp(value, min, max);

  public static float ClampLoop(this float value, float min, float max) {
    if(value < min) {
      value = max;
    }
    else if(value > max) {
      value = min;
    }

    return value;
  }

  public static float Clamp01(this float value) => Mathf.Clamp01(value);

  public static float MoveTowards(this float value, float target, float maxDelta) => Mathf.MoveTowards(value, target, maxDelta);

  public static float Rounded(this float value) => Mathf.Round(value);

  public static int RoundedInt(this float value) => Mathf.RoundToInt(value);

  public static float LerpTo(this float value, float target, float percent) => Mathf.Lerp(value, target, percent);

  public static float Absolute(this float value) => Mathf.Abs(value);

  public static bool IsBetween(this float value, float min, float max, bool inclusive = false) =>
    inclusive ?
    value >= min && value <= max :
    value > min && value < max;

  public static bool IsApproximately(this float value, float otherValue) => Mathf.Approximately(value, otherValue);

  //Similar methods for other numeric-types.
}

VectorExt - Extension methods for Vector-types

public static class VectorExt {
  public static Vector3 SetValues(this Vector3 value, float? x = null, float? y = null, float? z = null) => new Vector3(x ?? value.x, y ?? value.y, z ?? value.z);

  public static Vector3 ClampXYZ(this Vector3 value, float min, float max) => new Vector3(Mathf.Clamp(value.x, min, max), Mathf.Clamp(value.y, min, max), Mathf.Clamp(value.z, min, max));

  public static Vector3 ClampXYZ(this Vector3 value, Vector3 min, Vector3 max) => new Vector3(Mathf.Clamp(value.x, min.x, max.x), Mathf.Clamp(value.y, min.y, max.y), Mathf.Clamp(value.z, min.z, max.z));

  public static Vector3 MinXYZ(this Vector3 value, float min) => ClampXYZ(value, min, float.MaxValue);

  public static Vector3 MinXYZ(this Vector3 value, Vector3 min) => ClampXYZ(value, min, new Vector3(float.MaxValue, float.MaxValue, float.MaxValue));

  public static Vector3 MaxXYZ(this Vector3 value, float max) => ClampXYZ(value, float.MinValue, max);

  public static Vector3 MaxXYZ(this Vector3 value, Vector3 max) => ClampXYZ(value, new Vector3(float.MinValue, float.MinValue, float.MinValue), max);

  public static Vector3 ClampMagnitude(this Vector3 value, float maxLength) => Vector3.ClampMagnitude(value, maxLength);

  public static Vector3 Rounded(this Vector3 value) => new Vector3(Mathf.Round(value.x), Mathf.Round(value.y), Mathf.Round(value.z));

  public static Vector3 LerpTo(this Vector3 value, Vector3 target, float percent) => Vector3.Lerp(value, target, percent);

  public static Vector3 SlerpTo(this Vector3 value, Vector3 target, float percent) => Vector3.Slerp(value, target, percent);

  public static Vector3 MoveTowards(this Vector3 value, Vector3 target, float maxDistanceDelta) => Vector3.MoveTowards(value, target, maxDistanceDelta);

  public static float DistanceTo(this Vector3 value, Vector3 target) => (target - value).magnitude;

  public static float SqrDistanceTo(this Vector3 value, Vector3 target) => (target - value).sqrMagnitude;

  public static float AngleTo(this Vector3 value, Vector3 target) => Vector3.Angle(value, target);

  public static float DotTo(this Vector3 value, Vector3 target) => Vector3.Dot(value, target);

  public static Vector3 Absolute(this Vector3 value) => new Vector3(Mathf.Abs(value.x), Mathf.Abs(value.y), Mathf.Abs(value.z));

  public static Quaternion ToQuaternion(this Vector3 value) => Quaternion.Euler(value);

  //Similar methods for other Vector-types.
}

QuaternionExt - Extension methods for Quaternions

public static class QuaternionExt {
  public static Quaternion LookAt2D(this Quaternion value, Vector3 currentPosition, Vector3 targetPosition, float angleOffset = 0f) {
    Vector3 direction = targetPosition - currentPosition;
    float zAngle = (Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg) - 90f;

    Vector3 eulers = value.eulerAngles;
    return Quaternion.Euler(eulers.x, eulers.y, zAngle + angleOffset);
  }

  public static Quaternion LerpTo(this Quaternion value, Quaternion target, float percent) => Quaternion.Lerp(value, target, percent);

  public static Quaternion LerpTo(this Quaternion value, Vector3 targetEulers, float percent) => Quaternion.Lerp(value, targetEulers.ToQuaternion(), percent);

  public static Quaternion SlerpTo(this Quaternion value, Quaternion target, float percent) => Quaternion.Slerp(value, target, percent);

  public static Quaternion SlerpTo(this Quaternion value, Vector3 targetEulers, float percent) => Quaternion.Slerp(value, targetEulers.ToQuaternion(), percent);

  public static float AngleTo(this Quaternion value, Quaternion target) => Quaternion.Angle(value, target);

  public static float AngleTo(this Quaternion value, Vector3 targetEulers) => Quaternion.Angle(value, targetEulers.ToQuaternion());

  public static float DotTo(this Quaternion value, Quaternion target) => Quaternion.Dot(value, target);

  public static float DotTo(this Quaternion value, Vector3 targetEulers) => Quaternion.Dot(value, targetEulers.ToQuaternion());

  public static Quaternion RotateTowards(this Quaternion value, Quaternion target, float maxDegreesDelta) => Quaternion.RotateTowards(value, target, maxDegreesDelta);

  public static Quaternion RotateTowards(this Quaternion value, Vector3 targetEulers, float maxDegreesDelta) => Quaternion.RotateTowards(value, targetEulers.ToQuaternion(), maxDegreesDelta);

  public static Quaternion Clamp(this Quaternion value, Vector3 minEulers, Vector3 maxEulers) {
    Vector3 valueEulers = value.eulerAngles.AsNegativeEulers();
    valueEulers = valueEulers.ClampXYZ(minEulers, maxEulers);

    return Quaternion.Euler(valueEulers);
  }

  public static Quaternion Clamp(this Quaternion value, Quaternion minRotation, Quaternion maxRotation) => Clamp(value, minRotation.eulerAngles, maxRotation.eulerAngles);
}

TransformValues

A serializable struct that can be created from a Transform’s position/rotation/scale values.

[Serializable]
public struct TransformValues {
  [SerializeField] private Vector3 _position;
  [SerializeField] private Vector3 _rotation;
  [SerializeField] private Vector3 _scale;

  public TransformValues(Vector3 position, Vector3 rotation, Vector3 scale) {
    _position = position;
    _rotation = rotation;
    _scale = scale;
  }

  public TransformValues(Transform transform, bool useLocalValues = false) {
    if(useLocalValues) {
      _position = transform.localPosition;
      _rotation = transform.localEulerAngles;
      _scale = transform.localScale;
    }
    else {
      _position = transform.position;
      _rotation = transform.eulerAngles;
      _scale = transform.localScale;
    }
  }

  public void Apply(Transform transform) {
    transform.position = _position;
    transform.rotation = Quaternion.Euler(_rotation);
    transform.localScale = _scale;
  }

  public void ApplyLocal(Transform transform) {
    transform.localPosition = _position;
    transform.localRotation = Quaternion.Euler(_rotation);
    transform.localScale = _scale;
  }

  public Vector3 Position => _position;
  public Vector3 Rotation => _rotation;
  public Vector3 Scale => _scale;
}

FloatRange, IntRange, and Vector3Range

Like their names suggest, these are three different structs that represent a min/max range between float, int, and Vector3 values respectively.
They each contain custom property drawers as well as equality & arithmetic operator overloads.

Here is FloatRange as an example:

[System.Serializable]
public struct FloatRange {
  public static FloatRange Zero => new FloatRange(0f, 0f);

  public static FloatRange One => new FloatRange(1f, 1f);

  public static FloatRange ZeroToOne => new FloatRange(0f, 1f);

  public static FloatRange MinusOneToZero => new FloatRange(-1f, 0f);

  public static FloatRange MinusOneToOne => new FloatRange(-1f, 1f);

  public static FloatRange Clamp(FloatRange floatRange, float min = float.MinValue, float max = float.MaxValue) => new FloatRange(floatRange.Min.Clamp(min, max), floatRange.Max.Clamp(min, max));

  [SerializeField] private float _min;
  [SerializeField] private float _max;

  public FloatRange(float min, float max) {
    _min = Mathf.Min(min, max);
    _max = Mathf.Max(min, max);
  }

  public float GetLerpValue(float lerp) => Mathf.Lerp(_min, _max, lerp);

  public float GetRandomRange() => Random.Range(_min, _max);

  public float ClampValueInRange(float value) => value.Clamp(_min, _max);

  public bool IsValueInRange(float value, bool inclusive = false) => value.IsBetween(_min, _max, inclusive);

  public float Min => _min;
  public float Max => _max;

  #region OPERATORS
  public override int GetHashCode() => base.GetHashCode();
  public override bool Equals(object obj) => base.Equals(obj);

  public static bool operator ==(FloatRange a, FloatRange b) => a.Equals(b);
  public static bool operator !=(FloatRange a, FloatRange b) => !a.Equals(b);

  public static FloatRange operator +(FloatRange a, FloatRange b) => new FloatRange(a._min + b._min, a._max + b._max);
  public static FloatRange operator +(FloatRange a, float b) => new FloatRange(a._min + b, a._max + b);

  public static FloatRange operator -(FloatRange a, FloatRange b) => new FloatRange(a._min - b._min, a._max - b._max);
  public static FloatRange operator -(FloatRange a, float b) => new FloatRange(a._min - b, a._max - b);

  public static FloatRange operator *(FloatRange a, FloatRange b) => new FloatRange(a._min * b._min, a._max * b._max);
  public static FloatRange operator *(FloatRange a, float b) => new FloatRange(a._min * b, a._max * b);

  public static FloatRange operator /(FloatRange a, FloatRange b) => new FloatRange(a._min / b._min, a._max / b._max);
  public static FloatRange operator /(FloatRange a, float b) => new FloatRange(a._min / b, a._max / b);
  #endregion
}

Other than that, there are some MonoBehaviour components as well for simple & common use-cases.
Much of these components are really just created as I start needing them for my project, and I’ll likely keep expanding the list as I go on.

Some notable components:

  • Movers (components that alter a GameObject’s position or rotation)

  • TranslatePositioner

  • Translates an object’s position on a given Vector3 direction, at a given speed, relative to its local space or world space.

  • Can specify to move the object via Transform, Rigidbody, or Rigidbody2D.

  • FollowPositioner

  • Moves an object’s position towards a given global Vector3 point, or another Transfrom in the scene.

  • Can specify to move the object via Transform, Rigidbody, or Rigidbody2D.

  • FollowCursorPositioner

  • Same as FollowPositioner, but it follows your mouse cursor.

  • SpinRotator

  • Simply rotates an object with a speed/direction relative to each axis.

  • WobbleRotator

  • Rotates an object back-and-forth between a specified angle at a given speed.

  • Wobbling can be eased with Sin, Cos, or PingPong (linear) timing functions.

  • Each axis can wobble with their own settings independently of each other.

  • LookRotator

  • Rotates an object to face a given global Vector3 point or other Transform in the scene.

  • LookCursorRotator

  • Same as LookRotator, but it looks at your mouse cursor.

  • VelocityRotator

  • Rotates an object to face the direction it or another object its traveling in.

  • Object Detectors (components that use ray-casting or overlapping to detect things in the scene)

  • RaycastSource2D, BoxCastSource2D, CircleCastSource2D

  • Casts 2D rays/boxes/circles from the component’s position towards their local upward direction up to a given distance.

  • Can specify LayerMasks to detect.

  • Can specify a max number of objects to detect.
    I.E: A cast will not detect an object behind another object if the max limit is reached.

  • BoxOverlapper2D, CircleOverlapper2D

  • Overlaps a 2D box/circle area to detect objects within around the component’s position.

  • Can specify LayerMasks to detect.

  • Can specify a max number of objects to detect.

  • Can specify only detecting objects within line-of-sight of the component’s position.

  • A min area can be specified within the max area. Objects inside the min area will not be detected.

  • The min area’s origin position can be offset anywhere within the max area.

  • Misc.

  • RotationClamper

  • Constrains an object’s rotation so that it cannot rotate past a given angle.

  • Each axis can be constrained independently of each other.

  • PAEffect

  • Name stands for “Particle-Audio Effect”.

  • Contains a ParticleSystem and/or AudioSource reference & can play them both at a specified position.

  • ObjectSpawner

  • Spawns a given GameObject prefab with a wide variety of options.

  • Can spawn objects at the component’s position, inside a sphere/circle, or at set number of Vector3 points relative to the component’s position.

  • Objects can be spawned with a randomized rotation, an identity rotation, the same rotation as the component, or with an unset rotation.

  • Spawned objects can be re-used, effectively doubling as an object-pooling system.

  • A max number of active objects can be specified.

  • Spawned objects can be set to automatically disable or destroy after a constant or min/max lifetime.

  • TimeTracker, CountTracker

  • Tracks a current float & int value respectively up to a max value.

  • Invokes UnityEvents for when the value reaches maximum or zero.

  • WaveFormScroller

  • Evaluates a set AnimationCurve over time.

  • Contains common curve presets: Sine, Triangle, Sawtooth, Square.

  • A custom AnimationCurve can be defined instead of a preset.

I guess I lied a little in my first post above. I do keep and insert a library with many of my projects. This is my Datasacks package, a scriptable object based variable system that plays super-nice with Unity UI stuff.


But I do commit a static version of that with each game rather than submodule-ing it. It’s just simpler as it has become quite stable. It’s in about a dozen of my games and demos.

Datasacks is presently hosted at these locations:

https://bitbucket.org/kurtdekker/datasacks

1 Like