I'm working on some simple console game and I'm starting to get stranded in my modelling. There is some fragmentation and tight coupling going on!
In this version I would just want to be able to move my paddle over the screen (but this needs to be extended to multiple players, ill get back to that later).
This paddle is modeled as follows:
public class Paddle : IDynamicRenderable, ICommandObserver
{
public int Top { get; private set; }
public int Left { get; private set; }
public bool RenderStateChanged { get; private set; }
private int LastTop { get; set; }
private int LastLeft { get; set; }
private Dictionary<string, ConsoleKey> configuration;
public Paddle()
{
Top = LastTop = 10;
Left = LastLeft = 35;
RenderStateChanged = false;
configuration = new Dictionary<string, ConsoleKey>()
{
{ "UpCommand", ConsoleKey.UpArrow },
{ "DownCommand", ConsoleKey.DownArrow },
{ "LeftCommand", ConsoleKey.LeftArrow },
{ "RightCommand", ConsoleKey.RightArrow }
};
Draw();
}
public void Render()
{
Clear();
Draw();
RenderStateChanged = false;
}
private void Draw()
{
Console.SetCursorPosition(Left, Top);
Console.Write('█');
}
private void Clear()
{
Console.SetCursorPosition(LastLeft, LastTop);
Console.Write(' ');
}
public void Notify(ConsoleKey consoleCommand)
{
if (consoleCommand == configuration["DownCommand"])
{
LastLeft = Left;
LastTop = Top;
Top += 1;
RenderStateChanged = true;
}
else if (consoleCommand == configuration["UpCommand"])
{
LastLeft = Left;
LastTop = Top;
Top -= 1;
RenderStateChanged = true;
}
else if (consoleCommand == configuration["LeftCommand"])
{
LastTop = Top;
LastLeft = Left;
Left -= 1;
RenderStateChanged = true;
}
else if (consoleCommand == configuration["RightCommand"])
{
LastTop = Top;
LastLeft = Left;
Left += 1;
RenderStateChanged = true;
}
}
}
IDynamicRenderable
and ICommandObserver
only contain Render()
and Notify()
at the moment.
So you can already see, there is some fiddling with ConsoleKeys
going on: there is a real tight coupling between the pressed key and Paddle. This leads to issue 1:
Issue 1: In a later stage multiple paddles will be added, which need to be controlled independently. E.g. for a different paddle instance 'W' correlates to moving up, you get the drift.
Basically solved by using a dictionary for the keys (yes I know its still set in the constructor now). Any more suggestions are very welcome!
Then there's the user input handler, which is basically a loop in a different thread, writing console input to a queue:
public class UserInputProcessor
{
private CancellationTokenSource cts;
private Task task;
private Queue<ConsoleKey> Commands { get; private set; }
public List<ICommandObserver> CommandObservers { get; set; }
public UserInputProcessor()
{
CommandObservers = new List<ICommandObserver>();
Commands = new Queue<ConsoleKey>();
}
public void Start()
{
if (cts == null)
{
cts = new CancellationTokenSource();
task = Task.Factory.StartNew(() => Loop(cts.Token), cts.Token);
}
}
public void Stop()
{
if (cts != null)
{
cts.Cancel();
cts = null;
}
}
/// <summary>
/// Second game loop that collects user input and adds it in a queue for later processing.
/// </summary>
/// <param name="ct"></param>
private void Loop(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
ct.ThrowIfCancellationRequested();
var key = Console.ReadKey(true).Key;
Commands.Enqueue(key);
Trace.WriteLine("Added: " + key + " " + Commands.Count);
Thread.Sleep(100);
}
}
/// <summary>
/// Processes the first item in the queue. Notifies all observers about the key that has been fetched from the queue.
/// </summary>
public void Process()
{
if (Commands.Count > 0)
{
var key = Commands.Dequeue();
CommandObservers.ForEach(x => x.Notify(key));
Trace.WriteLine(Commands.Count);
}
}
}
This is all joined together in the might Program.cs file:
public class Program : ICommandObserver
{
UserInputProcessor uip;
List<IDynamicRenderable> renderables = new List<IDynamicRenderable>();
bool running = true;
static void Main(string[] args)
{
var p = new Program();
}
public Program()
{
Console.CursorVisible = false;
var paddle1 = new Paddle();
renderables.Add(paddle1);
uip = new UserInputProcessor();
uip.CommandObservers.Add(this);
uip.CommandObservers.Add(paddle1);
uip.Start();
while (running)
{
ProcessInput();
Render();
Thread.Sleep(100);
}
}
private void Render()
{
foreach (var renderable in renderables)
{
if (renderable.RenderStateChanged)
{
renderable.Render();
}
}
}
private void ProcessInput()
{
uip.Process();
}
public void Notify(ConsoleKey consoleCommand)
{
switch (consoleCommand)
{
case ConsoleKey.Q:
{
uip.Stop();
running = false;
} break;
}
}
}
Starting to feel better, any suggestions tips or plain 'youre doing it wrongs' are more than welcome, especially regarding the user input handling.
if
bodies to implement them), and then build a second map from console key codes to methods with it. \$\endgroup\$