Ruben Bimmel

Game Development

Fake Phone UI

Opdracht: Maak een mobile game dat gebruik maakt van de affordances van een telefoon
Duur: 4 maanden
Team grootte: 4
Rol: Development
Vaardigheden:
Links:

Dit is een preview van de Phone UI voor Unity waar ik nu aan werk. Deze UI simuleert de interface van een smartphone en is gemaakt voor touch input. De UI maakt gebruik van prefab elementen zoals bewegende panelen en standaard knoppen. Door middel van de hierarchy structuur van Unity kan je snel een UI opbouwen zonder te hoeven programmeren.

Hieronder staan een aantal stukjes code van mijn huidige project. De input wordt afgevangen door de Input class. Deze bekijkt wat voor soort input er gegeven wordt (tap, hold of swipe). Vervolgens gaat deze kijken naar het eerste element dat deze input kan afvangen.

Division is een base class voor alle elementen waarmee geïnteracteerd kan worden. Deze class bevat een aantal basisfuncties voor zijn grootte. Daarnaast heeft deze altijd een verwijzing naar de afmetingen van zijn parent division. De class heeft standaard geen functionaliteit voor input.

De Button class is een extensie van Division. Deze class maakt gebruik van de standaard input functies van een division om opdrachten uit te voeren en feedback te laten zien. Panels zijn Divisions die verplaatst kunnen worden door te swipen.

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;
        }
    }
}
« Terug naar het overzicht

Contact