5
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ I don't know how to write that in C#, but I would certainly change the dictionary to map key codes to delegates, thus avoiding to do potentially 4 (or more) dictionary lookups, which are already quite costly. \$\endgroup\$ – didierc Oct 24 '14 at 13:51
  • \$\begingroup\$ Perhaps I'm misunderstanding, but wouldn;t that reintroduce the tight coupling between paddle and the console key? \$\endgroup\$ – Apeiron Oct 24 '14 at 14:02
  • \$\begingroup\$ well, it depends what you mean by tight coupling: the Paddle class rely on the dictionary to work properly, and that dictionary is meant to store console keys. That said, if C# allows it, you should perhaps use an external object to populate the dictionary with a configuration file, ie. have an initial map from symbolic names to actual methods (using the if bodies to implement them), and then build a second map from console key codes to methods with it. \$\endgroup\$ – didierc Oct 24 '14 at 14:33
3
\$\begingroup\$

I'm going architectural because coupling is an architectural issue and there is a fairly common general architecture for screen oriented games.

Game Engine

I think it will be difficult to create a screen oriented game without using a traditional game loop architecture. The basic loop might look like:

while(true) {
   timerState = timerList.tick(timerState);
   keyboardState   = keyboardState.nextState(keyboardState);  
   UpdateScreen(timerState, keyboardState);
   pause(frameRate);       
 }

In the abstract screen oriented games are simple: every so often, process the event queue and update the screen. A screen oriented game is a controlled animation.

Events

The basic event in any animation is the the transition to the next frame. To insure that there is a next frame, games have timers to generate events. Keypress events are less frequent.

All the bussiness logic is in the handlers for events. The handlers transform events into messages and send them to the screen.

Screen

A screen interprets the messages and creates and then executes a set of instructions for painting the screen. The screen is where logic that scrolls the display or bounces a ball off a paddle lives.

Sprites

Given a location and a "color", a sprite knows how to draw itself. That's it. A sprite does not know anything about screens or events. It doesn't know anything about why it is being drawn. For example, if paddle1 is a sprite:

paddle1.draw(paddle1.x, (paddle1.y + player1.paddle().delta_y), foregroundColor);

might be executed by a screen in response to some particular event.

Game State

Their can be a data structure sitting above all this which keeps track of game configuration such as the list of high scores or the mapping of logical keys like paddle_move_up to physical keys such as w.

Analysis

The big issue is that paddles know too much...or rather way way too much. They know about the hardware [key presses], they know about rendering, eventually they will have to know about other paddles.

The second issue is that there are no timer events, so besides the paddle, there aren't a lot of places for render() to live.

\$\endgroup\$
  • \$\begingroup\$ Thanks for this! I will set out to work on having the paddles know alot less, before I venture to a more elaborate game :) \$\endgroup\$ – Apeiron Oct 27 '14 at 13:43
  • \$\begingroup\$ To be honest I already ran in to a problem with this architecture and the ball that does not move on user input. This is fun :) \$\endgroup\$ – Apeiron Oct 27 '14 at 13:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.