A game about forced loneliness, made by TACStudios
1// This example demonstrates how to display text in the UI that involves action bindings.
2// When the player switches control schemes or customizes controls (the latter is not set up
3// in this example but if supported, would work with the existing code as is), text that
4// is shown to the user may be affected.
5//
6// In the example, the player is able to move around the world and look at objects (simple
7// cubes). When an object is in sight, the player can pick the object with a button. While
8// having an object picked up, the player can then either throw the object or drop it back
9// on the ground.
10//
11// Depending on the current context, we display hints in the UI that reflect the currently
12// active bindings.
13
14using UnityEngine.UI;
15
16namespace UnityEngine.InputSystem.Samples.InGameHints
17{
18 public class InGameHintsExample : MonoBehaviour
19 {
20 public Text helpText;
21 public float moveSpeed;
22 public float rotateSpeed;
23 public float throwForce;
24 public float pickupDistance;
25 public float holdDistance;
26
27 private Vector2 m_Rotation;
28
29 private enum State
30 {
31 Wandering,
32 ObjectInSights,
33 ObjectPickedUp
34 }
35
36 private PlayerInput m_PlayerInput;
37 private State m_CurrentState;
38 private Transform m_CurrentObject;
39 private MaterialPropertyBlock m_PropertyBlock;
40
41 // Cached help texts so that we don't generate garbage all the time. Could even cache them by control
42 // scheme to not create garbage during control scheme switching but we consider control scheme switches
43 // rare so not worth the extra cost in complexity and memory.
44 private string m_LookAtObjectHelpText;
45 private string m_ThrowObjectHelpText;
46
47 private const string kDefaultHelpTextFormat = "Move close to one of the cubes and look at it to pick up";
48 private const string kLookAtObjectHelpTextFormat = "Press {pickup} to pick object up";
49 private const string kThrowObjectHelpTextFormat = "Press {throw} to throw object; press {drop} to drop object";
50
51 public void Awake()
52 {
53 m_PlayerInput = GetComponent<PlayerInput>();
54 }
55
56 public void OnEnable()
57 {
58 ChangeState(State.Wandering);
59 }
60
61 // This is invoked by PlayerInput when the controls on the player change. If the player switches control
62 // schemes or keyboard layouts, we end up here and re-generate our hints.
63 public void OnControlsChanged()
64 {
65 UpdateUIHints(regenerate: true); // Force re-generation of our cached text strings to pick up new bindings.
66 }
67
68 private int m_UpdateCount;
69
70 public void Update()
71 {
72 var move = m_PlayerInput.actions["move"].ReadValue<Vector2>();
73 var look = m_PlayerInput.actions["look"].ReadValue<Vector2>();
74
75 Move(move);
76 Look(look);
77
78 switch (m_CurrentState)
79 {
80 case State.Wandering:
81 case State.ObjectInSights:
82 // While looking around for an object to pick up, we constantly raycast into the world.
83 if (Physics.Raycast(transform.position, transform.forward, out var hitInfo,
84 pickupDistance) && !hitInfo.collider.gameObject.isStatic)
85 {
86 if (m_CurrentState != State.ObjectInSights)
87 ChangeState(State.ObjectInSights);
88 m_CurrentObject = hitInfo.transform;
89
90 // Set a custom color override on the object by installing our property block.
91 if (m_PropertyBlock == null)
92 {
93 m_PropertyBlock = new MaterialPropertyBlock();
94 m_PropertyBlock.SetColor("_Color", new Color(0.75f, 0, 0));
95 }
96 m_CurrentObject.GetComponent<MeshRenderer>().SetPropertyBlock(m_PropertyBlock);
97 }
98 else if (m_CurrentState != State.Wandering)
99 {
100 // No longer have object in sight.
101 ChangeState(State.Wandering);
102
103 if (m_CurrentObject != null)
104 {
105 // Clear property block on renderer to get rid of our custom color override.
106 m_CurrentObject.GetComponent<Renderer>().SetPropertyBlock(null);
107 m_CurrentObject = null;
108 }
109 }
110
111 if (m_PlayerInput.actions["pickup"].triggered && m_CurrentObject != null)
112 {
113 PickUp();
114 ChangeState(State.ObjectPickedUp);
115 }
116 break;
117
118 case State.ObjectPickedUp:
119 // If the player hits the throw button, throw the currently carried object.
120 // For this example, let's call this good enough. In a real game, we'd want to avoid the raycast
121 if (m_PlayerInput.actions["throw"].triggered)
122 {
123 Throw();
124 ChangeState(State.Wandering);
125 }
126 else if (m_PlayerInput.actions["drop"].triggered)
127 {
128 Throw(drop: true);
129 ChangeState(State.Wandering);
130 }
131 break;
132 }
133 }
134
135 private void ChangeState(State newState)
136 {
137 switch (newState)
138 {
139 case State.Wandering:
140 break;
141 case State.ObjectInSights:
142 break;
143 case State.ObjectPickedUp:
144 break;
145 }
146
147 m_CurrentState = newState;
148 UpdateUIHints();
149 }
150
151 private void UpdateUIHints(bool regenerate = false)
152 {
153 if (regenerate)
154 {
155 m_ThrowObjectHelpText = default;
156 m_LookAtObjectHelpText = default;
157 }
158
159 switch (m_CurrentState)
160 {
161 case State.ObjectInSights:
162 if (m_LookAtObjectHelpText == null)
163 m_LookAtObjectHelpText = kLookAtObjectHelpTextFormat.Replace("{pickup}",
164 m_PlayerInput.actions["pickup"].GetBindingDisplayString());
165 helpText.text = m_LookAtObjectHelpText;
166 break;
167
168 case State.ObjectPickedUp:
169 if (m_ThrowObjectHelpText == null)
170 m_ThrowObjectHelpText = kThrowObjectHelpTextFormat
171 .Replace("{throw}", m_PlayerInput.actions["throw"].GetBindingDisplayString())
172 .Replace("{drop}", m_PlayerInput.actions["drop"].GetBindingDisplayString());
173 helpText.text = m_ThrowObjectHelpText;
174 break;
175
176 default:
177 helpText.text = kDefaultHelpTextFormat;
178 break;
179 }
180 }
181
182 // Throw or drop currently picked up object.
183 private void Throw(bool drop = false)
184 {
185 // Unmount it.
186 m_CurrentObject.parent = null;
187
188 // Turn physics back on.
189 var rigidBody = m_CurrentObject.GetComponent<Rigidbody>();
190 rigidBody.isKinematic = false;
191
192 // Apply force.
193 if (!drop)
194 rigidBody.AddForce(transform.forward * throwForce, ForceMode.Impulse);
195
196 m_CurrentObject = null;
197 }
198
199 private void PickUp()
200 {
201 // Mount to our transform.
202 m_CurrentObject.position = default;
203 m_CurrentObject.SetParent(transform, worldPositionStays: false);
204 m_CurrentObject.localPosition += new Vector3(0, 0, holdDistance);
205
206 // Remove color override.
207 m_CurrentObject.GetComponent<Renderer>().SetPropertyBlock(null);
208
209 // We don't want the object to be governed by physics while we hold it so turn it into a
210 // kinematics body.
211 m_CurrentObject.GetComponent<Rigidbody>().isKinematic = true;
212 }
213
214 private void Move(Vector2 direction)
215 {
216 if (direction.sqrMagnitude < 0.01)
217 return;
218 var scaledMoveSpeed = moveSpeed * Time.deltaTime;
219 // For simplicity's sake, we just keep movement in a single plane here. Rotate
220 // direction according to world Y rotation of player.
221 var move = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(direction.x, 0, direction.y);
222 transform.position += move * scaledMoveSpeed;
223 }
224
225 private void Look(Vector2 rotate)
226 {
227 if (rotate.sqrMagnitude < 0.01)
228 return;
229 var scaledRotateSpeed = rotateSpeed * Time.deltaTime;
230 m_Rotation.y += rotate.x * scaledRotateSpeed;
231 m_Rotation.x = Mathf.Clamp(m_Rotation.x - rotate.y * scaledRotateSpeed, -89, 89);
232 transform.localEulerAngles = m_Rotation;
233 }
234 }
235}