Jarrah Technology logo

August 26, 2014

Category: News

Author: Charles

Working with Unity, rather than against it Twitter Facebook Share on Google+

Like the old cliche “to a hammer, everything looks like a nail”, as a programmer I think many of my game-dev problems can be solved with code. So at first many of my solutions to issues in Concealed Intent were to add more functions onto control classes. For instance, to handle screen size changes, each GUI element’s control class included a function that knew how to resize that GUI element. The problem with this is sometimes this is the only code required in such a class. Also, it requires the resizing logic to be hardcoded and thus changing it means changing the code. This is not the Unity way.

From my experience, Unity seems to prefer that functionality be broken into small (hopefully reusable) pieces that are then placed as Unity components (inheriting from Unity’s MonoBehaviour class). Public parameters on these components are then editable in the Unity editor and the class is added to the frame-by-frame update loop. The idea of public variables and adding unused code (as sizing is event based) to frequently run loops seems wrong to me after years of professional software development. However, that appears to be the way Unity is intended to be used, and it certainly makes things easier. Here is how I moved my element sizing code into Unity components.

I wrote the system based on a series of discrete screen sizes. That is, rather than have the GUI respond to a continuous stream of screen size changes or sizes, it will instead jump to a particular configuration when a particular screen size is applied. This is because while screen size checking will detect the smallest change, most of the changes will be the player changing the screen resolution.

So firstly, the program needs to be able to recognise what is the appropriate size. To do this I created a class to listen to NGUI screen events, calculate the appropriate internal screen size category, and if that category has changed fire a new event. The screen sizes are organised by width as that is what is important to me. Screens with a width below 1024 pixels use the W1024 sizing scheme, between 1025 & 1440 pixels wide use the W1440 scheme and larger screens use the W1920 scheme. It should be easy to add more.

// Copyright 2014 Jarrah Technology (http://www.jarrahtechnology.com). All Rights Reserved.

using UnityEngine;
using System;

public class ScreenControl : MonoBehaviour {
  public static ScreenControl Instance;
  public enum ScreenSize { W1024 = 1024, W1440 = 1440, W1920 = 1920 }

  public delegate void OnScreenHandler(ScreenSize size);
  public event OnScreenHandler OnScreenChange;

  public ScreenSize Size { get; private set; }

  void Awake() {
    if (Instance == null) {
            Instance = this;
            CalcIdx();
            UICamera.onScreenResize += OnScreenResize;
        } else if (Instance != this) {
            Destroy(gameObject);
        }
    }

    void OnScreenResize() {
        ScreenSize old = Size;
        CalcIdx();
        if (old != Size && OnScreenChange != null) {
            OnScreenChange(Size);
        }
    }

    void CalcIdx() {
        foreach (ScreenSize s in (ScreenSize[])Enum.GetValues(typeof(ScreenSize))) {
            if (Screen.width <= (int)s) {
                Size = s;
                return;
            }
        }
    }
}

Then on the root GameObject of my GUI goes a component which listens to these events and passes them on to a collection of sizing components in its children GameObject’s. The collection of sizing components can be reloaded programmatically. It will also not recurse past a child with another component of the same type attached. Thus if part of the GUI changes its children, a new SizerNode can be placed at the root of this part of the GUI and its Reload method called whenever children are added or removed. For example, a grid should have a SizerNode on it and when objects are added/removed from the grid, Reload should be called. In this way only the parts of the GUI that need reloading are reloaded.

// Copyright 2014 Jarrah Technology (http://www.jarrahtechnology.com). All Rights Reserved.

using UnityEngine;
using System.Collections.Generic;

public class SizerNode : MonoBehaviour {

    List<Sizer> sizers = new List<Sizer>();

    void Start() {
        Reload();
    }

    public void Reload() {
        sizers.Clear();
        LoadWidgets(gameObject);
        OnScreenChange(ScreenControl.Instance.Size);
    }

    void LoadWidgets(GameObject go) {
        foreach (Transform t in go.transform) {
            sizers.AddRange(t.GetComponents<Sizer>());
            if (t.GetComponents<SizerNode>().Length == 0) {
                LoadWidgets(t.gameObject);
            }
        }
    }   

    void OnScreenChange(ScreenControl.ScreenSize newSize) {
        foreach (Sizer s in sizers) {
            s.UpdateSize(newSize);
        }
    }
}

This just leaves the code to change the GUI element’s size. First an abstract superclass, so that many different sizing systems can be used and still picked up by the SizerNode’s.

// Copyright 2014 Jarrah Technology (http://www.jarrahtechnology.com). All Rights Reserved.

using UnityEngine;
using System.Collections;

public abstract class Sizer : MonoBehaviour {

    public void UpdateSize() {
        UpdateSize(ScreenControl.Instance.Size);
    }

    public abstract void UpdateSize(ScreenControl.ScreenSize size);
}

Then components like the below class can be placed on the elements to resize. This one changes the NGUI widget width and height. In the Unity editor it appears as an array of width/height pairs for various screen sizes. I’ll leave it as an exercise for the reader to write other sizing classes. So far I have used sizers that change local position, NGUI widget anchors, font sizes and grid parameters. All are basically the same as the code below and work nicely in the Unity editor. A GUI element can also have multiple sizers on it. I have elements with font and dimension resizers on them.

// Copyright 2014 Jarrah Technology (http://www.jarrahtechnology.com). All Rights Reserved.

using UnityEngine;
using System.Collections;

public class DimensionSizer : Sizer {

    UIWidget widget;

    public DimensionSize[] sizes;

    void Awake() {
        widget = GetComponent<UIWidget>();
    }

    public override void UpdateSize(ScreenControl.ScreenSize size) {
        foreach (DimensionSize fs in sizes) {
            if (size <= fs.size) {
                widget.width = fs.width;
                widget.height = fs.height;
                return;
            }
        }
    }
}

[System.Serializable]
public class DimensionSize {
    public ScreenControl.ScreenSize size;
    public int height;
    public int width;
}

This system works so much better in Unity than my previous system. It is so easy to change – I can tweak it in the editor while the game is running. I am a convert. Now I need to convert other parts of my code that can work in a similar way. I have already done so for handling colour changes (in Concealed Intent players can adjust the colours used to denote various game factions). It could also be done for highlighting and transparency changes when a ship or component is selected. It is on the (very long) todo list.

Tags: ConcealedIntent, DevDiary, and Unity
comments powered by Disqus