Ruben Bimmel

Game Development

Fake Phone UI

Assignment: Create a mobile game that makes use of the affordances of mobile phones
Duration: 4 months
Team size: 4
Role: Development
Skills:
Links:

This is a preview of the Phone UI for Unity that I am currently working on. This UI simulates a smartphone interface and is made for touch input. The UI uses a combination of prefab elements such as panels that can move and buttons. It also makes heavy use of the hierarchy system in unity. This makes it very easy to create menus and apps without programming.

Below are a few code examples from my current project. All touch input gets processed by the Input class. This class defines what type of input is given (tap, hold or swipe). It then searches for the first element that can catch this input.

Division is the base class for all interactive elements in the UI. This class has some basic functionality for its size. It also holds a reference to the bounds of his parent division. The base class has no functionality on input.

The Button class extends from Division. This class makes use of the default input classes from the Division to trigger events and create the visual feedback. Panels are Divisions that can be moved using swipe motions.

Input class

Open »
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace GamePhone {
    public class Input : MonoBehaviour {

        public static bool enabled = true;
        private float holdTime = 1f;
        private float dragStartDistance = 5f;

        private List<Division> divs;
        private Division activeDiv;
        private Vector3 lastMousePosition;
        private bool dragging;
        private float timer;

        // Called on initialisation
        private void Awake() {
            PrepareForInput ();
        }

        // Called at the beginning of every new input
        private void PrepareForInput () {
            divs = new List<Division>();
            lastMousePosition = UnityEngine.Input.mousePosition;
            dragging = false;
            timer = 0f;
        }

        // Update is called once per frame
        private void Update() {
            if (enabled) {
                // On mouse down
                if (UnityEngine.Input.GetMouseButtonDown (0)) {
                    PrepareForInput ();

                    StartInput ();
                }

                // If mouse is above a division
                if (divs.Count > 0) {
                    // On mouse hold
                    if (UnityEngine.Input.GetMouseButton (0)) {
                        UpdateInput ();
                    }
                
                    // On mouse up
                    if (UnityEngine.Input.GetMouseButtonUp (0)) {
                        EndInput ();
                    }

                    timer += Time.deltaTime;
                }
            }
        }

        // Called on mouse down
        private void StartInput () {
            // Store all divisions from raycast in array
            Ray ray = new Ray (transform.TransformPoint (UnityEngine.Input.mousePosition), Vector3.forward * 20f);
            RaycastHit2D[] hit = Physics2D.RaycastAll (transform.TransformPoint (UnityEngine.Input.mousePosition), Vector3.forward);

            // Store all divisions in a list
            divs = new List<Division>();
            for (int i = 0; i < hit.Length; i++) {
                Division newDiv = hit [i].transform.GetComponent<Division> ();
                if (newDiv) {
                    divs.Add (newDiv);
                }
            }

            if (divs.Count > 0) {
                ActivateDivision (divs [0]);
            }
        }

        // Called while mouse is down
        private void UpdateInput () {
            Vector3 offset = UnityEngine.Input.mousePosition - lastMousePosition;
            
            // Check if the user is going to drag
            if (!dragging && offset.magnitude > dragStartDistance) {
                StartMouseDrag (offset);
            }

            // If user is dragging
            if (dragging) {
                UpdateMouseDrag (offset);
                lastMousePosition = UnityEngine.Input.mousePosition;
            } 

            // check if the user is holding down
            if (!dragging && timer >= holdTime) {
                MouseHold ();
            }
        }

        // Called on mouse up
        private void EndInput () {
            if (!dragging && timer < holdTime) {
                MouseClick ();
            }
            DeactivateDivision ();
        }

        // Activate a division and deactivate the old active division
        private void ActivateDivision (Division div) {
            if (div) {
                DeactivateDivision ();
                div.OnMouseSelect ();
                activeDiv = div;
            }
        }

        // deactivate a division
        private void DeactivateDivision () {
            if (activeDiv)
                activeDiv.OnMouseRelease ();
            activeDiv = null;
        }

        // Trigger OnClick event on the active division
        private void MouseClick() {
            if (activeDiv) {
                activeDiv.OnMouseClick ();
            }
        }

        // Trigger OnHold event on the active division
        private void MouseHold() {
            if (activeDiv) {
                activeDiv.OnMouseHold ();
            }
        }

        // Loop through all hits until a division is found that can drag
        private void StartMouseDrag(Vector3 velocity) {
            for (int i = 0; i < divs.Count; i++) {
                if (divs [i].CanDrag (velocity)) {
                    dragging = true;
                    ActivateDivision (divs [i]);
                    return;
                }
            }
        }

        // Trigger OnDrag event on the active division
        private void UpdateMouseDrag(Vector3 velocity) {
            if (activeDiv) {
                activeDiv.OnMouseDrag (velocity);
            }
        }
    }
}

Division class

Open »
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace GamePhone {
    [RequireComponent(typeof(BoxCollider2D))]
    public class Division : MonoBehaviour {
        public Bounds bounds;
        public bool encapsulate;

        private Division parent;

        // Used to safely resize a division
        public virtual void Resize (float width, float height) {
            bounds.size = new Vector3(width, height, 0);
            CheckEncapsulation (this);
        }

        // Use this for initialization
        protected virtual void Start() {
            GetComponent<BoxCollider2D>().size = bounds.size;

            if (encapsulate) {
                foreach (Division div in GetComponentsInChildren<Division> ()) {
                    CheckEncapsulation (div);
                }
            }
        }

        // Check if div is still encapsulated by parent
        public void CheckEncapsulation (Division div) {
            if (encapsulate) {
                bounds.Encapsulate (div.bounds.min);
                bounds.Encapsulate (div.bounds.max);
            }

            if (!parent) {
                parent = transform.parent.GetComponentInParent<Division>();
            }
            if (parent) {
                parent.CheckEncapsulation (div);
            }
        }

        // Used to safely resize a division
        public virtual void Resize (Bounds newBounds) {
            bounds = newBounds;
            CheckEncapsulation (this);
        }

        // Gets called when the user touches this division
        public virtual void OnMouseSelect () {
            //Debug.Log (name + " Select");
        }

        // Gets called when the user resleases this division
        public virtual void OnMouseRelease () {
            //Debug.Log (name + " Release");
        }

        // Gets called when the user clicks on this division
        public virtual void OnMouseClick () {
            //Debug.Log (name + " Click");
        }

        // Gets called every frame (after the treshold) that the user holds this division
        public virtual void OnMouseHold () {
            //Debug.Log (name + " Hold");
        }

        // Gets called every frame the division is dragged
        public virtual void OnMouseDrag (Vector3 velocity) {
            //Debug.Log (name + " Dragging with velocity " + velocity);
        }

        // Returns if this division is allowed to drag
        public virtual bool CanDrag (Vector3 velocity) {
            return false;
        }

        // Returns width of the parent division, or width of the screen if it does not have a parent division
        protected Bounds parentBounds {
            get {
                if (transform.parent) {
                    if (!parent) {
                        parent = transform.parent.GetComponentInParent<Division>();
                    }
                    if (parent) {
                        return parent.bounds;
                    }
                }
                return new Bounds(Vector3.zero, UnityEngine.Screen.safeArea.size);
            }
        }

        // Draw the outline of this division inside the editor
        private void OnDrawGizmos() {
            Gizmos.color = Color.white;
            Gizmos.DrawLine(transform.TransformPoint(bounds.min), transform.TransformPoint(bounds.min + Vector3.up * bounds.size.y));
            Gizmos.DrawLine(transform.TransformPoint(bounds.min), transform.TransformPoint(bounds.min + Vector3.right * bounds.size.x));
            Gizmos.DrawLine(transform.TransformPoint(bounds.max), transform.TransformPoint(bounds.min + Vector3.up * bounds.size.y));
            Gizmos.DrawLine(transform.TransformPoint(bounds.max), transform.TransformPoint(bounds.min + Vector3.right * bounds.size.x));
        }

        // Draw the outline of this division inside the editor when selected
        private void OnDrawGizmosSelected() {
            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(transform.TransformPoint(bounds.min), transform.TransformPoint(bounds.min + Vector3.up * bounds.size.y));
            Gizmos.DrawLine(transform.TransformPoint(bounds.min), transform.TransformPoint(bounds.min + Vector3.right * bounds.size.x));
            Gizmos.DrawLine(transform.TransformPoint(bounds.max), transform.TransformPoint(bounds.min + Vector3.up * bounds.size.y));
            Gizmos.DrawLine(transform.TransformPoint(bounds.max), transform.TransformPoint(bounds.min + Vector3.right * bounds.size.x));
        }
    }
}

Button class

Open »
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

namespace GamePhone {
    [RequireComponent(typeof(SpriteRenderer))]
    [RequireComponent(typeof(BoxCollider2D))]
    public class Button : Division {

        public Sprite normalSprite;
        public Sprite activeSprite;

        public UnityEvent OnClick;
        public UnityEvent OnHold;

        private SpriteRenderer spriteRenderer;
        private bool triggered;

        // Called on initialisation
        protected virtual void Awake() {
            if (OnClick == null) {
                OnClick = new UnityEvent ();
            }
            if (OnHold == null) {
                OnHold = new UnityEvent ();
            }
            spriteRenderer = GetComponent<SpriteRenderer>();
            Reset();
        }

        // Resets the button size so that it is the same as the sprite
        public void Reset() {
            if (normalSprite) {
                spriteRenderer.sprite = normalSprite;
                bounds.size = normalSprite.bounds.size;
                bounds.center = normalSprite.bounds.center;
            }
            GetComponent<BoxCollider2D> ().size = bounds.size;
        }

        // Gets called when the user touches this division
        public override void OnMouseSelect () {
            if (activeSprite) {
                spriteRenderer.sprite = activeSprite;
            }
            base.OnMouseSelect();
        }

        // Gets called when the user resleases this division
        public override void OnMouseRelease () {
            if (normalSprite) {
                spriteRenderer.sprite = normalSprite;
            }
            triggered = false;
            base.OnMouseRelease ();
        }

        // Gets called when the user clicks on this division
        public override void OnMouseClick ()
        {
            OnClick.Invoke ();
            base.OnMouseClick ();
        }

        // Gets called every frame (after the treshold) that the user holds this division
        public override void OnMouseHold ()
        {
            if (!triggered) {
                OnHold.Invoke ();
                triggered = true;
            }
            base.OnMouseHold ();
        }

        // Used to safely resize a button
        public override void Resize(float width, float height) {
            Reset();
            float scale = Mathf.Min(width / (float)bounds.size.x, height / (float)bounds.size.y);
            bounds.size = scale * bounds.size;
        }
    }
}

Panel class

Open »
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace GamePhone {
    public class Panel : Division {

        public bool AllowScrollHorizontal;
        public bool AllowScrollVertical;
        public int margin = 100;
        public bool spring;
        public float springStrength;

        protected Vector3 mousePosition;
        protected Vector3 velocity;
        protected bool dragging;
        protected float clampSpeed = 20;
        protected float springVelocity;

        protected Vector3 startPosition;

        protected virtual void Awake () {
            startPosition = transform.localPosition;
        }

        // Called every frame
        protected virtual void Update() {
            if (!dragging) {
                velocity *= .8f;
                Release();
            }
        }

        // Gets called when the user touches this division
        public override void OnMouseSelect ()
        {
            dragging = true;
            base.OnMouseSelect ();
        }

        // Gets called when the user resleases this division
        public override void OnMouseRelease ()
        {
            dragging = false;
            base.OnMouseRelease ();
        }

        // Returns if this division is allowed to drag
        public override bool CanDrag (Vector3 velocity)
        {
            if (Mathf.Abs (velocity.x) > Mathf.Abs (velocity.y) && AllowScrollHorizontal) {
                return true;
            }
            if (Mathf.Abs (velocity.x) < Mathf.Abs (velocity.y) && AllowScrollVertical) {
                return true;
            }
            return base.CanDrag (velocity);
        }

        // Gets called every frame the division is dragged
        public override void OnMouseDrag (Vector3 velocity)
        {
            this.velocity = velocity;
            Vector3 position = transform.localPosition;

            // Update horizontal and veritcal position and clamp it to parent div size + margin
            if (AllowScrollHorizontal && bounds.size.x > parentBounds.size.x) {
                position.x += velocity.x;
                position.x = Mathf.Clamp(position.x, parentBounds.max.x - bounds.max.x - margin, parentBounds.min.x - bounds.min.x + margin);
            }
            if (AllowScrollVertical && bounds.size.y > parentBounds.size.y) {
                position.y += velocity.y;
                position.y = Mathf.Clamp(position.y, parentBounds.max.y - bounds.max.y - margin, parentBounds.min.y - bounds.min.y + margin);
            }

            transform.localPosition = position;
            mousePosition = UnityEngine.Input.mousePosition;
            base.OnMouseDrag (velocity);
        }

        // Gets called when the user is no longer dragging the panel
        protected virtual void Release() {
            if (spring) {
                ReleaseSpring ();
            } else {
                ReleaseFree ();
            }
        }

        // When there is no spring active the panel keeps moving until its velocity is zero
        protected virtual void ReleaseFree () {
            Vector3 position = transform.localPosition;

            if (AllowScrollHorizontal) {
                // Clamp the horizontal position so that the panel fully overlaps the parent division
                if (position.x < parentBounds.max.x - bounds.max.x) {
                    position.x = Mathf.MoveTowards (position.x, parentBounds.max.x - bounds.max.x, margin * clampSpeed * Time.deltaTime);
                    velocity.x = 0;
                } 
                else if (position.x > parentBounds.min.x - bounds.min.x) {
                    position.x = Mathf.MoveTowards (position.x, parentBounds.min.x - bounds.min.x, margin * clampSpeed * Time.deltaTime);
                    velocity.x = 0;
                }

                // Apply horizontal velocity
                position.x += velocity.x;
            }

            if (AllowScrollVertical) {
                // Clamp the vertical position so that the panel fully overlaps the parent division
                if (position.y < parentBounds.max.y - bounds.max.y) {
                    position.y = Mathf.MoveTowards(position.y, parentBounds.max.y - bounds.max.y, margin * clampSpeed * Time.deltaTime);
                    velocity.y = 0;
                }
                else if (position.y > parentBounds.min.y - bounds.min.y) {
                    position.y = Mathf.MoveTowards(position.y, parentBounds.min.y - bounds.min.y, margin * clampSpeed * Time.deltaTime);
                    velocity.y = 0;
                }

                // Apply vertical velocity
                position.y += velocity.y;
            }

            transform.localPosition = position;
        }

        // If the spring is active the panel will go back to its starting position;
        protected virtual void ReleaseSpring () {
            Vector3 position = transform.localPosition;

            if (position != startPosition) {
                springVelocity += springStrength * Time.deltaTime;
                transform.localPosition = Vector3.MoveTowards (position, startPosition, springVelocity);
            } else {
                springVelocity = 0f;
            }
        }

        // Called to reset the panels position
        public virtual void Reset () {
            transform.localPosition = startPosition;
            velocity = Vector3.zero;
        }
    }
}
« Back to portfolio

Contact