Merge pull request #126 from mrpimpunicorn/master

Added event-driven keyboard encapsulation
This commit is contained in:
Drew DeVault 2015-06-14 10:43:35 -06:00
commit bd8f649264
10 changed files with 315 additions and 90 deletions

View File

@ -0,0 +1,104 @@
using System;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
namespace TrueCraft.Client.Input
{
/// <summary>
/// Encapsulates keyboard input in an event-driven manner.
/// </summary>
public sealed class KeyboardComponent : GameComponent
{
/// <summary>
/// Raised when a key for this keyboard component is pressed.
/// </summary>
public event EventHandler<KeyboardKeyEventArgs> KeyDown;
/// <summary>
/// Raised when a key for this keyboard component is released.
/// </summary>
public event EventHandler<KeyboardKeyEventArgs> KeyUp;
/// <summary>
/// Gets the state for this keyboard component.
/// </summary>
public KeyboardState State { get; private set; }
/// <summary>
/// Creates a new keyboard component.
/// </summary>
/// <param name="game">The parent game for the component.</param>
public KeyboardComponent(Game game)
: base(game)
{
}
/// <summary>
/// Initializes this keyboard component.
/// </summary>
public override void Initialize()
{
State = Keyboard.GetState();
base.Initialize();
}
/// <summary>
/// Updates this keyboard component.
/// </summary>
/// <param name="gameTime">The game time for the update.</param>
public override void Update(GameTime gameTime)
{
var newState = Keyboard.GetState();
Process(newState, State);
State = newState;
base.Update(gameTime);
}
/// <summary>
/// Processes a change between two states.
/// </summary>
/// <param name="newState">The new state.</param>
/// <param name="oldState">The old state.</param>
private void Process(KeyboardState newState, KeyboardState oldState)
{
var currentKeys = newState.GetPressedKeys();
var lastKeys = oldState.GetPressedKeys();
// LINQ was a saviour here.
var pressed = currentKeys.Except(lastKeys);
var unpressed = lastKeys.Except(currentKeys);
foreach (var key in pressed)
{
var args = new KeyboardKeyEventArgs(key, true);
if (KeyDown != null)
KeyDown(this, args);
}
foreach (var key in unpressed)
{
var args = new KeyboardKeyEventArgs(key, false);
if (KeyUp != null)
KeyUp(this, args);
}
}
/// <summary>
/// Called when this keyboard component is being disposed of.
/// </summary>
/// <param name="disposing">Whether Dispose() called this method.</param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
KeyDown = null;
KeyUp = null;
}
base.Dispose(disposing);
}
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace TrueCraft.Client.Input
{
/// <summary>
/// Provides the event data for keyboard events.
/// </summary>
public class KeyboardEventArgs : EventArgs
{
}
}

View File

@ -0,0 +1,32 @@
using System;
using Microsoft.Xna.Framework.Input;
namespace TrueCraft.Client.Input
{
/// <summary>
/// Provides the event data for keyboard key events.
/// </summary>
public class KeyboardKeyEventArgs : KeyboardEventArgs
{
/// <summary>
/// Gets the key for the event.
/// </summary>
public Keys Key { get; private set; }
/// <summary>
/// Gets whether the key was pressed or released.
/// </summary>
public bool IsPressed { get; private set; }
/// <summary>
/// Creates new keyboard key event data.
/// </summary>
/// <param name="key">The key for the event.</param>
/// <param name="isPressed">Whether the key was pressed or released.</param>
public KeyboardKeyEventArgs(Keys key, bool isPressed)
{
Key = key;
IsPressed = isPressed;
}
}
}

View File

@ -3,27 +3,27 @@
namespace TrueCraft.Client.Input
{
/// <summary>
///
/// Provides the event data for mouse button events.
/// </summary>
public class MouseButtonEventArgs : MouseEventArgs
{
/// <summary>
///
/// Gets the mouse button for the event.
/// </summary>
public MouseButton Button { get; private set; }
/// <summary>
///
/// Gets whether the button was pressed or released.
/// </summary>
public bool IsPressed { get; private set; }
/// <summary>
///
/// Creates new mouse button event data.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="button"></param>
/// <param name="isPressed"></param>
/// <param name="x">The X coordinate for the event.</param>
/// <param name="y">The Y coordinate for the event.</param>
/// <param name="button">The mouse button for the event.</param>
/// <param name="isPressed">Whether the button was pressed or released.</param>
public MouseButtonEventArgs(int x, int y, MouseButton button, bool isPressed)
: base(x, y)
{

View File

@ -7,25 +7,25 @@ namespace TrueCraft.Client.Input
/// <summary>
/// Encapsulates mouse input in an event-driven manner.
/// </summary>
public class MouseComponent : GameComponent
public sealed class MouseComponent : GameComponent
{
/// <summary>
///
/// Raised when this mouse component is moved.
/// </summary>
public event EventHandler<MouseMoveEventArgs> Move;
/// <summary>
///
/// Raised when a button for this mouse component is pressed.
/// </summary>
public event EventHandler<MouseButtonEventArgs> ButtonDown;
/// <summary>
///
/// Raised when a button for this mouse component is released.
/// </summary>
public event EventHandler<MouseButtonEventArgs> ButtonUp;
/// <summary>
///
/// Raised when the scroll wheel for this mouse component is moved.
/// </summary>
public event EventHandler<MouseScrollEventArgs> Scroll;
@ -37,7 +37,7 @@ namespace TrueCraft.Client.Input
/// <summary>
/// Creates a new mouse component.
/// </summary>
/// <param name="game"></param>
/// <param name="game">The parent game for the component.</param>
public MouseComponent(Game game)
: base(game)
{
@ -56,7 +56,7 @@ namespace TrueCraft.Client.Input
/// <summary>
/// Updates this mouse component.
/// </summary>
/// <param name="gameTime"></param>
/// <param name="gameTime">The game time for the update.</param>
public override void Update(GameTime gameTime)
{
var newState = Mouse.GetState();
@ -69,22 +69,22 @@ namespace TrueCraft.Client.Input
/// <summary>
/// Processes a change between two states.
/// </summary>
/// <param name="newState"></param>
/// <param name="last"></param>
private void Process(MouseState current, MouseState last)
/// <param name="newState">The new state.</param>
/// <param name="oldState">The old state.</param>
private void Process(MouseState newState, MouseState oldState)
{
// Movement.
if ((current.X != last.X) || (current.Y != last.Y))
if ((newState.X != oldState.X) || (newState.Y != oldState.Y))
{
var args = new MouseMoveEventArgs(current.X, current.Y, (current.X - last.X), (current.Y - last.Y));
var args = new MouseMoveEventArgs(newState.X, newState.Y, (newState.X - oldState.X), (newState.Y - oldState.Y));
if (Move != null)
Move(this, args);
}
// Scrolling.
if (current.ScrollWheelValue != last.ScrollWheelValue)
if (newState.ScrollWheelValue != oldState.ScrollWheelValue)
{
var args = new MouseScrollEventArgs(current.X, current.Y, current.ScrollWheelValue, (current.ScrollWheelValue - last.ScrollWheelValue));
var args = new MouseScrollEventArgs(newState.X, newState.Y, newState.ScrollWheelValue, (newState.ScrollWheelValue - oldState.ScrollWheelValue));
if (Scroll != null)
Scroll(this, args);
}
@ -92,9 +92,9 @@ namespace TrueCraft.Client.Input
// A bit of code duplication here, shame XNA doesn't expose button state through an enumeration...
// Left button.
if (current.LeftButton != last.LeftButton)
if (newState.LeftButton != oldState.LeftButton)
{
var args = new MouseButtonEventArgs(current.X, current.Y, MouseButton.Left, (current.LeftButton == ButtonState.Pressed));
var args = new MouseButtonEventArgs(newState.X, newState.Y, MouseButton.Left, (newState.LeftButton == ButtonState.Pressed));
if (args.IsPressed)
{
if (ButtonDown != null)
@ -108,9 +108,9 @@ namespace TrueCraft.Client.Input
}
// Right button.
if (current.RightButton != last.RightButton)
if (newState.RightButton != oldState.RightButton)
{
var args = new MouseButtonEventArgs(current.X, current.Y, MouseButton.Right, (current.RightButton == ButtonState.Pressed));
var args = new MouseButtonEventArgs(newState.X, newState.Y, MouseButton.Right, (newState.RightButton == ButtonState.Pressed));
if (args.IsPressed)
{
if (ButtonDown != null)
@ -124,9 +124,9 @@ namespace TrueCraft.Client.Input
}
// Middle button.
if (current.MiddleButton != last.MiddleButton)
if (newState.MiddleButton != oldState.MiddleButton)
{
var args = new MouseButtonEventArgs(current.X, current.Y, MouseButton.Middle, (current.MiddleButton == ButtonState.Pressed));
var args = new MouseButtonEventArgs(newState.X, newState.Y, MouseButton.Middle, (newState.MiddleButton == ButtonState.Pressed));
if (args.IsPressed)
{
if (ButtonDown != null)
@ -141,15 +141,18 @@ namespace TrueCraft.Client.Input
}
/// <summary>
/// Disposes of this mouse component.
/// Called when this mouse component is being disposed of.
/// </summary>
/// <param name="disposing"></param>
/// <param name="disposing">Whether Dispose() called this method.</param>
protected override void Dispose(bool disposing)
{
Move = null;
ButtonDown = null;
ButtonUp = null;
Scroll = null;
if (disposing)
{
Move = null;
ButtonDown = null;
ButtonUp = null;
Scroll = null;
}
base.Dispose(disposing);
}

View File

@ -3,25 +3,25 @@
namespace TrueCraft.Client.Input
{
/// <summary>
///
/// Provides the event data for mouse events.
/// </summary>
public class MouseEventArgs : EventArgs
{
/// <summary>
///
/// Gets the X coordinate for the event.
/// </summary>
public int X { get; private set; }
/// <summary>
///
/// Gets the Y coordinate for the event.
/// </summary>
public int Y { get; private set; }
/// <summary>
///
/// Creates new mouse event data.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="x">The X coordinate for the event.</param>
/// <param name="y">The Y coordinate for the event.</param>
public MouseEventArgs(int x, int y)
{
X = x;

View File

@ -3,27 +3,27 @@
namespace TrueCraft.Client.Input
{
/// <summary>
///
/// Provides the event data for mouse movement events.
/// </summary>
public class MouseMoveEventArgs : MouseEventArgs
{
/// <summary>
///
/// Gets the X coordinate delta for the event.
/// </summary>
public int DeltaX { get; private set; }
/// <summary>
///
/// Gets the Y coordinate delta for the event.
/// </summary>
public int DeltaY { get; private set; }
/// <summary>
///
/// Creates new mouse movement event data.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="deltaX"></param>
/// <param name="deltaY"></param>
/// <param name="x">The X coordinate for the event.</param>
/// <param name="y">The Y coordinate for the event.</param>
/// <param name="deltaX">The X coordinate delta for the event.</param>
/// <param name="deltaY">The Y coordinate delta for the event.</param>
public MouseMoveEventArgs(int x, int y, int deltaX, int deltaY)
: base(x, y)
{

View File

@ -3,27 +3,27 @@
namespace TrueCraft.Client.Input
{
/// <summary>
///
/// Provides the event data for mouse scroll events.
/// </summary>
public class MouseScrollEventArgs : MouseEventArgs
{
/// <summary>
///
/// Gets the scroll value for the event.
/// </summary>
public int Value { get; private set; }
/// <summary>
///
/// Gets the scroll value delta for the event.
/// </summary>
public int DeltaValue { get; private set; }
/// <summary>
///
/// Creates new mouse scroll event data.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="value"></param>
/// <param name="deltaValue"></param>
/// <param name="x">The X coordinate for the event.</param>
/// <param name="y">The Y coordinate for the event.</param>
/// <param name="value">The scroll value for the event.</param>
/// <param name="deltaValue">The scroll value delta for the event.</param>
public MouseScrollEventArgs(int x, int y, int value, int deltaValue)
: base(x, y)
{

View File

@ -94,6 +94,9 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Input\KeyboardComponent.cs" />
<Compile Include="Input\KeyboardEventArgs.cs" />
<Compile Include="Input\KeyboardKeyEventArgs.cs" />
<Compile Include="Input\MouseButton.cs" />
<Compile Include="Input\MouseButtonEventArgs.cs" />
<Compile Include="Input\MouseComponent.cs" />

View File

@ -37,9 +37,10 @@ namespace TrueCraft.Client
private BoundingFrustum CameraView;
private Camera Camera;
private bool MouseCaptured;
private KeyboardState PreviousKeyboardState;
private KeyboardComponent KeyboardComponent { get; set; }
private MouseComponent MouseComponent { get; set; }
private GameTime GameTime { get; set; }
private Microsoft.Xna.Framework.Vector3 Delta { get; set; }
private BasicEffect OpaqueEffect, TransparentEffect;
@ -61,6 +62,10 @@ namespace TrueCraft.Client
PendingMainThreadActions = new ConcurrentBag<Action>();
MouseCaptured = true;
var keyboardComponent = new KeyboardComponent(this);
KeyboardComponent = keyboardComponent;
Components.Add(keyboardComponent);
var mouseComponent = new MouseComponent(this);
MouseComponent = mouseComponent;
Components.Add(mouseComponent);
@ -83,9 +88,10 @@ namespace TrueCraft.Client
Mouse.SetPosition(centerX, centerY);
Camera = new Camera(GraphicsDevice.Viewport.AspectRatio, 70.0f, 0.1f, 1000.0f);
UpdateCamera();
PreviousKeyboardState = Keyboard.GetState();
Window.ClientSizeChanged += (sender, e) => CreateRenderTarget();
MouseComponent.Move += OnMouseComponentMove;
KeyboardComponent.KeyDown += OnKeyboardKeyDown;
KeyboardComponent.KeyUp += OnKeyboardKeyUp;
CreateRenderTarget();
}
@ -151,46 +157,83 @@ namespace TrueCraft.Client
base.OnExiting(sender, args);
}
protected virtual void UpdateKeyboard(GameTime gameTime, KeyboardState state, KeyboardState oldState)
private void OnKeyboardKeyDown(object sender, KeyboardKeyEventArgs e)
{
if (state.IsKeyDown(Keys.Escape))
Exit();
// TODO: Rebindable keys
// TODO: Horizontal terrain collisions
if (state.IsKeyDown(Keys.F2) && oldState.IsKeyUp(Keys.F2)) // Take a screenshot
switch (e.Key)
{
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".truecraft", "screenshots", DateTime.Now.ToString("yyyy-MM-dd_H.mm.ss") + ".png");
if (!Directory.Exists(Path.GetDirectoryName(path)))
Directory.CreateDirectory(Path.GetDirectoryName(path));
using (var stream = File.OpenWrite(path))
RenderTarget.SaveAsPng(stream, RenderTarget.Width, RenderTarget.Height);
ChatInterface.AddMessage(string.Format("Screenshot saved as {0}", Path.GetFileName(path)));
// Quit the game.
case Keys.Escape:
Exit();
break;
// Take a screenshot.
case Keys.F2:
TakeScreenshot();
break;
// Move to the left.
case Keys.A:
case Keys.Left:
Delta += Microsoft.Xna.Framework.Vector3.Left;
break;
// Move to the right.
case Keys.D:
case Keys.Right:
Delta += Microsoft.Xna.Framework.Vector3.Right;
break;
// Move forwards.
case Keys.W:
case Keys.Up:
Delta += Microsoft.Xna.Framework.Vector3.Forward;
break;
// Move backwards.
case Keys.S:
case Keys.Down:
Delta += Microsoft.Xna.Framework.Vector3.Backward;
break;
// Toggle mouse capture.
case Keys.Tab:
MouseCaptured = !MouseCaptured;
break;
}
Microsoft.Xna.Framework.Vector3 delta = Microsoft.Xna.Framework.Vector3.Zero;
}
if (state.IsKeyDown(Keys.Left) || state.IsKeyDown(Keys.A))
delta += Microsoft.Xna.Framework.Vector3.Left;
if (state.IsKeyDown(Keys.Right) || state.IsKeyDown(Keys.D))
delta += Microsoft.Xna.Framework.Vector3.Right;
if (state.IsKeyDown(Keys.Up) || state.IsKeyDown(Keys.W))
delta += Microsoft.Xna.Framework.Vector3.Forward;
if (state.IsKeyDown(Keys.Down) || state.IsKeyDown(Keys.S))
delta += Microsoft.Xna.Framework.Vector3.Backward;
if (delta != Microsoft.Xna.Framework.Vector3.Zero)
private void OnKeyboardKeyUp(object sender, KeyboardKeyEventArgs e)
{
switch (e.Key)
{
var lookAt = Microsoft.Xna.Framework.Vector3.Transform(
delta, Matrix.CreateRotationY(MathHelper.ToRadians(Client.Yaw)));
// Stop moving to the left.
case Keys.A:
case Keys.Left:
Delta -= Microsoft.Xna.Framework.Vector3.Left;
break;
Client.Position += new TrueCraft.API.Vector3(lookAt.X, lookAt.Y, lookAt.Z) * (gameTime.ElapsedGameTime.TotalSeconds * 4.3717);
// Stop moving to the right.
case Keys.D:
case Keys.Right:
Delta -= Microsoft.Xna.Framework.Vector3.Right;
break;
// Stop moving forwards.
case Keys.W:
case Keys.Up:
Delta -= Microsoft.Xna.Framework.Vector3.Forward;
break;
// Stop moving backwards.
case Keys.S:
case Keys.Down:
Delta -= Microsoft.Xna.Framework.Vector3.Backward;
break;
}
if (state.IsKeyUp(Keys.Tab) && oldState.IsKeyDown(Keys.Tab))
MouseCaptured = !MouseCaptured;
}
private void OnMouseComponentMove(object sender, MouseMoveEventArgs e)
@ -214,6 +257,17 @@ namespace TrueCraft.Client
}
}
private void TakeScreenshot()
{
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".truecraft", "screenshots", DateTime.Now.ToString("yyyy-MM-dd_H.mm.ss") + ".png");
if (!Directory.Exists(Path.GetDirectoryName(path)))
Directory.CreateDirectory(Path.GetDirectoryName(path));
using (var stream = File.OpenWrite(path))
RenderTarget.SaveAsPng(stream, RenderTarget.Width, RenderTarget.Height);
ChatInterface.AddMessage(string.Format("Screenshot saved as {0}", Path.GetFileName(path)));
}
protected override void Update(GameTime gameTime)
{
GameTime = gameTime;
@ -254,9 +308,16 @@ namespace TrueCraft.Client
Client.Position.Y + MultiplayerClient.Height, Client.Position.Z, Client.Yaw, Client.Pitch, false));
NextPhysicsUpdate = DateTime.Now.AddMilliseconds(1000 / 20);
}
var state = Keyboard.GetState();
UpdateKeyboard(gameTime, state, PreviousKeyboardState);
PreviousKeyboardState = state;
if (Delta != Microsoft.Xna.Framework.Vector3.Zero)
{
var lookAt = Microsoft.Xna.Framework.Vector3.Transform(
Delta, Matrix.CreateRotationY(MathHelper.ToRadians(Client.Yaw)));
Client.Position += new TrueCraft.API.Vector3(lookAt.X, lookAt.Y, lookAt.Z)
* (gameTime.ElapsedGameTime.TotalSeconds * 4.3717);
}
base.Update(gameTime);
}
@ -328,5 +389,16 @@ namespace TrueCraft.Client
base.Draw(gameTime);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
KeyboardComponent.Dispose();
MouseComponent.Dispose();
}
base.Dispose(disposing);
}
}
}