AR Design
UBC EML collab with UBC SALA - visualizing IoT data in AR
Keyboard.cs
Go to the documentation of this file.
1 // Copyright (c) Microsoft Corporation. All rights reserved.
2 // Licensed under the MIT License. See LICENSE in the project root for license information.
3 
4 using System;
5 using UnityEngine;
6 using UnityEngine.UI;
7 using HoloToolkit.Unity;
9 
10 namespace HoloToolkit.UI.Keyboard
11 {
21  public class Keyboard : Singleton<Keyboard>, IDictationHandler
22  {
27  public enum LayoutType
28  {
29  Alpha,
30  Symbol,
31  URL,
32  Email,
33  }
34 
35  #region Callbacks
36 
42  public event EventHandler OnTextSubmitted = delegate { };
43 
48  public event Action<string> OnTextUpdated = delegate { };
49 
54  public event EventHandler OnClosed = delegate { };
55 
61  public event EventHandler OnPrevious = delegate { };
62 
68  public event EventHandler OnNext = delegate { };
69 
73  public event EventHandler OnPlacement = delegate { };
74 
75  #endregion Callbacks
76 
82  public InputField InputField = null;
83 
87  public AxisSlider InputFieldSlide = null;
88 
92  public bool SliderEnabled = true;
93 
97  public bool SubmitOnEnter = true;
98 
102  public Image AlphaKeyboard = null;
103 
107  public Image SymbolKeyboard = null;
108 
112  public Image AlphaSubKeys = null;
113 
117  public Image AlphaWebKeys = null;
118 
122  public Image AlphaMailKeys = null;
123 
124  private LayoutType m_LastKeyboardLayout = LayoutType.Alpha;
125 
129  [Header("Positioning")]
130  [SerializeField]
131  private float m_MaxScale = 1.0f;
132 
136  [SerializeField]
137  private float m_MinScale = 1.0f;
138 
142  [SerializeField]
143  private float m_MaxDistance = 3.5f;
144 
148  [SerializeField]
149  private float m_MinDistance = 0.25f;
150 
154  public bool CloseOnInactivity = true;
155 
159  public float CloseOnInactivityTime = 15;
160 
164  private float _closingTime;
165 
169  public event Action<bool> OnKeyboardShifted = delegate { };
170 
174  private bool m_IsShifted = false;
175 
179  private bool m_IsCapslocked = false;
180 
184  public bool IsShifted
185  {
186  get { return m_IsShifted; }
187  }
188 
192  public bool IsCapsLocked
193  {
194  get { return m_IsCapslocked; }
195  }
196 
200  private int m_CaretPosition = 0;
201 
205  private Vector3 m_StartingScale = Vector3.one;
206 
210  private Vector3 m_ObjectBounds;
211 
215  private Color _defaultColor;
216 
220  private Image _recordImage;
221 
225  private AudioSource _audioSource;
226 
230  protected override void Awake()
231  {
232  base.Awake();
233 
234  m_StartingScale = transform.localScale;
235  Bounds canvasBounds = RectTransformUtility.CalculateRelativeRectTransformBounds(transform);
236 
237  RectTransform rect = GetComponent<RectTransform>();
238  m_ObjectBounds = new Vector3(canvasBounds.size.x * rect.localScale.x, canvasBounds.size.y * rect.localScale.y, canvasBounds.size.z * rect.localScale.z);
239 
240  // Actually find microphone key in the keyboard
241  var dictationButton = HoloToolkit.Unity.Utils.GetChildRecursive(gameObject.transform, "Dictation");
242  if (dictationButton != null)
243  {
244  var dictationIcon = dictationButton.Find("keyboard_closeIcon");
245  if (dictationIcon != null)
246  {
247  _recordImage = dictationIcon.GetComponentInChildren<Image>();
248  var material = new Material(_recordImage.material);
249  _defaultColor = material.color;
250  _recordImage.material = material;
251  }
252  }
253 
254  // Setting the keyboardType to an undefined TouchScreenKeyboardType,
255  // which prevents the MRTK keyboard from triggering the system keyboard itself.
256  InputField.keyboardType = (TouchScreenKeyboardType)(int.MaxValue);
257 
258  // Keep keyboard deactivated until needed
259  gameObject.SetActive(false);
260  }
261 
262 
266  private void Start()
267  {
268  // Delegate Subscription
269  InputField.onValueChanged.AddListener(DoTextUpdated);
270  }
271 
277  private void DoTextUpdated(string value)
278  {
279  if (OnTextUpdated != null)
280  {
281  OnTextUpdated(value);
282  }
283  }
284 
288  private void LateUpdate()
289  {
290  // Axis Slider
291  if (SliderEnabled)
292  {
293  Vector3 nearPoint = Vector3.ProjectOnPlane(CameraCache.Main.transform.forward, transform.forward);
294  Vector3 relPos = transform.InverseTransformPoint(nearPoint);
295  InputFieldSlide.TargetPoint = relPos;
296  }
297  CheckForCloseOnInactivityTimeExpired();
298  }
299 
300  private void UpdateCaretPosition(int newPos)
301  {
302  InputField.caretPosition = newPos;
303  }
304 
308  private void OnDisable()
309  {
310  m_LastKeyboardLayout = LayoutType.Alpha;
311  Clear();
312  }
313 
314 
320  {
321  }
322 
327  public void OnDictationResult(DictationEventData eventData)
328  {
329  if (eventData.used)
330  {
331  return;
332  }
333  var text = eventData.DictationResult;
334  ResetClosingTime();
335  if (text != null)
336  {
337  m_CaretPosition = InputField.caretPosition;
338 
339  InputField.text = InputField.text.Insert(m_CaretPosition, text);
340  m_CaretPosition += text.Length;
341 
342  UpdateCaretPosition(m_CaretPosition);
343  eventData.Use();
344  }
345  }
346 
352  {
353  ResetClosingTime();
354  SetMicrophoneDefault();
355  }
356 
361  public void OnDictationError(DictationEventData eventData)
362  {
363  }
364 
368  protected override void OnDestroy()
369  {
370  if (IsMicrophoneActive())
371  {
372  StartCoroutine(DictationInputManager.StopRecording());
373  }
374  base.OnDestroy();
375  }
376 
377  #region Present Functions
378 
382  public void PresentKeyboard()
383  {
384  ResetClosingTime();
385  gameObject.SetActive(true);
386  ActivateSpecificKeyboard(LayoutType.Alpha);
387 
388  OnPlacement(this, EventArgs.Empty);
389 
390  // todo: if the app is built for xaml, our prefab and the system keyboard may be displayed.
391  InputField.ActivateInputField();
392 
393  SetMicrophoneDefault();
394  }
395 
396 
401  public void PresentKeyboard(string startText)
402  {
403  PresentKeyboard();
404  Clear();
405  InputField.text = startText;
406  }
407 
412  public void PresentKeyboard(LayoutType keyboardType)
413  {
414  PresentKeyboard();
415  ActivateSpecificKeyboard(keyboardType);
416  }
417 
423  public void PresentKeyboard(string startText, LayoutType keyboardType)
424  {
425  PresentKeyboard(startText);
426  ActivateSpecificKeyboard(keyboardType);
427  }
428 
429  #endregion Present Functions
430  public void RepositionKeyboard(Vector3 kbPos, float verticalOffset = 0.0f)
436  {
437  transform.position = kbPos;
438  ScaleToSize();
439  LookAtTargetOrigin();
440  }
441 
448  public void RepositionKeyboard(Transform objectTransform, BoxCollider aCollider = null, float verticalOffset = 0.0f)
449  {
450  transform.position = objectTransform.position;
451 
452  if (aCollider != null)
453  {
454  float yTranslation = -((aCollider.bounds.size.y * 0.5f) + verticalOffset);
455  transform.Translate(0.0f, yTranslation, -0.6f, objectTransform);
456  }
457  else
458  {
459  float yTranslation = -((m_ObjectBounds.y * 0.5f) + verticalOffset);
460  transform.Translate(0.0f, yTranslation, -0.6f, objectTransform);
461  }
462 
463  ScaleToSize();
464  LookAtTargetOrigin();
465  }
466 
470  private void ScaleToSize()
471  {
472  float distance = (transform.position - CameraCache.Main.transform.position).magnitude;
473  float distancePercent = (distance - m_MinDistance) / (m_MaxDistance - m_MinDistance);
474  float scale = m_MinScale + (m_MaxScale - m_MinScale) * distancePercent;
475 
476  scale = Mathf.Clamp(scale, m_MinScale, m_MaxScale);
477  transform.localScale = m_StartingScale * scale;
478 
479  Debug.LogFormat("Setting scale: {0} for distance: {1}", scale, distance);
480  }
481 
485  private void LookAtTargetOrigin()
486  {
487  transform.LookAt(CameraCache.Main.transform.position);
488  transform.Rotate(Vector3.up, 180.0f);
489  }
490 
495  private void ActivateSpecificKeyboard(LayoutType keyboardType)
496  {
497  DisableAllKeyboards();
498  ResetKeyboardState();
499 
500  switch (keyboardType)
501  {
502  case LayoutType.URL:
503  {
504  ShowAlphaKeyboard();
505  TryToShowURLSubkeys();
506  break;
507  }
508 
509  case LayoutType.Email:
510  {
511  ShowAlphaKeyboard();
512  TryToShowEmailSubkeys();
513  break;
514  }
515 
516  case LayoutType.Symbol:
517  {
518  ShowSymbolKeyboard();
519  break;
520  }
521 
522  case LayoutType.Alpha:
523  default:
524  {
525  ShowAlphaKeyboard();
526  TryToShowAlphaSubkeys();
527  break;
528  }
529  }
530  }
531 
532  #region Keyboard Functions
533 
534  #region Dictation
535 
539  private void BeginDictation()
540  {
541  ResetClosingTime();
542  StartCoroutine(DictationInputManager.StartRecording(gameObject));
543  SetMicrophoneRecording();
544  }
545 
546  private bool IsMicrophoneActive()
547  {
548  var result = _recordImage.color != _defaultColor;
549  return result;
550  }
551 
555  private void SetMicrophoneDefault()
556  {
557  _recordImage.color = _defaultColor;
558  }
559 
563  private void SetMicrophoneRecording()
564  {
565  _recordImage.color = Color.red;
566  }
567 
571  public void EndDictation()
572  {
573  StartCoroutine(DictationInputManager.StopRecording());
574  SetMicrophoneDefault();
575  }
576 
577  #endregion Dictation
578 
583  public void AppendValue(KeyboardValueKey valueKey)
584  {
585  IndicateActivity();
586  string value = "";
587 
588  // Shift value should only be applied if a shift value is present.
589  if (m_IsShifted && !string.IsNullOrEmpty(valueKey.ShiftValue))
590  {
591  value = valueKey.ShiftValue;
592  }
593  else
594  {
595  value = valueKey.Value;
596  }
597 
598  if (!m_IsCapslocked)
599  {
600  Shift(false);
601  }
602 
603  m_CaretPosition = InputField.caretPosition;
604 
605  InputField.text = InputField.text.Insert(m_CaretPosition, value);
606  m_CaretPosition += value.Length;
607 
608  UpdateCaretPosition(m_CaretPosition);
609  }
610 
615  public void FunctionKey(KeyboardKeyFunc functionKey)
616  {
617  IndicateActivity();
618  switch (functionKey.m_ButtonFunction)
619  {
620  case KeyboardKeyFunc.Function.Enter:
621  {
622  Enter();
623  break;
624  }
625 
626  case KeyboardKeyFunc.Function.Tab:
627  {
628  Tab();
629  break;
630  }
631 
632  case KeyboardKeyFunc.Function.ABC:
633  {
634  ActivateSpecificKeyboard(m_LastKeyboardLayout);
635  break;
636  }
637 
638  case KeyboardKeyFunc.Function.Symbol:
639  {
640  ActivateSpecificKeyboard(LayoutType.Symbol);
641  break;
642  }
643 
644  case KeyboardKeyFunc.Function.Previous:
645  {
646  MoveCaretLeft();
647  break;
648  }
649 
650  case KeyboardKeyFunc.Function.Next:
651  {
652  MoveCaretRight();
653  break;
654  }
655 
656  case KeyboardKeyFunc.Function.Close:
657  {
658  Close();
659  break;
660  }
661 
662  case KeyboardKeyFunc.Function.Dictate:
663  {
664  if (IsMicrophoneActive())
665  {
666  EndDictation();
667  }
668  else
669  {
670  BeginDictation();
671  }
672  break;
673  }
674 
675  case KeyboardKeyFunc.Function.Shift:
676  {
677  Shift(!m_IsShifted);
678  break;
679  }
680 
681  case KeyboardKeyFunc.Function.CapsLock:
682  {
683  CapsLock(!m_IsCapslocked);
684  break;
685  }
686 
687  case KeyboardKeyFunc.Function.Space:
688  {
689  Space();
690  break;
691  }
692 
693  case KeyboardKeyFunc.Function.Backspace:
694  {
695  Backspace();
696  break;
697  }
698 
699  case KeyboardKeyFunc.Function.UNDEFINED:
700  {
701  Debug.LogErrorFormat("The {0} key on this keyboard hasn't been assigned a function.", functionKey.name);
702  break;
703  }
704 
705  default:
706  throw new ArgumentOutOfRangeException();
707  }
708  }
709 
713  public void Backspace()
714  {
715  // check if text is selected
716  if (InputField.selectionFocusPosition != InputField.caretPosition || InputField.selectionAnchorPosition != InputField.caretPosition)
717  {
718  if (InputField.selectionAnchorPosition > InputField.selectionFocusPosition) // right to left
719  {
720  InputField.text = InputField.text.Substring(0, InputField.selectionFocusPosition) + InputField.text.Substring(InputField.selectionAnchorPosition);
721  InputField.caretPosition = InputField.selectionFocusPosition;
722  }
723  else // left to right
724  {
725  InputField.text = InputField.text.Substring(0, InputField.selectionAnchorPosition) + InputField.text.Substring(InputField.selectionFocusPosition);
726  InputField.caretPosition = InputField.selectionAnchorPosition;
727  }
728 
729  m_CaretPosition = InputField.caretPosition;
730  InputField.selectionAnchorPosition = m_CaretPosition;
731  InputField.selectionFocusPosition = m_CaretPosition;
732  }
733  else
734  {
735  m_CaretPosition = InputField.caretPosition;
736 
737  if (m_CaretPosition > 0)
738  {
739  --m_CaretPosition;
740  InputField.text = InputField.text.Remove(m_CaretPosition, 1);
741  UpdateCaretPosition(m_CaretPosition);
742  }
743  }
744  }
745 
749  public void Previous()
750  {
751  OnPrevious(this, EventArgs.Empty);
752  }
753 
757  public void Next()
758  {
759  OnNext(this, EventArgs.Empty);
760  }
761 
766  public void Enter()
767  {
768  if (SubmitOnEnter)
769  {
770  // Send text entered event and close the keyboard
771  if (OnTextSubmitted != null)
772  {
773  OnTextSubmitted(this, EventArgs.Empty);
774  }
775 
776  Close();
777  }
778  else
779  {
780  string enterString = "\n";
781 
782  m_CaretPosition = InputField.caretPosition;
783 
784  InputField.text = InputField.text.Insert(m_CaretPosition, enterString);
785  m_CaretPosition += enterString.Length;
786 
787  UpdateCaretPosition(m_CaretPosition);
788  }
789 
790  }
791 
796  public void Shift(bool newShiftState)
797  {
798  m_IsShifted = newShiftState;
799  OnKeyboardShifted(m_IsShifted);
800 
801  if (m_IsCapslocked && !newShiftState)
802  {
803  m_IsCapslocked = false;
804  }
805  }
806 
811  public void CapsLock(bool newCapsLockState)
812  {
813  m_IsCapslocked = newCapsLockState;
814  Shift(newCapsLockState);
815  }
816 
820  public void Space()
821  {
822  m_CaretPosition = InputField.caretPosition;
823  InputField.text = InputField.text.Insert(m_CaretPosition++, " ");
824 
825  UpdateCaretPosition(m_CaretPosition);
826  }
827 
831  public void Tab()
832  {
833  string tabString = "\t";
834 
835  m_CaretPosition = InputField.caretPosition;
836 
837  InputField.text = InputField.text.Insert(m_CaretPosition, tabString);
838  m_CaretPosition += tabString.Length;
839 
840  UpdateCaretPosition(m_CaretPosition);
841  }
842 
846  public void MoveCaretLeft()
847  {
848  m_CaretPosition = InputField.caretPosition;
849 
850  if (m_CaretPosition > 0)
851  {
852  --m_CaretPosition;
853  UpdateCaretPosition(m_CaretPosition);
854  }
855  }
856 
860  public void MoveCaretRight()
861  {
862  m_CaretPosition = InputField.caretPosition;
863 
864  if (m_CaretPosition < InputField.text.Length)
865  {
866  ++m_CaretPosition;
867  UpdateCaretPosition(m_CaretPosition);
868  }
869  }
870 
875  public void Close()
876  {
877  if (IsMicrophoneActive())
878  {
879  StartCoroutine(DictationInputManager.StopRecording());
880  }
881  SetMicrophoneDefault();
882  OnClosed(this, EventArgs.Empty);
883  gameObject.SetActive(false);
884  }
885 
889  public void Clear()
890  {
891  ResetKeyboardState();
892  InputField.MoveTextStart(false);
893  InputField.text = "";
894  m_CaretPosition = InputField.caretPosition;
895  }
896 
897  #endregion
898 
908  public void SetScaleSizeValues(float minScale, float maxScale, float minDistance, float maxDistance)
909  {
910  m_MinScale = minScale;
911  m_MaxScale = maxScale;
912  m_MinDistance = minDistance;
913  m_MaxDistance = maxDistance;
914  }
915 
916  #region Keyboard Layout Modes
917 
921  public void ShowAlphaKeyboard()
922  {
923  AlphaKeyboard.gameObject.SetActive(true);
924  m_LastKeyboardLayout = LayoutType.Alpha;
925  }
926 
931  private bool TryToShowAlphaSubkeys()
932  {
933  if (AlphaKeyboard.IsActive())
934  {
935  AlphaSubKeys.gameObject.SetActive(true);
936  return true;
937  }
938  else
939  {
940  return false;
941  }
942  }
943 
948  private bool TryToShowEmailSubkeys()
949  {
950  if (AlphaKeyboard.IsActive())
951  {
952  AlphaMailKeys.gameObject.SetActive(true);
953  m_LastKeyboardLayout = LayoutType.Email;
954  return true;
955  }
956  else
957  {
958  return false;
959  }
960  }
961 
966  private bool TryToShowURLSubkeys()
967  {
968  if (AlphaKeyboard.IsActive())
969  {
970  AlphaWebKeys.gameObject.SetActive(true);
971  m_LastKeyboardLayout = LayoutType.URL;
972  return true;
973  }
974  else
975  {
976  return false;
977  }
978  }
979 
983  public void ShowSymbolKeyboard()
984  {
985  SymbolKeyboard.gameObject.SetActive(true);
986  }
987 
991  private void DisableAllKeyboards()
992  {
993  AlphaKeyboard.gameObject.SetActive(false);
994  SymbolKeyboard.gameObject.SetActive(false);
995 
996  AlphaWebKeys.gameObject.SetActive(false);
997  AlphaMailKeys.gameObject.SetActive(false);
998  AlphaSubKeys.gameObject.SetActive(false);
999  }
1000 
1004  private void ResetKeyboardState()
1005  {
1006  CapsLock(false);
1007  }
1008 
1009  #endregion Keyboard Layout Modes
1010 
1014  private void IndicateActivity()
1015  {
1016  ResetClosingTime();
1017  if (_audioSource == null)
1018  {
1019  _audioSource = GetComponent<AudioSource>();
1020  }
1021  if (_audioSource != null)
1022  {
1023  _audioSource.Play();
1024  }
1025  }
1026 
1030  private void ResetClosingTime()
1031  {
1032  if (CloseOnInactivity)
1033  {
1034  _closingTime = Time.time + CloseOnInactivityTime;
1035  }
1036  }
1037 
1041  private void CheckForCloseOnInactivityTimeExpired()
1042  {
1043  if (Time.time > _closingTime && CloseOnInactivity)
1044  {
1045  Close();
1046  }
1047  }
1048  }
1049 }
void Next()
Send the "next" event.
Definition: Keyboard.cs:757
Represents a key on the keyboard that has a function.
Singleton class that implements the DictationRecognizer to convert the user&#39;s speech to text...
string Value
The default string value for this key.
void CapsLock(bool newCapsLockState)
Set the keyboard to a permanent shift state.
Definition: Keyboard.cs:811
void PresentKeyboard(string startText)
Presents the default keyboard to the camera, with start text.
Definition: Keyboard.cs:401
void RepositionKeyboard(Transform objectTransform, BoxCollider aCollider=null, float verticalOffset=0.0f)
Function to reposition the Keyboard based on target transform and collider information ...
Definition: Keyboard.cs:448
void MoveCaretRight()
Move caret to the right.
Definition: Keyboard.cs:860
Miscellaneous utility methods.
Definition: Utils.cs:11
void PresentKeyboard(LayoutType keyboardType)
Presents a specific keyboard to the camera.
Definition: Keyboard.cs:412
LayoutType
Layout type enum for the type of keyboard layout to use. This is used when spawning to enable the cor...
Definition: Keyboard.cs:27
void Previous()
Send the "previous" event.
Definition: Keyboard.cs:749
void Backspace()
Delete the character before the caret.
Definition: Keyboard.cs:713
void OnDictationHypothesis(DictationEventData eventData)
Called when dictation hypothesis is found. Not used here
Definition: Keyboard.cs:319
override void OnDestroy()
Destroy unmanaged memory links.
Definition: Keyboard.cs:368
void OnDictationComplete(DictationEventData eventData)
Called when dictation is completed
Definition: Keyboard.cs:351
void Shift(bool newShiftState)
Set the keyboard to a single action sift state.
Definition: Keyboard.cs:796
A simple general use keyboard that is ideal for AR/VR applications.
Definition: Keyboard.cs:21
Function m_ButtonFunction
Designer specified functionality of a keyboard button.
void AppendValue(KeyboardValueKey valueKey)
Primary method for typing individual characters to a text field.
Definition: Keyboard.cs:583
static Transform GetChildRecursive(Transform t, string name)
walk hierarchy looking for named transform
Definition: Utils.cs:157
Interface to implement dictation events.
void Close()
Close the keyboard. (Clears all event subscriptions.)
Definition: Keyboard.cs:875
void EndDictation()
Terminate dictation mode.
Definition: Keyboard.cs:571
void OnDictationResult(DictationEventData eventData)
Called when dictation result is obtained
Definition: Keyboard.cs:327
void Tab()
Insert a tab character.
Definition: Keyboard.cs:831
void ShowSymbolKeyboard()
Enable the symbol keyboard.
Definition: Keyboard.cs:983
void PresentKeyboard()
Present the default keyboard to the camera.
Definition: Keyboard.cs:382
The purpose of this class is to provide a cached reference to the main camera. Calling Camera...
Definition: CameraCache.cs:12
void FunctionKey(KeyboardKeyFunc functionKey)
Trigger specific keyboard functionality.
Definition: Keyboard.cs:615
static Camera Main
Returns a cached reference to the main camera and uses Camera.main if it hasn&#39;t been cached yet...
Definition: CameraCache.cs:20
static IEnumerator StopRecording()
Ends the recording session.
void Enter()
Fire the text entered event for objects listening to keyboard. Immediately closes keyboard...
Definition: Keyboard.cs:766
override void Awake()
Deactivate on Awake.
Definition: Keyboard.cs:230
string DictationResult
String result of the current dictation.
void OnDictationError(DictationEventData eventData)
Called on dictation error. Not used here.
Definition: Keyboard.cs:361
void SetScaleSizeValues(float minScale, float maxScale, float minDistance, float maxDistance)
Method to set the sizes by code, as the properties are private. Useful for scaling &#39;from the outside&#39;...
Definition: Keyboard.cs:908
Represents a key on the keyboard that has a string value for input.
void MoveCaretLeft()
Move caret to the left.
Definition: Keyboard.cs:846
string ShiftValue
The shifted string value for this key.
static IEnumerator StartRecording(GameObject listener=null, float initialSilenceTimeout=5f, float autoSilenceTimeout=20f, int recordingTime=10)
Turns on the dictation recognizer and begins recording audio from the default microphone.
void Clear()
Clear the text input field.
Definition: Keyboard.cs:889
void Space()
Insert a space character.
Definition: Keyboard.cs:820
void ShowAlphaKeyboard()
Enable the alpha keyboard.
Definition: Keyboard.cs:921
Axis slider is a script to lock a bar across a specific axis.
Definition: AxisSlider.cs:11
Function
Possible functionality for a button.
void PresentKeyboard(string startText, LayoutType keyboardType)
Presents a specific keyboard to the camera, with start text.
Definition: Keyboard.cs:423
Singleton behaviour class, used for components that should only have one instance.
Definition: Singleton.cs:14