Avoiding the "new" keyword in C# to aid read-ability

Started by
5 comments, last by Telastyn 13 years, 9 months ago
Hi all,

I've just moved back to some C#/XNA work and am bolstering up my input systems. A system I am adding is the ability to register key combinations (combinations of keyboard keys, mouse buttons, gamepad buttons) with appropriate logic (and, or) and then query the input system for whether that combination is true.

Here's an example, a little app makes the screen go black when:

- The G AND F keys are both down at the same time OR
- The frame that the left mouse button is clicked on OR
- the frame that the right mouse button is release while the W key is held down

To register this named event I write:

[source language="c#"]            input.CreateNamedInputEvent("Blackscreen",                new BIEOr(                    new BIEAnd(new BIEDown(Keys.G), new BIEDown(Keys.F)),                     new BIEPressed(MouseButtons.LeftButton),                     new BIEAnd(new BIEDown(Keys.W), new BIEReleased(MouseButtons.RightButton))));


And then to query I simply use:
[source language="c#"]            if (input.QueryNamedInputEvent("Blackscreen"))                clearColor = Color.Black;            else                clearColor = Color.CornflowerBlue;


Everything works fine, I was just wandering if there is a clean and easy way to avoid the "new" keyword in order to make the combinations easier on the eye.

Thanks, Scott
Game development blog and portfolio: http://gamexcore.co.uk
Advertisement
Nicest way to do that would be a small parser, so that your code could look like
input.CreateNamedInputEvent("Blackscreen", "(G.Down && F.Down) || LB.Pressed || (W.Down && RB.Down)"). Never done parsers in C#, so I have no idea what framework is available for you, but a simple recursive descent parser with inline lexing would be straightforward to implement, particularly if you didn't care about operator precedence. Alternatively, you can abuse operator overloading a little, so that at least you could rewrite it as
input.CreateNamedInputEvent("Blackscreen", (new BIEDown(Keys.G) && new BIEDown(Keys.F)) || new BIEPressed(MouseButtons.LeftButton) || (new BIEDown(Keys.W) && new BIEReleased(MouseButtons.RightButton))).
wow, fast reply, and good, thankyou.

I ruled out the parsing method because i decided that having a bullet-proof parsing method that could handle anything was going to be a mammoth task prone to error, not to mention little bugs that could arise from typing the string slightly wrong.

The second method looks rather interesting tho, I'm not sure how operater overloading could be done in this way?
Game development blog and portfolio: http://gamexcore.co.uk
While you could I'm not necessarily certain you should. Creating many small instances is going to be a performance issue, and leaving the batch of new will aid in identifying them.

That said:

I'd aim for extension methods (or perhaps plain old base class methods).

Override the key (might need to change their implementation to do this) and provide or/and methods on the event command objects to do composition. Those or/and methods could also be provided as operator overloads like Sneftel suggests (though iirc, && and || are not overridable, so you'd need to use & and |).

(Keys.G.DownEvent & Keys.F.DownEvent)|MouseButtons.Left.ClickEvent|(Keys.W.DownEvent & MouseButtons.Right.ReleaseEvent)


Or, abusing operator overloading more, you could make quake-style prefixes. ie:
+mouse1 // mouse down.-Keys.W // W up, etc.


I think that might do more harm than good though.

Anyways, I did something akin to this for the Tangent Parser Framework to combine snippets of smaller parsers into larger ones. It was designed during the .NET 2 days, so no extension methods, but...
How about using lambda expressions?
Quote:Original post by Shinkage
How about using lambda expressions?

This. When you're hard coding and duplicating the terminology of the programming language (and, or, etc), that's a big indicator to me that you can simplify things by just using the language itself.

For example, we could set up a system that would look like:

input.CreateNamedInputEvent( "Blackscreen", s => {     return ( s.IsDown(Keys.G) && s.IsDown(Keys.F) )         || ( s.IsPressed(MouseButtons.LeftButton) )         || ( s.IsDown(Keys.W) && s.IsReleased(MouseButtons.RightButton) )         ;});


I personally use an even simpler system -- I have a list of 'trigger's, each with a list of XInput buttons and keyboard buttons. If any are pressed, the trigger's member, .Value, is set to true. If it's a once off action (like spawning an asteroid in debug mode), it's reset to false by the game logic. If it's a continuous action like firing, it just stays set.

If all the relevant keys and buttons are released, then the .Value is also set to false.

Using the system is pretty simple:
	class AsteroidsInputBindings : InputBindings {		public readonly InputAxis			Thrust   = new InputAxis("Thrust")				{ Positive = new InputTrigger("Forward" ) { Keys = { Keys.Up  , Keys.W } }				, Negative = new InputTrigger("Backward") { Keys = { Keys.Down, Keys.S } }				, XInput = pad => pad.GetNormalizedLeftThumbY()				},			Rotation = new InputAxis("Rotation")				{ Positive = new InputTrigger("Left" ) { Keys = { Keys.Left , Keys.A } }				, Negative = new InputTrigger("Right") { Keys = { Keys.Right, Keys.D } }				, XInput = pad => -pad.GetNormalizedLeftThumbX()				},			Strafe   = new InputAxis("Strafe")				{ Negative = new InputTrigger("Strafe Left" ) { Keys = { Keys.Q } }				, Positive = new InputTrigger("Strafe Right") { Keys = { Keys.E } }				, XInput = pad => ((int)pad.RightTrigger-(int)pad.LeftTrigger)/255f				};		public readonly InputTrigger			Fire     = new InputTrigger("Fire"    ) { Keys = { Keys.Z, Keys.ControlKey }, Buttons = GamepadButtonFlags.A     },			Teleport = new InputTrigger("Teleport") { Keys = { Keys.X, Keys.Space      }, Buttons = GamepadButtonFlags.B     },			Suicide  = new InputTrigger("Suicide" ) { Keys = { Keys.C, Keys.Escape     }, Buttons = GamepadButtonFlags.Start },			Respawn  = new InputTrigger("Respawn" ) { Keys = { Keys.Z, Keys.ControlKey }, Buttons = GamepadButtonFlags.Start|GamepadButtonFlags.A },			DebugSpawnAsteroid = new InputTrigger("Debug: Spawn Asteroid") { Keys = { Keys.F1 } },			DebugReset         = new InputTrigger("Debug: Reset Worst")    { Keys = { Keys.F2 } };	}

Of note, I don't actually use the trigger string names at all right now -- I just directly use myinputsystem.Fire.Value, for example, which has the wonderful effect of compile-time checking for typos. InputBindings abuses reflection to make things just work (tm).

Full source (sorry for the terse and poorly commented code). Feel free to steal/abuse/ignore as you please:
using System;using System.Collections.Generic;using System.Linq;using System.Reflection;using System.Runtime.InteropServices;using System.Windows.Forms;using SlimDX.XInput;using Key = System.Windows.Forms.Keys; // Used exclusively to initialize KeyOverridesnamespace Asteroids {	class InputTrigger {		public InputTrigger( string name ) {}		public readonly List<Keys> Keys = new List<Keys>();		public GamepadButtonFlags Buttons { get; set; }		public bool Value { get; set; }		static readonly Dictionary<Keys,string> KeyOverrides = new Dictionary<Keys,string>()			{ { Key.NumPad0, "Numpad 0" }			, { Key.NumPad1, "Numpad 1" }, { Key.NumPad2, "Numpad 2" }, { Key.NumPad3, "Numpad 3" }			, { Key.NumPad4, "Numpad 4" }, { Key.NumPad5, "Numpad 5" }, { Key.NumPad6, "Numpad 6" }			, { Key.NumPad7, "Numpad 7" }, { Key.NumPad8, "Numpad 8" }, { Key.NumPad9, "Numpad 9" }			, { Key.ControlKey ,       "Control" }, { Key.ShiftKey ,       "Shift" }, { Key.Menu ,       "Alt" }			, { Key.LControlKey,  "Left Control" }, { Key.LShiftKey,  "Left Shift" }, { Key.LMenu,  "Left Alt" }			, { Key.RControlKey, "Right Control" }, { Key.RShiftKey, "Right Shift" }, { Key.RMenu, "Right Alt" }			};		public string BindingDescription { get {			var keys = Keys.Select(k=>KeyOverrides.ContainsKey(k)?KeyOverrides[k]:Enum.GetName(typeof(Keys),k));			var buttons = Enum.GetValues(typeof(GamepadButtonFlags)).Cast<GamepadButtonFlags>().Where(b=>(b&Buttons)!=GamepadButtonFlags.None).Select(b=>"("+Enum.GetName(typeof(GamepadButtonFlags),b)+")");			var joined = keys.Concat(buttons).ToArray();			switch ( joined.Length ) {			case 0: return "N/A";			case 1: return joined[0];			case 2: return joined[0] + " or " + joined[1];			default: return string.Join(", ",joined.Take(joined.Length-1).ToArray()) + ", or " + joined.Last();			}		}}	}	class InputAxis {		public InputAxis( string name ) {}		public InputTrigger Negative { get; set; }		public InputTrigger Positive { get; set; }		public Func<Gamepad,float> XInput { get; set; }		public float Value { get; set; }	}	class InputBindings {		private readonly Dictionary<Keys,bool> HeldKeys = new Dictionary<Keys,bool>();		Gamepad PreviousGamepad;		private static readonly BindingFlags BindingFlags = BindingFlags.FlattenHierarchy|BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance|BindingFlags.Static|BindingFlags;		public IEnumerable<InputAxis> Axises { get {			var fields = this				.GetType()				.GetFields(BindingFlags)				.Where(p=>typeof(InputAxis).IsAssignableFrom(p.FieldType))				.Select(p=>(InputAxis)p.GetValue(this))				;			var properties = this				.GetType()				.GetProperties(BindingFlags)				.Where(p=>typeof(InputAxis).IsAssignableFrom(p.PropertyType))				.Select(p=>(InputAxis)p.GetValue(this,null))				;			return fields.Concat(properties).Distinct();		}}		public IEnumerable<InputTrigger> SimpleTriggers { get {			var fields = this				.GetType()				.GetFields(BindingFlags)				.Where(p=>typeof(InputTrigger).IsAssignableFrom(p.FieldType))				.Select(p=>(InputTrigger)p.GetValue(this))				;			var properties = this				.GetType()				.GetProperties(BindingFlags)				.Where(p=>typeof(InputTrigger).IsAssignableFrom(p.PropertyType))				.Select(p=>(InputTrigger)p.GetValue(this,null))				;			return fields.Concat(properties).Distinct();		}}		public IEnumerable<InputTrigger> AllTriggers { get {			return SimpleTriggers				.Concat(Axises.Select(a=>a.Positive))				.Concat(Axises.Select(a=>a.Negative))				.Distinct()				;		}}		void UpdateAllTriggersAndAxises() {			// release trigger if all buttons released:			foreach ( var t in AllTriggers ) if ( t.Value && ((t.Buttons&PreviousGamepad.Buttons) == GamepadButtonFlags.None) && !t.Keys.Any(k=>HeldKeys.ContainsKey(k)&&HeldKeys[k]) ) t.Value = false;			foreach ( var a in Axises ) {				a.Value = a.XInput!=null ? a.XInput(PreviousGamepad) : 0f;				// Use trigger values (keys, buttons) for axis value if XInput controller is deadzoned or unavailable:				if ( a.Value == 0f ) a.Value = (a.Positive.Value == a.Negative.Value) ? 0f : a.Negative.Value ? -1f : +1f;			}		}		// DO NOT USE:		// [DllImport("user32.dll")] private static extern short GetAsyncKeyState(Keys key);		// "Although the least significant bit of the return value indicates whether the key has been pressed since the		//    last query, due to the pre-emptive multitasking nature of Windows, another application can call GetAsyncKeyState		//    and receive the "recently pressed" bit instead of your application."		// Source: http://msdn.microsoft.com/en-us/library/ms646293(VS.85).aspx		/// <summary>		/// Read the 256 key states		/// </summary>		/// <param name="lpKeyState">An array of at least 256 entries</param>		[DllImport("user32.dll", SetLastError=true)] static extern bool GetKeyboardState(byte [] lpKeyState); // TODO:  Wrap in an precondition enforcer?		static readonly Keys[] SpecialKeys = new[] { Keys.ControlKey, Keys.LControlKey, Keys.RControlKey, Keys.ShiftKey, Keys.LShiftKey, Keys.RShiftKey, Keys.Menu, Keys.LMenu, Keys.RMenu };		public void UpdateKeyboard( Keys key, bool down ) {			switch ( key ) {			case Keys.ControlKey:			case Keys.ShiftKey:			case Keys.Menu: // Alt				// This is a work around the fact that multiple physical keys have this enumeration, causing				// the sequence of down (key 1), down (key 2), up (either) to occur while still having one button held.				// We do this by using GetKeyboardState instead of assuming up means all instances are up.				//				// The alternative of counting ups and downs is not recommended due to focus change issues.				var keys = new byte[256];				if (!GetKeyboardState(keys)) throw new Exception( "Some sort of GetKeyboardState failure not reported by GetLastError" );				foreach ( var sk in SpecialKeys ) UpdateKeyboardRaw( sk, (keys[(int)sk]&0x80)!=0 );				break;			default:				UpdateKeyboardRaw(key,down);				break;			}		}		void UpdateKeyboardRaw( Keys key, bool down ) {			if (!HeldKeys.ContainsKey(key)) HeldKeys.Add(key,false);			bool previous = HeldKeys[key];			HeldKeys[key] = down;			if ( down && previous ) return; // ignore duplicate keypresses			if ( down ) foreach ( var t in AllTriggers ) if ( t.Keys.Contains(key) ) t.Value = true;			UpdateAllTriggersAndAxises();		}		public void ClearGamepad() {			PreviousGamepad = default(Gamepad);			UpdateAllTriggersAndAxises();		}		public void UpdateGamepad( Gamepad gamepad ) {			var newly_pressed = gamepad.Buttons & ~PreviousGamepad.Buttons;			PreviousGamepad = gamepad;			foreach ( var t in AllTriggers ) if ( (t.Buttons&newly_pressed) != GamepadButtonFlags.None ) t.Value = true;			UpdateAllTriggersAndAxises();		}	}	class AsteroidsInputBindings : InputBindings {		public readonly InputAxis			Thrust   = new InputAxis("Thrust")				{ Positive = new InputTrigger("Forward" ) { Keys = { Keys.Up  , Keys.W } }				, Negative = new InputTrigger("Backward") { Keys = { Keys.Down, Keys.S } }				, XInput = pad => pad.GetNormalizedLeftThumbY()				},			Rotation = new InputAxis("Rotation")				{ Positive = new InputTrigger("Left" ) { Keys = { Keys.Left , Keys.A } }				, Negative = new InputTrigger("Right") { Keys = { Keys.Right, Keys.D } }				, XInput = pad => -pad.GetNormalizedLeftThumbX()				},			Strafe   = new InputAxis("Strafe")				{ Negative = new InputTrigger("Strafe Left" ) { Keys = { Keys.Q } }				, Positive = new InputTrigger("Strafe Right") { Keys = { Keys.E } }				, XInput = pad => ((int)pad.RightTrigger-(int)pad.LeftTrigger)/255f				};		public readonly InputTrigger			Fire     = new InputTrigger("Fire"    ) { Keys = { Keys.Z, Keys.ControlKey }, Buttons = GamepadButtonFlags.A     },			Teleport = new InputTrigger("Teleport") { Keys = { Keys.X, Keys.Space      }, Buttons = GamepadButtonFlags.B     },			Suicide  = new InputTrigger("Suicide" ) { Keys = { Keys.C, Keys.Escape     }, Buttons = GamepadButtonFlags.Start },			Respawn  = new InputTrigger("Respawn" ) { Keys = { Keys.Z, Keys.ControlKey }, Buttons = GamepadButtonFlags.Start|GamepadButtonFlags.A },			DebugSpawnAsteroid = new InputTrigger("Debug: Spawn Asteroid") { Keys = { Keys.F1 } },			DebugReset         = new InputTrigger("Debug: Reset Worst")    { Keys = { Keys.F2 } };	}}


[Edited by - MaulingMonkey on July 10, 2010 1:16:09 AM]
Quote:Original post by Shinkage
How about using lambda expressions?


While that solves the problem of processing the event, the handlers often need to have more information (like some textual representation for the control config screen, 'how long have I been pressed', a reference to current gamestate). A command + composite pattern might be better depending on your needs.

This topic is closed to new replies.

Advertisement