diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityCameraService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityCameraService.cs index c4d17e5711..63fbe0de1e 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityCameraService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityCameraService.cs @@ -2,23 +2,25 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System; -using Stride.Core.Annotations; -using Stride.Core.Mathematics; +using System.Diagnostics; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; using Stride.Assets.Presentation.AssetEditors.GameEditor.Game; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; using Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels; -using Stride.Assets.Presentation.SceneEditor; +using Stride.Core; +using Stride.Core.Annotations; +using Stride.Core.Mathematics; using Stride.Editor.Engine; using Stride.Engine; -using Stride.Engine.Processors; +using Stride.Engine.InputInteractions; +using Stride.Games; using Stride.Input; using static Stride.Assets.Presentation.SceneEditor.SceneEditorSettings; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Game { - public class EditorGameEntityCameraService : EditorGameCameraService, IEditorGameEntityCameraViewModelService + public class EditorGameEntityCameraService : EditorGameCameraService, IEditorGameEntityCameraViewModelService, IInputInteraction { protected struct Input { @@ -30,6 +32,9 @@ protected struct Input public bool isShiftDown; }; private const float panningSpeedModifier = 0.033f; + private CameraMode cameraMode; + + private IInputInteractionService interactionService; private readonly EntityHierarchyEditorViewModel editor; private float revolutionRadius; @@ -117,7 +122,8 @@ protected override void UpdateCamera() if (duplicating) return; - if (!IsMouseAvailable) + interactionService ??= Game.Services.GetSafeServiceAs(); + if (interactionService.HasActiveInteraction && !interactionService.IsActiveInteractionOwner(this)) return; var yaw = Yaw; @@ -139,40 +145,135 @@ protected override void UpdateCamera() SetCurrentYaw(yaw); UpdateViewMatrix(); - var isAnyMouseButtonDown = (Game.Input.IsMouseButtonDown(MouseButton.Left) || Game.Input.IsMouseButtonDown(MouseButton.Middle) || Game.Input.IsMouseButtonDown(MouseButton.Right)); - var shouldControlMouse = IsMouseAvailable && isAnyMouseButtonDown && (input.isMoving || input.isPanning || input.isRotating || input.isOrbiting || input.isZooming); - if (shouldControlMouse != IsControllingMouse) + if (cameraMode == CameraMode.Inactive) { - IsControllingMouse = shouldControlMouse; - - if (IsControllingMouse) + bool wantsFreeLookMode = Game.Input.IsMouseButtonPressed(MouseButton.Right); + bool wantsPanMode = Game.Input.IsMouseButtonPressed(MouseButton.Middle); + bool isAltDown = Game.Input.IsKeyDown(Keys.LeftAlt) || Game.Input.IsKeyDown(Keys.RightAlt); + bool wantsOrbitMode = Game.Input.IsMouseButtonPressed(MouseButton.Left) && isAltDown; + if (wantsFreeLookMode) { - Game.Input.LockMousePosition(); - Game.IsMouseVisible = false; + interactionService.Request(new InputInteractionRequest + { + Name = "SceneCamera.FreeLook", + InteractionType = InputInteractionType.Camera, + Factory = () => + { + cameraMode = CameraMode.FreeLook; + return this; + }, + }); } - else + else if (wantsPanMode) + { + interactionService.Request(new InputInteractionRequest + { + Name = "SceneCamera.Pan", + InteractionType = InputInteractionType.Camera, + Factory = () => + { + cameraMode = CameraMode.Pan; + return this; + }, + }); + } + else if (wantsOrbitMode) { - Game.Input.UnlockMousePosition(); - Game.IsMouseVisible = true; + interactionService.Request(new InputInteractionRequest + { + Name = "SceneCamera.Orbit", + InteractionType = InputInteractionType.Camera, + Factory = () => + { + cameraMode = CameraMode.Orbit; + return this; + }, + }); } } } - private Input GetInput() + object IInputInteraction.Owner => this; + + void IInputInteraction.Start() { - Input input; + Game.Input.LockMousePosition(); + Game.IsMouseVisible = false; + } - bool lbDown = Game.Input.IsMouseButtonDown(MouseButton.Left); // TODO: Combine this with UpdateCameraAsOrthographic! - bool mbDown = Game.Input.IsMouseButtonDown(MouseButton.Middle); - bool rbDown = Game.Input.IsMouseButtonDown(MouseButton.Right); - bool isAltDown = Game.Input.IsKeyDown(Keys.LeftAlt) || Game.Input.IsKeyDown(Keys.RightAlt); - input.isShiftDown = Game.Input.IsKeyDown(Keys.LeftShift) || Game.Input.IsKeyDown(Keys.RightShift); + bool IInputInteraction.Update(GameTime gameTime) + { + // Check if we want to release control + switch (cameraMode) + { + case CameraMode.Inactive: + return false; + case CameraMode.FreeLook: + bool wantsFreeLookMode = Game.Input.IsMouseButtonDown(MouseButton.Right); + if (!wantsFreeLookMode) + { + return false; + } + break; + case CameraMode.Pan: + bool wantsPanMode = Game.Input.IsMouseButtonDown(MouseButton.Middle); + if (!wantsPanMode) + { + return false; + } + break; + case CameraMode.Orbit: + bool wantsOrbitMode = Game.Input.IsMouseButtonDown(MouseButton.Left); + if (!wantsOrbitMode) + { + return false; + } + break; + } - input.isPanning = mbDown && !rbDown; - input.isRotating = !isAltDown && !mbDown && rbDown; - input.isMoving = !isAltDown && mbDown && rbDown; - input.isZooming = (isAltDown && !lbDown && !mbDown && rbDown) || (MathF.Abs(Game.Input.MouseWheelDelta) > MathUtil.ZeroTolerance); - input.isOrbiting = isAltDown && lbDown && !mbDown && !rbDown; + return true; + } + + void IInputInteraction.End() + { + ReleaseInput(); + } + + void IInputInteraction.Cancel() + { + ReleaseInput(); + } + + private void ReleaseInput() + { + cameraMode = CameraMode.Inactive; + Game.Input.UnlockMousePosition(); + Game.IsMouseVisible = true; + } + + private Input GetInput() + { + Input input = default; + + if (cameraMode != CameraMode.Inactive) + { + bool lbDown = Game.Input.IsMouseButtonDown(MouseButton.Left); // TODO: Combine this with UpdateCameraAsOrthographic! + bool mbDown = Game.Input.IsMouseButtonDown(MouseButton.Middle); + bool rbDown = Game.Input.IsMouseButtonDown(MouseButton.Right); + bool isAltDown = Game.Input.IsKeyDown(Keys.LeftAlt) || Game.Input.IsKeyDown(Keys.RightAlt); + input.isShiftDown = Game.Input.IsKeyDown(Keys.LeftShift) || Game.Input.IsKeyDown(Keys.RightShift); + + input.isPanning = cameraMode == CameraMode.Pan; + input.isRotating = !isAltDown && !mbDown && cameraMode == CameraMode.FreeLook; + input.isMoving = !isAltDown && mbDown && cameraMode == CameraMode.FreeLook; + input.isZooming = (isAltDown && !mbDown && cameraMode == CameraMode.FreeLook) || (MathF.Abs(Game.Input.MouseWheelDelta) > MathUtil.ZeroTolerance); + input.isOrbiting = cameraMode == CameraMode.Orbit; + } + else + { + // Allow wheel 'zoom' (forward/back movement) even when not actively in control + input.isZooming = (MathF.Abs(Game.Input.MouseWheelDelta) > MathUtil.ZeroTolerance); + } return input; } @@ -273,7 +374,7 @@ protected void UpdateCameraBase(ref float yaw, ref float pitch, ref Vector3 posi // Move if (input.isMoving) { - if(asOrthographic) + if (asOrthographic) { zoomDelta -= MouseMoveSpeedFactor * Game.Input.MouseDelta.Y; } @@ -338,12 +439,20 @@ protected void UpdateCameraBase(ref float yaw, ref float pitch, ref Vector3 posi protected void UpdateCameraAsOrthographic(ref float yaw, ref float pitch, ref Vector3 position, Input input) { - UpdateCameraBase(ref yaw, ref pitch, ref position, true, input); + UpdateCameraBase(ref yaw, ref pitch, ref position, asOrthographic: true, input); } protected void UpdateCameraAsPerspective(ref float yaw, ref float pitch, ref Vector3 position, Input input) { - UpdateCameraBase(ref yaw, ref pitch, ref position, false, input); + UpdateCameraBase(ref yaw, ref pitch, ref position, asOrthographic: false, input); + } + + protected enum CameraMode + { + Inactive, + FreeLook, + Pan, + Orbit, } } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntitySelectionService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntitySelectionService.cs index 2a4b06f30c..bf572420e8 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntitySelectionService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntitySelectionService.cs @@ -5,20 +5,21 @@ using System.Collections.Specialized; using System.Linq; using System.Threading.Tasks; -using Stride.Core.Assets.Editor.Extensions; -using Stride.Core.BuildEngine; -using Stride.Core; -using Stride.Core.Annotations; -using Stride.Core.Extensions; -using Stride.Core.Mathematics; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; using Stride.Assets.Presentation.AssetEditors.GameEditor.Game; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; using Stride.Assets.Presentation.AssetEditors.Gizmos; using Stride.Assets.Presentation.AssetEditors.SceneEditor.ViewModels; using Stride.Assets.Presentation.SceneEditor; +using Stride.Core; +using Stride.Core.Annotations; +using Stride.Core.BuildEngine; +using Stride.Core.Extensions; +using Stride.Core.Mathematics; using Stride.Editor.EditorGame.Game; using Stride.Engine; +using Stride.Engine.InputInteractions; +using Stride.Games; using Stride.Input; using Stride.Rendering; using Stride.Rendering.Compositing; @@ -32,9 +33,11 @@ namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Game /// public class EditorGameEntitySelectionService : EditorGameMouseServiceBase, IEditorGameEntitySelectionService, IEditorGameSelectionViewModelService { + private bool isListeningForMouseClick = false; private Vector2 mouseMoveAccumulator; private PickingSceneRenderer entityPicker; private EntityHierarchyEditorGame game; + private IInputInteractionService interactionService; /// /// Initializes a new instance of the class. @@ -164,11 +167,18 @@ private void Clear() if (SelectedIds.Count == 0) return; - IsControllingMouse = true; - Editor.Dispatcher.InvokeAsync(() => + InteractionService.Request(new InputInteractionRequest { - Editor.ClearSelection(); - Editor.Controller.InvokeAsync(() => IsControllingMouse = false); + Name = "SelectionService.ClearEntitySelection", + InteractionType = InputInteractionType.SceneSelection, + Factory = () => + { + var task = Editor.Dispatcher.InvokeAsync(() => + { + Editor.ClearSelection(); + }); + return new BlockInteraction(this, task); + } }); } @@ -182,14 +192,21 @@ private void Set([NotNull] Entity entity) if (!SelectableIds.Contains(entityId) || SelectedIds.Count == 1 && SelectedIds.Contains(entityId)) return; - IsControllingMouse = true; - Editor.Dispatcher.InvokeAsync(() => + InteractionService.Request(new InputInteractionRequest { - var viewModel = (EntityHierarchyElementViewModel)Editor.FindPartViewModel(entityId); - Editor.ClearSelection(); - if (viewModel != null) - Editor.SelectedContent.Add(viewModel); - Editor.Controller.InvokeAsync(() => IsControllingMouse = false); + Name = "SelectionService.SetEntity", + InteractionType = InputInteractionType.SceneSelection, + Factory = () => + { + var task = Editor.Dispatcher.InvokeAsync(() => + { + var viewModel = (EntityHierarchyElementViewModel)Editor.FindPartViewModel(entityId); + Editor.ClearSelection(); + if (viewModel != null) + Editor.SelectedContent.Add(viewModel); + }); + return new BlockInteraction(this, task); + } }); } @@ -203,18 +220,25 @@ private void Add([NotNull] Entity entity) if (!SelectableIds.Contains(entityId) || SelectedIds.Contains(entityId)) return; - IsControllingMouse = true; - Editor.Dispatcher.InvokeAsync(() => + InteractionService.Request(new InputInteractionRequest { - var viewModel = (EntityHierarchyElementViewModel)Editor.FindPartViewModel(entityId); - if (viewModel?.IsSelectable == true) - Editor.SelectedContent.Add(viewModel); - Editor.Controller.InvokeAsync(() => IsControllingMouse = false); + Name = "SelectionService.AddEntity", + InteractionType = InputInteractionType.SceneSelection, + Factory = () => + { + var task = Editor.Dispatcher.InvokeAsync(() => + { + var viewModel = (EntityHierarchyElementViewModel)Editor.FindPartViewModel(entityId); + if (viewModel?.IsSelectable == true) + Editor.SelectedContent.Add(viewModel); + }); + return new BlockInteraction(this, task); + } }); } /// - /// Adds the given entity from the selection. + /// Removes the given entity from the selection. /// /// The entity that must be removed from selection. private void Remove([NotNull] Entity entity) @@ -223,13 +247,20 @@ private void Remove([NotNull] Entity entity) if (!SelectedIds.Contains(entityId)) return; - IsControllingMouse = true; - Editor.Dispatcher.InvokeAsync(() => + InteractionService.Request(new InputInteractionRequest { - var viewModel = (EntityHierarchyElementViewModel)Editor.FindPartViewModel(entityId); - if (viewModel != null) - Editor.SelectedContent.Remove(viewModel); - Editor.Controller.InvokeAsync(() => IsControllingMouse = false); + Name = "SelectionService.RemoveEntity", + InteractionType = InputInteractionType.SceneSelection, + Factory = () => + { + var task = Editor.Dispatcher.InvokeAsync(() => + { + var viewModel = (EntityHierarchyElementViewModel)Editor.FindPartViewModel(entityId); + if (viewModel != null) + Editor.SelectedContent.Remove(viewModel); + }); + return new BlockInteraction(this, task); + } }); } @@ -459,13 +490,19 @@ private async Task Execute() var screenSize = new Vector2(game.GraphicsDevice.Presenter.BackBuffer.Width, game.GraphicsDevice.Presenter.BackBuffer.Height); - if (game.Input.IsMouseButtonPressed(MouseButton.Left)) + if (game.Input.IsMouseButtonPressed(MouseButton.Left) && !InteractionService.HasActiveInteraction) { + isListeningForMouseClick = true; mouseMoveAccumulator = Vector2.Zero; } + if (isListeningForMouseClick && InteractionService.HasActiveInteraction) + { + isListeningForMouseClick = false; + } mouseMoveAccumulator += new Vector2(Math.Abs(game.Input.MouseDelta.X * screenSize.X), Math.Abs(game.Input.MouseDelta.Y * screenSize.Y)); - if (IsMouseAvailable && game.Input.IsMouseButtonReleased(MouseButton.Left) && !game.Input.IsMouseButtonDown(MouseButton.Right)) + if (isListeningForMouseClick + && game.Input.IsMouseButtonReleased(MouseButton.Left) && !game.Input.IsMouseButtonDown(MouseButton.Right)) { if (mouseMoveAccumulator.Length() >= TransformationGizmo.TransformationStartPixelThreshold) continue; @@ -512,6 +549,7 @@ private async Task Execute() protected override Task Initialize(EditorServiceGame editorGame) { game = (EntityHierarchyEditorGame)editorGame; + interactionService = game.Services.GetSafeServiceAs(); Editor.SelectedContent.CollectionChanged += SelectedContentChanged; game.Script.AddTask(Execute); @@ -618,5 +656,28 @@ public override bool IsVisible(RenderObject renderObject, RenderView renderView, return false; } } + + private class BlockInteraction(EditorGameEntitySelectionService EditorService, Task DispatchTask) : IInputInteraction + { + public object Owner => EditorService; + + public void Start() + { + } + + public bool Update(GameTime gameTime) + { + bool isStillControlling = !DispatchTask.IsCompleted; + return isStillControlling; + } + + public void End() + { + } + + public void Cancel() + { + } + } } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityTransformService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityTransformService.cs index ea187fb9cb..d31c12939c 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityTransformService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityTransformService.cs @@ -214,7 +214,7 @@ private async Task Update() { if (IsActive) { - if (IsMouseAvailable) + if (!InteractionService.HasActiveInteraction) { // Snap the current selection to the grid, on keypress if (game.Input.IsKeyPressed(SceneEditorSettings.SnapSelectionToGrid.GetValue())) @@ -258,8 +258,6 @@ private async Task Update() tasks = transformationGizmos.Select(x => x.Update()); } - IsControllingMouse = activeTransformationGizmo != null && activeTransformationGizmo.IsUnderMouse() && IsMouseAvailable; - await Task.WhenAll(tasks); } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameMaterialHighlightService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameMaterialHighlightService.cs index 0276e5d538..804f3b2264 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameMaterialHighlightService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameMaterialHighlightService.cs @@ -4,13 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Stride.Core.Assets.Editor.Services; -using Stride.Core; -using Stride.Core.Extensions; -using Stride.Core.Mathematics; -using Stride.Core.Serialization; -using Stride.Core.Presentation.Quantum; -using Stride.Core.Presentation.Quantum.ViewModels; using Stride.Assets.Entities; using Stride.Assets.Models; using Stride.Assets.Presentation.AssetEditors.AssetHighlighters; @@ -18,8 +11,17 @@ using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; using Stride.Assets.Presentation.AssetEditors.GameEditor.Game; using Stride.Assets.Presentation.SceneEditor; +using Stride.Core; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Extensions; +using Stride.Core.Mathematics; +using Stride.Core.Presentation.Quantum; +using Stride.Core.Presentation.Quantum.ViewModels; +using Stride.Core.Serialization; using Stride.Editor.EditorGame.Game; using Stride.Engine; +using Stride.Engine.InputInteractions; +using Stride.Games; using Stride.Input; using Stride.Rendering; using Stride.Rendering.Compositing; @@ -265,7 +267,8 @@ private async Task Update() entityId = editor.Controller.GetAbsoluteId(entityUnderMouse); entityUnderMouseInSelection = previouslySelected.Contains(entityId.Value); } - if (!game.Input.IsMousePositionLocked && entityUnderMouse != null && materialSelected >= 0 && entityUnderMouseInSelection) + bool canInteract = !InteractionService.HasActiveInteraction || InteractionService.IsActiveInteractionOwner(this); + if (canInteract && entityUnderMouse != null && materialSelected >= 0 && entityUnderMouseInSelection) { HighlightMaterial(entityUnderMouse, materialSelected); } @@ -277,23 +280,14 @@ private async Task Update() NotifyMaterialHighlighted(entityUnderMouseInSelection ? entityUnderMouse : null, materialSelected, meshSelected); - if (IsMouseAvailable && entityUnderMouse != null && previouslySelected.Contains(entityId.Value) && game.Input.IsMouseButtonPressed(MouseButton.Left)) + if (!InteractionService.HasActiveInteraction && entityUnderMouse != null && previouslySelected.Contains(entityId.Value) && game.Input.IsMouseButtonPressed(MouseButton.Left)) { - IsControllingMouse = true; - } - - if (IsControllingMouse && !game.Input.IsMouseButtonReleased(MouseButton.Left) && !game.Input.IsMouseButtonDown(MouseButton.Left)) - { - IsControllingMouse = false; - } - - if (IsControllingMouse && game.Input.IsMouseButtonReleased(MouseButton.Left)) - { - // Note: we set IsControllingMouse back to false next frame so that no other EditorGameMouseServiceBase take over during the same frame - if (entityUnderMouse != null && materialSelected >= 0 && previouslySelected.Contains(entityId.Value)) + InteractionService.Request(new InputInteractionRequest { - editor.Dispatcher.Invoke(() => SelectMaterialInAssetView(entityUnderMouse, materialSelected)); - } + Name = "MaterialHighlight", + InteractionType = InputInteractionType.SceneSelection, + Factory = () => new Interaction(this, previouslySelected) + }); } } } @@ -365,5 +359,58 @@ public override bool IsVisible(RenderObject renderObject, RenderView renderView, && HighlightRenderFeature.ModelHighlightColors.ContainsKey(component))); } } + + private class Interaction(EditorGameMaterialHighlightService EditorService, IReadOnlyCollection PreviouslySelected) : IInputInteraction + { + public object Owner => EditorService; + + public void Start() + { + } + + public bool Update(GameTime gameTime) + { + var game = EditorService.game; + if (game.Input.IsMouseButtonDown(MouseButton.Left)) + { + return true; + } + + return false; + } + + public void End() + { + var editor = EditorService.editor; + + var entityUnderMouse = EditorService.Gizmos.GetContentEntityUnderMouse(); + int materialSelected = -1; + + AbsoluteId? entityId = null; + if (entityUnderMouse is null) + { + var entityPicked = EditorService.Selection.Pick(); + materialSelected = entityPicked.MaterialIndex; + entityUnderMouse = entityPicked.Entity; + if (entityUnderMouse is not null) + { + entityId = editor.Controller.GetAbsoluteId(entityUnderMouse); + } + } + else + { + entityId = editor.Controller.GetAbsoluteId(entityUnderMouse); + } + + if (entityUnderMouse is not null && materialSelected >= 0 && PreviouslySelected.Contains(entityId.Value)) + { + editor.Dispatcher.Invoke(() => EditorService.SelectMaterialInAssetView(entityUnderMouse, materialSelected)); + } + } + + public void Cancel() + { + } + } } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EntityHierarchyEditorGame.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EntityHierarchyEditorGame.cs index 18909b26db..43b2c3ea83 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EntityHierarchyEditorGame.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EntityHierarchyEditorGame.cs @@ -18,6 +18,7 @@ using Stride.Editor.Extensions; using Stride.Engine; using Stride.Engine.Design; +using Stride.Engine.InputInteractions; using Stride.Games; using Stride.Physics; using Stride.Rendering; @@ -262,6 +263,10 @@ protected override void Initialize() var physicsSystem = new Bullet2PhysicsSystem(Services); Services.AddService(physicsSystem); GameSystems.Add(physicsSystem); + + var interactionSystem = new InputInteractionSystem(Services); + Services.AddService(interactionSystem); + GameSystems.Add(interactionSystem); } /// @@ -297,6 +302,7 @@ protected override async Task LoadContent() //OnClientSizeChanged(this, EventArgs.Empty); // Initialize the services + var inputInteractionService = Services.GetSafeServiceAs(); var initialized = new List(); foreach (var service in EditorServices.OrderByDependency()) { @@ -313,6 +319,7 @@ protected override async Task LoadContent() var mouseService = service as EditorGameMouseServiceBase; mouseService?.RegisterMouseServices(EditorServices); + mouseService?.InitializeMouseService(inputInteractionService); } // TODO: Maybe define this scene default graphics compositor as an asset? diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/IEditorGameComponentGizmoService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/IEditorGameComponentGizmoService.cs index 14de3c96d4..050a6266ea 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/IEditorGameComponentGizmoService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/IEditorGameComponentGizmoService.cs @@ -15,6 +15,9 @@ public interface IEditorGameComponentGizmoService : IEditorGameService void UpdateGizmoEntitiesSelection(Entity entity, bool isSelected); + /// + /// Returns the entity represented by its gizmo or the entity directly under the mouse. + /// Entity GetContentEntityUnderMouse(); } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameCameraOrientationService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameCameraOrientationService.cs index f9d18c7329..2ef1624a3b 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameCameraOrientationService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameCameraOrientationService.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Stride.Core.Mathematics; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Game; using Stride.Assets.Presentation.AssetEditors.Gizmos; +using Stride.Core.Mathematics; using Stride.Editor.EditorGame.Game; +using Stride.Engine.InputInteractions; +using Stride.Games; using Stride.Input; namespace Stride.Assets.Presentation.AssetEditors.GameEditor.Game @@ -15,12 +17,12 @@ public class EditorGameCameraOrientationService : EditorGameMouseServiceBase { private EntityHierarchyEditorGame game; private CameraOrientationGizmo gizmo; - private Int3? clickedElement; + [Obsolete] public override bool IsControllingMouse { get; protected set; } public override IEnumerable Dependencies { get { yield return typeof(EditorGameEntityCameraService); } } - + internal EditorGameEntityCameraService Camera => Services.Get(); protected override Task Initialize(EditorServiceGame editorGame) @@ -41,43 +43,68 @@ private async Task Update() { gizmo.Update(); - if (gizmo.HasSelection && IsMouseAvailable) + if (gizmo.HasSelection && !InteractionService.HasActiveInteraction) { - IsControllingMouse = true; if (game.Input.IsMouseButtonPressed(MouseButton.Left)) { - clickedElement = gizmo.SelectedElement; - } - else if (game.Input.IsMouseButtonReleased(MouseButton.Left)) - { - if (clickedElement.HasValue && clickedElement == gizmo.SelectedElement) + var clickedElement = gizmo.SelectedElement; + InteractionService.Request(new InputInteractionRequest { - Int3 selectedElement = clickedElement.Value; + Name = "CameraOrientation.Click", + InteractionType = InputInteractionType.Gizmo, + Factory = () => new Interaction(this, clickedElement) + }); + } + } + } - // If looking along a coordinate axis and the corresponding element is clicked, switch projection mode - if (gizmo.IsViewParallelToAxis && selectedElement.LengthSquared() == 1) - { - var camera = Camera.Camera; - camera.Dispatcher.Invoke(() => camera.OrthographicProjection = !camera.OrthographicProjection); - } - else - { - var viewDirection = new Vector3(-selectedElement.X, -selectedElement.Y, -selectedElement.Z); - Camera.ResetCamera(viewDirection); - } - } + await game.Script.NextFrame(); + } + } - clickedElement = null; - IsControllingMouse = false; - } + private class Interaction(EditorGameCameraOrientationService EditorService, Int3 ClickedElement) : IInputInteraction + { + public object Owner => EditorService; + + public void Start() + { + } + + public bool Update(GameTime gameTime) + { + var game = EditorService.game; + if (game.Input.IsMouseButtonDown(MouseButton.Left)) + { + return true; + } + return false; + } + + public void End() + { + var gizmo = EditorService.gizmo; + var cameraService = EditorService.Camera; + if (ClickedElement == gizmo.SelectedElement) + { + // Mouse release is still over the same element + Int3 selectedElement = ClickedElement; + + // If looking along a coordinate axis and the corresponding element is clicked, switch projection mode + if (gizmo.IsViewParallelToAxis && selectedElement.LengthSquared() == 1) + { + var camera = cameraService.Camera; + camera.Dispatcher.Invoke(() => camera.OrthographicProjection = !camera.OrthographicProjection); } else { - IsControllingMouse = false; + var viewDirection = new Vector3(-selectedElement.X, -selectedElement.Y, -selectedElement.Z); + cameraService.ResetCamera(viewDirection); } } + } - await game.Script.NextFrame(); + public void Cancel() + { } } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameCameraService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameCameraService.cs index cbbe9b3a70..ba73e0f7e6 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameCameraService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameCameraService.cs @@ -137,6 +137,7 @@ public bool IsOrthographic public Vector3 Position { get; private set; } /// + [Obsolete] public override bool IsControllingMouse { get; protected set; } protected IEditorGameController Controller { get; } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameMouseServiceBase.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameMouseServiceBase.cs index 6465c1ebc9..2d217287c2 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameMouseServiceBase.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/EditorGameMouseServiceBase.cs @@ -1,10 +1,12 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; using System.Collections.Generic; using System.Linq; using Stride.Core.Annotations; using Stride.Editor.EditorGame.Game; +using Stride.Engine.InputInteractions; namespace Stride.Assets.Presentation.AssetEditors.GameEditor.Game { @@ -15,7 +17,10 @@ public abstract class EditorGameMouseServiceBase : EditorGameServiceBase, IEdito { private readonly List mouseServices = new List(); + public IInputInteractionService InteractionService { get; private set; } + /// + [Obsolete("Use !InteractionService.HasActiveInteraction")] public abstract bool IsControllingMouse { get; protected set; } /// @@ -24,7 +29,8 @@ public abstract class EditorGameMouseServiceBase : EditorGameServiceBase, IEdito public override bool IsActive { get; set; } = true; /// - public bool IsMouseAvailable => mouseServices.All(x => x == this || !x.IsControllingMouse); + [Obsolete("Check with (!InteractionService.HasActiveInteraction || InteractionService.IsActiveInteractionOwner(this))")] + public bool IsMouseAvailable => !InteractionService.HasActiveInteraction || InteractionService.IsActiveInteractionOwner(this); internal void RegisterMouseServices([NotNull] EditorGameServiceRegistry serviceRegistry) { @@ -33,5 +39,10 @@ internal void RegisterMouseServices([NotNull] EditorGameServiceRegistry serviceR mouseServices.Add(service); } } + + internal void InitializeMouseService(IInputInteractionService interactionService) + { + InteractionService = interactionService; + } } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/IEditorGameMouseService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/IEditorGameMouseService.cs index cc2d9bf424..9d2be9c66c 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/IEditorGameMouseService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Game/IEditorGameMouseService.cs @@ -1,7 +1,9 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; using Stride.Editor.EditorGame.Game; +using Stride.Engine.InputInteractions; namespace Stride.Assets.Presentation.AssetEditors.GameEditor.Game { @@ -10,14 +12,18 @@ namespace Stride.Assets.Presentation.AssetEditors.GameEditor.Game /// public interface IEditorGameMouseService : IEditorGameService { + IInputInteractionService InteractionService { get; } + /// /// Gets whether this instance is currently controlling the mouse. /// + [Obsolete] bool IsControllingMouse { get; } /// /// Gets whether the mouse is available to be be controlled. /// + [Obsolete] bool IsMouseAvailable { get; } } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Services/EditorGameController.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Services/EditorGameController.cs index 4e99658ace..4d89ae0938 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Services/EditorGameController.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Services/EditorGameController.cs @@ -6,17 +6,19 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using Stride.Assets.Presentation.AssetEditors.GameEditor.Game; +using Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels; +using Stride.Core; +using Stride.Core.Annotations; using Stride.Core.Assets.Editor.Services; using Stride.Core.Assets.Editor.ViewModel; using Stride.Core.Assets.Quantum; -using Stride.Core; -using Stride.Core.Annotations; using Stride.Core.Diagnostics; +using Stride.Core.Extensions; using Stride.Core.Mathematics; using Stride.Core.Presentation.Controls; using Stride.Core.Presentation.Services; -using Stride.Assets.Presentation.AssetEditors.GameEditor.Game; -using Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels; +using Stride.Editor; using Stride.Editor.Build; using Stride.Editor.EditorGame.ContentLoader; using Stride.Editor.EditorGame.Game; @@ -417,6 +419,7 @@ private void SceneGameRunThread() // Create and register services serviceRegistry = new EditorGameServiceRegistry(); InitializeServices(serviceRegistry); + Editor.Session.ServiceProvider.Get().Plugins.ForEach(x => (x as StrideAssetsPlugin)?.RegisterGameServices(serviceRegistry)); Game.RegisterServices(serviceRegistry); // Notify game start diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationGizmo.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationGizmo.cs index 16e2588082..3559f649ef 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationGizmo.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationGizmo.cs @@ -4,12 +4,15 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Stride.Core.Mathematics; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Game; using Stride.Assets.Presentation.AssetEditors.GameEditor; using Stride.Assets.Presentation.AssetEditors.GameEditor.Game; +using Stride.Core; +using Stride.Core.Mathematics; using Stride.Engine; +using Stride.Engine.InputInteractions; using Stride.Engine.Processors; +using Stride.Games; using Stride.Graphics; using Stride.Input; using Stride.Rendering; @@ -19,7 +22,7 @@ namespace Stride.Assets.Presentation.AssetEditors.Gizmos /// /// Base class for all gizmo that applies a transformation on entity /// - public abstract class TransformationGizmo : AxialGizmo + public abstract class TransformationGizmo : AxialGizmo, IInputInteraction { public const float TransformationStartPixelThreshold = 8; @@ -42,6 +45,8 @@ public bool IsIdentity() public const RenderGroupMask TransformationGizmoGroupMask = RenderGroupMask.Group4; + private IInputInteractionService interactionService; + private bool isInteractionActive = false; private bool transformationInitialized; private bool duplicationDone; @@ -390,10 +395,21 @@ private async Task TransformSceneEntityBase() return; } - // initialize the start values at the beginning of the transformation - if (!transformationInitialized) + interactionService ??= Game.Services.GetSafeServiceAs(); + if (!isInteractionActive && Game.Input.IsMouseButtonPressed(MouseButton.Left)) + { + interactionService.Request(new InputInteractionRequest + { + Name = "TransformationGizmo.Drag", + InteractionType = InputInteractionType.Gizmo, + Factory = () => this, + }); + return; + } + + if (!isInteractionActive) { - InitializeTransformation(); + return; } // calculate the current drag translation in the screen normalized space @@ -475,6 +491,37 @@ private async Task TransformSceneEntityBase() } } + object IInputInteraction.Owner => this; + + void IInputInteraction.Start() + { + isInteractionActive = true; + + // initialize the start values at the beginning of the transformation + InitializeTransformation(); + } + + bool IInputInteraction.Update(GameTime gameTime) + { + bool isStillControlling = Game.Input.IsMouseButtonDown(MouseButton.Left); + return isStillControlling; + } + + void IInputInteraction.End() + { + ReleaseInput(); + } + + void IInputInteraction.Cancel() + { + ReleaseInput(); + } + + private void ReleaseInput() + { + isInteractionActive = false; + } + public virtual async Task Update() { if (!IsEnabled) diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Game/UIEditorGameCameraService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Game/UIEditorGameCameraService.cs index 17085d88a9..a04be26fe7 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Game/UIEditorGameCameraService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Game/UIEditorGameCameraService.cs @@ -3,22 +3,25 @@ using System; using System.Threading.Tasks; -using Stride.Core.Mathematics; using Stride.Assets.Presentation.AssetEditors.GameEditor.Game; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; -using Stride.Assets.Presentation.SceneEditor; +using Stride.Core; +using Stride.Core.Mathematics; using Stride.Editor.EditorGame.Game; using Stride.Engine; +using Stride.Engine.InputInteractions; +using Stride.Games; using Stride.Input; namespace Stride.Assets.Presentation.AssetEditors.UIEditor.Game { - internal sealed class UIEditorGameCameraService : EditorGameCameraService + internal sealed class UIEditorGameCameraService : EditorGameCameraService, IInputInteraction { public new static readonly Vector3 DefaultPosition = new Vector3(0, 0, 500); public new static readonly float DefaultPitch = 0.0f; public new static readonly float DefaultYaw = 0.0f; - + + private IInputInteractionService interactionService; private float desiredYaw; private float desiredPitch; private bool isUpdating; @@ -83,17 +86,15 @@ protected override void UpdateCamera() { base.UpdateCamera(); - if (IsMouseAvailable && Game.Input.IsMouseButtonPressed(MouseButton.Middle)) + interactionService ??= Game.Services.GetSafeServiceAs(); + if (Game.Input.IsMouseButtonPressed(MouseButton.Middle)) { - // Capture mouse when a button is pressed and the mouse is available - Game.Input.LockMousePosition(); - Game.IsMouseVisible = false; - IsControllingMouse = true; - } - else if (Game.Input.IsMouseButtonReleased(MouseButton.Middle)) - { - Game.Input.UnlockMousePosition(); - Game.IsMouseVisible = true; + interactionService.Request(new InputInteractionRequest + { + Name = "UIEditorCamera.Pan", + InteractionType = InputInteractionType.Camera, + Factory = () => this, + }); } // Compute translation speed according to framerate and modifiers @@ -126,7 +127,7 @@ protected override void UpdateCamera() var right = Vector3.Cross(forward, up); // Dolly (top, bottom, left and right) - if (IsMouseAvailable && Game.Input.IsMouseButtonDown(MouseButton.Middle)) + if (interactionService.IsActiveInteractionOwner(this)) { desiredYaw = yaw; desiredPitch = pitch; @@ -135,7 +136,7 @@ protected override void UpdateCamera() position += up * Game.Input.MouseDelta.Y * MouseMoveSpeedFactor * translationSpeed; } // Dolly (forward and backward) - else if (IsMouseAvailable && Math.Abs(Game.Input.MouseWheelDelta) > MathUtil.ZeroTolerance) + else if (!interactionService.HasActiveInteraction && Math.Abs(Game.Input.MouseWheelDelta) > MathUtil.ZeroTolerance) { desiredYaw = yaw; desiredPitch = pitch; @@ -155,5 +156,35 @@ protected override void UpdateCamera() UpdateViewMatrix(); isUpdating = false; } + + object IInputInteraction.Owner => this; + + void IInputInteraction.Start() + { + Game.Input.LockMousePosition(); + Game.IsMouseVisible = false; + } + + bool IInputInteraction.Update(GameTime gameTime) + { + bool isStillControlling = Game.Input.IsMouseButtonDown(MouseButton.Middle); + return isStillControlling; + } + + void IInputInteraction.End() + { + ReleaseInput(); + } + + void IInputInteraction.Cancel() + { + ReleaseInput(); + } + + private void ReleaseInput() + { + Game.Input.UnlockMousePosition(); + Game.IsMouseVisible = true; + } } } diff --git a/sources/editor/Stride.Editor/StrideAssetsPlugin.cs b/sources/editor/Stride.Editor/StrideAssetsPlugin.cs index 82240d5f36..4e1f53f349 100644 --- a/sources/editor/Stride.Editor/StrideAssetsPlugin.cs +++ b/sources/editor/Stride.Editor/StrideAssetsPlugin.cs @@ -11,11 +11,12 @@ using Stride.Core.Assets.Editor.ViewModel; using Stride.Core.Diagnostics; using Stride.Core.Extensions; +using Stride.Core.Presentation.View; using Stride.Core.Reflection; using Stride.Core.Serialization; using Stride.Core.Serialization.Contents; -using Stride.Core.Presentation.View; using Stride.Editor.Annotations; +using Stride.Editor.EditorGame.Game; using Stride.Editor.Preview; using Stride.Editor.Preview.ViewModel; @@ -119,5 +120,13 @@ public override void RegisterAssetPreviewViewModelTypes(IDictionary } } } + + /// + /// Invoked on the game thread. + /// + public virtual void RegisterGameServices(EditorGameServiceRegistry serviceRegistry) + { + // Do nothing by default + } } } diff --git a/sources/engine/Stride.Engine/Engine/InputInteractions/IInputInteraction.cs b/sources/engine/Stride.Engine/Engine/InputInteractions/IInputInteraction.cs new file mode 100644 index 0000000000..843c4444c5 --- /dev/null +++ b/sources/engine/Stride.Engine/Engine/InputInteractions/IInputInteraction.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Games; + +namespace Stride.Engine.InputInteractions; + +public interface IInputInteraction +{ + /// + /// The object that requested for the interaction. + /// + object Owner { get; } + + /// + /// Called after it has been instantiated. + /// + void Start(); + + /// + /// Returns true if this interaction should continue running. + /// + bool Update(GameTime gameTime); + + /// + /// Called after returns false (ie. completed its interaction run). + /// + void End(); + + /// + /// Called if the interaction has been terminated externally. + /// + void Cancel(); +} diff --git a/sources/engine/Stride.Engine/Engine/InputInteractions/IInputInteractionService.cs b/sources/engine/Stride.Engine/Engine/InputInteractions/IInputInteractionService.cs new file mode 100644 index 0000000000..631a4b4130 --- /dev/null +++ b/sources/engine/Stride.Engine/Engine/InputInteractions/IInputInteractionService.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Engine.InputInteractions; + +public interface IInputInteractionService +{ + /// + /// Returns true if there is an in-progress interaction. + /// + bool HasActiveInteraction { get; } + + /// + /// Request for interaction capture. + /// + void Request(InputInteractionRequest request); + + /// + /// Returns true if is the in-progress interaction. + /// + bool IsActiveInteraction(IInputInteraction interaction); + + /// + /// Returns true if the in-progress interaction's is the same as . + /// + bool IsActiveInteractionOwner(object owner); + + /// + /// Force the in-progress interaction to cancel. + /// + void ForceTerminateActiveInteraction(); +} diff --git a/sources/engine/Stride.Engine/Engine/InputInteractions/InputInteractionRequest.cs b/sources/engine/Stride.Engine/Engine/InputInteractions/InputInteractionRequest.cs new file mode 100644 index 0000000000..bee9bbeb24 --- /dev/null +++ b/sources/engine/Stride.Engine/Engine/InputInteractions/InputInteractionRequest.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; + +namespace Stride.Engine.InputInteractions; + +public class InputInteractionRequest +{ + public string Name { get; init; } + + public InputInteractionType InteractionType { get; init; } = InputInteractionType.Tools; + /// + /// Higher value is given priority to tie-break multiple requests from the same . + /// + public int Order { get; init; } + + /// + /// Function that instantiates the interaction if the request is accepted. + /// + public required Func Factory { get; init; } +} diff --git a/sources/engine/Stride.Engine/Engine/InputInteractions/InputInteractionSystem.cs b/sources/engine/Stride.Engine/Engine/InputInteractions/InputInteractionSystem.cs new file mode 100644 index 0000000000..b185765385 --- /dev/null +++ b/sources/engine/Stride.Engine/Engine/InputInteractions/InputInteractionSystem.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Stride.Core; +using Stride.Games; +using Stride.Input; + +namespace Stride.Engine.InputInteractions; + +public class InputInteractionSystem : GameSystemBase, IInputInteractionService +{ + private static Comparison InteractionRequestComparer = (x, y) => + { + int interactionTypeComparison = y.InteractionType.CompareTo(x.InteractionType); + if (interactionTypeComparison != 0) + { + return interactionTypeComparison; + } + return y.Order.CompareTo(x.Order); + }; + + private readonly List requests = []; + + private InputManager inputManager; + + private IInputInteraction activeInteraction; + + public bool HasActiveInteraction => activeInteraction is not null; + + public InputInteractionSystem([NotNull] IServiceRegistry registry) + : base(registry) + { + } + + public override void Initialize() + { + base.Initialize(); + + var inputSystem = Game?.GameSystems.FirstOrDefault(x => x is InputSystem) as InputSystem; + inputManager = inputSystem?.Manager; + + UpdateOrder = (inputSystem?.UpdateOrder ?? InputSystem.DefaultUpdateOrder) + 1; + + Enabled = inputManager is not null; + Visible = false; + } + + public void Request(InputInteractionRequest request) + { + requests.Add(request); + } + + public bool IsActiveInteraction(IInputInteraction interaction) => activeInteraction == interaction; + + public bool IsActiveInteractionOwner(object owner) => activeInteraction?.Owner == owner; + + public void ForceTerminateActiveInteraction() + { + activeInteraction?.Cancel(); + activeInteraction = null; + } + + public override void Update(GameTime gameTime) + { + if (requests.Count > 0) + { + if (activeInteraction is null) + { + if (requests.Count > 1) + { + requests.Sort(InteractionRequestComparer); + } + var nextRequest = requests[0]; + if (nextRequest != null) + { + activeInteraction = nextRequest.Factory(); + + activeInteraction.Start(); + } + } + + requests.Clear(); // All requests are rejected if there is an active interaction + } + + if (activeInteraction is not null) + { + bool isStillRunning = activeInteraction.Update(gameTime); + if (!isStillRunning) + { + activeInteraction.End(); + activeInteraction = null; + } + } + } +} diff --git a/sources/engine/Stride.Engine/Engine/InputInteractions/InputInteractionType.cs b/sources/engine/Stride.Engine/Engine/InputInteractions/InputInteractionType.cs new file mode 100644 index 0000000000..5bf64f164d --- /dev/null +++ b/sources/engine/Stride.Engine/Engine/InputInteractions/InputInteractionType.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Engine.InputInteractions; + +public enum InputInteractionType +{ + Default = 0, + Camera = 100, + SceneSelection = 200, + Tools = 1000, + Gizmo = 10000, +}