encompass-cs-docs/content/pong/draw_paddle/initialize_world.md

8.0 KiB

title date weight
Initializing the World 2019-05-23T12:06:18-07:00 15

It's time to put it all together.

Let's look at our PongFE/PongFEGame.cs file. The LoadContent method looks like this:

    protected override void LoadContent()
    {
        World = WorldBuilder.Build();
    }

In the beginning, the World is without form, and void; and darkness is upon the face of the deep. Let's fix that!

Obviously, if we want to draw a Pong paddle, we will need a Texture2D that represents the paddle. A Pong paddle is just a white rectangle. One approach we could take is open up an image editor, draw a white rectangle, save it to a PNG file, and then tell our game to load it. That sounds annoying though - what if we want to tweak the dimensions slightly? We have to repeat the entire process. That's a lot of work just to draw a silly rectangle.

Let's define our Pong paddle programmatically instead.

First things first - at the top of the PongFEGame class, let's define a SpriteBatch instance and a Texture2D instance.

    WorldBuilder WorldBuilder { get; } = new WorldBuilder();
    World World { get; set; }

    SpriteBatch SpriteBatch { get; set; }
    Texture2D WhitePixel { get; set; }
    RenderTarget2D PaddleTexture { get; set; }

We haven't seen RenderTarget2D yet. Basically, a render target is a special kind of texture that we can draw to instead of drawing directly to the screen. These can come in extremely handy for many use cases, like generating textures at runtime and creating fullscreen post-processing effects.

Now, at the top of the LoadContent method, add the following lines:

    protected override void LoadContent()
    {
        SpriteBatch = new SpriteBatch(GraphicsDevice);

        WhitePixel = new Texture2D(GraphicsDevice, 1, 1);
        WhitePixel.SetData(new Color[] { Color.White });

        ...

GraphicsDevice is an instance attached to the Game class. It basically represents the handle that our program has to the device on our computer that will be handling the draw calls. All draw-related operations will be going through the GraphicsDevice. GraphicsDevice is guaranteed to be initialized by the time LoadContent is called, so we will be doing our graphics-related initialization work in there.

First, we create a SpriteBatch instance. We can re-use the same SpriteBatch instance for each batch we need, so we'll just create one of these and re-use it throughout our game.

Next, we create a new 1x1 Texture. Texture2D.SetData lets us set the color values of each pixel in the Texture. We just want a single white pixel.

Now, we can use our pixel to draw a rectangle of any size we want. Check this out;

    PaddleTexture = new RenderTarget2D(GraphicsDevice, 20, 80);
    GraphicsDevice.SetRenderTarget(PaddleTexture);
    SpriteBatch.Begin();
    SpriteBatch.Draw(WhitePixel, new Rectangle(0, 0, 20, 80), Color.White);
    SpriteBatch.End();
    GraphicsDevice.SetRenderTarget(null);

First, we instantiate our PaddleTexture render target. Then, we set the current render target to PaddleTexture. This means that all draw calls will now draw to this texture instead of to the screen. Next, we begin the SpriteBatch, draw our white pixel to a rectangle of size 20x80, and end the SpriteBatch. Finally, setting the current render target back to null means that draw calls will go to the screen.

Now we have a Pong paddle of size 20x80! All that remains is to attach it to an Entity.

    var paddle = WorldBuilder.CreateEntity();
    WorldBuilder.SetComponent(paddle, new PositionComponent(new MoonTools.Structs.Position2D(5, 5)));
    WorldBuilder.SetComponent(paddle, new Texture2DComponent(PaddleTexture, 0));

WorldBuilder is the class we use to set up the starting state of our World.

First, we tell WorldBuilder to create a new Entity. This creates an empty Entity with no components.

Next, we create a new PositionComponent and tell the WorldBuilder to attach it to the paddle entity by calling SetComponent.

Finally, we create a new Texture2DComponent using our PaddleTexture, and put it on layer 0. This is attached to the paddle entity with SetComponent as well.

Now we want to attach our Texture2DRenderer to the World.

    ...
    WorldBuilder.AddOrderedRenderer(new Texture2DRenderer(SpriteBatch));

    var paddle = WorldBuilder.CreateEntity();
    ...

We set up our Renderers before we create any entities and components. We give our SpriteBatch instance to the new Texture2DRenderer. WorldBuilder.AddOrderedRenderer tells the World to use this Renderer. That's it!

We have one little bit of housekeeping to take care of before we can run the game. SpriteBatch can only draw after Begin has been called, and it must call End before anything will draw to the screen. Recall that our Texture2DRenderer does not call Begin or End, because that would only batch one Texture2D at a time, which is very inefficient.

Our game's Draw method should look like this:

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);
        World.Draw();
        SpriteBatch.End();

        base.Draw(gameTime);
    }

SpriteSortMode.Deferred is the most efficient SpriteBatch drawing mode. It waits as long as possible to send data to the GPU. BlendState.NonPremultiplied is a "blend mode". We will talk more about these later on, but feel free to read about them on your own. There are better ways we might need to structure our SpriteBatch Begins and Ends, but this will do just fine for now.

Our final PongFEGame should look like this:

using Encompass;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using PongFE.Components;
using PongFE.Renderers;

namespace PongFE
{
    class PongFEGame : Game
    {
        GraphicsDeviceManager graphics;

        WorldBuilder WorldBuilder { get; } = new WorldBuilder();
        World World { get; set; }

        SpriteBatch SpriteBatch { get; set; }
        Texture2D WhitePixel { get; set; }
        RenderTarget2D PaddleTexture { get; set; }

        public PongFEGame()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;
            graphics.PreferMultiSampling = true;
            Content.RootDirectory = "Content";

            Window.AllowUserResizing = true;
            IsMouseVisible = true;
        }

        protected override void LoadContent()
        {
            SpriteBatch = new SpriteBatch(GraphicsDevice);

            WhitePixel = new Texture2D(GraphicsDevice, 1, 1);
            WhitePixel.SetData(new Color[] { Color.White });

            PaddleTexture = new RenderTarget2D(GraphicsDevice, 20, 80);
            GraphicsDevice.SetRenderTarget(PaddleTexture);
            SpriteBatch.Begin();
            SpriteBatch.Draw(WhitePixel, new Rectangle(0, 0, 20, 80), Color.White);
            SpriteBatch.End();
            GraphicsDevice.SetRenderTarget(null);

            WorldBuilder.AddOrderedRenderer(new Texture2DRenderer(SpriteBatch));

            var paddle = WorldBuilder.CreateEntity();
            WorldBuilder.SetComponent(paddle, new PositionComponent(new MoonTools.Structs.Position2D(5, 5)));
            WorldBuilder.SetComponent(paddle, new Texture2DComponent(PaddleTexture, 0));

            World = WorldBuilder.Build();
        }

        protected override void UnloadContent()
        {
            base.UnloadContent();
        }

        protected override void Update(GameTime gameTime)
        {
            World.Update(gameTime.ElapsedGameTime.TotalSeconds);

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);
            World.Draw();
            SpriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Let's run the game!!