// 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)) ); }
private Range RigAttachComponent(float min, float max, ITypedDataCursor <float> cursor, float scale) { return(new Range( min * scale, max * scale, () => cursor.Get() * scale, value => cursor.Set(value / scale), NumberOfSteps)); }
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)); }); }
/// <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(); }
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 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 Playing(IStateMachine machine, Data data, FlyWingsuit.Data wingsuitData, ParachuteStates.Data parachuteData, SpectatorMode.Data spectatorData) : base(machine) { _data = data; _data.PlayerPilotSpawner.ActiveNetwork = data.ActiveNetwork; wingsuitData.ActiveNetwork = data.ActiveNetwork; wingsuitData.PlayerPilotSpawner.ActiveNetwork = data.ActiveNetwork; var parachuteConfigPath = Path.Combine(VoloAirsportFileStorage.StorageDir.Value, "ParachuteConfig_v" + ParachuteConfig.VersionNumber + ".json"); Debug.Log("parachute config path: " + parachuteConfigPath); var defaultParachuteStorage = new ParachuteStorage(ParachuteStorage.DefaultChutesDir.Value, parachuteData.InitialConfig, parachuteData.HardCodedAirfoilDefinition, isEditable: false); var parachuteStorage = new ParachuteStorage(ParachuteStorage.StorageDir.Value, parachuteData.InitialConfig, parachuteData.HardCodedAirfoilDefinition, isEditable: true); var allParachutes = defaultParachuteStorage.StoredChutes.Concat(parachuteStorage.StoredChutes).ToList(); var initialChute = ParachuteStorage.SelectParachute(allParachutes, _data.GameSettingsProvider.ActiveSettings.Other.SelectedParachuteId); var storageState = TypedDataCursor <ParachuteStorageViewState> .Root(new ParachuteStorageViewState(initialChute, allParachutes, ParachuteStorage.StorageDir.Value)); _activeParachuteConfig = storageState.To(s => s.EditorState).To(s => s.Config); _activeParachuteConfig.OnUpdate.Subscribe(selectedParachute => { var gameSettings = _data.GameSettingsProvider.ActiveSettings; if (gameSettings.Other.SelectedParachuteId != selectedParachute.Id) { gameSettings.Other.SelectedParachuteId = selectedParachute.Id; _data.GameSettingsProvider.UpdateGameSettings(gameSettings); } }); _playingStateMachine = BuildPlayingStateMachine(data.CoroutineScheduler, wingsuitData, parachuteData, parachuteStorage, storageState, spectatorData); _playingStateMachine.Transition(PlayingStates.Initial); }
private IStateMachine BuildPlayingStateMachine( ICoroutineScheduler scheduler, FlyWingsuit.Data wingsuitData, ParachuteStates.Data parachuteData, ParachuteStorage parachuteStorage, ITypedDataCursor <ParachuteStorageViewState> parachuteStorageViewState, SpectatorMode.Data spectatorData) { var machine = new StateMachine <Playing>(this, scheduler); machine.AddState(PlayingStates.Initial, new ParachuteStates.InitialState(machine)) .Permit(PlayingStates.FlyingWingsuit) .PermitChild(PlayingStates.Suspended); machine.AddState(PlayingStates.FlyingWingsuit, new FlyWingsuit(machine, wingsuitData)) .Permit(PlayingStates.EditingParachute) .Permit(PlayingStates.FlyingParachute) .PermitChild(PlayingStates.Suspended) .PermitChild(PlayingStates.Spectating) .Permit(PlayingStates.Initial); machine.AddState(PlayingStates.FlyingParachute, new ParachuteStates.Flying(machine, parachuteData)) .Permit(PlayingStates.EditingParachute) .Permit(PlayingStates.FlyingWingsuit) .PermitChild(PlayingStates.Spectating) .PermitChild(PlayingStates.Suspended) .Permit(PlayingStates.Initial); machine.AddState(PlayingStates.Spectating, new SpectatorMode(machine, spectatorData)) .PermitChild(PlayingStates.Suspended); machine.AddState(PlayingStates.EditingParachute, new ParachuteStates.Editing(machine, parachuteData, parachuteStorage, parachuteStorageViewState)) .Permit(PlayingStates.FlyingParachute) .PermitChild(PlayingStates.Suspended) .Permit(PlayingStates.Initial); machine.AddState(PlayingStates.Suspended, new ParachuteStates.InitialState(machine)) .Permit(PlayingStates.Initial); return(machine); }
private Range CreateRange(float min, float max, ITypedDataCursor <float> cursor) { return(new Range(min, max, cursor.Get, cursor.Set, NumberOfSteps)); }
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; } }
// TODO Would be even better if the highlighter doesn't know anything // about the different types of highlighted indices /// <summary> /// </summary> /// <param name="cursor"> /// <code> /// {"items": GameObject list, /// "highlightedIndex": int} /// </code> /// </param> public static IDisposable Initialize(ITypedDataCursor <UIListState> cursor) { return(cursor.OnUpdate.Subscribe(_ => Update(cursor))); }
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); } } }); }
public readonly GuiComponentDescriptor.Range NumToggleControlledCells; // int public ParachuteConfigViewModel(ITypedDataCursor <ParachuteConfig> parachuteConfig) { var c = parachuteConfig.Get; NumCells = new GuiComponentDescriptor.Range( "Number of cells", minValue: 3f, maxValue: 27f, updateValue: value => { var config = c(); int val = (int)value; val = (val % 2 == 0) ? val + 1 : val; //Todo: respect min/max config.NumCells = (int)val; parachuteConfig.Set(config); }, currentValue: () => c().NumCells, updateDisplayValue: (descriptor, str) => { GuiComponentDescriptor.DisplayNumber(c().NumCells, str, decimalPlaces: 0); }, stepSize: 2f); NumToggleControlledCells = new GuiComponentDescriptor.Range( "Number of braked cells", minValue: 0f, maxValue: 13f, updateValue: value => { var config = c(); config.NumToggleControlledCells = (int)value; parachuteConfig.Set(config); }, currentValue: () => c().NumToggleControlledCells, updateDisplayValue: (descriptor, str) => { descriptor.MaxValue = Mathf.Floor(c().NumCells / 2f); GuiComponentDescriptor.DisplayNumber(c().NumToggleControlledCells, str, decimalPlaces: 0); }, stepSize: 1f); PressureMultiplier = new GuiComponentDescriptor.Range( "Pressure multiplier", minValue: 0.5f, maxValue: 4.0f, updateValue: value => { var config = c(); config.PressureMultiplier = value; parachuteConfig.Set(config); }, currentValue: () => c().PressureMultiplier, updateDisplayValue: (descriptor, str) => { GuiComponentDescriptor.DisplayNumber(c().PressureMultiplier, str, decimalPlaces: 1, postFix: "×"); }, stepSize: 0.2f); // RearRiserPullMagnitude = new GuiComponentDescriptor.Range( // "Rear riser pull magnitude", // minValue: 0.01f, // maxValue: 0.1f, // updateValue: value => { // var config = c(); // config.RearRiserPullMagnitude = value; // parachuteConfig.Set(config); // }, // currentValue: () => c().RearRiserPullMagnitude, // updateDisplayValue: (descriptor, str) => { // GuiComponentDescriptor.DisplayNumber(c().RearRiserPullMagnitude, str, decimalPlaces: 2); // }, // stepSize: 0.01f); // FrontRiserPullMagnitude = new GuiComponentDescriptor.Range( // "Front riser pull magnitude", // minValue: 0.01f, // maxValue: 0.2f, // updateValue: value => { // var config = c(); // config.FrontRiserPullMagnitude = value; // parachuteConfig.Set(config); // }, // currentValue: () => c().FrontRiserPullMagnitude, // updateDisplayValue: (descriptor, str) => { // GuiComponentDescriptor.DisplayNumber(c().FrontRiserPullMagnitude, str, decimalPlaces: 2); // }, // stepSize: 0.01f); }