Unity SDK Docs 1.5.0-beta.6
Loading...
Searching...
No Matches
TiltFiveManager2.cs
1/*
2 * Copyright (C) 2020-2023 Tilt Five, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16using System.Collections.Generic;
17using UnityEngine;
18
19#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
20using UnityEngine.InputSystem;
21using UnityEngine.InputSystem.Users;
22#endif
23
24using TiltFive;
26
27using Obsolete = System.ObsoleteAttribute;
28
29namespace TiltFive
30{
31
35 [DisallowMultipleComponent]
36#if !UNITY_2019_1_OR_NEWER || !INPUTSYSTEM_AVAILABLE
37 // Workaround to enable inputs to be collected before other scripts execute their Update() functions.
38 // This is unnecessary if we're using the Input System's OnBeforeUpdate() to collect fresh inputs.
39 [DefaultExecutionOrder(-500)]
40#else
41 // If the Input System's OnBeforeUpdate is available, set TiltFiveManager's execution order to be very late.
42 // This is desirable in two similar scenarios:
43 // - Our Update() executes last, providing the freshest pose data possible to any scripts using LateUpdate().
44 // - Our LateUpdate() executes last, providing the freshest pose data possible before we render to the glasses.
45 [DefaultExecutionOrder(500)]
46#endif
47 public class TiltFiveManager2 : TiltFive.SingletonComponent<TiltFiveManager2>, ISceneInfo
48 {
53 {
54 get
55 {
56 if(allPlayerSettings[0] == null)
57 {
58 allPlayerSettings[0] = new PlayerSettings() { PlayerIndex = PlayerIndex.One };
59 }
60 return allPlayerSettings[0];
61 }
62 }
63
68 {
69 get
70 {
71 if (allPlayerSettings[1] == null)
72 {
73 allPlayerSettings[1] = new PlayerSettings() { PlayerIndex = PlayerIndex.Two };
74 }
75 return allPlayerSettings[1];
76 }
77 }
78
83 {
84 get
85 {
86 if (allPlayerSettings[2] == null)
87 {
88 allPlayerSettings[2] = new PlayerSettings() { PlayerIndex = PlayerIndex.Three };
89 }
90 return allPlayerSettings[2];
91 }
92 }
93
98 {
99 get
100 {
101 if (allPlayerSettings[3] == null)
102 {
103 allPlayerSettings[3] = new PlayerSettings() { PlayerIndex = PlayerIndex.Four };
104 }
105 return allPlayerSettings[3];
106 }
107 }
108
109 public PlayerSettings[] allPlayerSettings = new PlayerSettings[PlayerSettings.MAX_SUPPORTED_PLAYERS];
110
111 public uint supportedPlayerCount = 3;
112
117
122
127
128#if UNITY_EDITOR
132 public EditorSettings2 editorSettings = new EditorSettings2();
133 public PlayerIndex selectedPlayer => editorSettings.selectedPlayer;
134
135 private HashSet<GameBoard> renderedGameboards = new HashSet<GameBoard>();
136#endif
137
138 private bool needsDriverUpdateNotifiedOnce = false;
139 private bool needsDriverUpdateErroredOnce = false;
140
141 private static bool upgradeInProgress = false;
142
143#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
149 private int[] playerIndexMapping = {0,1,2,3};
150#endif
151
155 protected override void Awake()
156 {
157 base.Awake();
158
159 // Apply log settings
161 Log.TAG = logSettings.TAG;
162
163 // Store graphics settings
164 graphicsSettings.applicationTargetFramerate = Application.targetFrameRate;
165 graphicsSettings.applicationVSyncCount = QualitySettings.vSyncCount;
166
167 if (!SystemControl.SetPlatformContext())
168 {
169 Log.Warn("Failed to set application context.");
170 enabled = false;
171 }
172
173 if (!SystemControl.SetApplicationInfo())
174 {
175 Log.Warn("Failed to send application info to the T5 Service.");
176 enabled = false;
177 }
178
179 // Initialize the player settings if necessary
180 for (int i = 0; i < allPlayerSettings.Length; i++)
181 {
182 var currentPlayerSettings = allPlayerSettings[i];
183 if(currentPlayerSettings == null)
184 {
185 allPlayerSettings[i] = new PlayerSettings() { PlayerIndex = (PlayerIndex) i + 1 };
186 }
187 }
188 }
189
190
191#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
195 private void OnBeforeUpdate()
196 {
197#if UNITY_EDITOR
198 if (UnityEditor.EditorApplication.isPaused)
199 {
200 return;
201 }
202#endif
203 if (Player.scanningForPlayers)
204 {
205 return;
206 }
207
209 Player.ScanForNewPlayers(); // Should only be executed once per frame
210 Wand.GetLatestInputs(); // Should only be executed once per frame
211
212 // OnBeforeUpdate can get called multiple times per frame. Unity seems to not properly utilize the camera positions for rendering
213 // if they are updated after Late Update and before render, causing a disparity between render pose and camera position leading to
214 // shaky displays in the headset. To avoid this, we prevent updating the camera positions during the BeforeRender Input State.
215 if (UnityEngine.InputSystem.LowLevel.InputState.currentUpdateType != UnityEngine.InputSystem.LowLevel.InputUpdateType.BeforeRender)
216 {
217 Update();
218 }
219 }
220#endif
221
225 void Update()
226 {
227#if !UNITY_2019_1_OR_NEWER || !INPUTSYSTEM_AVAILABLE
229 Player.ScanForNewPlayers(); // Should only be executed once per frame
230 Wand.GetLatestInputs(); // Should only be executed once per frame
231#endif
232 RefreshSpectatorSettings();
233 Display.ApplyGraphicsSettings(graphicsSettings);
234
235 for (int i = 0; i < supportedPlayerCount; i++)
236 {
237 var playerSettings = allPlayerSettings[i];
238 if (playerSettings != null)
239 {
240 Player.Update(playerSettings, spectatorSettings);
241 }
242 }
243
244 var spectatedPlayer = spectatorSettings.spectatedPlayer;
245 if (Glasses.TryGetPreviewPose(spectatedPlayer, out var spectatedPlayerPose))
246 {
247 spectatorSettings.spectatorCamera?.transform.SetPositionAndRotation(
248 spectatedPlayerPose.position,
249 spectatedPlayerPose.rotation);
250 }
251
252#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
253 var devices = InputUser.GetUnpairedInputDevices();
254 if (devices.Count > 0)
255 {
256 foreach (InputDevice dev in devices)
257 {
258 if (dev is WandDevice)
259 {
260 var headPoseRoot = Glasses.GetPoseRoot(((WandDevice)dev).playerIndex);
261
262 if (headPoseRoot != null)
263 {
264 var playerInput = headPoseRoot.GetComponentInChildren<PlayerInput>();
265
266 if (playerInput != null && playerInput.user.valid)
267 {
268 Log.Warn($"Unpaired Wand Device [{((WandDevice)dev).ControllerIndex}] found and paired to Player [{((WandDevice)dev).playerIndex}].");
269 InputUser.PerformPairingWithDevice(dev, playerInput.user);
270 playerInput.user.ActivateControlScheme("XR");
271 }
272 }
273 }
274 }
275 }
276#endif
277 }
278
279
283 void LateUpdate()
284 {
285 // Trackables should be updated just before rendering occurs,
286 // after all Update() calls are completed.
287 // This allows any Game Board movements to be finished before we base the
288 // Glasses/Wand poses off of its pose, preventing perceived jittering.
289 for (int i = 0; i < supportedPlayerCount; i++)
290 {
291 var playerSettings = allPlayerSettings[i];
292 if (playerSettings != null)
293 {
294 Player.Update(playerSettings, spectatorSettings);
295 }
296 }
297 }
298
310 public bool NeedsDriverUpdate()
311 {
312 if (!needsDriverUpdateErroredOnce)
313 {
314 try
315 {
316 ServiceCompatibility compatibility = SystemControl.GetServiceCompatibility();
317 bool needsUpdate = compatibility == ServiceCompatibility.Incompatible;
318
319 if (needsUpdate)
320 {
321 if (!needsDriverUpdateNotifiedOnce)
322 {
323 Log.Warn("Incompatible Tilt Five service. Please update driver package.");
324 needsDriverUpdateNotifiedOnce = true;
325 }
326 }
327 else
328 {
329 // Not incompatible. Reset the incompatibility warning.
330 needsDriverUpdateNotifiedOnce = false;
331 }
332 return needsUpdate;
333 }
334 catch (System.DllNotFoundException e)
335 {
336 Log.Info(
337 "Could not connect to Tilt Five plugin for compatibility check: {0}",
338 e.Message);
339 needsDriverUpdateErroredOnce = true;
340 }
341 catch (System.Exception e)
342 {
343 Log.Error(e.Message);
344 needsDriverUpdateErroredOnce = true;
345 }
346 }
347
348 // Failed to communicate with Tilt Five plugin at some point, so don't know whether
349 // an update is needed or not. Just say no.
350 return false;
351 }
352
359 public bool TryGetPlayerSettings(PlayerIndex playerIndex, out PlayerSettings playerSettings)
360 {
361 switch (playerIndex)
362 {
363 case PlayerIndex.One:
364 playerSettings = playerOneSettings;
365 return true;
366 case PlayerIndex.Two:
367 playerSettings = playerTwoSettings;
368 return true;
369 case PlayerIndex.Three:
370 playerSettings = playerThreeSettings;
371 return true;
372 case PlayerIndex.Four:
373 playerSettings = playerFourSettings;
374 return true;
375 default:
376 playerSettings = null;
377 return false;
378 }
379 }
380
381#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
382 internal void RefreshInputDevicePairings()
383 {
384 foreach (WandDevice wand in Input.wandDevices)
385 {
386 PlayerInput playerInput = null;
387 if (wand != null)
388 {
389 playerInput = PlayerInput.GetPlayerByIndex(playerIndexMapping[(int)wand.playerIndex - 1]);
390 if (playerInput != null)
391 {
392 InputUser.PerformPairingWithDevice(wand, playerInput.user);
393 }
394 }
395 }
396 foreach (GlassesDevice glasses in Input.glassesDevices)
397 {
398 PlayerInput playerInput = null;
399 if(glasses != null)
400 {
401 playerInput = PlayerInput.GetPlayerByIndex(playerIndexMapping[(int)glasses.PlayerIndex - 1]);
402 if (playerInput != null)
403 {
404 InputUser.PerformPairingWithDevice(glasses, playerInput.user);
405 }
406 }
407 }
408 }
409
410 internal void ReassignPlayerIndexMapping(int[] mapping)
411 {
412 if(mapping.Length != 4)
413 {
414 throw new System.ArgumentException("Invalid player index mapping argument - mapping should be 4 values long");
415 }
416 for (var i = 0; i < mapping.Length; i++)
417 {
418 if(mapping[i] < 0)
419 {
420 throw new System.ArgumentException("Invalid player index mapping argument - mapping should contain positive values only");
421 }
422 for (var j = 0; j < i; j++)
423 {
424 if (mapping[i] == mapping[j])
425 {
426 throw new System.ArgumentException("Invalid player index mapping argument - mapping should contain no duplicates");
427 }
428 }
429 }
430 playerIndexMapping = mapping;
431 RefreshInputDevicePairings();
432 }
433#endif //UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
434
435
436
440 private void OnEnable()
441 {
442 try
443 {
444 // TODO: change this to something in the settings drawer once that exists
445 NativePlugin.SetMaxDesiredGlasses((byte)GlassesSettings.MAX_SUPPORTED_GLASSES_COUNT);
446 }
447 catch (System.DllNotFoundException e)
448 {
449 Log.Info(
450 "Could not connect to Tilt Five plugin for setting max glasses: {0}",
451 e.Message);
452 }
453 catch (System.Exception e)
454 {
455 Log.Error(e.Message);
456 }
457
458 // Initialize the player settings if necessary
459 for (int i = 0; i < allPlayerSettings.Length; i++)
460 {
461 var currentPlayerSettings = allPlayerSettings[i];
462 if (currentPlayerSettings == null)
463 {
464 allPlayerSettings[i] = new PlayerSettings() { PlayerIndex = (PlayerIndex)i + 1 };
465 }
466 }
467
468 for (int i = 0; i < supportedPlayerCount; i++)
469 {
470 var playerSettings = allPlayerSettings[i];
471 if (playerSettings != null)
472 {
473 Player.Reset(playerSettings, spectatorSettings);
474 }
475 }
476
477#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
478 InputSystem.onBeforeUpdate += OnBeforeUpdate;
479#endif
480 }
481
482 private void OnDisable()
483 {
484#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
485 InputSystem.onBeforeUpdate -= OnBeforeUpdate;
486 //Input.OnDisable();
487#endif
488 Player.OnDisable();
489 }
490
491 private void OnDestroy()
492 {
493 Player.OnDisable();
494 }
495
496 private void OnApplicationQuit()
497 {
498 OnDisable();
499 }
500
501 // There's a longstanding bug where UnityPluginUnload isn't called.
502 // - https://forum.unity.com/threads/unitypluginunload-never-called.414066/
503 // - https://gamedev.stackexchange.com/questions/200118/unity-native-plugin-unitypluginload-is-called-but-unitypluginunload-is-not
504 // - https://issuetracker.unity3d.com/issues/unitypluginunload-is-never-called-in-a-standalone-build
505 // Work around this by invoking it via Application.quitting.
506 private static void Quit()
507 {
508 try
509 {
510 NativePlugin.UnloadWorkaround();
511 }
512 catch (System.DllNotFoundException)
513 {
514 // nothing to report on quit if the plugin isn't present
515 }
516 catch (System.Exception e)
517 {
518 Log.Error(e.Message);
519 }
520 }
521
522 [RuntimeInitializeOnLoadMethod]
523 private static void RunOnStart()
524 {
525 Application.quitting += Quit;
526 }
527
528 private void RefreshSpectatorSettings()
529 {
530 // Warn developers if they've left the spectatorCamera field empty
531 // TiltFiveManager2's custom inspector should already warn them in the editor, but this warns them again at runtime.
532 if (spectatorSettings.spectatorCamera == null)
533 {
534 Log.Warn("No spectator camera detected in TiltFiveManager2's spectator settings. A spectator camera is required.");
535 }
536
537 // Make sure that the spectated player isn't set to a player index higher than what TiltFiveManager2 supports
538 var highestSupportedPlayer = (PlayerIndex)supportedPlayerCount;
539 if (spectatorSettings.spectatedPlayer > highestSupportedPlayer)
540 {
541 Log.Warn($"Invalid spectatorSettings.spectatedPlayer [{spectatorSettings.spectatedPlayer}]. TiltFiveManager2 currently only supports up to Player {highestSupportedPlayer}.");
542 spectatorSettings.spectatedPlayer = highestSupportedPlayer;
543 }
544 }
545
546#if UNITY_EDITOR
547
551 void OnValidate()
552 {
553 // Don't do any validation if we're in the middle of copying settings.
554 if(upgradeInProgress)
555 {
556 return;
557 }
558
559 Log.LogLevel = logSettings.level;
560 Log.TAG = logSettings.TAG;
561
562 playerOneSettings.PlayerIndex = PlayerIndex.One;
563 playerTwoSettings.PlayerIndex = PlayerIndex.Two;
564 playerThreeSettings.PlayerIndex = PlayerIndex.Three;
565 playerFourSettings.PlayerIndex = PlayerIndex.Four;
566
567 supportedPlayerCount = (uint) Mathf.Clamp(supportedPlayerCount, 1, PlayerSettings.MAX_SUPPORTED_PLAYERS);
568
569 for (int i = 0; i < allPlayerSettings.Length; i++)
570 {
571 var playerSettings = allPlayerSettings[i];
572 if (playerSettings != null)
573 {
574 Player.Validate(playerSettings);
575 playerSettings.glassesSettings.glassesMirrorMode = spectatorSettings.glassesMirrorMode;
576 }
577 }
578 RefreshSpectatorSettings();
579 }
580
584 void OnDrawGizmos()
585 {
586 if (!enabled)
587 {
588 return;
589 }
590
591 renderedGameboards.Clear();
592
593 for (int i = 0; i < supportedPlayerCount; i++)
594 {
595 var playerSettings = allPlayerSettings[i];
596 if (playerSettings != null)
597 {
598 var currentGameboard = playerSettings.gameboardSettings.currentGameBoard;
599 if (!renderedGameboards.Contains(currentGameboard))
600 {
601 renderedGameboards.Add(currentGameboard);
602 Player.DrawGizmos(playerSettings);
603 }
604 }
605 }
606 }
607
608 public static void CreateFromTiltFiveManager(TiltFiveManager tiltFiveManager)
609 {
610 var parentGameObject = tiltFiveManager.gameObject;
611
612 // Ideally, we only want one TiltFiveManager2 in the scene.
613 // If the developer clicks the upgrade button repeatedly, we don't want to keep creating more of them.
614 // In this scenario, ask the developer whether they'd like to overwrite the settings on the existing
615 // TiltFiveManager2 with the TiltFiveManager's settings.
616 var isTiltFiveManager2AlreadyPresent = parentGameObject.TryGetComponent<TiltFiveManager2>(out var existingTiltFiveManager2);
617 var confirmationDialogTitle = "Existing TiltFiveManager2 detected";
618 var confirmationDialogText = $"The GameObject \"{parentGameObject.name}\" already has a TiltFiveManager2 component." +
619 System.Environment.NewLine + System.Environment.NewLine +
620 "Overwrite the existing TiltFiveManager2 component values?" +
621 System.Environment.NewLine + System.Environment.NewLine +
622 "Warning: This cannot be undone via Edit > Undo (Ctrl+Z)";
623 var confirmButtonLabel = "Overwrite";
624 var cancelButtonLabel = "Cancel";
625 var overwriteExistingTiltFiveManager2 = isTiltFiveManager2AlreadyPresent
626 && UnityEditor.EditorUtility.DisplayDialog(confirmationDialogTitle, confirmationDialogText, confirmButtonLabel, cancelButtonLabel);
627
628 if(isTiltFiveManager2AlreadyPresent && !overwriteExistingTiltFiveManager2)
629 {
630 Debug.Log($"Aborted attempt to upgrade TiltFiveManager.");
631 return;
632 }
633
634 upgradeInProgress = true;
635
636 TiltFiveManager2 tiltFiveManager2 = overwriteExistingTiltFiveManager2
637 ? existingTiltFiveManager2
638 : parentGameObject.AddComponent<TiltFiveManager2>();
639
640 // Disable the old TiltFiveManager.
641 tiltFiveManager.enabled = false;
642
643 // Default to supporting a single player, just like TiltFiveManager did.
644 tiltFiveManager2.supportedPlayerCount = 1;
645
646 // Copy the various settings objects from TiltFiveManager to playerOneSettings.
647 tiltFiveManager2.playerOneSettings.glassesSettings = tiltFiveManager.glassesSettings.Copy();
648
649 tiltFiveManager2.playerOneSettings.scaleSettings = tiltFiveManager.scaleSettings.Copy();
650
651 tiltFiveManager2.playerOneSettings.gameboardSettings = tiltFiveManager.gameBoardSettings.Copy();
652
653 tiltFiveManager2.playerOneSettings.leftWandSettings = tiltFiveManager.leftWandSettings.Copy();
654 tiltFiveManager2.playerOneSettings.rightWandSettings = tiltFiveManager.rightWandSettings.Copy();
655
656 // Emulate TiltFiveManager, which used a single camera internally for eye camera cloning and onscreen previews
657 tiltFiveManager2.spectatorSettings.spectatorCamera = tiltFiveManager.glassesSettings.cameraTemplate;
658
659 // Copy TiltFiveManager's GlassesSettings' mirror mode, which has moved to SpectatorSettings for TiltFiveManager2
660 tiltFiveManager2.spectatorSettings.glassesMirrorMode = tiltFiveManager.glassesSettings.glassesMirrorMode;
661
662 // For the sake of thoroughness, let's copy the old log settings, too.
663 tiltFiveManager2.logSettings = tiltFiveManager.logSettings.Copy();
664
665 upgradeInProgress = false;
666
667 var resultText = overwriteExistingTiltFiveManager2
668 ? $"Successfully overwrote component values on the existing TiltFiveManager2 component attached to \"{parentGameObject.name}\" using the old TiltFiveManager component values."
669 : $"Successfully attached a new TiltFiveManager2 component to \"{parentGameObject.name}\" and imported the old TiltFiveManager component values.";
670 Debug.Log($"{resultText}{System.Environment.NewLine}The old TiltFiveManager has been disabled - it can safely be removed.");
671 }
672
673#endif
674
675 #region ISceneInfo Implementation
676
677 [Obsolete("TiltFiveManager2.GetScaleToUWRLD_UGBD is deprecated. Please use TiltFiveManager2.GetScaleToWorldSpaceFromGameboardSpace instead.")]
678 public float GetScaleToUWRLD_UGBD() { return GetScaleToWorldSpaceFromGameboardSpace(); }
679
680 public float GetScaleToWorldSpaceFromGameboardSpace()
681 {
682 return playerOneSettings.scaleSettings.GetScaleToWorldSpaceFromGameboardSpace(playerOneSettings.gameboardSettings.gameBoardScale);
683 }
684
685 public Pose GetGameboardPose()
686 {
687 return new Pose(playerOneSettings.gameboardSettings.gameBoardCenter, Quaternion.Euler(playerOneSettings.gameboardSettings.gameBoardRotation));
688 }
689
690 public Camera GetEyeCamera()
691 {
692 return Glasses.GetLeftEye(PlayerIndex.One);
693 }
694
695 public uint GetSupportedPlayerCount()
696 {
697 return supportedPlayerCount;
698 }
699
700 public bool IsActiveAndEnabled()
701 {
702 return isActiveAndEnabled;
703 }
704
705 #endregion ISceneInfo Implementation
706 }
707
708}
Camera cameraTemplate
The camera used as a template for creating the eye cameras at runtime.
GraphicsSettings encapsulates configuration data related to the project's graphics settings,...
The Logger.
Definition Log.cs:42
static void Warn(string m, params object[] list)
WARN logging function call.
Definition Log.cs:166
static void Info(string m, params object[] list)
INFO logging function call.
Definition Log.cs:140
static void Error(string m, params object[] list)
ERROR logging function call.
Definition Log.cs:127
static int LogLevel
Gets or sets the logging level.
Definition Log.cs:68
static string TAG
Gets or sets the logging tag.
Definition Log.cs:58
Provides access to player settings and functionality.
Definition Player.cs:31
The Tilt Five manager.
LogSettings logSettings
The log settings.
bool NeedsDriverUpdate()
Check if a driver update is needed.
SpectatorSettings spectatorSettings
The spectator camera's runtime configuration data.
override void Awake()
Awake this instance.
GraphicsSettings graphicsSettings
Project-wide graphics settings related to Tilt Five.
PlayerSettings playerFourSettings
The fourth player's runtime configuration data.
PlayerSettings playerOneSettings
The first player's runtime configuration data.
bool TryGetPlayerSettings(PlayerIndex playerIndex, out PlayerSettings playerSettings)
Gets the player settings for the specified player.
PlayerSettings playerTwoSettings
The second player's runtime configuration data.
PlayerSettings playerThreeSettings
The third player's runtime configuration data.
LogSettings logSettings
The log settings.
GameBoardSettings gameBoardSettings
The game board runtime configuration data.
WandSettings rightWandSettings
The wand runtime configuration data for the right hand wand.
ScaleSettings scaleSettings
The scale conversion runtime configuration data.
GlassesSettings glassesSettings
The glasses runtime configuration data.
WandSettings leftWandSettings
The wand runtime configuration data for the left hand wand.
ServiceCompatibility
Whether the running service is compatible.
PlayerIndex
The Player index (e.g. Player One, Player Two, etc)