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.
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);
}
}
}
}
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));
}
}
}
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;
}
}
}
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;
}
}
}
![]() |
contact@rubenbimmel.nl | ![]() |
![]() |
Artstation | ![]() |
GitHub |