diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7debff1..aac47d9 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "unofficial.Urho3DNet.Editor": { - "version": "0.3.7.757", + "version": "0.3.7.785", "commands": [ "rbfx" ] diff --git a/Directory.Build.props b/Directory.Build.props index 9e9fdfc..8b3dca3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.3.7.757 + 0.3.7.785 \ No newline at end of file diff --git a/RbfxTemplate/GameRmlUIComponent.cs b/RbfxTemplate/GameRmlUIComponent.cs index cceb317..fe02536 100644 --- a/RbfxTemplate/GameRmlUIComponent.cs +++ b/RbfxTemplate/GameRmlUIComponent.cs @@ -1,4 +1,5 @@ -using Urho3DNet; +using System.ComponentModel; +using Urho3DNet; namespace RbfxTemplate { @@ -6,11 +7,34 @@ namespace RbfxTemplate [Preserve(AllMembers = true)] public class GameRmlUIComponent : RmlUIComponent { + private RmlUIStateBase _state; + public GameRmlUIComponent(Context context) : base(context) { } - public RmlUIStateBase State { get; set; } + public RmlUIStateBase State + { + get => _state; + set + { + if (_state != value) + { + if (_state != null) + _state.PropertyChanged -= OnPropertyChanged; + + _state = value; + + if (_state != null) + _state.PropertyChanged += OnPropertyChanged; + } + } + } + + private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + DirtyVariable(e.PropertyName); + } public void UpdateProperties() { diff --git a/RbfxTemplate/GameState.cs b/RbfxTemplate/GameState.cs index f0f0ae9..780fdec 100644 --- a/RbfxTemplate/GameState.cs +++ b/RbfxTemplate/GameState.cs @@ -22,6 +22,16 @@ public sealed class GameState : RmlUIStateBase private readonly Random _random = new Random(); private readonly List _currentTiles = new List(); + /// + /// State interaction manager. Track active interactions. + /// + private readonly StatePointerInteractionManager _stateManager; + + /// + /// Input adapter to handle arrow keys. + /// + private readonly DirectionalPadAdapter _directionalPad; + private readonly Tuple[] _pairs = { Tuple.Create("emoji_u1f436", "emoji_u1f431"), @@ -63,6 +73,10 @@ public GameState(UrhoPluginApplication app) : base(app, "UI/GameScreen.rml") _raycastResult = new PhysicsRaycastResult(); + // Initialize state interaction manager. + _stateManager = new StatePointerInteractionManager(); + _stateManager.LastKnownMousePosition = Input.MousePosition; + _app = app; _scene = Context.CreateObject(); _scene.Ptr.LoadXML("Scenes/Scene.scene"); @@ -87,6 +101,8 @@ public GameState(UrhoPluginApplication app) : base(app, "UI/GameScreen.rml") _dragState = new DragState(this); _victoryState = new VictoryState(this); + _directionalPad = new DirectionalPadAdapter(Context); + _confettiAnimation = new ActionBuilder(Context).Enable().ShaderParameterFromTo(1.0f, "AnimationPhase", 0.0f, 1.0f).Disable().Build(); NextLevel(null); @@ -94,20 +110,6 @@ public GameState(UrhoPluginApplication app) : base(app, "UI/GameScreen.rml") Deactivate(); } - public StateBase State - { - get => _state; - set - { - if (_state != value) - { - _state?.Deactivate(); - _state = value; - _state?.Activate(); - } - } - } - public override void OnDataModelInitialized(GameRmlUIComponent component) { component.BindDataModelProperty("Level", _ => _.Set("Level " + _levelIndex), _ => { }); @@ -125,7 +127,7 @@ private void Settings(VariantList obj) public override void Activate(StringVariantMap bundle) { - SubscribeToEvent(E.KeyUp, HandleKeyUp); + SubscribeToEvent(E.KeyUp, HandleEscKeyUp); SubscribeToEvent(E.MouseButtonDown, HandleMouseDown); SubscribeToEvent(E.MouseButtonUp, HandleMouseUp); @@ -135,6 +137,8 @@ public override void Activate(StringVariantMap bundle) SubscribeToEvent(E.TouchEnd, HandleTouchEnd); SubscribeToEvent(E.TouchMove, HandleTouchMove); + _directionalPad.IsEnabled = true; + _scene.Ptr.IsUpdateEnabled = true; @@ -143,7 +147,7 @@ public override void Activate(StringVariantMap bundle) public override void Update(float timeStep) { - _state.Update(timeStep); + _stateManager.State.Update(timeStep); var orthoSizeY = _areaSize.Y; var orthoSizeX = _areaSize.X * UI.Size.Y / UI.Size.X; @@ -238,15 +242,15 @@ public Tile PickTile(IntVector2 inputMousePosition) return null; } - public void DragTile(Tile tile, int? touchId) + public void DragTile(Tile tile, InteractionKey interactionKey) { - _dragState.TrackTile(tile, touchId); - State = _dragState; + _dragState.TrackTile(tile, interactionKey); + _stateManager.State = _dragState; } public void StartPicking() { - State = _pickState; + _stateManager.State = _pickState; var isComplete = true; var isCorrect = true; @@ -316,7 +320,7 @@ private void Victory() { ActionManager.AddAction(_confettiAnimation, node); } - State = _victoryState; + _stateManager.State = _victoryState; _victory = true; RmlUiComponent.UpdateProperties(); } @@ -332,54 +336,55 @@ private void Randomize(List rightTiles) private void HandleTouchBegin(VariantMap args) { - var touchId = args[E.TouchBegin.TouchID].Int; - var x = args[E.TouchBegin.X].Int; - var y = args[E.TouchBegin.Y].Int; - _state.HandleTouchBegin(touchId, new IntVector2(x, y)); + _stateManager.HandleTouchBegin(args); } private void HandleTouchEnd(VariantMap args) { - var touchId = args[E.TouchEnd.TouchID].Int; - var x = args[E.TouchEnd.X].Int; - var y = args[E.TouchEnd.Y].Int; - _state.HandleTouchEnd(touchId, new IntVector2(x, y)); + _stateManager.HandleTouchEnd(args); } private void HandleTouchMove(VariantMap args) { - var touchId = args[E.TouchMove.TouchID].Int; - var x = args[E.TouchMove.X].Int; - var y = args[E.TouchMove.Y].Int; - _state.HandleTouchMove(touchId, new IntVector2(x, y)); + _stateManager.HandleTouchMove(args); } private void HandleMouseMove(VariantMap args) { - var buttons = args[E.MouseMove.Buttons].Int; - var qualifiers = args[E.MouseMove.Qualifiers].Int; - var x = args[E.MouseMove.X].Int; - var y = args[E.MouseMove.Y].Int; - _state.HandleMouseMove(new IntVector2(x, y), buttons, qualifiers); + _stateManager.HandleMouseMove(args); + } + + private void HandleMouseUp(VariantMap args) + { + _stateManager.HandleMouseUp(args); } - private void HandleMouseUp(StringHash arg1, VariantMap args) + private void HandleMouseDown(VariantMap args) { - var buttons = args[E.MouseButtonUp.Buttons].Int; - var qualifiers = args[E.MouseButtonUp.Qualifiers].Int; - var button = args[E.MouseButtonUp.Button].Int; - _state.HandleMouseUp(button, Context.Input.MousePosition, buttons, qualifiers); + _stateManager.HandleMouseDown(args); } - private void HandleMouseDown(StringHash arg1, VariantMap args) + private void HandleDpadKeyDown(VariantMap args) { - var buttons = args[E.MouseButtonDown.Buttons].Int; - var qualifiers = args[E.MouseButtonDown.Qualifiers].Int; - var button = args[E.MouseButtonDown.Button].Int; - _state.HandleMouseDown(button, Context.Input.MousePosition, buttons, qualifiers); + var scanCode =(Scancode)args[E.KeyUp.Scancode].Int; + switch (scanCode) + { + case Scancode.ScancodeUp: + _stateManager.HandleDpad(HatPosition.HatUp); + break; + case Scancode.ScancodeDown: + _stateManager.HandleDpad(HatPosition.HatDown); + break; + case Scancode.ScancodeLeft: + _stateManager.HandleDpad(HatPosition.HatLeft); + break; + case Scancode.ScancodeRight: + _stateManager.HandleDpad(HatPosition.HatRight); + break; + } } - private void HandleKeyUp(VariantMap args) + private void HandleEscKeyUp(VariantMap args) { var key = (Key)args[E.KeyUp.Key].Int; switch (key) diff --git a/RbfxTemplate/GameStates/DragState.cs b/RbfxTemplate/GameStates/DragState.cs index 28c319f..32ca25f 100644 --- a/RbfxTemplate/GameStates/DragState.cs +++ b/RbfxTemplate/GameStates/DragState.cs @@ -4,8 +4,9 @@ namespace RbfxTemplate.GameStates { public class DragState : StateBase { + InteractionKey? _interactionKey; + private Tile _tile; - private int? _touchId; public DragState(GameState game) : base(game) { @@ -13,47 +14,48 @@ public DragState(GameState game) : base(game) public override void Activate() { - _tile.Link.IsEnabled = true; - base.Activate(); - } + if (_tile != null) + { + _tile.Link.IsEnabled = true; + } - public override void Deactivate() - { - base.Deactivate(); + base.Activate(); } - public override void HandleMouseMove(IntVector2 intVector2, int buttons, int qualifiers) + public override void EndInteraction(InteractionKey interactionKey, IntVector2 interactionPosition) { - if (_touchId.HasValue) return; + if (interactionKey != _interactionKey) + return; - MoveLink(intVector2); + CompleteLink(interactionPosition); + _interactionKey = null; } - public override void HandleMouseUp(int button, IntVector2 inputMousePosition, int buttons, int qualifiers) + public override void CancelInteraction(InteractionKey interactionKey, IntVector2 interactionPosition) { - if (_touchId.HasValue) return; + if (interactionKey != _interactionKey) + return; - CompleteLink(inputMousePosition); + _interactionKey = null; + Game.StartPicking(); } - public override void HandleTouchMove(int touchId, IntVector2 intVector2) + public override void UpdateInteraction(InteractionKey interactionKey, IntVector2 interactionPosition) { - if (_touchId != touchId) return; + if (interactionKey != _interactionKey) + return; - MoveLink(intVector2); + MoveLink(interactionPosition); } - public override void HandleTouchEnd(int touchId, IntVector2 intVector2) + public override void Update(float timeStep) { - if (_touchId != touchId) return; - - CompleteLink(intVector2); } - public void TrackTile(Tile tile, int? touchId) + public void TrackTile(Tile tile, InteractionKey interactionKey) { _tile = tile; - _touchId = touchId; + _interactionKey = interactionKey; tile.LinkTo(null); var material = Context.ResourceCache.GetResource("Materials/White.material"); tile.Link.GetComponent(true).SetMaterial(material); diff --git a/RbfxTemplate/GameStates/InteractionKey.cs b/RbfxTemplate/GameStates/InteractionKey.cs new file mode 100644 index 0000000..c738ea8 --- /dev/null +++ b/RbfxTemplate/GameStates/InteractionKey.cs @@ -0,0 +1,86 @@ +using System; +using Urho3DNet; + +namespace RbfxTemplate.GameStates +{ + /// + /// Touch or mouse interaction key. + /// + public readonly struct InteractionKey : IEquatable + { + /// + /// Is source a mouse or a touch? + /// + public readonly bool IsMouse; + + /// + /// Mouse button or Touch identifier. + /// + public readonly int SourceId; + + /// + /// Qualifiers mask. + /// + public readonly Qualifier Qualifiers; + + /// + /// Construct interaction key from touch event. + /// + /// Touch identifier. + public InteractionKey(int touchId) + { + IsMouse = false; + SourceId = touchId; + Qualifiers = 0; + } + + /// + /// Construct interaction key from mouse event. + /// + /// Mouse button identifier. + public InteractionKey(MouseButton buttonId, Qualifier qualifiers) + { + IsMouse = true; + SourceId = (int)buttonId; + Qualifiers = qualifiers; + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = IsMouse.GetHashCode(); + hashCode = (hashCode * 397) ^ SourceId; + hashCode = (hashCode * 397) ^ (int)Qualifiers; + return hashCode; + } + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// Returns true if the current object is equal to the other parameter; otherwise returns false. + public bool Equals(InteractionKey other) + { + return IsMouse == other.IsMouse && SourceId == other.SourceId && Qualifiers == other.Qualifiers; + } + + /// + public override bool Equals(object obj) + { + return obj is InteractionKey other && Equals(other); + } + + public static bool operator ==(InteractionKey left, InteractionKey right) + { + return left.Equals(right); + } + + public static bool operator !=(InteractionKey left, InteractionKey right) + { + return !left.Equals(right); + } + } +} \ No newline at end of file diff --git a/RbfxTemplate/GameStates/PickState.cs b/RbfxTemplate/GameStates/PickState.cs index 1d1465a..e2b2bc5 100644 --- a/RbfxTemplate/GameStates/PickState.cs +++ b/RbfxTemplate/GameStates/PickState.cs @@ -14,14 +14,12 @@ public PickState(GameState game, Node pointer) : base(game) _pointer.SetDeepEnabled(false); } - public override void HandleMouseDown(int button, IntVector2 inputMousePosition, int buttons, int qualifiers) + public override void StartInteraction(InteractionKey interactionKey, IntVector2 interactionPosition) { - if (button != 1) return; - - var tile = Game.PickTile(inputMousePosition); + var tile = Game.PickTile(interactionPosition); if (tile != null) { - Game.DragTile(tile, null); + Game.DragTile(tile, interactionKey); _pointer.SetDeepEnabled(false); _hintTile = null; } @@ -41,17 +39,6 @@ public override void Update(float timeStep) } } - public override void HandleTouchBegin(int touchId, IntVector2 touchPosition) - { - var tile = Game.PickTile(touchPosition); - if (tile != null) - { - Game.DragTile(tile, touchId); - _pointer.SetDeepEnabled(false); - _hintTile = null; - } - } - public void ShowHint(Tile tile) { _hintTile = tile; diff --git a/RbfxTemplate/GameStates/StateBase.cs b/RbfxTemplate/GameStates/StateBase.cs index bfd6fa0..adde25e 100644 --- a/RbfxTemplate/GameStates/StateBase.cs +++ b/RbfxTemplate/GameStates/StateBase.cs @@ -2,50 +2,94 @@ namespace RbfxTemplate.GameStates { + /// + /// Base class for game state. + /// public abstract class StateBase { - public StateBase(GameState game) + /// + /// Construct state. + /// + /// Game state. + protected StateBase(GameState game) { Game = game; } + /// + /// Game application state. + /// public GameState Game { get; } + /// + /// Engine context. + /// public Context Context => Game.Context; - public virtual void HandleTouchBegin(int touchId, IntVector2 intVector2) - { - } - - public virtual void HandleTouchMove(int touchId, IntVector2 intVector2) + /// + /// Activate game state. + /// + public virtual void Activate() { } - public virtual void HandleTouchEnd(int touchId, IntVector2 intVector2) + /// + /// Deactivate game state. + /// + public virtual void Deactivate() { } - public virtual void HandleMouseMove(IntVector2 intVector2, int buttons, int qualifiers) + /// + /// Handle state update. + /// + /// Update time step in seconds. + public virtual void Update(float timeStep) { } - public virtual void HandleMouseDown(int button, IntVector2 inputMousePosition, int buttons, int qualifiers) + /// + /// Start interaction. + /// + /// Interaction key. + /// Interaction position. + public virtual void StartInteraction(InteractionKey interactionKey, IntVector2 interactionPosition) { } - public virtual void HandleMouseUp(int button, IntVector2 inputMousePosition, int buttons, int qualifiers) + /// + /// End interaction. + /// + /// Interaction key. + /// Interaction position. + public virtual void EndInteraction(InteractionKey interactionKey, IntVector2 interactionPosition) { + } - public virtual void Activate() + /// + /// Cancel interaction. + /// + /// Interaction key. + /// Interaction position. + public virtual void CancelInteraction(InteractionKey interactionKey, IntVector2 interactionPosition) { } - public virtual void Deactivate() + /// + /// Update interaction. + /// + /// Interaction key. + /// Interaction position. + public virtual void UpdateInteraction(InteractionKey interactionKey, IntVector2 interactionPosition) { } - public virtual void Update(float timeStep) + /// + /// Handle direction pad. + /// + /// Direction pad position. Only left, right, up or down are valid arguments here. + public virtual void HandleDirectionPad(HatPosition direction) { } } diff --git a/RbfxTemplate/GameStates/StatePointerInteractionManager.cs b/RbfxTemplate/GameStates/StatePointerInteractionManager.cs new file mode 100644 index 0000000..fb2e46e --- /dev/null +++ b/RbfxTemplate/GameStates/StatePointerInteractionManager.cs @@ -0,0 +1,155 @@ +using Urho3DNet; + +namespace RbfxTemplate.GameStates +{ + /// + /// Class to manage touch and mouse interactions. + /// + /// Interaction starts when you press mouse button or touch screen. + /// It ends when you release mouse button or remove finger from screen. + /// Mouse interaction may be canceled when you press another mouse button. + /// For mouse interactions the qualifier is "sticky", i.e. it will keep value it had when interaction started even if you release a key. + /// + public class StatePointerInteractionManager + { + /// + /// Current game state. + /// + private StateBase _state; + + /// + /// Get or set current game state. + /// + public StateBase State + { + get => _state; + set + { + if (_state != value) + { + _state?.Deactivate(); + _state = value; + _state?.Activate(); + } + } + } + + /// + /// Key of an active interaction. + /// + private InteractionKey? _activeInteractionKey = null; + + + /// + /// Last known mouse cursor position. + /// + public IntVector2 LastKnownMousePosition { get; set; } + + /// + /// Handle touch begin event. + /// + /// Event arguments. + public void HandleTouchBegin(VariantMap args) + { + var touchId = args[E.TouchBegin.TouchID].Int; + var x = args[E.TouchBegin.X].Int; + var y = args[E.TouchBegin.Y].Int; + _state.StartInteraction(new InteractionKey(touchId), new IntVector2(x, y)); + } + + /// + /// Handle touch end event. + /// + /// Event arguments. + public void HandleTouchEnd(VariantMap args) + { + var touchId = args[E.TouchEnd.TouchID].Int; + var x = args[E.TouchEnd.X].Int; + var y = args[E.TouchEnd.Y].Int; + _state.EndInteraction(new InteractionKey(touchId), new IntVector2(x, y)); + } + + /// + /// Handle touch move event. + /// + /// Event arguments. + public void HandleTouchMove(VariantMap args) + { + var touchId = args[E.TouchMove.TouchID].Int; + var x = args[E.TouchMove.X].Int; + var y = args[E.TouchMove.Y].Int; + _state.UpdateInteraction(new InteractionKey(touchId), new IntVector2(x, y)); + } + + /// + /// Handle mouse move event. + /// + /// Event arguments. + public void HandleMouseMove(VariantMap args) + { + var buttons = (MouseButton)args[E.MouseMove.Buttons].Int; + var qualifiers = (Qualifier)args[E.MouseMove.Qualifiers].Int; + var x = args[E.MouseMove.X].Int; + var y = args[E.MouseMove.Y].Int; + LastKnownMousePosition = new IntVector2(x, y); + var interactionKey = new InteractionKey(buttons, _activeInteractionKey?.Qualifiers ?? qualifiers); + RestartMouseInteractionIfNecessary(interactionKey); + _state.UpdateInteraction(interactionKey, LastKnownMousePosition); + } + + /// + /// Handle mouse button up event. + /// + /// Event arguments. + public void HandleMouseUp(VariantMap args) + { + var buttons = (MouseButton)args[E.MouseButtonUp.Buttons].Int; + var qualifiers = (Qualifier)args[E.MouseButtonUp.Qualifiers].Int; + var button = (MouseButton)args[E.MouseButtonUp.Button].Int; + _state.EndInteraction(new InteractionKey(buttons | button, _activeInteractionKey?.Qualifiers ?? qualifiers), LastKnownMousePosition); + _activeInteractionKey = null; + } + + /// + /// Handle mouse button down event. + /// + /// Event arguments. + public void HandleMouseDown(VariantMap args) + { + var buttons = (MouseButton)args[E.MouseButtonDown.Buttons].Int; + var qualifiers = (Qualifier)args[E.MouseButtonDown.Qualifiers].Int; + var button = args[E.MouseButtonDown.Button].Int; + var interactionKey = new InteractionKey(buttons, _activeInteractionKey?.Qualifiers ?? qualifiers); + RestartMouseInteractionIfNecessary(interactionKey); + _activeInteractionKey = interactionKey; + _state.StartInteraction(interactionKey, LastKnownMousePosition); + } + + /// + /// Handle directional pad event. + /// + /// Dpad direction. + public void HandleDpad(HatPosition direction) + { + _state.HandleDirectionPad(direction); + } + + /// + /// Cancel active events and start new one if mouse event changed. + /// + /// New mouse interaction key. + private void RestartMouseInteractionIfNecessary(InteractionKey newKey) + { + if (!_activeInteractionKey.HasValue) + return; + + var oldKey = _activeInteractionKey.Value; + if (oldKey.SourceId != newKey.SourceId || oldKey.Qualifiers != newKey.Qualifiers) + { + _state.CancelInteraction(oldKey, LastKnownMousePosition); + _activeInteractionKey = null; + } + } + + } +} \ No newline at end of file diff --git a/RbfxTemplate/GameStates/VictoryState.cs b/RbfxTemplate/GameStates/VictoryState.cs index 23e0aa6..3b93b88 100644 --- a/RbfxTemplate/GameStates/VictoryState.cs +++ b/RbfxTemplate/GameStates/VictoryState.cs @@ -8,15 +8,9 @@ public VictoryState(GameState game) : base(game) { } - public override void HandleMouseUp(int button, IntVector2 inputMousePosition, int buttons, int qualifiers) + /// + public override void EndInteraction(InteractionKey interactionKey, IntVector2 interactionPosition) { - base.HandleMouseUp(button, inputMousePosition, buttons, qualifiers); - //Game.NextLevel(); - } - - public override void HandleTouchEnd(int touchId, IntVector2 intVector2) - { - base.HandleTouchEnd(touchId, intVector2); //Game.NextLevel(); } } diff --git a/RbfxTemplate/RmlUIStateBase.cs b/RbfxTemplate/RmlUIStateBase.cs index 971110e..12c03f0 100644 --- a/RbfxTemplate/RmlUIStateBase.cs +++ b/RbfxTemplate/RmlUIStateBase.cs @@ -1,17 +1,26 @@ -using Urho3DNet; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Urho3DNet; namespace RbfxTemplate { /// /// Base class for a game state with RmlUI document. /// - public abstract class RmlUIStateBase : ApplicationState + public abstract class RmlUIStateBase : ApplicationState, INotifyPropertyChanged { private readonly SharedPtr _scene; private readonly GameRmlUIComponent _uiComponent; /// - /// Construct RmlUIStateBase. + /// Occurs when a property value changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + + /// + /// Construct RmlUIStateBase. /// /// Application instance. /// Path to RmlUI document resource. @@ -83,5 +92,20 @@ private void HandleKeyUp(VariantMap args) return; } } + + protected void SetRmlVariable(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + + protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } } \ No newline at end of file