Board Game Programming Tutorial – Utilities

This post is part of my series on computerizing board games and will describe the Utility classes that we use.

We normally use a plugin called UniExtender which adds a lot of capabilities to the Unity classes. It is $5 and so for this tutorial, instead of importing that plugin, I just re-created the few functions that we’ll need. I’ve added them to the Utility class below.

AspectUtility

This utility class can be added to a camera to keep the game canvas set to the correct aspect ratio with black bars on either the sides or top and bottom. It also provides some utility functions for getting positions on a scaled screen.

AspectUtility.cs

AudioPlayer

The AudioPlayer is added to an object that holds the AudioSource. For most games, I use a single AudioSource and swap out the clip for each sound I want to play. This script provides a static function to play an enumerated audio clip with an optional delay and volume and another function to repeat a clip a given number of times. This behavior works pretty well and can handle multiple sounds playing at the same time and setting a volume. However, since it just uses the one AudioSource for all the sounds, the AudioSource setting have to be shared. After adding this behavior, add a value to the AudioClipEnum for each sound effect in your project, then add those sound effects (in the same order) to the EnumeratedClips in the game object.

[RequireComponent(typeof(AudioSource))]
public class AudioPlayer : MonoBehaviour
{
  public enum AudioClipEnum
  {
    INVALID = -1, INTRO, CLICK
  }
  static AudioPlayer thePlayer = null;

  public AudioClip[] EnumeratedClips;
  AudioSource _mySource = null;

  void Awake()
  {
    thePlayer = this;
    _mySource = GetComponent<AudioSource>();
  }

  void Destroy()
  {
    if (thePlayer == this)
      thePlayer = null;
  }
  public static void PlayClip(AudioClipEnum clipEnum, float delay = 0, float volume = 1)
  {
    int index = (int)clipEnum;
    if (index < 0 || index >= thePlayer.EnumeratedClips.Length)
    {
      Debug.LogError("Count out of range: " + clipEnum.ToString());
      return;
    }
    AudioClip clip = thePlayer.EnumeratedClips[index];
    if (clip == null)
    {
      Debug.LogError("Clip not found: " + clipEnum.ToString());
      return;
    }
    if (delay > 0)
    {
      thePlayer.ExecuteLater(delay, () =>
      {
        thePlayer._mySource.PlayOneShot(clip, volume);
      });
    }
    else
    {
      thePlayer._mySource.PlayOneShot(clip, volume);
    }
  }
  public static void RepeatClip(AudioClipEnum clipEnum, int times, float delay = 0)
  {
    int index = (int)clipEnum;
    if (index < 0 || index >= thePlayer.EnumeratedClips.Length)
    {
      Debug.LogError("Count out of range: " + clipEnum.ToString());
      return;
    }
    AudioClip clip = thePlayer.EnumeratedClips[index];
    if (clip == null)
    {
      Debug.LogError("Clip not found: " + clipEnum.ToString());
      return;
    }
    thePlayer._mySource.clip = clip;
    for (int i = 0; i < times; ++i) { thePlayer.ExecuteLater(delay + clip.length * i, () => thePlayer._mySource.Play());
    }
  }
  public static void Stop()
  {
    thePlayer._mySource.Stop();
  }
}

Draggable

The Draggable behavior can be added to any GameObject in the scene to allow the user to drag that object around.

public class Draggable : MonoBehaviour, IPointerDownHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
{
  private bool QDragging = false;
  private Vector2 myOffset;
  private RectTransform myParentRT;
  private RectTransform myRT;

  void Awake()
  {
    myParentRT = transform.parent as RectTransform;
    myRT = transform as RectTransform;
  }

  public void OnPointerDown( PointerEventData data )
  {
    RectTransformUtility.ScreenPointToLocalPointInRectangle(
      myRT, data.position, data.pressEventCamera, out myOffset );
    QDragging = true;
  }

  public void OnBeginDrag( PointerEventData data ) { }

  public void OnDrag( PointerEventData data )
  {
    if( !QDragging )
      return;

    Vector2 localPointerPosition;
    if( RectTransformUtility.ScreenPointToLocalPointInRectangle(
        myParentRT, data.position, data.pressEventCamera, out localPointerPosition
    ) )
    {
      myRT.localPosition = localPointerPosition - myOffset;
    }
  }

  public void OnEndDrag( PointerEventData data )
  {
    QDragging = false;
  }
}

Collider2DRaycastFilter

This behavior can be added to any GameObject that wants to use a custom collider instead of it’s RectTransform. We use this to make buttons with non-rectangular shapes.

[RequireComponent( typeof( RectTransform ), typeof( Collider2D ) )]
public class Collider2DRaycastFilter : MonoBehaviour, ICanvasRaycastFilter
{
  Collider2D myCollider;
  RectTransform myRectTransform;

  void Awake()
  {
    myCollider = GetComponent<Collider2D>();
    myRectTransform = GetComponent<RectTransform>();
  }

  public bool IsRaycastLocationValid( Vector2 screenPos, Camera eventCamera )
  {
    var worldPoint = Vector3.zero;
    var isInside = RectTransformUtility.ScreenPointToWorldPointInRectangle(
        myRectTransform, screenPos, eventCamera,
        out worldPoint );
    if( isInside )
      isInside = myCollider.OverlapPoint( worldPoint );

    return isInside;
  }
}

VersionInfo

This behavior can be added to any Text object to add the Unity version number and date the project was built. The behavior takes a version number and sets the text of the element to: “version <version> (<build date>) \r\n Unity: <Application.unityVersion>”

[ExecuteInEditMode]
public class VersionInfo : MonoBehaviour
{
  public string versionNumber;
  public string buildName;

  void Start()
  {
    gameObject.GetComponent<Text>().text = "version "+buildName+"\r\nUnity: "+Application.unityVersion;
  }
#if UNITY_EDITOR
  public void Awake()
  {
    if (Application.isEditor && !Application.isPlaying)
    {
      buildName = versionNumber + " (" + System.DateTime.Now.ToShortDateString() + ")";
    }
  }
#endif
}

Misc Functions

We have added a few functions to Unity classes that we’ve found useful.

public static class Utility
{ 
  public static T GetComponentOnChild<T>(this Transform tx, string pathToChild)
  {
    return tx.FindChild(pathToChild).GetComponent<T>();
  }
  public static T GetComponentOnChild<T>(this GameObject obj, string pathToChild)
  {
    return obj.transform.FindChild(pathToChild).GetComponent<T>();
  }
  public static bool IsEmpty<T>(this IEnumerable<T> source)
  {
    if (source == null)
      return true;
    return !source.Any();
  }
  public static void setAlpha(this Image img, float alpha)
  {
    img.color = new Color(img.color.r, img.color.g, img.color.b, alpha);
  }
  public static string FullPath(this Transform tx)
  {
    string msg = tx.name;
    while ( tx.parent != null )
    {
      msg = tx.parent.name + "/" + msg;
      tx = tx.parent;
    }
    return msg;
  }

  public static void ExecuteLater(this MonoBehaviour behaviour, float delay, System.Action fn)
  {
    behaviour.StartCoroutine(_realExecute(delay, fn));
  }
  static IEnumerator _realExecute(float delay, System.Action fn)
  {
    yield return new WaitForSeconds(delay);
    fn();
  }

  public static T GetRandom<T>(this IList<T> list)
  {
    return list[Random.Range(0, list.Count)];
  }
  public static T PopRandom<T>(this IList<T> list)
  {
    return list.Pop(Random.Range(0, list.Count));
  }
  public static T Pop<T>(this IList<T> list, int index)
  {
    T item = list[index];
    list.RemoveAt(index);
    return item;
  }
  public static void DestroyChildrenImmediate(this GameObject go)
  {
    GameObject[] toDestroy = new GameObject[go.transform.childCount];
    for (int i = 0; i < go.transform.childCount; ++i) toDestroy[i] = go.transform.GetChild(i).gameObject;
    for (int i = 0; i < toDestroy.Length; ++i)
      GameObject.DestroyImmediate(toDestroy[i]);
  }
  public static void Shuffle<T>(this IList<T> list)
  {
    // I copied this one from: http://stackoverflow.com/questions/273313/randomize-a-listt
    int n = list.Count;
    while (n > 1)
    {
      n--;
      int k = Random.Range(0, n + 1);
      T value = list[k];
      list[k] = list[n];
      list[n] = value;
    }
  }