Sunday, April 6, 2014

SharpDX Toolkit Tutorial 2: Game structure

In previous tutorial we have created a base game project which contains the bare minimum for a SharpDX Toolkit game to run. We will use this as a basis for next tutorials. In this post we will draw something more than an empty screen and will discuss the general structure of a game.

To make easier to follow these tutorial series I have created a repository which will contain all related code. Feel free to examine, download, play with it and contribute.

While the Game class is the central point of interaction of all components, keeping all functionality here makes it difficult to maintain, extend and reuse. To improve maintainability and allow easier integration of S.O.L.I.D. design principles the Toolkit library introduced the concept of GameSystem and Service.

A game system can encapsulate some reusable functionality like a particle system, camera management or AI logic. The important thing is that a game system should not be created for every entity in the game - as this would be a waste of resources and it will be difficult to maintain all relations between components.

A service represents, well, just a service in IoC terms. It allows easier separation of concerns and improved testability.

Let's try to apply this in practice. For this post we will draw a simple scene and we need to have independent camera control. To keep things simple, we will just rotate the camera around the scene. At this point we identified 2 main components - camera provider and scene renderer.

For improved maintainability, the game class will be responsible just for creating instances of the components and clearing the render target:

internal sealed class MyGame : Game
{
    private readonly GraphicsDeviceManager _graphicsDeviceManager;
    private readonly SceneRenderer _sceneRenderer;
    private readonly CameraProvider _cameraProvider;

    public MyGame()
    {
        _graphicsDeviceManager = new GraphicsDeviceManager(this);
        _sceneRenderer = new SceneRenderer(this);
        _cameraProvider = new CameraProvider(this);

        Content.RootDirectory = "Content";
    }

In the code above, we are creating instances of our components and setting the root directory of the content folder from where all assets should be loaded. The GraphicsDeviceManager compoent is responsible for instantiating, resetting and changing settings of the GraphicsDevice and is mandatory for every Toolkit game. The other two components are custom created for our game. The Game.Content property provides a reference to the content manager, responsible for loading and unloading assets (textures, shaders, models and the recently added sound effects).

Next, let's have a look at our CameraProvider class:

internal sealed class CameraProvider : GameSystem, ICameraService
{
    private Matrix _view;
    private Matrix _projection;

    public CameraProvider(Game game)
        : base(game)
    {
        Enabled = true;
        game.GameSystems.Add(this);
        game.Services.AddService(typeof(ICameraService), this);
    }

    public Matrix View { get { return _view; } }
    public Matrix Projection { get { return _projection; } }

    public override void Update(GameTime gameTime)
    {
        base.Update(gameTime);

        var viewRotationAngle = (float)(gameTime.TotalGameTime.TotalSeconds * 0.2f);
        var eyePosition = Vector3.Transform(new Vector3(0, 2, 5), Quaternion.RotationAxis(Vector3.UnitY, viewRotationAngle));

        _view = Matrix.LookAtRH(eyePosition, new Vector3(0, 0, 0), Vector3.UnitY);
        _projection = Matrix.PerspectiveFovRH(MathUtil.PiOverFour, (float)GraphicsDevice.BackBuffer.Width / GraphicsDevice.BackBuffer.Height, 0.1f, 200.0f);
    }
}

In constructor it enables calls to Update method (they are disabled by default), registers itself as a game system and adds itself to the game services registry as a provider for ICameraService interface. In the Update method the camera provider computes the view matrix (animated rotation around center) and the projection matrix. For performance reasons it is recommended to cache the computed matrix values and update them only when needed.

The ICameraService interface provides access to camera-related matrix values:

internal interface ICameraService
{
    Matrix View { get; }
    Matrix Projection { get; }
}

The next class, SceneRenderer as you can understand from its name - is reposnsible for scene rendering. It makes use of the camera service and enables itself both Update and Draw calls:

internal sealed class SceneRenderer : GameSystem
{
    private ICameraService _cameraService;

    private GeometricPrimitive _cube;
    private Texture2D _cubeTexture;
    private Matrix _cubeTransform;

    private GeometricPrimitive _plane;
    private Texture2D _planeTexture;
    private Matrix _planeTransform;

    private BasicEffect _basicEffect;

    public SceneRenderer(Game game)
        : base(game)
    {
        Visible = true;
        Enabled = true;

        game.GameSystems.Add(this);
    }

    public override void Initialize()
    {
        base.Initialize();

        _cameraService = Services.GetService();
    }

The obtaining of the camera service reference is done in the Initialize method to make sure all constructors have been called and all services are added to the registry, therefore the recommended pattern is to register everything in the Game's constructor, then bind services and components to each other in the Initialize method. Of course this is not a silver-bullet method, so make sure you carefully examine your situation and fully understand why you implement a certain pattern.

Next, the content is loaded in the following methods:

protected override void LoadContent()
{
    base.LoadContent();

    _basicEffect = ToDisposeContent(new BasicEffect(GraphicsDevice));
    _basicEffect.EnableDefaultLighting();
    _basicEffect.TextureEnabled = true;

    LoadCube();
    LoadPlane();
}

private void LoadCube()
{
    _cube = ToDisposeContent(GeometricPrimitive.Cube.New(GraphicsDevice));
    _cubeTexture = Content.Load("logo_large");
    _cubeTransform = Matrix.Identity;
}

private void LoadPlane()
{
    _plane = ToDisposeContent(GeometricPrimitive.Plane.New(GraphicsDevice, 50f, 50f));
    _planeTexture = Content.Load("GeneticaMortarlessBlocks");
    _planeTransform = Matrix.RotationX(-MathUtil.PiOverTwo) * Matrix.Translation(0f, -5f, 0f);
}

As the assets loaded via ContentManager are disposed automatically like in XNA, we can add any other disposable object to be disposed at the same time if his lifetime is tightly coupled to an asset, to do this - we are calling the ToDisposeContent method. When loading assets, the extension is not required (but can be provided if necessary) as ContentManager will try to append the default asset extension (".tkb", from ToolKitBinary) if one is not supplied.

The drawing of both objects uses the same basic effect:

public override void Draw(GameTime gameTime)
{
    base.Draw(gameTime);

    _basicEffect.Texture = _cubeTexture;
    _basicEffect.World = _cubeTransform;
    _cube.Draw(_basicEffect);

    _basicEffect.Texture = _planeTexture;
    _basicEffect.World = _planeTransform;
    _plane.Draw(_basicEffect);
}

Before drawing an object, we are setting the needed effect parameters (texture and world transform), then we are calling the GraphicsPrimitive.Draw method which applied the provided effect and performs the draw calls on GraphicsDevice.

The update method reads the data provided by the camera service and updates the animation of the cube:

public override void Update(GameTime gameTime)
{
    base.Update(gameTime);

    var time = (float)gameTime.TotalGameTime.TotalSeconds;
    _cubeTransform = Matrix.RotationX(time) * Matrix.RotationY(time * 2f) * Matrix.RotationZ(time * .7f);

    _basicEffect.View = _cameraService.View;
    _basicEffect.Projection = _cameraService.Projection;
}

In the final you should get something like this:

Full code with detailed comments is uploaded here. Feel free to do anything you want with it. Keep in mind that there doesn't exist an architecture that will fit all scenarios, so don't follow blindly all described here - make sure you understand what and when to apply.

Let me know in comments about which Toolkit functionality you would like to read next.

Happy coding!

8 comments:

  1. Great tutorial. I hope there will be more soon.

    ReplyDelete
  2. I personally am working on a Kinect 2 + SharpDX application that uses video looping. It would be great to hear a bit about working with texture compression using SharpDX's toolkit, especially if it is possible to:

    - get an uncompressed 200x200 RGBA texture in CPU memory
    - convert to a compressed RGBA texture in GPU memory
    - get some kind of SharpDX texture reference to that compressed GPU texture, WITHOUT requiring any allocations on the .NET heap

    I am going to have thousands of frames of video flying around memory for this video animation system, and I really don't want one GC heap object per compressed texture frame! The simple thing is just to keep all the video frames in CPU memory and copy them to the Texture2D data on every rendering pass; I will try this first, but I want some kind of GPU strategy in case of memory capacity or bandwidth problems.

    Thanks!

    ReplyDelete
    Replies
    1. In SharpDX Toolkit, the Texture2D object represents a reference to a texture in GPU memory. If you can reuse the same object and CPU buffer - you can avoid much allocation on the .NET heap.

      Regarding texture compression - SharpDX should support everything that is available in DirectX.

      Delete
  3. When I try to draw a 3d object after a spritebatch, it looks inside out; almost like it is in the wrong projection. Any ideas on what is causing it?

    ReplyDelete
  4. Sorry for late response. Try to setup an explicit culling mode and the z-buffer. Looks like SpriteBatch is leaving the graphics device in a wrong state so you need to set up the drawing explicitly, alternatively you can draw your model before calling the sprite batch.

    ReplyDelete
  5. Nice, Artiom! Thank you so much for putting these tutorials together.

    I'm trying to transition some MonoGame code to SharpDX in order to take advantage of SwapChainPanel. MonoGame doesn't work with SwapChainPanels (only SwapChainBackgroundPanel) unfortunately, so SharpDX seems like the best choice for multiple D3D objects in one XAML canvas. Is that your experience too?

    ReplyDelete
    Replies
    1. You can use multiple SwapChainPanels on the same XAML page, but you should check the resulting performance and resource usage. Also not all XNA/MonoGame code may be applicable one-to-one to Toolkit - if you will find any issues - feel free to post them on github.

      Delete
  6. Best SharpDX tutorial!

    Please carry on.

    ReplyDelete