5
\$\begingroup\$

I hope this isn't getting annoying, but I've asked a lot of questions about improving my game lately:

No, you don't have to read all of those. I'm starting to think all my questions stem from problems I have with XNA's default project template. After creating a new Windows game project in Visual Studio, this is what it gives you:

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    static void Main(string[] args)
    {
        using (Game1 game = new Game1())
        {
            game.Run();
        }
    }
}

/// <summary>
/// This is the main type for your game
/// </summary>
public class Game1 : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    /// <summary>
    /// Allows the game to perform any initialization it needs to before starting to run.
    /// This is where it can query for any required services and load any non-graphic
    /// related content.  Calling base.Initialize will enumerate through any components
    /// and initialize them as well.
    /// </summary>
    protected override void Initialize()
    {
        // TODO: Add your initialization logic here

        base.Initialize();
    }

    /// <summary>
    /// LoadContent will be called once per game and is the place to load
    /// all of your content.
    /// </summary>
    protected override void LoadContent()
    {
        // Create a new SpriteBatch, which can be used to draw textures.
        spriteBatch = new SpriteBatch(GraphicsDevice);

        // TODO: use this.Content to load your game content here
    }

    /// <summary>
    /// UnloadContent will be called once per game and is the place to unload
    /// all content.
    /// </summary>
    protected override void UnloadContent()
    {
        // TODO: Unload any non ContentManager content here
    }

    /// <summary>
    /// Allows the game to run logic such as updating the world,
    /// checking for collisions, gathering input, and playing audio.
    /// </summary>
    /// <param name="gameTime">Provides a snapshot of timing values.</param>
    protected override void Update(GameTime gameTime)
    {
        // Allows the game to exit
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
            this.Exit();

        // TODO: Add your update logic here

        base.Update(gameTime);
    }

    /// <summary>
    /// This is called when the game should draw itself.
    /// </summary>
    /// <param name="gameTime">Provides a snapshot of timing values.</param>
    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        // TODO: Add your drawing code here

        base.Draw(gameTime);
    }
}

I feel like I've been fighting the framework all along because I don't like how it is structured. There are several tutorials around the net, and most (if not all) of them show you how to add your code to the Game class directly - loading sprites, animating sprites, drawing sprites, adding sound effects, responding to input, etc. I've tried several times to abstract this as much as possible, but my results haven't been great because I'm not the best designer. This is my latest iteration:

public static class Program
{
    private static void Main( string[] args )
    {
        IFileHandler fileHandler = new JsonHandler();

        var settings = fileHandler.Read<Settings>( "settings.json" );
        var level = fileHandler.Read<Level>( "level.json" );
        var player = fileHandler.Read<Entity>( "player.json" );

        using ( var game = new MyGame( new Graphics( settings.DisplaySettings, level, player ),
                                       new Input( settings.GamePadSettings, settings.KeyboardSettings, level, player ) ) )
        {
            game.Run();
            fileHandler.Write( "player.json", player );
            fileHandler.Write( "level.json", level );
        }
    }
}

public class MyGame : Game
{
    private readonly IGraphics graphics;
    private readonly IInput input;

    public MyGame( IGraphics graphics, IInput input )
    {
        if ( graphics == null || input == null )
        {
            throw new ArgumentNullException();
        }

        this.graphics = graphics;
        this.input = input;

        graphics.Setup( this );
        input.Setup( this );
    }

    protected override void LoadContent()
    {
        Content.RootDirectory = "Content";

        graphics.Load( GraphicsDevice, Content );
    }

    protected override void Update( GameTime gameTime )
    {
        input.Process();

        base.Update( gameTime );
    }

    protected override void Draw( GameTime gameTime )
    {
        graphics.Draw();

        base.Draw( gameTime );
    }
}

Not only are there several problems with my code, but in a way I feel like it doesn't make sense for me to try and do this. One of the biggest problems I have is GraphicsDeviceManager is, by its very definition, tightly coupled to the Game class - you can't instantiate it without giving it an instance of Game. Furthermore, Game already has a GraphicsDevice property (separate from GraphicsDeviceManager) that's needed to load sprites into the content pipeline. I really wanted to get this stuff - loading, drawing, input - out of my base class, but I can't figure out how to create a class that does this without tightly coupling it to Game. See:

public class Graphics : IGraphics
{
    private readonly DisplaySettings displaySettings;
    private readonly Level level;
    private readonly Entity player;
    private GraphicsDeviceManager graphicsDeviceManager;
    private SpriteBatch spriteBatch;

    public Graphics( DisplaySettings displaySettings, Level level, Entity player )
    {
        if ( displaySettings == null || level == null || player == null )
        {
            throw new ArgumentNullException();
        }

        this.displaySettings = displaySettings;
        this.level = level;
        this.player = player;
    }

    public void Setup( Game game )
    {
        if ( game == null )
        {
            throw new ArgumentNullException();
        }

        graphicsDeviceManager = new GraphicsDeviceManager( game )
            {
                IsFullScreen = displaySettings.IsFullScreen,
                PreferredBackBufferWidth = displaySettings.PreferredBackBufferWidth,
                PreferredBackBufferHeight = displaySettings.PreferredBackBufferHeight
            };
    }

    public void Load( GraphicsDevice graphicsDevice, ContentManager contentManager )
    {
        if ( graphicsDevice == null || contentManager == null )
        {
            throw new ArgumentNullException();
        }

        spriteBatch = new SpriteBatch( graphicsDevice );

        level.Load( contentManager );
        player.Load( contentManager );
    }

    public void Draw()
    {
        level.Clear( graphicsDeviceManager.GraphicsDevice );

        spriteBatch.Begin( SpriteSortMode.BackToFront, BlendState.AlphaBlend, SamplerState.LinearWrap, DepthStencilState.Default,
                           RasterizerState.CullNone );

        level.Draw( spriteBatch );
        player.Draw( spriteBatch );

        spriteBatch.End();
    }
}

Like I said, I feel like I'm fighting the framework. I'm not convinced that what I've come up with is any better, and it might even be worse than if I simply stuck everything in the Game class. At least that seems to be the way the XNA team intended for us to work with it.

TLDR: How can I use XNA without shooting myself in the foot? How would you work with the template to make it more SOLID?

\$\endgroup\$
  • \$\begingroup\$ My strategy so far has been to go ahead and use the main game loop, rather than avoiding it. This doesn't mean I put all my code there, however. I'm really doing a lot of it iteratively. As I get farther along, where I put things is becoming more and more obvious. Input ended up being inside my character, for instance, but when first designing it, it was in the main Game class. Soon, my character class will be removed from Game. It would take a serious architect to plan something like this ahead of time. \$\endgroup\$ – Magus Feb 24 '14 at 16:51
  • \$\begingroup\$ I don't know XNA, so I want to ask a few questions first. SpriteBatch constructor gets a GraphicsDevice argument, later in draw you get a GraphicsDevice from GraphicsDeviceManager. How does that work, are these two required to be same, how does spritebatch know current device? \$\endgroup\$ – abuzittin gillifirca Feb 26 '14 at 8:13
  • 2
    \$\begingroup\$ "SpriteBatch constructor gets a GraphicsDevice argument, later in draw you get a GraphicsDevice from GraphicsDeviceManager... are these two required to be same" I assume so - in my "improved" version I tried to get the GraphicsDeviceManager and GraphicsDevice stuff out of the Game class but in reality they are in both now. "how does spritebatch know current device?" I don't know - I assume it holds a reference to the device but it's all very magical. \$\endgroup\$ – David Kennedy Feb 26 '14 at 16:33
  • \$\begingroup\$ Seen this answer? \$\endgroup\$ – Mathieu Guindon Mar 19 '14 at 2:18
  • 1
    \$\begingroup\$ I wish there was an easy way to ask that dude directly. He seems to have a lot of good advice on XNA. \$\endgroup\$ – David Kennedy Mar 21 '14 at 23:34
2
\$\begingroup\$

I haven't done enough XNA to really get to the core of this question, I hope to see this reviewed from an XNA perspective.

I'll only make a few observations about the Program class.

Main()

Looking at the Main method and the Program class, I see mixed abstraction levels and some temporal coupling, I'm tempted to move stuff around a bit:

public static class Program
{
    private static void Main(string[] args)
    {
        var data = GetGameFileData(); // temporal coupling removed!

        // extracted variables improve readability...
        var graphics = new Graphics(data.Settings.DisplaySettings, 
                                    data.Level, 
                                    data.Player);

        var input = new Input(data.Settings.GamePadSettings, 
                              data.Settings.KeyboardSettings, 
                              data.Level, 
                              data.Player);

        // ...and makes the intent clearer:
        using (var game = new MyGame(graphics, input))
        {
            game.Run();
            data.Save();
        }
    }

    private static GameFileData GetGameFileData()
    {
        var result = new GameFileData();
        result.Load();

        return result;
    }
}
private class GameFileData
{
    private readonly IFileHandler _fileHandler;

    public GameFileData() 
        : this(new FileHandler()) 
    {
    }

    // probably YAGNI
    public GameFileData(IFileHandler fileHandler)
    {
        _fileHandler = fileHandler;
    }

    public Settings Settings { get; private set; }
    public Level Level { get; private set; }
    public Entity Player { get; private set; }

    public void Load()
    {
        Settings = _fileHandler.Read<Settings>("settings.json"),
        Level = _fileHandler.Read<Level>("level.json"),
        Player = _fileHandler.Read<Entity>("player.json")
    }

    public void Save()
    {
        _fileHandler.Write("level.json", Level);
        _fileHandler.Write("player.json", Player);
    }
}

Nitpick

new Graphics( settings.DisplaySettings, level, player )
fileHandler.Write( "player.json", player );

If this is really how much whitespace your actual code has, ...you're fighting the IDE to achieve it - Visual Studio automatically removes these spaces then you hit ; or when you close a scope with }.

new Graphics(settings.DisplaySettings, level, player)
fileHandler.Write("player.json", player);
\$\endgroup\$
  • \$\begingroup\$ Thanks for your advice. I'm a bit skeptical on moving the file names inside of GameFileData though - isn't it a good idea to have implementation details like that as close to the composition root as possible? Now they're buried. As for the spacing, the conventions I use are enforced by ReSharper, and I use them because of work. \$\endgroup\$ – David Kennedy Mar 29 '14 at 21:01

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.