// TODO Track updates of individual config values to improve rendering performance public ConfigDescription(ITypedDataCursor <ParachuteConfig> config, Vector3 pilotTorsoScale) { Radius = new InputRange( CreateRange(-8f, 8f, config.To(c => c.RadiusVertical)), CreateRange(1f, 16f, config.To(c => c.RadiusHorizontal)) ); Volume = new InputRange( CreateRange(1f, 16f, config.To(c => c.Span)), zAxisDescription: CreateRange(0.5f, 5, config.To(c => c.Chord)) ); RiggingAngle = new InputRange( CreateRange(0, 30, config.To(c => c.RiggingAngle)) ); HeightOffset = new InputRange( yAxisDescription: CreateRange(0f, 16f, config.To(c => c.HeightOffset)) ); var rigAttachCursor = config.To(c => c.RigAttachPos); RigAttachPosition = new InputRange( RigAttachComponent(0f, 1f, rigAttachCursor.To(DefaultPath.Vector3X), pilotTorsoScale.x), RigAttachComponent(0f, 1f, rigAttachCursor.To(DefaultPath.Vector3Y), pilotTorsoScale.y), RigAttachComponent(-1f, 1f, rigAttachCursor.To(DefaultPath.Vector3Z), pilotTorsoScale.z) ); PilotWeight = new InputRange( CreateRange(40f, 200f, config.To(c => c.PilotWeight)) ); WeightShiftMagnitude = new InputRange( CreateRange(0.1f, 0.5f, config.To(c => c.WeightshiftMagnitude)) ); }
/// <summary> /// </summary> /// <param name="cursor"> /// <code> /// {"items": GameObject list, /// "isInteractable": bool} /// </code> /// </param> /// <param name="indexCursor"><code>int?</code></param> public static IDisposable Initialize(ITypedDataCursor <UIListState> cursor) { var isInteractable = cursor.To(s => s.IsInteractable); var highlightedIndex = cursor.To(s => s.HighlightedIndex); return(AddMouseHighlight( updateHighlightedIndex: index => { if (isInteractable.Get() && index.HasValue) { highlightedIndex.Set(index.Value); } }, items: cursor.To(s => s.Items).Get())); }
private static void Update(ITypedDataCursor <UIListState> cursor) { var items = cursor.To(s => s.Items).Get(); for (int i = 0; i < items.Count; i++) { var item = items[i]; var highlightable = item.GetComponentOfInterface <IHighlightable>(); highlightable.UnHighlight(); } var highlightedIndex = cursor.To(s => s.HighlightedIndex).Get(); items[highlightedIndex].GetComponentOfInterface <IHighlightable>().Highlight(); }
IEnumerator <WaitCommand> OnEnter() { _data.CameraManager.SwitchMount(_data.CameraMount.GetComponent <ICameraMount>()); var spawnpointBillboards = _uiState.To(s => s.SpawnpointUIList).To(s => s.Items).Get(); for (int i = 0; i < _spawnpoints.Count; i++) { var spawnpoint = _spawnpoints[i]; spawnpointBillboards[i].SetActive(spawnpoint.IsDiscovered); } OnResume(); yield return(WaitCommand.WaitRoutine(CameraTransitions.FadeIn(_data.CameraManager.Rig.ScreenFader, _data.MenuClock, _data.FaderSettings))); }
public NavigableUIList(ITypedDataCursor <UIListState> state, Camera camera, Action <int> selectItem) { _isInteractable = state.To(s => s.IsInteractable); _selectItem = selectItem; _highlightedIndex = state.To(s => s.HighlightedIndex); MouseHighlight.Initialize(state); MouseInteraction.LeftMouseClick(state.To(s => s.Items).Get(), selectItem); _controllerHighlight = ControllerHighlight.CreateControllerIndexUpdater( cursor: state, itemSelector: (items, currentIndex, inputDirection) => { return(ControllerHighlight.MoveControllerIndex(camera.WorldToScreenPoint, items, currentIndex, inputDirection)); }); Highlighter.Initialize(state); }
/// <summary> /// </summary> /// <param name="cursor"> /// <code> /// {"items": GameObject list, /// "isInteractable": bool} /// </code> /// </param> /// <param name="indexCursor"><code>int?</code></param> public static Action <Vector2> CreateControllerIndexUpdater(ITypedDataCursor <UIListState> cursor, Func <IList <GameObject>, int, Vector2, int> itemSelector) { // Everytime there is new keyboard input, feed it // the currently highlighted controller index var items = cursor.To(s => s.Items); var isInteractable = cursor.To(s => s.IsInteractable); var highlightedIndex = cursor.To(s => s.HighlightedIndex); return(keyboardInput => { // Magnitude check for performance reasons if (keyboardInput.magnitude > 0 && isInteractable.Get()) { var currentIndex = highlightedIndex.Get(); highlightedIndex.Set(itemSelector(items.Get(), currentIndex, keyboardInput)); } }); }
public SpawnScreen(IStateMachine machine, Data data) : base(machine) { _data = data; var billboardDependencies = new DependencyContainer(new Dictionary <string, object> { { "clock", _data.MenuClock }, { "cameraTransform", _data.CameraMount.transform } }); _spawnpoints = GameObject.FindObjectOfType <SpawnpointDiscoverer>().Spawnpoints; var spawnpointBillboards = new List <GameObject>(_spawnpoints.Count); var billboardGroup = new GameObject("SpawnpointBillboards"); for (int i = 0; i < _spawnpoints.Count; i++) { var spawnpoint = _spawnpoints[i]; var spawnpointBillBoard = (GameObject)GameObject.Instantiate(_data.SpawnpointBillboardPrefab); spawnpointBillBoard.SetActive(spawnpoint.IsDiscovered); DependencyInjector.Default.Inject(spawnpointBillBoard, billboardDependencies); spawnpointBillBoard.SetParent(billboardGroup); spawnpointBillBoard.transform.Set(spawnpoint.Location.AsTransform); spawnpointBillBoard.SetActive(false); spawnpointBillboards.Add(spawnpointBillBoard); } _uiState = TypedDataCursor <SpawnScreenUIState> .Root(new SpawnScreenUIState( spawnpointUiList : new UIListState(spawnpointBillboards))); var spawnpointUiState = _uiState.To(s => s.SpawnpointUIList); var spawnpointIndexChanged = spawnpointUiState .To(s => s.HighlightedIndex) .OnUpdate .DistinctUntilChanged(IntComparer.Instance) .Skip(1); _data.SelectedSpawnpointName.text = _spawnpoints[spawnpointUiState.Get().HighlightedIndex].Name; spawnpointIndexChanged.Subscribe(i => { Fmod.PlayOneShot("event:/ui/drop_hover"); var spawnpoint = _spawnpoints[i]; _data.SelectedSpawnpointName.text = spawnpoint.Name; _data.CameraAnimator.LookTarget = spawnpointBillboards[i].transform; }); _uiList = new UISketch.NavigableUIList( spawnpointUiState, data.CameraManager.Rig.GetMainCamera(), spawnpointIndex => { _data.EventSystem.Emit(new Events.SpawnpointSelected(_spawnpoints[spawnpointIndex].Location)); }); }
public Editing(IStateMachine machine, Data data, ParachuteStorage parachuteStorage, ITypedDataCursor <ParachuteStorageViewState> storageState) : base(machine) { _editorParachuteChanges = new ReplaySubject <Parachute>(1); _data = data; _editorState = storageState.To(c => c.EditorState); _activeParachuteConfig = _editorState.To(editorState => editorState.Config); // Toggle editor camera off when a gizmo interaction starts to avoid feedback loops //_data._cameraRig.Initialize(); storageState.OnUpdate // Store parachute every time it is changed (by sampling or throttling to reduce disk I/O) // Store parachute every time the use selects a different parachute .Throttle(TimeSpan.FromSeconds(2), Scheduler.ThreadPool) .ObserveOn(UnityThreadScheduler.MainThread) .Select(state => { return(state.AvailableParachutes .Where(p => p.IsEditable) .Select(p => { var config = GameObject.Instantiate(p); var json = JsonUtility.ToJson(config, prettyPrint: true); return new { config, json }; }) .ToList()); }) // Copy object to prevent thread-unsafe editing .ObserveOn(Schedulers.FileWriterScheduler) .Subscribe(parachutes => { parachuteStorage.DeleteAllStoredParachutes(); for (int i = 0; i < parachutes.Count; i++) { var parachute = parachutes[i]; parachuteStorage.StoreParachute(parachute.config, parachute.json); } }); data.ParachuteSelectionView.Initialize(storageState, data.GameSettingsProvider.IsVrActive); data.ParachuteSelectionView.BackToFlight += TransitToFlyingState; }
public void Initialize(ITypedDataCursor <EditorState> editorState, IObservable <Parachute> editorParachute) { if (!_isInitialized) { var configCursor = editorState.To(c => c.Config); var parachuteConfigViewModel = new ParachuteConfigViewModel(configCursor); _parachuteConfigView.Initialize(parachuteConfigViewModel); RegisterUIHover(_additionalSettingsParent.AddComponent <DefaultHoverEventSource>()); var parachuteColor = configCursor.To(c => c.Color); _cellColorPicker.onValueChanged.AddListener(color => parachuteColor.Set(color)); RegisterUIHover(_cellColorPicker.gameObject.AddComponent <DefaultHoverEventSource>()); _selectedWidget = editorState.To(c => c.SelectedWidget); configDescription = new ConfigDescription(configCursor, _pilotTorsoScale); // TODO Use GUI camera? var mainCamera = _cameraManager.Rig.GetMainCamera(); _radiusGizmo.InputRange = configDescription.Radius; _pilotWeightGizmo.InputRange = configDescription.PilotWeight; _weightShiftMagnitudeGizmo.InputRange = configDescription.WeightShiftMagnitude; _heightOffsetWidget.InputRange = configDescription.HeightOffset; _rigAttachPosition.InputRange = configDescription.RigAttachPosition; _riggingAngleGizmo.InputRange = configDescription.RiggingAngle; _riggingAngleGizmo.Camera = mainCamera; RegisterDraggable(_radiusGizmo.gameObject, WidgetId.Radius); RegisterDraggable(_pilotWeightGizmo.gameObject, WidgetId.PilotWeight); RegisterDraggable(_heightOffsetWidget.gameObject, WidgetId.HeightOffset); RegisterDraggable(_weightShiftMagnitudeGizmo.gameObject, WidgetId.WeightShiftMagnitude); RegisterGizmoHandlers(_riggingAngleGizmo, WidgetId.RiggingAngle); editorState.To(c => c.SelectedWidget) .OnUpdate .Subscribe(selectedWidget => { // Render tooltip //_rigAttachPositionText.gameObject.SetActive(selectedWidget == WidgetId.RigAttachPosition); if (selectedWidget.HasValue) { var tooltip = Tooltips[selectedWidget.Value]; var description = tooltip.Description + "\n\n<i>" + tooltip.Effect + "</i>"; _tooltipView.SetState(tooltip.Name, description); _tooltipView.gameObject.SetActive(true); } else { _tooltipView.gameObject.SetActive(false); } }); var colliderSet = ColliderSet.Box("ParachuteUICellColliders", LayerMask.NameToLayer("UI")); editorState.To(c => c.IsEditing) .OnUpdate .Subscribe(isEditing => { colliderSet.Parent.SetActive(isEditing); _gizmosParent.SetActive(isEditing); _additionalSettingsParent.SetActive(isEditing); _cellColorPicker.gameObject.SetActive(isEditing); }); configCursor.OnUpdate .CombineLatest( _gameSettingsProvider.SettingChanges, _activeLanguage.TableUpdates, (config, settings, languageTable) => new { config, settings, languageTable }) .Subscribe(x => { _parachuteConfigView.SetState(x.languageTable.AsFunc); var props = ParachuteProperties.FromConfig(x.config); if (x.settings.Gameplay.UnitSystem == UnitSystem.Metric) { UpdateEditorState(x.config, props); } else { UpdateEditorState(x.config, props.ToImperial()); } }); colliderSet.Parent.SetParent(gameObject); var canopyDragHandler = colliderSet.Parent.AddComponent <SurfaceDragHandler>(); canopyDragHandler.Dragging += (camTransform, value) => { var currentValue = configDescription.Volume.Value; currentValue.x += value.x; currentValue.z -= value.y; configDescription.Volume.SetValue(currentValue); }; _canopyHighlight.Highlightable = canopyDragHandler; RegisterDraggable(colliderSet.Parent, WidgetId.Area); editorParachute.Subscribe(p => { _canopyHighlight.Renderer = p.CanopyMesh; _rigAttachPosition.transform.position = p.Pilot.Torso.transform.position; _rigAttachPosition.transform.rotation = p.Pilot.Torso.transform.rotation; colliderSet.SetSize(p.Sections.Count); for (var i = 0; i < p.Sections.Count; i++) { var cell = p.Sections[i].Cell; colliderSet.UpdateCollider(i, cell.Collider); var isLastCell = i == p.Sections.Count - 1; if (isLastCell) { var gizmoTransform = cell.transform.MakeImmutable() .TranslateLocally(0.8f) .UpdateScale(Vector3.one) .UpdateRotation(p.Root.rotation); _radiusGizmo.transform.Set(gizmoTransform); } } }); // _rigAttachPosition.OnPositionChanged += newPosition => { // configDescription.RigAttachPosition.SetValue(newPosition); // }; // _rigAttachPosition.ActiveAxes = ActiveAxis.X | ActiveAxis.Y | ActiveAxis.Z; RegisterDraggable(_rigAttachPosition.gameObject, WidgetId.RigAttachPosition); /* * What are the problems that we need to solve: * - Use a single source of truth to render the parachute * - Use a single source of truth to render the GUI * - The GUI widgets should not know more than just the value they need to update and * what the value is they need to render (use cursors into the app state to do this) * - Use unity as a rendering engine but not as a logic engine (possibly too hard to do this now) * * * * Gizmo: * * - Show scaling and rotation widgets at all times and * couple them to an InputRange. * - (optional) Draw box around selected thing * */ _isInitialized = true; } }
public void Initialize(ITypedDataCursor <ParachuteStorageViewState> storage, bool isVrActive) { _openParachuteFolder.gameObject.SetActive(!isVrActive); _addParachute.gameObject.SetActive(!isVrActive); _backToFlight.onClick.AddListener(() => { if (BackToFlight != null) { BackToFlight(); } }); var editorState = storage.To(s => s.EditorState); var availableChutes = storage.To(s => s.AvailableParachutes); var parachuteName = editorState.To(s => s.Config).To(c => c.Name); Action <string> updateParachuteName = value => { parachuteName.Set(value.Limit(_parachuteNameCharLimit, "")); }; _parachuteNameEditor.Limit = _parachuteNameCharLimit; _parachuteNameEditor.TextChanged += updateParachuteName; _parachuteThumbnails = new ParachuteThumbnail[_maxParachutes]; for (int i = 0; i < _maxParachutes; i++) { var parachuteThumbnail = GameObject.Instantiate(_parachuteThumbnailPrefab).GetComponent <ParachuteThumbnail>(); parachuteThumbnail.transform.SetParent(_thumbnailRoot.transform); parachuteThumbnail.transform.localPosition = Vector3.zero; parachuteThumbnail.transform.localRotation = Quaternion.identity; parachuteThumbnail.transform.localScale = Vector3.one; parachuteThumbnail.gameObject.SetActive(false); _parachuteThumbnails[i] = parachuteThumbnail; } var storageDir = storage.To(s => s.StorageDir); _openParachuteFolder.onClick.AddListener(() => { UnityFileBrowserUtil.Open(storageDir.Get()); }); Action <string> selectChute = parachuteId => { var state = storage.Get(); if (state.EditorState.Config.Id != parachuteId) { for (int i = 0; i < state.AvailableParachutes.Count; i++) { var parachute = state.AvailableParachutes[i]; if (parachute.Id == parachuteId) { editorState.Set(new ParachuteEditor.EditorState(parachute)); } } } }; Action <string> deleteChute = parachuteId => { var state = storage.Get(); int selectedParachuteIndex = 0; for (int i = state.AvailableParachutes.Count - 1; i >= 0; i--) { var parachute = state.AvailableParachutes[i]; if (parachute.Id == parachuteId) { state.AvailableParachutes.RemoveAt(i); selectedParachuteIndex = Math.Max(i - 1, 0); break; } } if (state.EditorState.Config.Id == parachuteId) { var defaultChute = state.AvailableParachutes[selectedParachuteIndex]; editorState.Set(new ParachuteEditor.EditorState(defaultChute)); } availableChutes.Set(state.AvailableParachutes); }; Action <string> cloneChute = parachuteId => { var state = storage.Get(); if (state.AvailableParachutes.Count < _maxParachutes) { ParachuteConfig existingParachute = null; for (int i = 0; i < state.AvailableParachutes.Count; i++) { var parachute = state.AvailableParachutes[i]; if (parachute.Id == parachuteId) { existingParachute = parachute; } } var newParachute = ParachuteConfig.CreateNew(existingParachute); newParachute.Name = (existingParachute.Name + " Copy").Limit(_parachuteNameCharLimit, ""); state.AvailableParachutes.Add(newParachute); availableChutes.Set(state.AvailableParachutes); selectChute(newParachute.Id); } }; var isEditingState = storage.To(s => s.EditorState).To(s => s.IsEditing); Action <string> editChute = parachuteId => { var state = storage.Get(); string currentlyEditingParachute = state.EditorState.IsEditing ? state.EditorState.Config.Id : null; if (currentlyEditingParachute == parachuteId) { // Switch to read-only mode isEditingState.Set(false); } else { // Enable editing // TODO Do selection and edit switching in one transformation selectChute(parachuteId); isEditingState.Set(true); } }; _addParachute.onClick.AddListener(() => { var state = storage.Get(); if (state.AvailableParachutes.Count < _maxParachutes) { var newParachute = ParachuteConfig.CreateNew(_parachuteConfigTemplate); newParachute.Name = "New " + _parachuteConfigTemplate.Name; state.AvailableParachutes.Add(newParachute); availableChutes.Set(state.AvailableParachutes); editorState.Set(new ParachuteEditor.EditorState(newParachute)); } }); storage.OnUpdate.Subscribe(state => { for (int i = 0; i < _parachuteThumbnails.Length; i++) { var thumbnail = _parachuteThumbnails[i]; thumbnail.OnSelect -= selectChute; thumbnail.OnDelete -= deleteChute; thumbnail.OnClone -= cloneChute; thumbnail.OnEdit -= editChute; } _parachuteNameEditor.IsEditable = state.EditorState.IsEditing; _parachuteNameEditor.Text = state.EditorState.Config.Name; for (int i = 0; i < _parachuteThumbnails.Length; i++) { var thumbnail = _parachuteThumbnails[i]; if (i < state.AvailableParachutes.Count) { var parachute = state.AvailableParachutes[i]; var isSelected = state.EditorState.Config.Id == parachute.Id; var parachuteControlState = new ParachuteThumbnail.ParachuteControlState( parachute.Id, parachute.Name, isEditorAvailable: !isVrActive, isSelected: state.EditorState.Config.Id == parachute.Id, isEditable: parachute.IsEditable, isDeletable: state.AvailableParachutes.Count > 1 && parachute.IsEditable, isEditing: state.EditorState.IsEditing && isSelected); thumbnail.SetState(parachuteControlState); if (parachuteControlState.IsDeletable) { thumbnail.OnDelete += deleteChute; } if (parachuteControlState.IsEditable) { thumbnail.OnEdit += editChute; } thumbnail.OnSelect += selectChute; thumbnail.OnClone += cloneChute; thumbnail.gameObject.SetActive(true); if (parachuteControlState.IsSelected) { FirstObject = thumbnail.gameObject; } } else { thumbnail.gameObject.SetActive(false); } } }); }